WIP: functional checkpoint!
This commit is contained in:
21 files changed
+679
-147
No files matched your search
@@ -0,0 +1,86 @@
|
|||||||
|
# Add non-blocking `connect` variant
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
`connect(None, timeout)` in `src/connect.rs` has a three-step fallback:
|
||||||
|
|
||||||
|
1. `DONGLORA_MUX_TCP` env var -> TCP mux
|
||||||
|
2. Unix socket mux (if socket file exists)
|
||||||
|
3. Direct USB serial via `find_port().unwrap_or_else(wait_for_device)`
|
||||||
|
|
||||||
|
Step 3 calls `wait_for_device()` which polls `find_port()` every 500ms
|
||||||
|
**indefinitely** until a USB device appears. This makes `connect(None, ...)`
|
||||||
|
unsuitable for callers that need to retry with backoff or report disconnected
|
||||||
|
status (e.g. donglora-bridge's reconnect loop).
|
||||||
|
|
||||||
|
Today, donglora-bridge works around this by manually reimplementing the
|
||||||
|
fallback chain:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if let Ok(c) = connect_mux_auto(timeout) {
|
||||||
|
(c, "mux".to_string())
|
||||||
|
} else {
|
||||||
|
let port_path = find_port()
|
||||||
|
.ok_or_else(|| anyhow!("no mux or USB dongle found"))?;
|
||||||
|
let c = connect(Some(&port_path), timeout)?;
|
||||||
|
(c, shorten_path(&port_path))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This duplicates logic from `connect()` (steps 1-2 via `connect_mux_auto`, step
|
||||||
|
3 minus the blocking wait) and won't pick up any future changes to the
|
||||||
|
`connect()` fallback order.
|
||||||
|
|
||||||
|
## Proposed solution
|
||||||
|
|
||||||
|
Add a `try_connect` (or similar) that runs the same mux -> serial fallback as
|
||||||
|
`connect(None, timeout)` but replaces `wait_for_device()` with a single
|
||||||
|
`find_port()` call, returning an error immediately if no device is found:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Like [`connect`] but returns an error instead of blocking when no USB
|
||||||
|
/// device is present. Suitable for callers with their own retry logic.
|
||||||
|
pub fn try_connect(timeout: Duration) -> Result<Client<AnyTransport>> {
|
||||||
|
// Steps 1-2: identical to connect() — try TCP mux, then Unix socket mux
|
||||||
|
|
||||||
|
// Step 3: non-blocking USB scan
|
||||||
|
let port_path = discovery::find_port()
|
||||||
|
.ok_or_else(|| anyhow!("no DongLoRa device found (no mux, no USB device)"))?;
|
||||||
|
let transport = SerialTransport::open(&port_path, timeout)?;
|
||||||
|
Ok(Client::new(AnyTransport::Serial(transport)))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would let donglora-bridge collapse its connection code to:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let (mut client, device) = if let Some(port) = port {
|
||||||
|
let c = connect(Some(port), timeout)?;
|
||||||
|
(c, shorten_path(port))
|
||||||
|
} else {
|
||||||
|
let c = try_connect(timeout)?;
|
||||||
|
let device = match c.transport() {
|
||||||
|
AnyTransport::Mux(_) => "mux".to_string(),
|
||||||
|
AnyTransport::Serial(_) => find_port()
|
||||||
|
.map(|p| shorten_path(&p))
|
||||||
|
.unwrap_or_else(|| "serial".to_string()),
|
||||||
|
};
|
||||||
|
(c, device)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device name from transport
|
||||||
|
|
||||||
|
The bridge also needs to know *what* it connected to for display purposes. Two
|
||||||
|
options:
|
||||||
|
|
||||||
|
1. Match on `AnyTransport` variant after connecting (shown above) -- works but
|
||||||
|
requires a redundant `find_port()` call for the display name since
|
||||||
|
`SerialTransport` has private fields.
|
||||||
|
2. Have `try_connect` return the port path alongside the client, e.g.
|
||||||
|
`-> Result<(Client<AnyTransport>, ConnectionInfo)>` where `ConnectionInfo`
|
||||||
|
is an enum like `Mux { socket_path }` | `Serial { port_path }`. Cleaner
|
||||||
|
but a larger API change.
|
||||||
|
|
||||||
|
Either works. Option 2 would also benefit `connect()` callers who want to log
|
||||||
|
what they connected to.
|
||||||
@@ -4,3 +4,9 @@ rustflags = [
|
|||||||
"-C", "link-arg=-Wl,-Tdefmt.x",
|
"-C", "link-arg=-Wl,-Tdefmt.x",
|
||||||
"-C", "link-arg=-nostartfiles",
|
"-C", "link-arg=-nostartfiles",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[target.thumbv7em-none-eabihf]
|
||||||
|
rustflags = [
|
||||||
|
"-C", "link-arg=-Tlink.x",
|
||||||
|
"-C", "link-arg=-Tdefmt.x",
|
||||||
|
]
|
||||||
Generated
+13
@@ -111,6 +111,7 @@ checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bare-metal",
|
"bare-metal",
|
||||||
"bitfield 0.13.2",
|
"bitfield 0.13.2",
|
||||||
|
"critical-section",
|
||||||
"embedded-hal 0.2.7",
|
"embedded-hal 0.2.7",
|
||||||
"volatile-register",
|
"volatile-register",
|
||||||
]
|
]
|
||||||
@@ -301,6 +302,16 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "defmt-rtt"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93d5a25c99d89c40f5676bec8cefe0614f17f0f40e916f98e345dae941807f9e"
|
||||||
|
dependencies = [
|
||||||
|
"critical-section",
|
||||||
|
"defmt 1.0.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "delegate"
|
name = "delegate"
|
||||||
version = "0.13.5"
|
version = "0.13.5"
|
||||||
@@ -364,9 +375,11 @@ name = "donglora"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"cortex-m",
|
||||||
"cortex-m-rt",
|
"cortex-m-rt",
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
|
"defmt-rtt",
|
||||||
"display-interface",
|
"display-interface",
|
||||||
"embassy-embedded-hal 0.6.0",
|
"embassy-embedded-hal 0.6.0",
|
||||||
"embassy-executor",
|
"embassy-executor",
|
||||||
|
|||||||
+14
-1
@@ -62,7 +62,9 @@ esp-rtos = { version = "0.2", optional = true, features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
# nRF52840 (RAK 4631)
|
# nRF52840 (RAK 4631)
|
||||||
cortex-m-rt = { version = "0.7", optional = true }
|
cortex-m = { version = "0.7", optional = true, features = ["critical-section-single-core"] }
|
||||||
|
cortex-m-rt = { version = "0.7", optional = true, features = ["set-vtor"] }
|
||||||
|
defmt-rtt = { version = "1.0", optional = true }
|
||||||
embassy-nrf = { version = "0.10", optional = true, features = [
|
embassy-nrf = { version = "0.10", optional = true, features = [
|
||||||
"nrf52840",
|
"nrf52840",
|
||||||
"gpiote",
|
"gpiote",
|
||||||
@@ -89,7 +91,18 @@ heltec_v4 = [
|
|||||||
"dep:esp-rtos",
|
"dep:esp-rtos",
|
||||||
]
|
]
|
||||||
rak_wisblock_4631 = [
|
rak_wisblock_4631 = [
|
||||||
|
"dep:cortex-m",
|
||||||
"dep:cortex-m-rt",
|
"dep:cortex-m-rt",
|
||||||
|
"dep:defmt-rtt",
|
||||||
|
"dep:embassy-nrf",
|
||||||
|
"dep:panic-probe",
|
||||||
|
"embassy-executor/arch-cortex-m",
|
||||||
|
"embassy-executor/executor-thread",
|
||||||
|
]
|
||||||
|
wio_tracker_l1 = [
|
||||||
|
"dep:cortex-m",
|
||||||
|
"dep:cortex-m-rt",
|
||||||
|
"dep:defmt-rtt",
|
||||||
"dep:embassy-nrf",
|
"dep:embassy-nrf",
|
||||||
"dep:panic-probe",
|
"dep:panic-probe",
|
||||||
"embassy-executor/arch-cortex-m",
|
"embassy-executor/arch-cortex-m",
|
||||||
|
|||||||
+14
-2
@@ -50,8 +50,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut env = Environment::new();
|
let mut env = Environment::new();
|
||||||
let template =
|
let template = fs::read_to_string("src/board/mod.rs.j2").expect("missing src/board/mod.rs.j2");
|
||||||
fs::read_to_string("src/board/mod.rs.j2").expect("missing src/board/mod.rs.j2");
|
|
||||||
env.add_template("mod", &template)
|
env.add_template("mod", &template)
|
||||||
.expect("failed to parse board template");
|
.expect("failed to parse board template");
|
||||||
let rendered = env
|
let rendered = env
|
||||||
@@ -63,4 +62,17 @@ fn main() {
|
|||||||
|
|
||||||
println!("cargo:rerun-if-changed=src/board/mod.rs.j2");
|
println!("cargo:rerun-if-changed=src/board/mod.rs.j2");
|
||||||
println!("cargo:rerun-if-changed={}", board_dir);
|
println!("cargo:rerun-if-changed={}", board_dir);
|
||||||
|
|
||||||
|
// cortex-m-rt's link.x does `INCLUDE memory.x`. Copy our nRF layout into
|
||||||
|
// OUT_DIR (which cortex-m-rt already adds to the linker search path) so it's
|
||||||
|
// only visible to ARM builds. Keeping it out of the project root prevents
|
||||||
|
// the linker from shadowing esp-hal's generated memory.x on Xtensa builds.
|
||||||
|
let target = std::env::var("TARGET").unwrap_or_default();
|
||||||
|
if !target.starts_with("xtensa") {
|
||||||
|
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||||
|
fs::copy("ld/nrf52840-memory.x", format!("{out_dir}/memory.x"))
|
||||||
|
.expect("failed to copy ld/nrf52840-memory.x");
|
||||||
|
println!("cargo:rustc-link-search={out_dir}");
|
||||||
|
}
|
||||||
|
println!("cargo:rerun-if-changed=ld/nrf52840-memory.x");
|
||||||
}
|
}
|
||||||
+39
-4
@@ -6,11 +6,12 @@ set shell := ["bash", "-c"]
|
|||||||
heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3"
|
heltec_v3 := "heltec_v3 xtensa-esp32s3-none-elf esp32s3"
|
||||||
heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3"
|
heltec_v4 := "heltec_v4 xtensa-esp32s3-none-elf esp32s3"
|
||||||
rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA"
|
rak_wisblock_4631 := "rak_wisblock_4631 thumbv7em-none-eabihf nRF52840_xxAA"
|
||||||
|
wio_tracker_l1 := "wio_tracker_l1 thumbv7em-none-eabihf nRF52840_xxAA"
|
||||||
|
|
||||||
builds_dir := "builds"
|
builds_dir := "builds"
|
||||||
|
|
||||||
# All known boards
|
# All known boards
|
||||||
boards := "heltec_v3 heltec_v4 rak_wisblock_4631"
|
boards := "heltec_v3 heltec_v4 rak_wisblock_4631 wio_tracker_l1"
|
||||||
|
|
||||||
# Install all required tools and toolchains
|
# Install all required tools and toolchains
|
||||||
setup:
|
setup:
|
||||||
@@ -65,7 +66,7 @@ build board profile="release":
|
|||||||
@just _cargo {{board}} "build --{{profile}}"
|
@just _cargo {{board}} "build --{{profile}}"
|
||||||
@just _copy_firmware {{board}} {{profile}}
|
@just _copy_firmware {{board}} {{profile}}
|
||||||
|
|
||||||
# Build and flash a board (espflash for Xtensa, probe-rs for ARM)
|
# Build and flash a board (espflash for Xtensa, UF2 DFU for ARM)
|
||||||
flash board:
|
flash board:
|
||||||
@just _ensure_tools
|
@just _ensure_tools
|
||||||
@just build {{board}} release
|
@just build {{board}} release
|
||||||
@@ -80,9 +81,16 @@ flash board:
|
|||||||
echo "No port found, falling back to espflash auto-detection..." >&2; \
|
echo "No port found, falling back to espflash auto-detection..." >&2; \
|
||||||
espflash flash "{{builds_dir}}/donglora-{{board}}-release.elf"; \
|
espflash flash "{{builds_dir}}/donglora-{{board}}-release.elf"; \
|
||||||
fi ;; \
|
fi ;; \
|
||||||
*) probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-release.elf" ;; \
|
*) just _flash_uf2 {{board}} ;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Flash ARM board via debug probe (requires J-Link or similar)
|
||||||
|
flash-probe board:
|
||||||
|
@just _ensure_tools
|
||||||
|
@just build {{board}} release
|
||||||
|
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||||
|
probe-rs run --chip $chip "{{builds_dir}}/donglora-{{board}}-release.elf"
|
||||||
|
|
||||||
# Show binary size for a release build
|
# Show binary size for a release build
|
||||||
size board:
|
size board:
|
||||||
@just _cargo {{board}} "size --release"
|
@just _cargo {{board}} "size --release"
|
||||||
@@ -163,11 +171,38 @@ _copy_firmware board profile:
|
|||||||
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
@read -r feat target chip <<< "$(just _info {{board}})"; \
|
||||||
src="target/$target/{{profile}}/donglora"; \
|
src="target/$target/{{profile}}/donglora"; \
|
||||||
dst="{{builds_dir}}/donglora-{{board}}-{{profile}}"; \
|
dst="{{builds_dir}}/donglora-{{board}}-{{profile}}"; \
|
||||||
if [ -f "$src" ]; then cp "$src" "$dst.elf"; echo "→ $dst.elf"; fi
|
if [ -f "$src" ]; then \
|
||||||
|
cp "$src" "$dst.elf"; echo "→ $dst.elf"; \
|
||||||
|
case "$target" in \
|
||||||
|
thumbv7em-*) \
|
||||||
|
mise exec -- rust-objcopy -O ihex "$src" "$dst.hex"; \
|
||||||
|
mise exec -- cargo-hex-to-uf2 hex-to-uf2 --input-path "$dst.hex" --output-path "$dst.uf2" --family nrf52840; \
|
||||||
|
rm "$dst.hex"; \
|
||||||
|
echo "→ $dst.uf2"; \
|
||||||
|
;; \
|
||||||
|
esac; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy UF2 to a mounted UF2 bootloader drive
|
||||||
|
[private]
|
||||||
|
_flash_uf2 board:
|
||||||
|
@uf2="{{builds_dir}}/donglora-{{board}}-release.uf2"; \
|
||||||
|
if [ ! -f "$uf2" ]; then \
|
||||||
|
echo "error: $uf2 not found — run 'just build {{board}}' first" >&2; exit 1; \
|
||||||
|
fi; \
|
||||||
|
mnt="$(find /run/media -maxdepth 3 -name INFO_UF2.TXT -printf '%h\n' -quit 2>/dev/null)"; \
|
||||||
|
if [ -z "$mnt" ]; then \
|
||||||
|
echo "error: no UF2 drive found — double-tap reset to enter bootloader" >&2; exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo "Copying to $mnt ..."; \
|
||||||
|
cp "$uf2" "$mnt/"; \
|
||||||
|
sync; \
|
||||||
|
echo "Done — board will reboot automatically"
|
||||||
|
|
||||||
[private]
|
[private]
|
||||||
_info name:
|
_info name:
|
||||||
@if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \
|
@if [ "{{name}}" == "heltec_v3" ]; then echo "{{heltec_v3}}"; \
|
||||||
elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \
|
elif [ "{{name}}" == "heltec_v4" ]; then echo "{{heltec_v4}}"; \
|
||||||
elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \
|
elif [ "{{name}}" == "rak_wisblock_4631" ]; then echo "{{rak_wisblock_4631}}"; \
|
||||||
|
elif [ "{{name}}" == "wio_tracker_l1" ]; then echo "{{wio_tracker_l1}}"; \
|
||||||
else echo "Unknown board: {{name}}" >&2; exit 1; fi
|
else echo "Unknown board: {{name}}" >&2; exit 1; fi
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* nRF52840 memory layout — Adafruit UF2 bootloader
|
||||||
|
*
|
||||||
|
* 0x00000 – 0x01000 MBR (Master Boot Record)
|
||||||
|
* 0x01000 – 0xEA000 Application ← we live here
|
||||||
|
* 0xEA000 – 0xF4000 Reserved (app data)
|
||||||
|
* 0xF4000 – 0xFC000 UF2 Bootloader
|
||||||
|
* 0xFF000 – 0x100000 Bootloader settings
|
||||||
|
*
|
||||||
|
* RAM: first 8 bytes reserved for MBR interrupt forwarding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
MEMORY
|
||||||
|
{
|
||||||
|
FLASH : ORIGIN = 0x00001000, LENGTH = 0xE9000
|
||||||
|
RAM : ORIGIN = 0x20000008, LENGTH = 255K
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ rust = "1.93"
|
|||||||
just = "latest"
|
just = "latest"
|
||||||
|
|
||||||
"cargo:cargo-binutils" = "latest" # `cargo size`, `cargo objdump`, etc.
|
"cargo:cargo-binutils" = "latest" # `cargo size`, `cargo objdump`, etc.
|
||||||
|
"cargo:cargo-hex-to-uf2" = "latest" # ELF → UF2 via Intel HEX for nRF52840 DFU
|
||||||
"cargo:espup" = "0.16"
|
"cargo:espup" = "0.16"
|
||||||
"cargo:espflash" = "3" # 4.x has broken nix dependency on current stable Rust
|
"cargo:espflash" = "3" # 4.x has broken nix dependency on current stable Rust
|
||||||
"cargo:probe-rs-tools" = "0.31"
|
"cargo:probe-rs-tools" = "0.31"
|
||||||
@@ -37,8 +37,7 @@ impl Board {
|
|||||||
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.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13,
|
||||||
p.GPIO13,
|
|
||||||
);
|
);
|
||||||
let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19);
|
let usb = esp32s3::init_usb(p.USB0, p.GPIO20, p.GPIO19);
|
||||||
let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
let i2c = esp32s3::init_display_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ impl Board {
|
|||||||
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.SPI2, p.DMA_CH0, p.GPIO9, p.GPIO10, p.GPIO11, p.GPIO8, p.GPIO12, p.GPIO14, p.GPIO13,
|
||||||
p.GPIO13,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Note: switches internal USB PHY from Serial-JTAG to OTG.
|
// Note: switches internal USB PHY from Serial-JTAG to OTG.
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ impl super::traits::LoRaBoard for Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
|
|
||||||
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
@@ -108,7 +107,8 @@ impl Board {
|
|||||||
let dio1 = Input::new(p.P1_15, Pull::Down);
|
let dio1 = Input::new(p.P1_15, Pull::Down);
|
||||||
let busy = Input::new(p.P1_14, Pull::Down);
|
let busy = Input::new(p.P1_14, Pull::Down);
|
||||||
|
|
||||||
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None).expect("SX1262 interface init");
|
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, None, None)
|
||||||
|
.expect("SX1262 interface init");
|
||||||
|
|
||||||
let sx_config = sx126x::Config {
|
let sx_config = sx126x::Config {
|
||||||
chip: Sx1262,
|
chip: Sx1262,
|
||||||
@@ -125,7 +125,9 @@ impl Board {
|
|||||||
// ── USB ──────────────────────────────────────────────────
|
// ── USB ──────────────────────────────────────────────────
|
||||||
static VBUS: StaticCell<embassy_nrf::usb::vbus_detect::SoftwareVbusDetect> =
|
static VBUS: StaticCell<embassy_nrf::usb::vbus_detect::SoftwareVbusDetect> =
|
||||||
StaticCell::new();
|
StaticCell::new();
|
||||||
let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(true, false));
|
let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(
|
||||||
|
true, false,
|
||||||
|
));
|
||||||
let usb = UsbParts {
|
let usb = UsbParts {
|
||||||
driver: Driver::new(p.USBD, Irqs, vbus),
|
driver: Driver::new(p.USBD, Irqs, vbus),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
|
||||||
|
use embassy_nrf::gpio::{Input, Level, Output, OutputDrive, Pull};
|
||||||
|
use embassy_nrf::spim::{self, Spim};
|
||||||
|
use embassy_nrf::twim::{self, Twim};
|
||||||
|
use embassy_nrf::usb::Driver;
|
||||||
|
use embassy_nrf::{bind_interrupts, Peripherals};
|
||||||
|
use embassy_sync::blocking_mutex::raw::NoopRawMutex;
|
||||||
|
use embassy_time::Delay;
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
use lora_phy::iv::GenericSx126xInterfaceVariant;
|
||||||
|
use lora_phy::sx126x::{self, Sx1262, Sx126x};
|
||||||
|
|
||||||
|
use super::traits::LoRaBoard;
|
||||||
|
|
||||||
|
bind_interrupts!(struct Irqs {
|
||||||
|
USBD => embassy_nrf::usb::InterruptHandler<embassy_nrf::peripherals::USBD>;
|
||||||
|
SPIM3 => embassy_nrf::spim::InterruptHandler<embassy_nrf::peripherals::SPI3>;
|
||||||
|
TWISPI0 => embassy_nrf::twim::InterruptHandler<embassy_nrf::peripherals::TWISPI0>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Concrete peripheral types ────────────────────────────────────────
|
||||||
|
|
||||||
|
type SpiBus = Spim<'static>;
|
||||||
|
type Nss = Output<'static>;
|
||||||
|
type Iv = GenericSx126xInterfaceVariant<Output<'static>, Input<'static>>;
|
||||||
|
type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, SpiBus, Nss>;
|
||||||
|
/// SX126x driver (pre-LoRa construction — LoRa::new is async).
|
||||||
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
|
|
||||||
|
/// USB driver for CDC-ACM.
|
||||||
|
pub type UsbDriver = Driver<'static, &'static embassy_nrf::usb::vbus_detect::SoftwareVbusDetect>;
|
||||||
|
|
||||||
|
/// I2C bus for SH1106 OLED.
|
||||||
|
pub type DisplayI2c = Twim<'static>;
|
||||||
|
|
||||||
|
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct RadioParts {
|
||||||
|
pub driver: RadioDriver,
|
||||||
|
pub delay: Delay,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UsbParts {
|
||||||
|
pub driver: UsbDriver,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DisplayParts {
|
||||||
|
pub i2c: DisplayI2c,
|
||||||
|
pub mac: [u8; 6],
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Board init ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct Board {
|
||||||
|
p: Peripherals,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::traits::LoRaBoard for Board {
|
||||||
|
const NAME: &'static str = "Wio Tracker L1";
|
||||||
|
const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||||
|
|
||||||
|
fn init() -> Self {
|
||||||
|
let mut config = embassy_nrf::config::Config::default();
|
||||||
|
config.hfclk_source = embassy_nrf::config::HfclkSource::ExternalXtal;
|
||||||
|
config.lfclk_source = embassy_nrf::config::LfclkSource::ExternalXtal;
|
||||||
|
let p = embassy_nrf::init(config);
|
||||||
|
Self { p }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mac_address() -> [u8; 6] {
|
||||||
|
// SAFETY: 0x10000000 is the nRF52840 FICR (Factory Information Configuration
|
||||||
|
// Registers) base address. Offsets 0x0A4 and 0x0A8 are the DEVICEADDR[0] and
|
||||||
|
// DEVICEADDR[1] registers containing the factory-programmed device address.
|
||||||
|
// read_volatile is correct for memory-mapped hardware registers.
|
||||||
|
unsafe {
|
||||||
|
let ficr = 0x10000000 as *const u32;
|
||||||
|
let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4));
|
||||||
|
let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8));
|
||||||
|
[
|
||||||
|
addr0 as u8,
|
||||||
|
(addr0 >> 8) as u8,
|
||||||
|
(addr0 >> 16) as u8,
|
||||||
|
(addr0 >> 24) as u8,
|
||||||
|
addr1 as u8,
|
||||||
|
(addr1 >> 8) as u8,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
pub fn into_parts(self) -> (RadioParts, UsbParts, Option<DisplayParts>) {
|
||||||
|
let p = self.p;
|
||||||
|
|
||||||
|
// ── SPI bus for SX1262 ───────────────────────────────────
|
||||||
|
let mut spi_cfg = spim::Config::default();
|
||||||
|
spi_cfg.frequency = spim::Frequency::M1;
|
||||||
|
let spi = Spim::new(p.SPI3, Irqs, p.P0_30, p.P0_03, p.P0_28, spi_cfg);
|
||||||
|
|
||||||
|
static SPI_BUS: StaticCell<embassy_sync::mutex::Mutex<NoopRawMutex, SpiBus>> =
|
||||||
|
StaticCell::new();
|
||||||
|
let spi_bus = SPI_BUS.init(embassy_sync::mutex::Mutex::new(spi));
|
||||||
|
|
||||||
|
let nss = Output::new(p.P1_14, Level::High, OutputDrive::Standard);
|
||||||
|
let spi_device = SpiDevice::new(spi_bus, nss);
|
||||||
|
|
||||||
|
// ── SX1262 control pins ──────────────────────────────────
|
||||||
|
let reset = Output::new(p.P1_07, Level::High, OutputDrive::Standard);
|
||||||
|
let dio1 = Input::new(p.P0_07, Pull::Down);
|
||||||
|
let busy = Input::new(p.P1_10, Pull::Down);
|
||||||
|
|
||||||
|
// RF switch: RXEN on P1.08 (GPIO, toggled by lora_phy), TX via DIO2 (SX1262 internal).
|
||||||
|
let rx_enable = Output::new(p.P1_08, Level::Low, OutputDrive::Standard);
|
||||||
|
let iv = GenericSx126xInterfaceVariant::new(reset, dio1, busy, Some(rx_enable), None)
|
||||||
|
.expect("SX1262 interface init");
|
||||||
|
|
||||||
|
let sx_config = sx126x::Config {
|
||||||
|
chip: Sx1262,
|
||||||
|
tcxo_ctrl: Some(sx126x::TcxoCtrlVoltage::Ctrl1V8),
|
||||||
|
use_dcdc: true,
|
||||||
|
rx_boost: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let radio = RadioParts {
|
||||||
|
driver: Sx126x::new(spi_device, iv, sx_config),
|
||||||
|
delay: Delay,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── USB ──────────────────────────────────────────────────
|
||||||
|
static VBUS: StaticCell<embassy_nrf::usb::vbus_detect::SoftwareVbusDetect> =
|
||||||
|
StaticCell::new();
|
||||||
|
let vbus = VBUS.init(embassy_nrf::usb::vbus_detect::SoftwareVbusDetect::new(
|
||||||
|
true, true,
|
||||||
|
));
|
||||||
|
let usb = UsbParts {
|
||||||
|
driver: Driver::new(p.USBD, Irqs, vbus),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── BAT_CTL: P0.04, active HIGH — enables power to OLED and battery sense
|
||||||
|
let bat_ctl = Output::new(p.P0_04, Level::High, OutputDrive::Standard);
|
||||||
|
core::mem::forget(bat_ctl);
|
||||||
|
|
||||||
|
// ── Display (SH1106 OLED on I2C) ─────────────────────────
|
||||||
|
static TWIM_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||||
|
let twim_buf = TWIM_BUF.init([0u8; 256]);
|
||||||
|
let i2c_cfg = twim::Config::default();
|
||||||
|
let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_06, p.P0_05, i2c_cfg, twim_buf);
|
||||||
|
let mac = Self::mac_address();
|
||||||
|
let display = Some(DisplayParts { i2c, mac });
|
||||||
|
|
||||||
|
(radio, usb, display)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! OLED display driver with radio status dashboard.
|
//! OLED display driver with radio status dashboard.
|
||||||
|
|
||||||
mod render;
|
mod render;
|
||||||
|
pub mod sh1106;
|
||||||
mod task;
|
mod task;
|
||||||
pub use task::display_task;
|
pub use task::display_task;
|
||||||
@@ -69,7 +69,10 @@ pub fn dashboard(
|
|||||||
match status.state {
|
match status.state {
|
||||||
RadioState::Receiving => {
|
RadioState::Receiving => {
|
||||||
// "RX" inverted on row 0
|
// "RX" inverted on row 0
|
||||||
Rectangle::new(Point::new(0, 0), Size::new(MODE_BOX_W as u32, FONT_H as u32))
|
Rectangle::new(
|
||||||
|
Point::new(0, 0),
|
||||||
|
Size::new(MODE_BOX_W as u32, FONT_H as u32),
|
||||||
|
)
|
||||||
.into_styled(fill)
|
.into_styled(fill)
|
||||||
.draw(target)
|
.draw(target)
|
||||||
.ok();
|
.ok();
|
||||||
@@ -217,10 +220,7 @@ pub fn dashboard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render the splash/waiting screen (shown when idle or no config).
|
/// Render the splash/waiting screen (shown when idle or no config).
|
||||||
pub fn splash(
|
pub fn splash(target: &mut impl DrawTarget<Color = BinaryColor>, board: &BoardInfo) {
|
||||||
target: &mut impl DrawTarget<Color = BinaryColor>,
|
|
||||||
board: &BoardInfo,
|
|
||||||
) {
|
|
||||||
let bb = target.bounding_box();
|
let bb = target.bounding_box();
|
||||||
let w = bb.size.width as i32;
|
let w = bb.size.width as i32;
|
||||||
|
|
||||||
@@ -324,11 +324,8 @@ fn rssi_sparkline(
|
|||||||
i + committed.saturating_sub(effective_bars)
|
i + committed.saturating_sub(effective_bars)
|
||||||
} else {
|
} else {
|
||||||
let start = count - RSSI_HISTORY_LEN;
|
let start = count - RSSI_HISTORY_LEN;
|
||||||
let skip = RSSI_HISTORY_LEN.saturating_sub(if live {
|
let skip =
|
||||||
visible_bars - 1
|
RSSI_HISTORY_LEN.saturating_sub(if live { visible_bars - 1 } else { visible_bars });
|
||||||
} else {
|
|
||||||
visible_bars
|
|
||||||
});
|
|
||||||
(start + skip + i) % RSSI_HISTORY_LEN
|
(start + skip + i) % RSSI_HISTORY_LEN
|
||||||
};
|
};
|
||||||
let is_tx = tx_history[idx];
|
let is_tx = tx_history[idx];
|
||||||
@@ -361,7 +358,11 @@ fn bar_height(rssi: i16, is_tx: bool, spark_h: i32) -> Option<i32> {
|
|||||||
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
|
let clamped = rssi.clamp(RSSI_MIN, RSSI_MAX);
|
||||||
((clamped - RSSI_MIN) as i32 * spark_h) / (RSSI_MAX - RSSI_MIN) as i32
|
((clamped - RSSI_MIN) as i32 * spark_h) / (RSSI_MAX - RSSI_MIN) as i32
|
||||||
};
|
};
|
||||||
if h == 0 { None } else { Some(h) }
|
if h == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a single sparkline bar (solid for RX, dotted for TX).
|
/// Draw a single sparkline bar (solid for RX, dotted for TX).
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
//! Minimal async SH1106 I2C OLED driver (128x64, page addressing).
|
||||||
|
//!
|
||||||
|
//! The SH1106 is very similar to the SSD1306 but only supports page-mode
|
||||||
|
//! addressing (no horizontal/vertical auto-advance). This driver sends
|
||||||
|
//! framebuffer data page-by-page with explicit page/column commands.
|
||||||
|
|
||||||
|
use embedded_graphics::draw_target::DrawTarget;
|
||||||
|
use embedded_graphics::geometry::{OriginDimensions, Size};
|
||||||
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
|
use embedded_graphics::Pixel;
|
||||||
|
use embedded_hal_async::i2c::I2c;
|
||||||
|
|
||||||
|
const WIDTH: usize = 128;
|
||||||
|
const HEIGHT: usize = 64;
|
||||||
|
const PAGES: usize = HEIGHT / 8;
|
||||||
|
const BUF_SIZE: usize = WIDTH * PAGES; // 1024
|
||||||
|
|
||||||
|
/// SH1106 has a 132-column RAM; visible 128 columns are centered with a
|
||||||
|
/// 2-column offset on each side.
|
||||||
|
const COL_OFFSET: u8 = 2;
|
||||||
|
|
||||||
|
pub struct Sh1106<I> {
|
||||||
|
i2c: I,
|
||||||
|
addr: u8,
|
||||||
|
buffer: [u8; BUF_SIZE],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: I2c> Sh1106<I> {
|
||||||
|
pub fn new(i2c: I, addr: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
i2c,
|
||||||
|
addr,
|
||||||
|
buffer: [0u8; BUF_SIZE],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a sequence of commands (each prefixed with the I2C command byte 0x00).
|
||||||
|
async fn cmd(&mut self, commands: &[u8]) -> Result<(), I::Error> {
|
||||||
|
for &c in commands {
|
||||||
|
self.i2c.write(self.addr, &[0x00, c]).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the display with sensible defaults.
|
||||||
|
pub async fn init(&mut self) -> Result<(), I::Error> {
|
||||||
|
self.cmd(&[
|
||||||
|
0xAE, // display off
|
||||||
|
0xD5, 0x80, // clock div: default
|
||||||
|
0xA8, 0x3F, // multiplex ratio: 64
|
||||||
|
0xD3, 0x00, // display offset: 0
|
||||||
|
0x40, // start line: 0
|
||||||
|
0x8D, 0x14, // charge pump: enable
|
||||||
|
0xA1, // segment remap: column 127 mapped to SEG0
|
||||||
|
0xC8, // COM scan direction: remapped
|
||||||
|
0xDA, 0x12, // COM pin config: alternative, no L/R remap
|
||||||
|
0x81, 0xCF, // contrast: 207
|
||||||
|
0xD9, 0xF1, // pre-charge: phase1=1, phase2=15
|
||||||
|
0xDB, 0x40, // VCOMH deselect: ~0.77×Vcc
|
||||||
|
0xA4, // entire display ON (follow RAM)
|
||||||
|
0xA6, // normal display (not inverted)
|
||||||
|
0xAF, // display on
|
||||||
|
])
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush the entire framebuffer to the display, page by page.
|
||||||
|
pub async fn flush(&mut self) -> Result<(), I::Error> {
|
||||||
|
// Each I2C data write is prefixed with 0x40 (data continuation byte).
|
||||||
|
let mut page_buf = [0u8; 1 + WIDTH]; // 0x40 + 128 data bytes
|
||||||
|
page_buf[0] = 0x40;
|
||||||
|
|
||||||
|
for page in 0..PAGES {
|
||||||
|
// Set page address and column start (with SH1106 2-column offset).
|
||||||
|
self.cmd(&[
|
||||||
|
0xB0 | page as u8, // page address
|
||||||
|
0x00 | (COL_OFFSET & 0x0F), // column low nibble
|
||||||
|
0x10 | (COL_OFFSET >> 4), // column high nibble
|
||||||
|
])
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let start = page * WIDTH;
|
||||||
|
page_buf[1..].copy_from_slice(&self.buffer[start..start + WIDTH]);
|
||||||
|
self.i2c.write(self.addr, &page_buf).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set display brightness (contrast).
|
||||||
|
pub async fn set_brightness(&mut self, value: u8) -> Result<(), I::Error> {
|
||||||
|
self.cmd(&[0x81, value]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the framebuffer (call flush() to update the display).
|
||||||
|
pub fn clear_buffer(&mut self) {
|
||||||
|
self.buffer.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: I2c> DrawTarget for Sh1106<I> {
|
||||||
|
type Color = BinaryColor;
|
||||||
|
type Error = core::convert::Infallible;
|
||||||
|
|
||||||
|
fn draw_iter<T>(&mut self, pixels: T) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = Pixel<Self::Color>>,
|
||||||
|
{
|
||||||
|
for Pixel(pos, color) in pixels {
|
||||||
|
let x = pos.x;
|
||||||
|
let y = pos.y;
|
||||||
|
if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
|
||||||
|
let x = x as usize;
|
||||||
|
let y = y as usize;
|
||||||
|
let idx = (y / 8) * WIDTH + x;
|
||||||
|
let bit = (y % 8) as u8;
|
||||||
|
if color.is_on() {
|
||||||
|
self.buffer[idx] |= 1 << bit;
|
||||||
|
} else {
|
||||||
|
self.buffer[idx] &= !(1 << bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
||||||
|
self.buffer.fill(if color.is_on() { 0xFF } else { 0x00 });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: I2c> OriginDimensions for Sh1106<I> {
|
||||||
|
fn size(&self) -> Size {
|
||||||
|
Size::new(WIDTH as u32, HEIGHT as u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
use embassy_executor::task;
|
use embassy_executor::task;
|
||||||
use embassy_futures::select::{select3, Either3};
|
use embassy_futures::select::{select3, Either3};
|
||||||
use embassy_time::Timer;
|
use embassy_time::Timer;
|
||||||
use ssd1306::mode::DisplayConfigAsync;
|
use embedded_graphics::draw_target::DrawTarget;
|
||||||
use ssd1306::prelude::DisplayRotation;
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
use ssd1306::size::DisplaySize128x64;
|
|
||||||
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
|
||||||
|
|
||||||
use crate::board::DisplayParts;
|
use crate::board::DisplayParts;
|
||||||
use crate::channel::{
|
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch};
|
||||||
DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::board::{Board, LoRaBoard};
|
use crate::board::{Board, LoRaBoard};
|
||||||
|
|
||||||
@@ -80,6 +76,28 @@ impl DisplayState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the appropriate screen for the current state into the display buffer.
|
||||||
|
fn render_current(
|
||||||
|
display: &mut impl DrawTarget<Color = BinaryColor>,
|
||||||
|
state: &DisplayState,
|
||||||
|
board: &render::BoardInfo<'_>,
|
||||||
|
) {
|
||||||
|
if state.is_active() {
|
||||||
|
render::dashboard(
|
||||||
|
display,
|
||||||
|
&state.last_status,
|
||||||
|
&state.rssi_history,
|
||||||
|
&state.tx_history,
|
||||||
|
state.rssi_count,
|
||||||
|
state.current_slot_rssi,
|
||||||
|
state.current_slot_tx,
|
||||||
|
board,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
render::splash(display, board);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[task]
|
#[task]
|
||||||
pub async fn display_task(
|
pub async fn display_task(
|
||||||
parts: DisplayParts,
|
parts: DisplayParts,
|
||||||
@@ -103,14 +121,43 @@ pub async fn display_task(
|
|||||||
mac: &mac_str,
|
mac: &mac_str,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature = "wio_tracker_l1")] {
|
||||||
|
let mut display = super::sh1106::Sh1106::new(parts.i2c, 0x3D);
|
||||||
|
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
|
||||||
|
if display.init().await.is_err() {
|
||||||
|
defmt::error!("SH1106 init failed — retrying");
|
||||||
|
Timer::after_millis(500).await;
|
||||||
|
if display.init().await.is_err() {
|
||||||
|
defmt::error!("SH1106 init failed twice, giving up");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if display.set_brightness(0xFF).await.is_err() {
|
||||||
|
defmt::warn!("display brightness set failed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
use ssd1306::mode::DisplayConfigAsync;
|
||||||
|
use ssd1306::prelude::DisplayRotation;
|
||||||
|
use ssd1306::size::DisplaySize128x64;
|
||||||
|
use ssd1306::{I2CDisplayInterface, Ssd1306Async};
|
||||||
|
|
||||||
let interface = I2CDisplayInterface::new(parts.i2c);
|
let interface = I2CDisplayInterface::new(parts.i2c);
|
||||||
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||||
.into_buffered_graphics_mode();
|
.into_buffered_graphics_mode();
|
||||||
|
|
||||||
|
Timer::after_millis(100).await;
|
||||||
|
|
||||||
if display.init().await.is_err() {
|
if display.init().await.is_err() {
|
||||||
defmt::error!("SSD1306 init failed");
|
defmt::error!("SSD1306 init failed — retrying");
|
||||||
|
Timer::after_millis(500).await;
|
||||||
|
if display.init().await.is_err() {
|
||||||
|
defmt::error!("SSD1306 init failed twice, giving up");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if display
|
if display
|
||||||
.set_brightness(ssd1306::prelude::Brightness::BRIGHTEST)
|
.set_brightness(ssd1306::prelude::Brightness::BRIGHTEST)
|
||||||
.await
|
.await
|
||||||
@@ -118,6 +165,8 @@ pub async fn display_task(
|
|||||||
{
|
{
|
||||||
defmt::warn!("display brightness set failed");
|
defmt::warn!("display brightness set failed");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut state = DisplayState::new();
|
let mut state = DisplayState::new();
|
||||||
let Some(mut receiver) = status.receiver() else {
|
let Some(mut receiver) = status.receiver() else {
|
||||||
@@ -126,7 +175,8 @@ pub async fn display_task(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Show splash/waiting screen
|
// Show splash/waiting screen
|
||||||
render_splash(&mut display, &board_info).await;
|
render::splash(&mut display, &board_info);
|
||||||
|
let _ = display.flush().await;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match select3(
|
match select3(
|
||||||
@@ -151,7 +201,8 @@ pub async fn display_task(
|
|||||||
state.last_status = radio_status;
|
state.last_status = radio_status;
|
||||||
|
|
||||||
if state.display_on {
|
if state.display_on {
|
||||||
render_current(&mut display, &state, &board_info).await;
|
render_current(&mut display, &state, &board_info);
|
||||||
|
let _ = display.flush().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Either3::Second(cmd) => match cmd {
|
Either3::Second(cmd) => match cmd {
|
||||||
@@ -167,73 +218,23 @@ pub async fn display_task(
|
|||||||
if let Some(s) = receiver.try_get() {
|
if let Some(s) = receiver.try_get() {
|
||||||
state.last_status = s;
|
state.last_status = s;
|
||||||
}
|
}
|
||||||
render_current(&mut display, &state, &board_info).await;
|
render_current(&mut display, &state, &board_info);
|
||||||
|
let _ = display.flush().await;
|
||||||
}
|
}
|
||||||
DisplayCommand::Reset => {
|
DisplayCommand::Reset => {
|
||||||
state = DisplayState::new();
|
state = DisplayState::new();
|
||||||
render_splash(&mut display, &board_info).await;
|
render::splash(&mut display, &board_info);
|
||||||
|
let _ = display.flush().await;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Either3::Third(()) => {
|
Either3::Third(()) => {
|
||||||
// Timer tick: advance sparkline slot
|
// Timer tick: advance sparkline slot
|
||||||
state.advance_slot();
|
state.advance_slot();
|
||||||
if state.display_on && state.is_active() {
|
if state.display_on && state.is_active() {
|
||||||
render_dashboard(&mut display, &state, &board_info).await;
|
render_current(&mut display, &state, &board_info);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The concrete display type is verbose but avoids making this generic
|
|
||||||
// over both DrawTarget and the SSD1306-specific flush method.
|
|
||||||
type Display<I> = ssd1306::Ssd1306Async<
|
|
||||||
I,
|
|
||||||
DisplaySize128x64,
|
|
||||||
ssd1306::mode::BufferedGraphicsModeAsync<DisplaySize128x64>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Render the appropriate screen (active dashboard or splash) and flush.
|
|
||||||
async fn render_current<I>(
|
|
||||||
display: &mut Display<I>,
|
|
||||||
state: &DisplayState,
|
|
||||||
board: &render::BoardInfo<'_>,
|
|
||||||
) where
|
|
||||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
|
||||||
{
|
|
||||||
if state.is_active() {
|
|
||||||
render_dashboard(display, state, board).await;
|
|
||||||
} else {
|
|
||||||
render_splash(display, board).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the active dashboard and flush.
|
|
||||||
async fn render_dashboard<I>(
|
|
||||||
display: &mut Display<I>,
|
|
||||||
state: &DisplayState,
|
|
||||||
board: &render::BoardInfo<'_>,
|
|
||||||
) where
|
|
||||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
|
||||||
{
|
|
||||||
render::dashboard(
|
|
||||||
display,
|
|
||||||
&state.last_status,
|
|
||||||
&state.rssi_history,
|
|
||||||
&state.tx_history,
|
|
||||||
state.rssi_count,
|
|
||||||
state.current_slot_rssi,
|
|
||||||
state.current_slot_tx,
|
|
||||||
board,
|
|
||||||
);
|
|
||||||
let _ = display.flush().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render the splash/waiting screen and flush.
|
|
||||||
async fn render_splash<I>(display: &mut Display<I>, board: &render::BoardInfo<'_>)
|
|
||||||
where
|
|
||||||
I: display_interface::AsyncWriteOnlyDataCommand,
|
|
||||||
{
|
|
||||||
render::splash(display, board);
|
|
||||||
let _ = display.flush().await;
|
let _ = display.flush().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,8 @@ use crate::channel::{CommandChannel, DisplayCommandChannel, ResponseChannel, Sta
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(feature = "rak_wisblock_4631")] {
|
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] {
|
||||||
|
use defmt_rtt as _;
|
||||||
use panic_probe as _;
|
use panic_probe as _;
|
||||||
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] {
|
} else if #[cfg(any(feature = "heltec_v3", feature = "heltec_v4"))] {
|
||||||
use esp_backtrace as _;
|
use esp_backtrace as _;
|
||||||
@@ -56,7 +57,7 @@ static DISPLAY_COMMANDS: DisplayCommandChannel = DisplayCommandChannel::new();
|
|||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
cfg_if::cfg_if! {
|
cfg_if::cfg_if! {
|
||||||
if #[cfg(feature = "rak_wisblock_4631")] {
|
if #[cfg(any(feature = "rak_wisblock_4631", feature = "wio_tracker_l1"))] {
|
||||||
#[embassy_executor::main]
|
#[embassy_executor::main]
|
||||||
async fn main(spawner: Spawner) {
|
async fn main(spawner: Spawner) {
|
||||||
run(spawner).await;
|
run(spawner).await;
|
||||||
|
|||||||
+61
-15
@@ -17,7 +17,6 @@ pub const TX_POWER_MAX: i8 = i8::MIN; // -128 on the wire
|
|||||||
/// Sentinel value for `preamble_len`: use the firmware default (16 symbols).
|
/// Sentinel value for `preamble_len`: use the firmware default (16 symbols).
|
||||||
pub const PREAMBLE_DEFAULT: u16 = 0;
|
pub const PREAMBLE_DEFAULT: u16 = 0;
|
||||||
|
|
||||||
|
|
||||||
/// LoRa signal bandwidth.
|
/// LoRa signal bandwidth.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[cfg_attr(not(test), derive(defmt::Format))]
|
#[cfg_attr(not(test), derive(defmt::Format))]
|
||||||
@@ -188,7 +187,10 @@ impl Command {
|
|||||||
let (config, pos) = if rest[0] == 0 {
|
let (config, pos) = if rest[0] == 0 {
|
||||||
(None, 1)
|
(None, 1)
|
||||||
} else if rest[0] == 1 && rest.len() > RADIO_CONFIG_SIZE {
|
} else if rest[0] == 1 && rest.len() > RADIO_CONFIG_SIZE {
|
||||||
(Some(RadioConfig::from_bytes(&rest[1..])?), 1 + RADIO_CONFIG_SIZE)
|
(
|
||||||
|
Some(RadioConfig::from_bytes(&rest[1..])?),
|
||||||
|
1 + RADIO_CONFIG_SIZE,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@@ -370,7 +372,10 @@ mod tests {
|
|||||||
let power_range = (-9, 22);
|
let power_range = (-9, 22);
|
||||||
let base = make_config();
|
let base = make_config();
|
||||||
|
|
||||||
let mut cfg = RadioConfig { freq_hz: 150_000_000, ..base };
|
let mut cfg = RadioConfig {
|
||||||
|
freq_hz: 150_000_000,
|
||||||
|
..base
|
||||||
|
};
|
||||||
assert!(cfg.validate(power_range).is_ok());
|
assert!(cfg.validate(power_range).is_ok());
|
||||||
|
|
||||||
cfg.freq_hz = 960_000_000;
|
cfg.freq_hz = 960_000_000;
|
||||||
@@ -392,7 +397,9 @@ mod tests {
|
|||||||
assert!(RadioConfig { sf, ..base }.validate(power_range).is_ok());
|
assert!(RadioConfig { sf, ..base }.validate(power_range).is_ok());
|
||||||
}
|
}
|
||||||
assert!(RadioConfig { sf: 4, ..base }.validate(power_range).is_err());
|
assert!(RadioConfig { sf: 4, ..base }.validate(power_range).is_err());
|
||||||
assert!(RadioConfig { sf: 13, ..base }.validate(power_range).is_err());
|
assert!(RadioConfig { sf: 13, ..base }
|
||||||
|
.validate(power_range)
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -409,22 +416,34 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_tx_power_max_sentinel() {
|
fn validate_tx_power_max_sentinel() {
|
||||||
let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
tx_power_dbm: TX_POWER_MAX,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert!(cfg.validate((-9, 22)).is_ok());
|
assert!(cfg.validate((-9, 22)).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_tx_power_out_of_range() {
|
fn validate_tx_power_out_of_range() {
|
||||||
let cfg = RadioConfig { tx_power_dbm: 23, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
tx_power_dbm: 23,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert!(cfg.validate((-9, 22)).is_err());
|
assert!(cfg.validate((-9, 22)).is_err());
|
||||||
|
|
||||||
let cfg = RadioConfig { tx_power_dbm: -10, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
tx_power_dbm: -10,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert!(cfg.validate((-9, 22)).is_err());
|
assert!(cfg.validate((-9, 22)).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn validate_preamble_default_sentinel() {
|
fn validate_preamble_default_sentinel() {
|
||||||
let cfg = RadioConfig { preamble_len: PREAMBLE_DEFAULT, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
preamble_len: PREAMBLE_DEFAULT,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert!(cfg.validate((-9, 22)).is_ok());
|
assert!(cfg.validate((-9, 22)).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,33 +452,55 @@ mod tests {
|
|||||||
let power_range = (-9, 22);
|
let power_range = (-9, 22);
|
||||||
let base = make_config();
|
let base = make_config();
|
||||||
|
|
||||||
assert!(RadioConfig { preamble_len: 6, ..base }.validate(power_range).is_ok());
|
assert!(RadioConfig {
|
||||||
assert!(RadioConfig { preamble_len: 5, ..base }.validate(power_range).is_err());
|
preamble_len: 6,
|
||||||
|
..base
|
||||||
|
}
|
||||||
|
.validate(power_range)
|
||||||
|
.is_ok());
|
||||||
|
assert!(RadioConfig {
|
||||||
|
preamble_len: 5,
|
||||||
|
..base
|
||||||
|
}
|
||||||
|
.validate(power_range)
|
||||||
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── RadioConfig::resolve ──────────────────────────────────────
|
// ── RadioConfig::resolve ──────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_power_max_sentinel() {
|
fn resolve_power_max_sentinel() {
|
||||||
let cfg = RadioConfig { tx_power_dbm: TX_POWER_MAX, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
tx_power_dbm: TX_POWER_MAX,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert_eq!(cfg.resolve((-9, 22)).tx_power_dbm, 22);
|
assert_eq!(cfg.resolve((-9, 22)).tx_power_dbm, 22);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_power_explicit_unchanged() {
|
fn resolve_power_explicit_unchanged() {
|
||||||
let cfg = RadioConfig { tx_power_dbm: 10, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
tx_power_dbm: 10,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert_eq!(cfg.resolve((-9, 22)).tx_power_dbm, 10);
|
assert_eq!(cfg.resolve((-9, 22)).tx_power_dbm, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_preamble_default() {
|
fn resolve_preamble_default() {
|
||||||
let cfg = RadioConfig { preamble_len: PREAMBLE_DEFAULT, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
preamble_len: PREAMBLE_DEFAULT,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert_eq!(cfg.resolve((-9, 22)).preamble_len, 16);
|
assert_eq!(cfg.resolve((-9, 22)).preamble_len, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_preamble_explicit_unchanged() {
|
fn resolve_preamble_explicit_unchanged() {
|
||||||
let cfg = RadioConfig { preamble_len: 32, ..make_config() };
|
let cfg = RadioConfig {
|
||||||
|
preamble_len: 32,
|
||||||
|
..make_config()
|
||||||
|
};
|
||||||
assert_eq!(cfg.resolve((-9, 22)).preamble_len, 32);
|
assert_eq!(cfg.resolve((-9, 22)).preamble_len, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,7 +649,12 @@ mod tests {
|
|||||||
let mut payload = Vec::new();
|
let mut payload = Vec::new();
|
||||||
let _ = payload.extend_from_slice(b"data");
|
let _ = payload.extend_from_slice(b"data");
|
||||||
let mut buf = [0u8; 64];
|
let mut buf = [0u8; 64];
|
||||||
let n = Response::RxPacket { rssi: -80, snr: 10, payload }.write_to(&mut buf);
|
let n = Response::RxPacket {
|
||||||
|
rssi: -80,
|
||||||
|
snr: 10,
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
.write_to(&mut buf);
|
||||||
assert_eq!(n, 7 + 4); // tag(1) + rssi(2) + snr(2) + len(2) + "data"(4)
|
assert_eq!(n, 7 + 4); // tag(1) + rssi(2) + snr(2) + len(2) + "data"(4)
|
||||||
assert_eq!(buf[0], 2);
|
assert_eq!(buf[0], 2);
|
||||||
assert_eq!(i16::from_le_bytes([buf[1], buf[2]]), -80);
|
assert_eq!(i16::from_le_bytes([buf[1], buf[2]]), -80);
|
||||||
|
|||||||
+23
-13
@@ -125,9 +125,7 @@ pub async fn radio_task(
|
|||||||
warn!("restart RX failed: {}", e);
|
warn!("restart RX failed: {}", e);
|
||||||
state.state = RadioState::Idle;
|
state.state = RadioState::Idle;
|
||||||
status.sender().send(state.clone());
|
status.sender().send(state.clone());
|
||||||
responses
|
responses.send(Response::Error(ErrorCode::RadioBusy)).await;
|
||||||
.send(Response::Error(ErrorCode::RadioBusy))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -135,9 +133,7 @@ pub async fn radio_task(
|
|||||||
if start_rx(&mut lora, &cfg).await.is_err() {
|
if start_rx(&mut lora, &cfg).await.is_err() {
|
||||||
state.state = RadioState::Idle;
|
state.state = RadioState::Idle;
|
||||||
status.sender().send(state.clone());
|
status.sender().send(state.clone());
|
||||||
responses
|
responses.send(Response::Error(ErrorCode::RadioBusy)).await;
|
||||||
.send(Response::Error(ErrorCode::RadioBusy))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -218,7 +214,9 @@ async fn handle_cmd(
|
|||||||
}
|
}
|
||||||
Command::DisplayOn | Command::DisplayOff | Command::GetMac => {}
|
Command::DisplayOn | Command::DisplayOff | Command::GetMac => {}
|
||||||
Command::Transmit { config, payload } => {
|
Command::Transmit { config, payload } => {
|
||||||
let tx_config = config.map(|c| c.resolve(Board::TX_POWER_RANGE)).or(state.config);
|
let tx_config = config
|
||||||
|
.map(|c| c.resolve(Board::TX_POWER_RANGE))
|
||||||
|
.or(state.config);
|
||||||
if let Some(cfg) = tx_config {
|
if let Some(cfg) = tx_config {
|
||||||
if let Err(reason) = cfg.validate(Board::TX_POWER_RANGE) {
|
if let Err(reason) = cfg.validate(Board::TX_POWER_RANGE) {
|
||||||
warn!("TX config rejected: {}", reason);
|
warn!("TX config rejected: {}", reason);
|
||||||
@@ -239,9 +237,7 @@ async fn handle_cmd(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("TX failed: {}", e);
|
warn!("TX failed: {}", e);
|
||||||
responses
|
responses.send(Response::Error(ErrorCode::TxTimeout)).await;
|
||||||
.send(Response::Error(ErrorCode::TxTimeout))
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +327,12 @@ fn modulation_params(
|
|||||||
async fn start_rx(lora: &mut Radio, cfg: &RadioConfig) -> Result<(), RadioError> {
|
async fn start_rx(lora: &mut Radio, cfg: &RadioConfig) -> Result<(), RadioError> {
|
||||||
let mdltn = modulation_params(lora, cfg)?;
|
let mdltn = modulation_params(lora, cfg)?;
|
||||||
let pkt = lora.create_rx_packet_params(
|
let pkt = lora.create_rx_packet_params(
|
||||||
cfg.preamble_len, IMPLICIT_HEADER, MAX_PAYLOAD as u8, CRC_ON, IQ_INVERTED, &mdltn,
|
cfg.preamble_len,
|
||||||
|
IMPLICIT_HEADER,
|
||||||
|
MAX_PAYLOAD as u8,
|
||||||
|
CRC_ON,
|
||||||
|
IQ_INVERTED,
|
||||||
|
&mdltn,
|
||||||
)?;
|
)?;
|
||||||
lora.prepare_for_rx(RxMode::Continuous, &mdltn, &pkt).await
|
lora.prepare_for_rx(RxMode::Continuous, &mdltn, &pkt).await
|
||||||
}
|
}
|
||||||
@@ -343,7 +344,12 @@ async fn rx_once(
|
|||||||
) -> Result<(u8, lora_phy::mod_params::PacketStatus), RadioError> {
|
) -> Result<(u8, lora_phy::mod_params::PacketStatus), RadioError> {
|
||||||
let mdltn = modulation_params(lora, cfg)?;
|
let mdltn = modulation_params(lora, cfg)?;
|
||||||
let pkt = lora.create_rx_packet_params(
|
let pkt = lora.create_rx_packet_params(
|
||||||
cfg.preamble_len, IMPLICIT_HEADER, MAX_PAYLOAD as u8, CRC_ON, IQ_INVERTED, &mdltn,
|
cfg.preamble_len,
|
||||||
|
IMPLICIT_HEADER,
|
||||||
|
MAX_PAYLOAD as u8,
|
||||||
|
CRC_ON,
|
||||||
|
IQ_INVERTED,
|
||||||
|
&mdltn,
|
||||||
)?;
|
)?;
|
||||||
lora.rx(&pkt, buf).await
|
lora.rx(&pkt, buf).await
|
||||||
}
|
}
|
||||||
@@ -368,7 +374,11 @@ async fn do_tx(lora: &mut Radio, cfg: &RadioConfig, payload: &[u8]) -> Result<()
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut tx_pkt = lora.create_tx_packet_params(
|
let mut tx_pkt = lora.create_tx_packet_params(
|
||||||
cfg.preamble_len, IMPLICIT_HEADER, CRC_ON, IQ_INVERTED, &mdltn,
|
cfg.preamble_len,
|
||||||
|
IMPLICIT_HEADER,
|
||||||
|
CRC_ON,
|
||||||
|
IQ_INVERTED,
|
||||||
|
&mdltn,
|
||||||
)?;
|
)?;
|
||||||
lora.prepare_for_tx(&mdltn, &mut tx_pkt, cfg.tx_power_dbm as i32, payload)
|
lora.prepare_for_tx(&mdltn, &mut tx_pkt, cfg.tx_power_dbm as i32, payload)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
+19
-15
@@ -10,9 +10,7 @@ use embassy_usb::class::cdc_acm::{CdcAcmClass, Receiver, Sender, State};
|
|||||||
use embassy_usb::Builder;
|
use embassy_usb::Builder;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
use crate::channel::{
|
use crate::channel::{CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel};
|
||||||
CommandChannel, DisplayCommand, DisplayCommandChannel, ResponseChannel,
|
|
||||||
};
|
|
||||||
use crate::protocol::{Command, ErrorCode, Response};
|
use crate::protocol::{Command, ErrorCode, Response};
|
||||||
|
|
||||||
const MAX_FRAME: usize = 512;
|
const MAX_FRAME: usize = 512;
|
||||||
@@ -68,14 +66,7 @@ pub async fn usb_task(
|
|||||||
let ctrl_buf = CTRL_BUF.init([0; 64]);
|
let ctrl_buf = CTRL_BUF.init([0; 64]);
|
||||||
let cdc_state = CDC_STATE.init(State::new());
|
let cdc_state = CDC_STATE.init(State::new());
|
||||||
|
|
||||||
let mut builder = Builder::new(
|
let mut builder = Builder::new(parts.driver, config, desc_buf, conf_buf, bos_buf, ctrl_buf);
|
||||||
parts.driver,
|
|
||||||
config,
|
|
||||||
desc_buf,
|
|
||||||
conf_buf,
|
|
||||||
bos_buf,
|
|
||||||
ctrl_buf,
|
|
||||||
);
|
|
||||||
|
|
||||||
let class = CdcAcmClass::new(&mut builder, cdc_state, MAX_PACKET as u16);
|
let class = CdcAcmClass::new(&mut builder, cdc_state, MAX_PACKET as u16);
|
||||||
let mut usb_dev = builder.build();
|
let mut usb_dev = builder.build();
|
||||||
@@ -84,7 +75,15 @@ pub async fn usb_task(
|
|||||||
// ── Run USB device + protocol loop concurrently ────────────────
|
// ── Run USB device + protocol loop concurrently ────────────────
|
||||||
join(
|
join(
|
||||||
usb_dev.run(),
|
usb_dev.run(),
|
||||||
protocol_loop(sender, receiver, commands, responses, display_commands, has_display, mac),
|
protocol_loop(
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
commands,
|
||||||
|
responses,
|
||||||
|
display_commands,
|
||||||
|
has_display,
|
||||||
|
mac,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,12 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
|
|||||||
{
|
{
|
||||||
if let Some(cmd) = Command::from_bytes(&decode_buf[..decoded_len]) {
|
if let Some(cmd) = Command::from_bytes(&decode_buf[..decoded_len]) {
|
||||||
route_command(
|
route_command(
|
||||||
cmd, commands, responses, display_commands, has_display, mac,
|
cmd,
|
||||||
|
commands,
|
||||||
|
responses,
|
||||||
|
display_commands,
|
||||||
|
has_display,
|
||||||
|
mac,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -160,8 +164,8 @@ async fn protocol_loop<'d, D: embassy_usb_driver::Driver<'d>>(
|
|||||||
Either3::Second(response) => {
|
Either3::Second(response) => {
|
||||||
// Serialize response to fixed-size LE bytes, then COBS encode
|
// Serialize response to fixed-size LE bytes, then COBS encode
|
||||||
let raw_len = response.write_to(&mut write_buf);
|
let raw_len = response.write_to(&mut write_buf);
|
||||||
let encoded_len = ucobs::encode(&write_buf[..raw_len], &mut cobs_encode_buf)
|
let encoded_len =
|
||||||
.unwrap_or(0);
|
ucobs::encode(&write_buf[..raw_len], &mut cobs_encode_buf).unwrap_or(0);
|
||||||
// Append 0x00 sentinel
|
// Append 0x00 sentinel
|
||||||
if encoded_len < cobs_encode_buf.len() {
|
if encoded_len < cobs_encode_buf.len() {
|
||||||
cobs_encode_buf[encoded_len] = 0x00;
|
cobs_encode_buf[encoded_len] = 0x00;
|
||||||
|
|||||||
@@ -50,16 +50,14 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
// Resolve serial port
|
// Resolve serial port
|
||||||
let port = match args.port {
|
let port = match args.port {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => {
|
None => tokio::task::spawn_blocking(|| {
|
||||||
tokio::task::spawn_blocking(|| {
|
|
||||||
donglora_client::find_port().unwrap_or_else(|| {
|
donglora_client::find_port().unwrap_or_else(|| {
|
||||||
info!("waiting for DongLoRa device...");
|
info!("waiting for DongLoRa device...");
|
||||||
donglora_client::wait_for_device()
|
donglora_client::wait_for_device()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("device discovery task failed: {e}"))?
|
.map_err(|e| anyhow::anyhow!("device discovery task failed: {e}"))?,
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolve socket path
|
// Resolve socket path
|
||||||
@@ -102,11 +100,9 @@ fn parse_tcp_addr(tcp: &str) -> Option<(String, u16)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn signal_handler(shutdown: CancellationToken) -> anyhow::Result<()> {
|
async fn signal_handler(shutdown: CancellationToken) -> anyhow::Result<()> {
|
||||||
let mut sigint =
|
let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
|
||||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to register SIGINT handler: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("failed to register SIGINT handler: {e}"))?;
|
||||||
let mut sigterm =
|
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
|
||||||
.map_err(|e| anyhow::anyhow!("failed to register SIGTERM handler: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("failed to register SIGTERM handler: {e}"))?;
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|||||||
Reference in new issue
Block a user