diff --git a/CHANGELOG.md b/CHANGELOG.md index c51e71e8..275dd74f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Added - Transparent LoRa radio over USB CDC-ACM (VID `1209`, PID `5741`) -- COBS-framed fixed-size LE binary protocol (8 commands, 6 responses) +- COBS-framed fixed-size LE binary protocol (9 commands, 7 responses) - Radio configuration: frequency, bandwidth, spreading factor, coding rate, TX power - Continuous RX mode with RSSI/SNR per packet - Single-shot TX with optional per-packet config override diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 03cbf331..81be702e 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -59,6 +59,7 @@ Each command is a tag byte followed by fixed-size fields. | 5 | Transmit | has_config (1) + [RadioConfig if 1] + len (u16 LE) + payload | | 6 | DisplayOn | — | | 7 | DisplayOff | — | +| 8 | GetMac | — | ## Responses (Firmware → Host) @@ -70,6 +71,7 @@ Each command is a tag byte followed by fixed-size fields. | 3 | TxDone | — | | 4 | Ok | — | | 5 | Error | code (u8) | +| 6 | MacAddress | 6 raw bytes (board's MAC/device address) | ## Bandwidth @@ -128,9 +130,10 @@ Read back the response, COBS-decode it. First byte `0x04` = Ok. 1. Open USB serial port (find by VID:PID `1209:5741`) 2. Implement COBS encode/decode (libraries exist for every language) 3. Pack commands with `struct.pack` or equivalent — all fields are fixed-size LE -4. Send `SetConfig`, read back `Ok` -5. Send `StartRx`, read back `Ok` -6. Loop: read frames, decode `RxPacket` (rssi, snr, payload at known offsets) -7. To transmit: send `Transmit`, read back `TxDone` +4. Optionally send `GetMac`, read back `MacAddress` (useful with multiple dongles) +5. Send `SetConfig`, read back `Ok` +6. Send `StartRx`, read back `Ok` +7. Loop: read frames, decode `RxPacket` (rssi, snr, payload at known offsets) +8. To transmit: send `Transmit`, read back `TxDone` See `examples/` for working Python implementations. diff --git a/examples/all_commands.py b/examples/all_commands.py index 74e9cc33..533ee703 100755 --- a/examples/all_commands.py +++ b/examples/all_commands.py @@ -42,7 +42,10 @@ try: print("\n── DisplayOn ──") print(dl.send(ser, "DisplayOn")) - print("\nAll 8 commands exercised successfully.") + print("\n── GetMac ──") + print(dl.send(ser, "GetMac")) + + print("\nAll 9 commands exercised successfully.") except KeyboardInterrupt: print("\nInterrupted.") diff --git a/examples/donglora.py b/examples/donglora.py index 5645eee8..0c0cfb19 100644 --- a/examples/donglora.py +++ b/examples/donglora.py @@ -99,6 +99,7 @@ def encode_command(cmd: str, **kwargs) -> bytes: tags = { "Ping": 0, "GetConfig": 1, "SetConfig": 2, "StartRx": 3, "StopRx": 4, "Transmit": 5, "DisplayOn": 6, "DisplayOff": 7, + "GetMac": 8, } out = bytes([tags[cmd]]) if cmd == "SetConfig": @@ -144,6 +145,11 @@ def decode_response(data: bytes) -> dict: elif tag == 5: code = rest[0] if rest else -1 return {"type": "Error", "code": code} + elif tag == 6: + if len(rest) >= 6: + mac = ":".join(f"{b:02X}" for b in rest[:6]) + return {"type": "MacAddress", "mac": mac} + return {"type": "MacAddress", "raw": rest.hex()} else: return {"type": f"Unknown({tag})"} diff --git a/src/protocol.rs b/src/protocol.rs index be8e660a..7dcfdc54 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -139,6 +139,7 @@ pub enum Command { }, DisplayOn, DisplayOff, + GetMac, } impl Command { @@ -178,6 +179,7 @@ impl Command { } 6 => Some(Self::DisplayOn), 7 => Some(Self::DisplayOff), + 8 => Some(Self::GetMac), _ => None, } } @@ -197,6 +199,7 @@ pub enum Response { TxDone, Ok, Error(ErrorCode), + MacAddress([u8; 6]), } impl Response { @@ -232,6 +235,11 @@ impl Response { buf[1] = code as u8; 2 } + Self::MacAddress(mac) => { + buf[0] = 6; + buf[1..7].copy_from_slice(&mac); + 7 + } } } } diff --git a/src/radio/task.rs b/src/radio/task.rs index 2bfbe268..769a4f11 100644 --- a/src/radio/task.rs +++ b/src/radio/task.rs @@ -219,7 +219,7 @@ async fn handle_cmd( status.sender().send(state.clone()); responses.send(Response::Ok).await; } - Command::DisplayOn | Command::DisplayOff => {} + Command::DisplayOn | Command::DisplayOff | Command::GetMac => {} Command::Transmit { config, payload } => { let tx_config = config.map(|c| c.resolve_power(Board::TX_POWER_RANGE)).or(state.config); if let Some(cfg) = tx_config { diff --git a/src/usb/task.rs b/src/usb/task.rs index 9fa8a780..20762cca 100644 --- a/src/usb/task.rs +++ b/src/usb/task.rs @@ -207,6 +207,12 @@ async fn route_command( responses.send(Response::Error(ErrorCode::NoDisplay)).await; } } + Command::GetMac => { + use crate::board::{Board, LoRaBoard}; + responses + .send(Response::MacAddress(Board::mac_address())) + .await; + } other => { commands.send(other).await; }