WIP: get Heltec V3 working
This commit is contained in:
11 files changed
+333
-32
No files matched your search
@@ -19,9 +19,17 @@ from cobs import cobs
|
|||||||
|
|
||||||
USB_VID_PID = "1209:5741"
|
USB_VID_PID = "1209:5741"
|
||||||
|
|
||||||
|
# Known USB-UART bridge VID:PIDs found on some board revisions.
|
||||||
|
BRIDGE_VID_PIDS = {"10c4:ea60", "1a86:55d4", "1a86:7523", "0403:6001"}
|
||||||
|
|
||||||
|
|
||||||
def find_port() -> str | None:
|
def find_port() -> str | None:
|
||||||
"""Find the DongLoRa serial port by USB VID:PID."""
|
"""Find the DongLoRa serial port by USB VID:PID.
|
||||||
|
|
||||||
|
Checks for native USB CDC-ACM first (1209:5741), then falls back to
|
||||||
|
known USB-UART bridge chips found on some board revisions.
|
||||||
|
"""
|
||||||
|
bridge_match = None
|
||||||
for path in sorted(glob.glob("/dev/ttyACM*")) + sorted(glob.glob("/dev/ttyUSB*")):
|
for path in sorted(glob.glob("/dev/ttyACM*")) + sorted(glob.glob("/dev/ttyUSB*")):
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -33,11 +41,16 @@ def find_port() -> str | None:
|
|||||||
)
|
)
|
||||||
vid = props.get("ID_VENDOR_ID", "").lower()
|
vid = props.get("ID_VENDOR_ID", "").lower()
|
||||||
pid = props.get("ID_MODEL_ID", "").lower()
|
pid = props.get("ID_MODEL_ID", "").lower()
|
||||||
if f"{vid}:{pid}" == USB_VID_PID:
|
vid_pid = f"{vid}:{pid}"
|
||||||
|
if vid_pid == USB_VID_PID:
|
||||||
return path
|
return path
|
||||||
|
if bridge_match is None and vid_pid in BRIDGE_VID_PIDS:
|
||||||
|
bridge_match = path
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
ports = sorted(glob.glob("/dev/ttyACM*"))
|
if bridge_match is not None:
|
||||||
|
return bridge_match
|
||||||
|
ports = sorted(glob.glob("/dev/ttyACM*")) + sorted(glob.glob("/dev/ttyUSB*"))
|
||||||
return ports[0] if ports else None
|
return ports[0] if ports else None
|
||||||
|
|
||||||
|
|
||||||
@@ -345,7 +358,7 @@ def connect(port: str | None = None, timeout: float = 2.0) -> serial.Serial | Mu
|
|||||||
# No mux available — direct USB serial
|
# No mux available — direct USB serial
|
||||||
if port is None:
|
if port is None:
|
||||||
port = wait_for_device()
|
port = wait_for_device()
|
||||||
ser = serial.Serial(port, timeout=timeout)
|
ser = serial.Serial(port, baudrate=115200, timeout=timeout)
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
return ser
|
return ser
|
||||||
|
|
||||||
|
|||||||
@@ -13,18 +13,41 @@ pub const USB_VID: u16 = 0x1209;
|
|||||||
/// DongLoRa USB Product ID.
|
/// DongLoRa USB Product ID.
|
||||||
pub const USB_PID: u16 = 0x5741;
|
pub const USB_PID: u16 = 0x5741;
|
||||||
|
|
||||||
|
/// Known USB-UART bridge VID:PIDs found on some board revisions.
|
||||||
|
const BRIDGE_VID_PIDS: &[(u16, u16)] = &[
|
||||||
|
(0x10C4, 0xEA60), // CP2102 (Silicon Labs)
|
||||||
|
(0x1A86, 0x55D4), // CH9102
|
||||||
|
(0x1A86, 0x7523), // CH340
|
||||||
|
(0x0403, 0x6001), // FT232R (FTDI)
|
||||||
|
];
|
||||||
|
|
||||||
/// Find the DongLoRa serial port by USB VID:PID.
|
/// Find the DongLoRa serial port by USB VID:PID.
|
||||||
///
|
///
|
||||||
|
/// Checks for native USB CDC-ACM first (VID:PID 1209:5741), then falls
|
||||||
|
/// back to known USB-UART bridge chips found on some board revisions.
|
||||||
/// Returns the first matching port path, or `None` if no device is found.
|
/// Returns the first matching port path, or `None` if no device is found.
|
||||||
pub fn find_port() -> Option<String> {
|
pub fn find_port() -> Option<String> {
|
||||||
let ports = serialport::available_ports().ok()?;
|
let ports = serialport::available_ports().ok()?;
|
||||||
|
|
||||||
|
// Prefer native USB (our custom VID:PID)
|
||||||
|
if let Some(port) = ports.iter().find(|p| {
|
||||||
|
matches!(
|
||||||
|
&p.port_type,
|
||||||
|
serialport::SerialPortType::UsbPort(info)
|
||||||
|
if info.vid == USB_VID && info.pid == USB_PID
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return Some(port.port_name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to known USB-UART bridge chips
|
||||||
ports
|
ports
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|p| {
|
.find(|p| {
|
||||||
matches!(
|
matches!(
|
||||||
&p.port_type,
|
&p.port_type,
|
||||||
serialport::SerialPortType::UsbPort(info)
|
serialport::SerialPortType::UsbPort(info)
|
||||||
if info.vid == USB_VID && info.pid == USB_PID
|
if BRIDGE_VID_PIDS.contains(&(info.vid, info.pid))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map(|p| p.port_name)
|
.map(|p| p.port_name)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
- firmware file naming, no "release", add version
|
||||||
|
- LED blinking on TX/RX? colored?
|
||||||
|
- display having board specific stuff in it
|
||||||
|
- publish new client, mux, bridge (updates in client for UART crap)
|
||||||
|
- try to get heltec v3 back to the right USB port+VID:PID
|
||||||
|
- split out mux and client repos
|
||||||
|
- move to GH organization
|
||||||
+1
-1
@@ -73,7 +73,7 @@ flash board:
|
|||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||||
case "$target" in \
|
case "$target" in \
|
||||||
xtensa-*) \
|
xtensa-*) \
|
||||||
port=$(just _find_port "303a:1001" "1209:5741"); \
|
port=$(just _find_port "303a:1001" "1209:5741" "10c4:ea60"); \
|
||||||
if [ -n "$port" ]; then \
|
if [ -n "$port" ]; then \
|
||||||
echo "Using $port"; \
|
echo "Using $port"; \
|
||||||
espflash flash -p "$port" "{{builds_dir}}/donglora-{{board}}-release.elf"; \
|
espflash flash -p "$port" "{{builds_dir}}/donglora-{{board}}-release.elf"; \
|
||||||
|
|||||||
@@ -28,8 +28,12 @@ type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
|||||||
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "heltec_v3"))]
|
||||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||||
|
|
||||||
|
#[cfg(feature = "heltec_v3")]
|
||||||
|
pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>;
|
||||||
|
|
||||||
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||||
|
|
||||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||||
@@ -39,10 +43,16 @@ pub struct RadioParts {
|
|||||||
pub delay: Delay,
|
pub delay: Delay,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "heltec_v3"))]
|
||||||
pub struct UsbParts {
|
pub struct UsbParts {
|
||||||
pub driver: UsbDriver,
|
pub driver: UsbDriver,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "heltec_v3")]
|
||||||
|
pub struct UartParts {
|
||||||
|
pub driver: UartDriver,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DisplayParts {
|
pub struct DisplayParts {
|
||||||
pub i2c: DisplayI2c,
|
pub i2c: DisplayI2c,
|
||||||
pub mac: [u8; 6],
|
pub mac: [u8; 6],
|
||||||
@@ -107,7 +117,7 @@ pub fn init_radio(
|
|||||||
|
|
||||||
let sx_config = sx126x::Config {
|
let sx_config = sx126x::Config {
|
||||||
chip: Sx1262,
|
chip: Sx1262,
|
||||||
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V8),
|
||||||
use_dcdc: true,
|
use_dcdc: true,
|
||||||
rx_boost: false,
|
rx_boost: false,
|
||||||
};
|
};
|
||||||
@@ -118,6 +128,7 @@ pub fn init_radio(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "heltec_v3"))]
|
||||||
/// Initialize USB OTG CDC-ACM driver.
|
/// Initialize USB OTG CDC-ACM driver.
|
||||||
pub fn init_usb(
|
pub fn init_usb(
|
||||||
usb0: esp_hal::peripherals::USB0<'static>,
|
usb0: esp_hal::peripherals::USB0<'static>,
|
||||||
@@ -136,6 +147,24 @@ pub fn init_usb(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "heltec_v3")]
|
||||||
|
/// Initialize UART0 for boards with USB-UART bridge chips (e.g. CP2102).
|
||||||
|
pub fn init_uart(
|
||||||
|
uart0: esp_hal::peripherals::UART0<'static>,
|
||||||
|
tx: esp_hal::peripherals::GPIO43<'static>,
|
||||||
|
rx: esp_hal::peripherals::GPIO44<'static>,
|
||||||
|
) -> UartParts {
|
||||||
|
use esp_hal::uart::{Config as UartConfig, Uart};
|
||||||
|
|
||||||
|
let uart = Uart::new(uart0, UartConfig::default())
|
||||||
|
.expect("UART init")
|
||||||
|
.with_tx(tx)
|
||||||
|
.with_rx(rx)
|
||||||
|
.into_async();
|
||||||
|
|
||||||
|
UartParts { driver: uart }
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize I2C bus for display.
|
/// Initialize I2C bus for display.
|
||||||
pub fn init_display_i2c(
|
pub fn init_display_i2c(
|
||||||
i2c0: esp_hal::peripherals::I2C0<'static>,
|
i2c0: esp_hal::peripherals::I2C0<'static>,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ use esp_hal::gpio::{Level, Output, OutputConfig};
|
|||||||
use super::esp32s3;
|
use super::esp32s3;
|
||||||
use super::traits::LoRaBoard;
|
use super::traits::LoRaBoard;
|
||||||
|
|
||||||
#[allow(unused_imports)] // Re-exported for other modules (display, radio, usb tasks)
|
#[allow(unused_imports)] // Re-exported for other modules (display, radio, uart tasks)
|
||||||
pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UsbDriver, UsbParts};
|
pub use super::esp32s3::{DisplayI2c, DisplayParts, RadioDriver, RadioParts, UartDriver, UartParts};
|
||||||
|
|
||||||
// ── Board init ───────────────────────────────────────────────────────
|
// ── Board init ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -27,23 +27,31 @@ impl LoRaBoard for Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
pub fn into_parts(self) -> (RadioParts, UartParts, Option<DisplayParts>) {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
esp32s3::start_timer(p.TIMG0);
|
esp32s3::start_timer(p.TIMG0);
|
||||||
|
|
||||||
// Vext power: GPIO21, active LOW to enable OLED
|
// Vext power: GPIO36, active LOW to enable peripherals
|
||||||
let vext = Output::new(p.GPIO21, Level::Low, OutputConfig::default());
|
let vext = Output::new(p.GPIO36, Level::Low, OutputConfig::default());
|
||||||
core::mem::forget(vext); // hold pin low permanently; drop would reset it
|
core::mem::forget(vext); // hold pin low permanently; drop would reset it
|
||||||
|
|
||||||
let radio = esp32s3::init_radio(
|
let radio = esp32s3::init_radio(
|
||||||
p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13,
|
p.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13,
|
||||||
);
|
);
|
||||||
let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19);
|
let uart = esp32s3::init_uart(p.UART0, p.GPIO43, p.GPIO44);
|
||||||
|
|
||||||
|
// SSD1306 display reset: pulse GPIO21 low->high before I2C init
|
||||||
|
let mut display_rst = Output::new(p.GPIO21, Level::Low, OutputConfig::default());
|
||||||
|
esp_hal::delay::Delay::new().delay_millis(10);
|
||||||
|
display_rst.set_high();
|
||||||
|
esp_hal::delay::Delay::new().delay_millis(10);
|
||||||
|
core::mem::forget(display_rst); // hold reset high permanently
|
||||||
|
|
||||||
let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||||
let mac = Self::mac_address();
|
let mac = Self::mac_address();
|
||||||
let display = Some(DisplayParts { i2c, mac });
|
let display = Some(DisplayParts { i2c, mac });
|
||||||
|
|
||||||
(radio, usb, display)
|
(radio, uart, display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ impl Board {
|
|||||||
|
|
||||||
let sx_config = sx126x::Config {
|
let sx_config = sx126x::Config {
|
||||||
chip: Sx1262,
|
chip: Sx1262,
|
||||||
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V7),
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V8),
|
||||||
use_dcdc: true,
|
use_dcdc: true,
|
||||||
rx_boost: false,
|
rx_boost: false,
|
||||||
};
|
};
|
||||||
|
|||||||
+37
-17
@@ -3,11 +3,11 @@
|
|||||||
//! Three async tasks communicate via static channels:
|
//! Three async tasks communicate via static channels:
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! usb_task ──Command──► radio_task ──► SX1262
|
//! host_task ──Command──► radio_task ──► SX1262
|
||||||
//! ◄──Response── │
|
//! ◄──Response── │
|
||||||
//! StatusWatch
|
//! StatusWatch
|
||||||
//! ▼
|
//! ▼
|
||||||
//! display_task (optional)
|
//! display_task (optional)
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The host drives everything. The radio idles until commanded.
|
//! The host drives everything. The radio idles until commanded.
|
||||||
@@ -24,7 +24,11 @@ mod display;
|
|||||||
mod protocol;
|
mod protocol;
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
mod radio;
|
mod radio;
|
||||||
#[cfg(not(test))]
|
#[cfg(all(not(test), feature = "heltec_v3"))]
|
||||||
|
mod protocol_io;
|
||||||
|
#[cfg(all(not(test), feature = "heltec_v3"))]
|
||||||
|
mod uart;
|
||||||
|
#[cfg(all(not(test), not(feature = "heltec_v3")))]
|
||||||
mod usb;
|
mod usb;
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
@@ -74,23 +78,39 @@ cfg_if::cfg_if! {
|
|||||||
async fn run(spawner: Spawner) {
|
async fn run(spawner: Spawner) {
|
||||||
let board = <board::Board as LoRaBoard>::init();
|
let board = <board::Board as LoRaBoard>::init();
|
||||||
let mac = board::Board::mac_address();
|
let mac = board::Board::mac_address();
|
||||||
let (radio, usb, display) = board.into_parts();
|
let (radio, comm, display) = board.into_parts();
|
||||||
|
|
||||||
let has_display = display.is_some();
|
let has_display = display.is_some();
|
||||||
|
|
||||||
spawner
|
spawner
|
||||||
.spawn(radio::radio_task(radio, &COMMANDS, &RESPONSES, &STATUS))
|
.spawn(radio::radio_task(radio, &COMMANDS, &RESPONSES, &STATUS))
|
||||||
.expect("spawn radio_task");
|
.expect("spawn radio_task");
|
||||||
spawner
|
|
||||||
.spawn(usb::usb_task(
|
cfg_if::cfg_if! {
|
||||||
usb,
|
if #[cfg(feature = "heltec_v3")] {
|
||||||
&COMMANDS,
|
spawner
|
||||||
&RESPONSES,
|
.spawn(uart::uart_task(
|
||||||
&DISPLAY_COMMANDS,
|
comm,
|
||||||
has_display,
|
&COMMANDS,
|
||||||
mac,
|
&RESPONSES,
|
||||||
))
|
&DISPLAY_COMMANDS,
|
||||||
.expect("spawn usb_task");
|
has_display,
|
||||||
|
mac,
|
||||||
|
))
|
||||||
|
.expect("spawn uart_task");
|
||||||
|
} else {
|
||||||
|
spawner
|
||||||
|
.spawn(usb::usb_task(
|
||||||
|
comm,
|
||||||
|
&COMMANDS,
|
||||||
|
&RESPONSES,
|
||||||
|
&DISPLAY_COMMANDS,
|
||||||
|
has_display,
|
||||||
|
mac,
|
||||||
|
))
|
||||||
|
.expect("spawn usb_task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(dp) = display {
|
if let Some(dp) = display {
|
||||||
spawner
|
spawner
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
//! Shared COBS protocol helpers for USB and UART transports.
|
||||||
|
//!
|
||||||
|
//! Both `usb_task` and `uart_task` use these helpers for frame
|
||||||
|
//! accumulation, response encoding, and command routing.
|
||||||
|
|
||||||
|
use defmt::warn;
|
||||||
|
|
||||||
|
use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel};
|
||||||
|
use crate::protocol::{Command, ErrorCode, Response};
|
||||||
|
|
||||||
|
/// Maximum COBS frame size (bytes).
|
||||||
|
pub const MAX_FRAME: usize = 512;
|
||||||
|
|
||||||
|
// Worst case: tag(1) + rssi(2) + snr(2) + len(2) + payload(256) = 263 bytes.
|
||||||
|
// COBS adds ceil(263/254)+1 = 3 bytes. 266 < 512. Plenty of room.
|
||||||
|
const _: () = assert!(
|
||||||
|
MAX_FRAME >= crate::protocol::MAX_PAYLOAD + 64,
|
||||||
|
"MAX_FRAME too small for max payload + COBS overhead"
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Accumulates bytes into complete COBS frames.
|
||||||
|
pub struct FrameAccumulator {
|
||||||
|
buf: [u8; MAX_FRAME],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameAccumulator {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
buf: [0u8; MAX_FRAME],
|
||||||
|
len: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the accumulator (discard partial frame).
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed a chunk of bytes, calling `on_command` for each decoded command.
|
||||||
|
pub fn feed(&mut self, data: &[u8], mut on_command: impl FnMut(Command)) {
|
||||||
|
for &byte in data {
|
||||||
|
if byte == 0x00 {
|
||||||
|
// End of COBS frame — decode and dispatch
|
||||||
|
if self.len > 0 {
|
||||||
|
let mut decode_buf = [0u8; MAX_FRAME];
|
||||||
|
if let Some(decoded_len) =
|
||||||
|
ucobs::decode(&self.buf[..self.len], &mut decode_buf)
|
||||||
|
{
|
||||||
|
if let Some(cmd) = Command::from_bytes(&decode_buf[..decoded_len]) {
|
||||||
|
on_command(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.len = 0;
|
||||||
|
} else if self.len < MAX_FRAME {
|
||||||
|
self.buf[self.len] = byte;
|
||||||
|
self.len += 1;
|
||||||
|
} else {
|
||||||
|
// Frame too large — discard
|
||||||
|
self.len = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// COBS-encode a response into `encode_buf` with trailing 0x00 sentinel.
|
||||||
|
///
|
||||||
|
/// Returns the slice to send (encoded frame + sentinel), or `None` on overflow.
|
||||||
|
pub fn encode_response<'a>(
|
||||||
|
response: Response,
|
||||||
|
write_buf: &mut [u8; MAX_FRAME],
|
||||||
|
encode_buf: &'a mut [u8; MAX_FRAME],
|
||||||
|
) -> Option<&'a [u8]> {
|
||||||
|
let raw_len = response.write_to(write_buf);
|
||||||
|
let encoded_len = ucobs::encode(&write_buf[..raw_len], encode_buf).unwrap_or(0);
|
||||||
|
if encoded_len < encode_buf.len() {
|
||||||
|
encode_buf[encoded_len] = 0x00;
|
||||||
|
Some(&encode_buf[..encoded_len + 1])
|
||||||
|
} else {
|
||||||
|
warn!("COBS encode buffer overflow");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Route a parsed command to the appropriate handler.
|
||||||
|
///
|
||||||
|
/// Display and MAC commands are handled locally (response sent immediately).
|
||||||
|
/// All other commands are forwarded to the radio task (response sent later).
|
||||||
|
/// Both paths feed the same `ResponseChannel`, so the one-outstanding-command
|
||||||
|
/// rule in PROTOCOL.md is required to keep solicited responses ordered.
|
||||||
|
pub async fn route_command(
|
||||||
|
cmd: Command,
|
||||||
|
commands: &CommandChannel,
|
||||||
|
responses: &ResponseChannel,
|
||||||
|
display_commands: &DisplayCommandChannel,
|
||||||
|
has_display: bool,
|
||||||
|
mac: [u8; 6],
|
||||||
|
) {
|
||||||
|
match cmd {
|
||||||
|
Command::DisplayOn => {
|
||||||
|
if has_display {
|
||||||
|
display_commands.send(DisplayCommand::On).await;
|
||||||
|
responses.send(Response::Ok).await;
|
||||||
|
} else {
|
||||||
|
responses.send(Response::Error(ErrorCode::NoDisplay)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::DisplayOff => {
|
||||||
|
if has_display {
|
||||||
|
display_commands.send(DisplayCommand::Off).await;
|
||||||
|
responses.send(Response::Ok).await;
|
||||||
|
} else {
|
||||||
|
responses.send(Response::Error(ErrorCode::NoDisplay)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::GetMac => {
|
||||||
|
responses.send(Response::MacAddress(mac)).await;
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
commands.send(other).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
//! UART interface with COBS-framed command/response protocol.
|
||||||
|
|
||||||
|
mod task;
|
||||||
|
pub use task::uart_task;
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
//! UART task: COBS-framed fixed-size LE command/response protocol.
|
||||||
|
//!
|
||||||
|
//! Reads COBS frames from UART, decodes them, parses commands, routes them
|
||||||
|
//! to the radio or display task, and sends COBS-framed responses back.
|
||||||
|
//! Used for boards with USB-UART bridge chips (e.g. CP2102 on some Heltec V3).
|
||||||
|
|
||||||
|
use defmt::warn;
|
||||||
|
use embassy_executor::task;
|
||||||
|
use embedded_io_async::Write;
|
||||||
|
|
||||||
|
use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel};
|
||||||
|
use crate::protocol_io::{self, FrameAccumulator, MAX_FRAME};
|
||||||
|
|
||||||
|
#[task]
|
||||||
|
pub async fn uart_task(
|
||||||
|
parts: crate::board::UartParts,
|
||||||
|
commands: &'static CommandChannel,
|
||||||
|
responses: &'static ResponseChannel,
|
||||||
|
display_commands: &'static DisplayCommandChannel,
|
||||||
|
has_display: bool,
|
||||||
|
mac: [u8; 6],
|
||||||
|
) {
|
||||||
|
let (mut rx, mut tx) = parts.driver.split();
|
||||||
|
|
||||||
|
let mut read_buf = [0u8; 64];
|
||||||
|
let mut write_buf = [0u8; MAX_FRAME];
|
||||||
|
let mut cobs_encode_buf = [0u8; MAX_FRAME];
|
||||||
|
let mut accumulator = FrameAccumulator::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
use embassy_futures::select::select;
|
||||||
|
use embassy_futures::select::Either;
|
||||||
|
|
||||||
|
match select(
|
||||||
|
embedded_io_async::Read::read(&mut rx, &mut read_buf),
|
||||||
|
responses.receive(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Either::First(result) => {
|
||||||
|
let n = match result {
|
||||||
|
Ok(0) => continue,
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(_) => {
|
||||||
|
accumulator.reset();
|
||||||
|
embassy_time::Timer::after_millis(100).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cmds = heapless::Vec::<_, 4>::new();
|
||||||
|
accumulator.feed(&read_buf[..n], |cmd| {
|
||||||
|
let _ = cmds.push(cmd);
|
||||||
|
});
|
||||||
|
for cmd in cmds {
|
||||||
|
protocol_io::route_command(
|
||||||
|
cmd, commands, responses, display_commands, has_display, mac,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Either::Second(response) => {
|
||||||
|
if let Some(frame) =
|
||||||
|
protocol_io::encode_response(response, &mut write_buf, &mut cobs_encode_buf)
|
||||||
|
{
|
||||||
|
if tx.write_all(frame).await.is_err() {
|
||||||
|
warn!("UART write failed, response dropped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user