mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-16 00:19:59 +00:00
Merge pull request #109 from recrof/kiss-radio-support
add KISS modem role (port of upstream examples/kiss_modem)
This commit is contained in:
+1
-1
@@ -48,7 +48,7 @@ indent_size = unset
|
||||
# changes, each fenced `// ZEPHCORE:`. Upstream mixes 2-space and tab indentation
|
||||
# and has trailing whitespace, so none of it is enforced here. Add a file to this
|
||||
# list when it is converted to upstream shape.
|
||||
[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h,zephcore/helpers/BaseChatMesh.cpp,zephcore/helpers/BaseChatMesh.h,zephcore/helpers/ContactInfo.h,zephcore/helpers/ChannelDetails.h,zephcore/helpers/ClientACL.cpp,zephcore/helpers/ClientACL.h,zephcore/helpers/RegionMap.cpp,zephcore/helpers/RegionMap.h,zephcore/helpers/TransportKeyStore.cpp,zephcore/helpers/TransportKeyStore.h,zephcore/helpers/BaseSerialInterface.h,zephcore/helpers/MultiSerialInterface.h,zephcore/helpers/ConfigSerializer.cpp,zephcore/helpers/ConfigSerializer.h,zephcore/helpers/SensorManager.h,zephcore/helpers/sensors/LPPDataHelpers.h}]
|
||||
[{zephcore/src/Mesh.cpp,zephcore/src/Dispatcher.cpp,zephcore/src/Packet.cpp,zephcore/src/StaticPoolPacketManager.cpp,zephcore/include/mesh/MeshCore.h,zephcore/include/mesh/Dispatcher.h,zephcore/include/mesh/Mesh.h,zephcore/include/mesh/Packet.h,zephcore/include/mesh/Identity.h,zephcore/include/mesh/Utils.h,zephcore/include/mesh/SimpleMeshTables.h,zephcore/include/mesh/StaticPoolPacketManager.h,zephcore/helpers/BaseChatMesh.cpp,zephcore/helpers/BaseChatMesh.h,zephcore/helpers/ContactInfo.h,zephcore/helpers/ChannelDetails.h,zephcore/helpers/ClientACL.cpp,zephcore/helpers/ClientACL.h,zephcore/helpers/RegionMap.cpp,zephcore/helpers/RegionMap.h,zephcore/helpers/TransportKeyStore.cpp,zephcore/helpers/TransportKeyStore.h,zephcore/helpers/BaseSerialInterface.h,zephcore/helpers/MultiSerialInterface.h,zephcore/helpers/ConfigSerializer.cpp,zephcore/helpers/ConfigSerializer.h,zephcore/helpers/SensorManager.h,zephcore/helpers/sensors/LPPDataHelpers.h,zephcore/app/KissModem.cpp,zephcore/app/KissModem.h}]
|
||||
indent_style = unset
|
||||
indent_size = unset
|
||||
tab_width = unset
|
||||
|
||||
@@ -65,6 +65,7 @@ the [supported boards list](docs/supported_boards.md).
|
||||
| **Repeater** | Relays packets for everyone else | [Text CLI](docs/CLI_commands.md) over USB, or remote admin over the mesh |
|
||||
| **Room server** | A shared message board that keeps posts for clients who were away | Same CLI as the repeater; users log in from the app |
|
||||
| **Observer** | Listens only and publishes what it hears to MQTT over WiFi (ESP32) | [Its own small CLI](docs/CLI_commands.md#observer-zephcore-only) over USB |
|
||||
| **KISS modem** | Raw LoRa packets for a host program (KISS TNC, as upstream's `kiss_modem`) | [KISS protocol](docs/kiss_modem_protocol.md) over USB |
|
||||
|
||||
One role per firmware image; you choose it when you flash.
|
||||
|
||||
@@ -140,6 +141,7 @@ Roles and extras are config fragments in `zephcore/boards/common/`, added throug
|
||||
| `repeater.conf` | Repeater |
|
||||
| `room_server.conf` | Room server |
|
||||
| `observer.conf` | Observer (ESP32) |
|
||||
| `kiss_modem.conf` | KISS modem |
|
||||
| `debug.conf` | Logging and asserts |
|
||||
| `packet_logging.conf` | One clean RAW/RX/TX line per packet, nothing else |
|
||||
|
||||
|
||||
+28
-7
@@ -27,12 +27,13 @@
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
ZephCore is a LoRa mesh networking firmware running on Zephyr RTOS. It supports four device roles:
|
||||
ZephCore is a LoRa mesh networking firmware running on Zephyr RTOS. It supports five device roles:
|
||||
|
||||
- **Companion**: BLE-connected device paired with a phone app. Full contact/channel/message management.
|
||||
- **Repeater**: Autonomous headless relay node. CLI administration via authenticated mesh connections or serial UART.
|
||||
- **Room Server**: Headless store-and-forward shared message room (BBS). Reuses the repeater's ACL/region/CLI; pushes new posts to logged-in clients (per-client sync cursor + ACK).
|
||||
- **Observer** (ESP32): Listen-only node that publishes received LoRa packets to MQTT over WiFi.
|
||||
- **KISS modem**: Raw LoRa packets to and from a host over serial (upstream's `kiss_modem`).
|
||||
|
||||
Supported hardware: nRF52840, nRF54L15, nRF54LM20A, ESP32 (classic PICO-D4 and C3/C6/S3), EFR32MG24, and STM32WL (LoRa-E5). Radios: SX126x family (SX1261/62/68, LLCC68, STM32WL sub-GHz), LR1110, SX127x (SX1272/76/78, loramac-node backend), and LR2021 (validated on the MeshTracker X1). A native Linux port runs the full stack on SBCs (Femtofox, Raspberry Pi) via Zephyr `native_sim` — see `LINUX_NATIVE.md`.
|
||||
|
||||
@@ -103,7 +104,8 @@ zephcore/
|
||||
│ ├── RepeaterDataStore.cpp/h # Repeater-specific persistence paths
|
||||
│ ├── RoomServerMesh.cpp/h # Store-and-forward room server (BBS)
|
||||
│ ├── ObserverMesh.cpp/h # Listen-only WiFi+MQTT observer (ESP32)
|
||||
│ └── main_observer.cpp, observer_creds.cpp/h
|
||||
│ ├── main_observer.cpp, observer_creds.cpp/h
|
||||
│ └── KissModem.cpp/h # KISS TNC + SetHardware extensions (upstream verbatim)
|
||||
│
|
||||
├── helpers/ # Shared utilities
|
||||
│ ├── compat/ # Arduino Stream/SHA256/FILESYSTEM over Zephyr, for upstream-verbatim code
|
||||
@@ -742,6 +744,24 @@ Headless store-and-forward shared message room (BBS). Clients log in with the ad
|
||||
|
||||
Listen-only node (ESP32 only): receives LoRa packets and publishes them to an MQTT broker over WiFi STA (`adapters/mqtt/`, `adapters/wifi/`). Never transmits. Configured at runtime via serial CLI (credentials in `observer_creds.cpp`); build with `boards/common/observer.conf`.
|
||||
|
||||
### 6.5.1 KISS modem
|
||||
|
||||
Upstream's `examples/kiss_modem` (protocol: [kiss_modem_protocol.md](kiss_modem_protocol.md)). `app/KissModem.cpp/h`
|
||||
keep upstream's text, with ZephCore changes fenced; `src/main_kiss_modem.cpp` is the composition root. No `Mesh`
|
||||
or `Dispatcher`: the modem drives `LoRaRadio` directly, and the main loop runs the radio's maintenance (noise floor,
|
||||
CAD, AGC/drift) as the Dispatcher would. The host serial is the CDC-ACM port, else the console UART, through
|
||||
`adapters/transport/ZephyrUartStream` (an Arduino `Stream` over interrupt-driven RX/TX rings); the console driver
|
||||
is off in this role so nothing else writes to the port. Event-driven: serial input, LoRa RX and TX-done, host
|
||||
output drained, and a one-shot for the sooner of `KissModem::msUntilNextTx()` and the radio's maintenance
|
||||
deadline. Differences from upstream:
|
||||
- An LBT refusal from `startSendRaw()` returns to the CSMA slot wait until the wait-clear bound (1.5 × the
|
||||
airtime of a 255-byte packet from the start of CSMA), then reports TX failed; upstream reports it at once.
|
||||
- A transmit times out only once the radio has concluded it, as in `Dispatcher::loop()`.
|
||||
- `SetRadio` outside the `ZC_RADIO_*` ranges (NodePrefs.h) is refused with `HW_ERR_INVALID_PARAM`.
|
||||
- No periodic AGC reset: the radio's own deaf-aware maintenance replaces it, as in every role.
|
||||
Radio settings live in RAM only (upstream: the same); the identity is `/lfs/repeater/_main.id`, shared with the
|
||||
server roles. Build with `boards/common/kiss_modem.conf`.
|
||||
|
||||
### 6.6 CommonCLI Commands
|
||||
|
||||
System: `ver`, `board`, `reboot`, `start dfu`, `start ota`, `erase`
|
||||
@@ -857,7 +877,8 @@ Repeaters and room servers default to `CONFIG_ZEPHCORE_REPEATER_GPS_INTERVAL_SEC
|
||||
While the bus is suspended (the computer asleep with the port open) the session is kept but is not a connected
|
||||
client: nothing is queued for it, and it does not hold up the clean-up that runs when the last client leaves. Each
|
||||
change reaches the main loop as a connect or a disconnect.
|
||||
- Servers (repeater, room server, observer) use the CDC port as their serial console.
|
||||
- Servers (repeater, room server, observer) use the CDC port as their serial console; the KISS modem uses it for
|
||||
its binary stream.
|
||||
|
||||
### 7.5 Board (`adapters/board/`)
|
||||
|
||||
@@ -1028,7 +1049,7 @@ prj.conf (base: console; production defaults — LOG=n, ASSERT=n)
|
||||
|
||||
### 9.2 Key Kconfig Choices
|
||||
|
||||
- **Role**: `ZEPHCORE_ROLE_COMPANION` (default) vs `ZEPHCORE_ROLE_REPEATER` vs `ZEPHCORE_ROLE_ROOM_SERVER` vs `ZEPHCORE_ROLE_OBSERVER` (selected via `repeater.conf` / `room_server.conf` / `observer.conf`)
|
||||
- **Role**: `ZEPHCORE_ROLE_COMPANION` (default) vs `ZEPHCORE_ROLE_REPEATER` vs `ZEPHCORE_ROLE_ROOM_SERVER` vs `ZEPHCORE_ROLE_OBSERVER` vs `ZEPHCORE_ROLE_KISS_MODEM` (selected via `repeater.conf` / `room_server.conf` / `observer.conf` / `kiss_modem.conf`)
|
||||
- **Radio**: `ZEPHCORE_RADIO_NATIVE` (SX126x, default) vs `ZEPHCORE_RADIO_LR1110` vs `ZEPHCORE_RADIO_LR2021` vs `ZEPHCORE_RADIO_SX127X`
|
||||
- **Features**: Display, buzzer, buttons, multi-tap, Doom (auto-enabled from DT); PSRAM auto-enable from DT (`Kconfig.psram`)
|
||||
|
||||
@@ -1226,21 +1247,21 @@ Codes `0x80`–`0x90` (`PUSH_CODE_*` in `app/CompanionMesh.h`). Most used:
|
||||
| `/lfs/contacts3` or `/ext/contacts3` | Contacts | 152B × N records |
|
||||
| `/lfs/channels2` or `/ext/channels2` | Channels | 68B × N records |
|
||||
| `/lfs/adv_blobs` or `/ext/adv_blobs` | Advert cache | Fixed-size blob records |
|
||||
| `/lfs/repeater/_main.id`, `prefs.json` | Repeater/room-server/observer identity + prefs | `prefs.json` as above; legacy 311B `prefs` kept after migration; atomic-replace writes |
|
||||
| `/lfs/repeater/_main.id`, `prefs.json` | Repeater/room-server/observer identity + prefs (KISS modem: identity only) | `prefs.json` as above; legacy 311B `prefs` kept after migration; atomic-replace writes |
|
||||
| `/lfs/repeater/acl` | Client ACL | 136B × N records |
|
||||
| `/lfs/repeater/regions2` | Region map | Header + 164B × N entries |
|
||||
| `storage_partition` (NVS, 0xD0000 nRF52) | BLE bonds + Zephyr settings | NVS settings backend (≥1.16.2; old `/lfs/settings` file detected by self-heal) |
|
||||
|
||||
> **Roles are not interchangeable.** Each role formats the whole volume on its first boot if the
|
||||
> volume holds no data for that role: the companion checks `/lfs/prefs.json` and `/lfs/new_prefs`
|
||||
> (`ZephyrDataStore::hasPrefs()`), the repeater/room-server/observer check `/lfs/repeater/prefs.json`,
|
||||
> (`ZephyrDataStore::hasPrefs()`), the repeater/room-server/observer/KISS modem check `/lfs/repeater/prefs.json`,
|
||||
> `/lfs/repeater/prefs` and `/lfs/repeater/_main.id` (`RepeaterDataStore::hasRoleData()`). So flashing a repeater over
|
||||
> a companion — or the reverse — erases the previous role's identity, prefs and contacts, plus
|
||||
> `storage_partition` and QSPI. Export your identity before switching roles. The roles' files
|
||||
> never overlap physically (one LittleFS volume, one allocator); the reason for the wipe is that
|
||||
> they share 128 KB and the other role's data crowds out writes. Repeater, room server and
|
||||
> observer share `/lfs/repeater/` and the same prefs layout, so switching among *those three*
|
||||
> preserves the identity.
|
||||
> (or to the KISS modem, which keeps only the identity there) preserves the identity.
|
||||
|
||||
### Preferences (`prefs.json`)
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ west build -b <board> zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/<f
|
||||
| `repeater.conf` | Repeater | On ESP32-S3/C-series this pulls in `wifi_ota.conf`, so add `--sysbuild` |
|
||||
| `room_server.conf` | Room server | |
|
||||
| `observer.conf` | Observer | ESP32 only (WiFi + MQTT) |
|
||||
| `kiss_modem.conf` | KISS modem | Raw packets over the USB/console serial port; the console is off |
|
||||
| `debug.conf` | Logging and asserts | Combine with any role |
|
||||
| `packet_logging.conf` | One RAW/RX/TX line per packet | Logging subsystem stays off, so the console carries only these lines and the CLI |
|
||||
| `repeater_uplink.conf` | Repeater WiFi + MQTT uplink | ESP32, with `repeater.conf` |
|
||||
@@ -72,6 +73,9 @@ west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/ro
|
||||
# Observer
|
||||
west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/observer.conf"
|
||||
|
||||
# KISS modem
|
||||
west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf"
|
||||
|
||||
# BLE adapter at debug level (debug.conf turns logging on, the flag raises BLE to DBG)
|
||||
west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/debug.conf" -DCONFIG_ZEPHCORE_BLE_LOG_LEVEL_DBG=y
|
||||
|
||||
|
||||
+6
-4
@@ -77,9 +77,11 @@ A **role** is a firmware personality chosen at build time. Exactly one role per
|
||||
| Repeater | Autonomous relay: ACL, regions, neighbours, admin CLI | Operator | `examples/simple_repeater` | `/lfs/repeater/` |
|
||||
| Room server | Store-and-forward board, per-client sync cursor | Operator + logged-in clients | `examples/simple_room_server` | `/lfs/repeater/` |
|
||||
| Observer | Listen-only; publishes packets to MQTT (ESP32) | Broker | none (ZephCore-only) | `/lfs/repeater/` |
|
||||
| KISS modem | Raw packets, radio and identity services for a host | Host program (KISS) | `examples/kiss_modem` | `/lfs/repeater/` (identity) |
|
||||
|
||||
Roles share the mesh core; they differ in policy. **Switching between the companion and a server role formats the
|
||||
volume** (the identity is lost unless exported first); repeater, room server and observer share one layout.
|
||||
volume** (the identity is lost unless exported first); repeater, room server, observer and KISS modem share one
|
||||
layout.
|
||||
|
||||
---
|
||||
|
||||
@@ -106,7 +108,7 @@ flowchart LR
|
||||
|---|---|---|
|
||||
| Platform | nRF52840, nRF54L15, nRF54LM20A, ESP32 classic, ESP32-S3/C3/C6, EFR32MG24, STM32WL, native Linux | board directory |
|
||||
| Radio | SX126x family (default), LR1110, LR2021, SX127x | devicetree / board conf |
|
||||
| Role | companion (default), repeater, room server, observer | user `EXTRA_CONF_FILE` |
|
||||
| Role | companion (default), repeater, room server, observer, KISS modem | user `EXTRA_CONF_FILE` |
|
||||
| UI | none, button UI, joystick/keypad UI (companion) | board Kconfig select |
|
||||
| Features | debug, WiFi companion, WiFi OTA, uplink, packet logging, PM | user confs; some auto-added |
|
||||
|
||||
@@ -129,11 +131,11 @@ The board list is one manifest per board (`zephcore.yml`), read by `build.sh`, t
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph L5["L5 Composition roots — src/main_companion.cpp, src/server_main_common.cpp, app/main_observer.cpp"]
|
||||
subgraph L5["L5 Composition roots — src/main_companion.cpp, src/server_main_common.cpp, app/main_observer.cpp, src/main_kiss_modem.cpp"]
|
||||
MAIN["construct objects, wire callbacks, run the event loop"]
|
||||
end
|
||||
subgraph L4["L4 Roles and app policy — app/"]
|
||||
ROLES["CompanionMesh · RepeaterMesh · RoomServerMesh · ObserverMesh · PowerPolicy"]
|
||||
ROLES["CompanionMesh · RepeaterMesh · RoomServerMesh · ObserverMesh · KissModem · PowerPolicy"]
|
||||
end
|
||||
subgraph UI["Presentation — helpers/ui*"]
|
||||
UIX["display, button UI, joystick UI, buzzer, LEDs"]
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
# MeshCore KISS Modem Protocol
|
||||
|
||||
Standard KISS TNC firmware for MeshCore LoRa radios. Compatible with any KISS client (Direwolf, APRSdroid, YAAC, etc.) for sending and receiving raw packets. MeshCore-specific extensions (cryptography, radio configuration, telemetry) are available through the standard SetHardware (0x06) command.
|
||||
|
||||
## Serial Configuration
|
||||
|
||||
115200 baud, 8N1, no flow control.
|
||||
|
||||
## Frame Format
|
||||
|
||||
Standard KISS framing per the KA9Q/K3MC specification.
|
||||
|
||||
| Byte | Name | Description |
|
||||
|--------|-------|------------------------------------|
|
||||
| `0xC0` | FEND | Frame delimiter |
|
||||
| `0xDB` | FESC | Escape character |
|
||||
| `0xDC` | TFEND | Escaped FEND (FESC + TFEND = 0xC0) |
|
||||
| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) |
|
||||
|
||||
```
|
||||
┌──────┬───────────┬──────────────┬──────┐
|
||||
│ FEND │ Type Byte │ Data (escaped)│ FEND │
|
||||
│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │
|
||||
└──────┴───────────┴──────────────┴──────┘
|
||||
```
|
||||
|
||||
### Type Byte
|
||||
|
||||
The type byte is split into two nibbles:
|
||||
|
||||
| Bits | Field | Description |
|
||||
|------|---------|-------------------------------------|
|
||||
| 7-4 | Port | Port number (0 for single-port TNC) |
|
||||
| 3-0 | Command | Command number |
|
||||
|
||||
Maximum unescaped frame size: 512 bytes.
|
||||
|
||||
## Standard KISS Commands
|
||||
|
||||
### Host to TNC
|
||||
|
||||
| Command | Value | Data | Description |
|
||||
|-------------|--------|--------------------|-------------------------------------------------------------|
|
||||
| Data | `0x00` | Raw packet | Queue packet for transmission (one pending at a time) |
|
||||
| TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) |
|
||||
| Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) |
|
||||
| SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) |
|
||||
| TXtail | `0x04` | Delay (1 byte) | Post-TX hold time in 10ms units (default: 0) |
|
||||
| FullDuplex | `0x05` | Mode (1 byte) | 0 = half duplex, nonzero = full duplex (default: 0) |
|
||||
| SetHardware | `0x06` | Sub-command + data | MeshCore extensions (see below) |
|
||||
| Return | `0xFF` | - | Exit KISS mode (no-op) |
|
||||
|
||||
### TNC to Host
|
||||
|
||||
| Type | Value | Data | Description |
|
||||
|------|--------|------------|----------------------------|
|
||||
| Data | `0x00` | Raw packet | Received packet from radio |
|
||||
|
||||
Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes.
|
||||
|
||||
Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07).
|
||||
|
||||
### Host Output Backpressure
|
||||
|
||||
Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; `loop()` never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses.
|
||||
|
||||
### CSMA Behavior
|
||||
|
||||
The TNC implements p-persistent CSMA for half-duplex operation:
|
||||
|
||||
1. When a packet is queued, monitor carrier detect
|
||||
2. When the channel clears, generate a random value 0-255
|
||||
3. If the value is less than or equal to P (Persistence), wait TXDELAY then transmit
|
||||
4. Otherwise, wait SlotTime and repeat from step 1
|
||||
|
||||
In full-duplex mode, CSMA is bypassed and packets transmit after TXDELAY.
|
||||
|
||||
## SetHardware Extensions (0x06)
|
||||
|
||||
MeshCore-specific functionality uses the standard KISS SetHardware command. The first byte of SetHardware data is a sub-command. Standard KISS clients ignore these frames.
|
||||
|
||||
### Frame Format
|
||||
|
||||
```
|
||||
┌──────┬──────┬─────────────┬──────────────┬──────┐
|
||||
│ FEND │ 0x06 │ Sub-command │ Data (escaped)│ FEND │
|
||||
│ 0xC0 │ │ 1 byte │ variable │ 0xC0 │
|
||||
└──────┴──────┴─────────────┴──────────────┴──────┘
|
||||
```
|
||||
|
||||
### Request Sub-commands (Host to TNC)
|
||||
|
||||
| Sub-command | Value | Data |
|
||||
|-----------------|--------|------------------------------------------|
|
||||
| GetIdentity | `0x01` | - |
|
||||
| GetRandom | `0x02` | Length (1 byte, 1-64) |
|
||||
| VerifySignature | `0x03` | PubKey (32) + Signature (64) + Data |
|
||||
| SignData | `0x04` | Data to sign |
|
||||
| EncryptData | `0x05` | Key (32) + Plaintext |
|
||||
| DecryptData | `0x06` | Key (32) + MAC (2) + Ciphertext |
|
||||
| KeyExchange | `0x07` | Remote PubKey (32) |
|
||||
| Hash | `0x08` | Data to hash |
|
||||
| SetRadio | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) |
|
||||
| SetTxPower | `0x0A` | Power dBm (1) |
|
||||
| GetRadio | `0x0B` | - |
|
||||
| GetTxPower | `0x0C` | - |
|
||||
| GetCurrentRssi | `0x0D` | - |
|
||||
| IsChannelBusy | `0x0E` | - |
|
||||
| GetAirtime | `0x0F` | Packet length (1) |
|
||||
| GetNoiseFloor | `0x10` | - |
|
||||
| GetVersion | `0x11` | - |
|
||||
| GetStats | `0x12` | - |
|
||||
| GetBattery | `0x13` | - |
|
||||
| GetMCUTemp | `0x14` | - |
|
||||
| GetSensors | `0x15` | Permissions (1) |
|
||||
| GetDeviceName | `0x16` | - |
|
||||
| Ping | `0x17` | - |
|
||||
| Reboot | `0x18` | - |
|
||||
| SetSignalReport | `0x19` | Enable (1): 0x00=disable, nonzero=enable |
|
||||
| GetSignalReport | `0x1A` | - |
|
||||
|
||||
### Response Sub-commands (TNC to Host)
|
||||
|
||||
Response codes use the high-bit convention: `response = command | 0x80`. Generic and unsolicited responses use the `0xF0`+ range.
|
||||
|
||||
| Sub-command | Value | Data |
|
||||
|--------------|--------|-----------------------------------------|
|
||||
| Identity | `0x81` | PubKey (32) |
|
||||
| Random | `0x82` | Random bytes (1-64) |
|
||||
| Verify | `0x83` | Result (1): 0x00=invalid, 0x01=valid |
|
||||
| Signature | `0x84` | Signature (64) |
|
||||
| Encrypted | `0x85` | MAC (2) + Ciphertext |
|
||||
| Decrypted | `0x86` | Plaintext |
|
||||
| SharedSecret | `0x87` | Shared secret (32) |
|
||||
| Hash | `0x88` | SHA-256 hash (32) |
|
||||
| Radio | `0x8B` | Freq (4) + BW (4) + SF (1) + CR (1) |
|
||||
| TxPower | `0x8C` | Power dBm (1) |
|
||||
| CurrentRssi | `0x8D` | RSSI dBm (1, signed) |
|
||||
| ChannelBusy | `0x8E` | Result (1): 0x00=clear, 0x01=busy |
|
||||
| Airtime | `0x8F` | Milliseconds (4) |
|
||||
| NoiseFloor | `0x90` | dBm (2, signed) |
|
||||
| Version | `0x91` | Version (1) + Reserved (1) |
|
||||
| Stats | `0x92` | RX (4) + TX (4) + Errors (4) |
|
||||
| Battery | `0x93` | Millivolts (2) |
|
||||
| MCUTemp | `0x94` | Temperature (2, signed) |
|
||||
| Sensors | `0x95` | CayenneLPP payload |
|
||||
| DeviceName | `0x96` | Name (variable, UTF-8) |
|
||||
| Pong | `0x97` | - |
|
||||
| SignalReport | `0x9A` | Status (1): 0x00=disabled, 0x01=enabled |
|
||||
| OK | `0xF0` | - |
|
||||
| Error | `0xF1` | Error code (1) |
|
||||
| TxDone | `0xF8` | Result (1): 0x00=failed, 0x01=success |
|
||||
| RxMeta | `0xF9` | SNR (1) + RSSI (1) |
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | Value | Description |
|
||||
|---------------|--------|-------------------------|
|
||||
| InvalidLength | `0x01` | Request data too short |
|
||||
| InvalidParam | `0x02` | Invalid parameter value |
|
||||
| NoCallback | `0x03` | Feature not available |
|
||||
| MacFailed | `0x04` | MAC verification failed |
|
||||
| UnknownCmd | `0x05` | Unknown sub-command |
|
||||
| EncryptFailed | `0x06` | Encryption failed |
|
||||
| TxBusy | `0x07` | Radio TX busy, or host output queue full |
|
||||
|
||||
### Unsolicited Events
|
||||
|
||||
The TNC sends these SetHardware frames without a preceding request:
|
||||
|
||||
**TxDone (0xF8)**: Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped.
|
||||
|
||||
**RxMeta (0xF9)**: Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame.
|
||||
|
||||
## Data Formats
|
||||
|
||||
### Radio Parameters (SetRadio / Radio response)
|
||||
|
||||
All values little-endian.
|
||||
|
||||
| Field | Size | Description |
|
||||
|-----------|---------|-------------------------|
|
||||
| Frequency | 4 bytes | Hz (e.g., 869618000) |
|
||||
| Bandwidth | 4 bytes | Hz (e.g., 62500) |
|
||||
| SF | 1 byte | Spreading factor (5-12) |
|
||||
| CR | 1 byte | Coding rate (5-8) |
|
||||
|
||||
### Version (Version response)
|
||||
|
||||
| Field | Size | Description |
|
||||
|----------|--------|------------------|
|
||||
| Version | 1 byte | Firmware version |
|
||||
| Reserved | 1 byte | Always 0 |
|
||||
|
||||
### Encrypted (Encrypted response)
|
||||
|
||||
| Field | Size | Description |
|
||||
|------------|----------|------------------------------------------------|
|
||||
| MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes |
|
||||
| Ciphertext | variable | AES-128 block-encrypted data with zero padding |
|
||||
|
||||
### Airtime (Airtime response)
|
||||
|
||||
All values little-endian.
|
||||
|
||||
| Field | Size | Description |
|
||||
|---------|---------|----------------------------------------------|
|
||||
| Airtime | 4 bytes | uint32_t, estimated air time in milliseconds |
|
||||
|
||||
### Noise Floor (NoiseFloor response)
|
||||
|
||||
All values little-endian.
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------------|---------|-----------------------|
|
||||
| Noise floor | 2 bytes | int16_t, dBm (signed) |
|
||||
|
||||
The modem recalibrates the noise floor every 2 seconds with an AGC reset every 30 seconds.
|
||||
|
||||
### Stats (Stats response)
|
||||
|
||||
All values little-endian.
|
||||
|
||||
| Field | Size | Description |
|
||||
|--------|---------|---------------------|
|
||||
| RX | 4 bytes | Packets received |
|
||||
| TX | 4 bytes | Packets transmitted |
|
||||
| Errors | 4 bytes | Receive errors |
|
||||
|
||||
### Battery (Battery response)
|
||||
|
||||
All values little-endian.
|
||||
|
||||
| Field | Size | Description |
|
||||
|------------|---------|---------------------------------|
|
||||
| Millivolts | 2 bytes | uint16_t, battery voltage in mV |
|
||||
|
||||
### MCU Temperature (MCUTemp response)
|
||||
|
||||
All values little-endian.
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------------|---------|--------------------------------------------|
|
||||
| Temperature | 2 bytes | int16_t, tenths of °C (e.g., 253 = 25.3°C) |
|
||||
|
||||
Returns `NoCallback` error if the board does not support temperature readings.
|
||||
|
||||
### Device Name (DeviceName response)
|
||||
|
||||
| Field | Size | Description |
|
||||
|-------|----------|----------------------------------|
|
||||
| Name | variable | UTF-8 string, no null terminator |
|
||||
|
||||
### Reboot
|
||||
|
||||
Sends an `OK` response, flushes serial, then reboots the device. The host should expect the connection to drop.
|
||||
|
||||
### Sensor Permissions (GetSensors)
|
||||
|
||||
| Bit | Value | Description |
|
||||
|-----|--------|----------------------------------------|
|
||||
| 0 | `0x01` | Base (battery) |
|
||||
| 1 | `0x02` | Location (GPS) |
|
||||
| 2 | `0x04` | Environment (temp, humidity, pressure) |
|
||||
|
||||
Use `0x07` for all permissions.
|
||||
|
||||
### Sensor Data (Sensors response)
|
||||
|
||||
Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing.
|
||||
|
||||
## Cryptographic Algorithms
|
||||
|
||||
| Operation | Algorithm |
|
||||
|-----------------------------------|-------------------------------------------------------------------------------------|
|
||||
| Identity / Signing / Verification | Ed25519 |
|
||||
| Key Exchange | X25519 (ECDH) |
|
||||
| Encryption | AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) |
|
||||
| Hashing | SHA-256 |
|
||||
|
||||
## Notes
|
||||
|
||||
- Data payload limit (255 bytes) matches MeshCore MAX_TRANS_UNIT; no change needed for KISS “1024+ recommended” (that applies to general TNCs, not MeshCore)
|
||||
- Modem generates identity on first boot (stored in flash)
|
||||
- All multi-byte values are little-endian unless stated otherwise
|
||||
- SNR values in RxMeta are multiplied by 4 for 0.25 dB precision
|
||||
- TxDone is sent as a SetHardware event after each transmission
|
||||
- Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames
|
||||
- See [packet_format.md](./packet_format.md) for packet format
|
||||
+20
-3
@@ -105,6 +105,8 @@ foreach(_c IN LISTS ZEPHCORE_USER_CONFS)
|
||||
endforeach()
|
||||
if("observer" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_OBSERVER)
|
||||
set(ZEPHCORE_BUILD_ROLE observer)
|
||||
elseif("kiss_modem" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_KISS_MODEM)
|
||||
set(ZEPHCORE_BUILD_ROLE kiss_modem)
|
||||
elseif("room_server" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_ROOM_SERVER)
|
||||
set(ZEPHCORE_BUILD_ROLE room_server)
|
||||
elseif("repeater" IN_LIST _user_conf_names OR CONFIG_ZEPHCORE_ROLE_REPEATER)
|
||||
@@ -302,7 +304,7 @@ project(zephcore)
|
||||
# wrong role (e.g. the ESP32 USB companion transport in a server build, which
|
||||
# strands the serial CLI). Pass the role via boards/common/<role>.conf.
|
||||
set(_kconfig_role companion)
|
||||
foreach(_r repeater room_server observer)
|
||||
foreach(_r repeater room_server observer kiss_modem)
|
||||
string(TOUPPER "${_r}" _R)
|
||||
if(CONFIG_ZEPHCORE_ROLE_${_R})
|
||||
set(_kconfig_role ${_r})
|
||||
@@ -604,6 +606,21 @@ elseif(CONFIG_ZEPHCORE_ROLE_OBSERVER)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/app
|
||||
)
|
||||
target_compile_definitions(app PRIVATE ZEPHCORE_OBSERVER=1)
|
||||
elseif(CONFIG_ZEPHCORE_ROLE_KISS_MODEM)
|
||||
message(STATUS "ZephCore Role: KISS MODEM")
|
||||
target_sources(app PRIVATE
|
||||
src/main_kiss_modem.cpp
|
||||
app/KissModem.cpp
|
||||
app/RepeaterDataStore.cpp
|
||||
adapters/transport/ZephyrUartStream.cpp
|
||||
)
|
||||
# USBD CDC ACM init + 1200-baud DFU, where the board has the class driver.
|
||||
if(NOT CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT AND (CONFIG_USB_CDC_ACM OR CONFIG_USBD_CDC_ACM_CLASS))
|
||||
target_sources(app PRIVATE adapters/usb/ZephyrUSBCDC.cpp)
|
||||
endif()
|
||||
target_include_directories(app PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/adapters/usb
|
||||
)
|
||||
else()
|
||||
message(STATUS "ZephCore Role: COMPANION")
|
||||
target_sources(app PRIVATE
|
||||
@@ -720,8 +737,8 @@ if(CONFIG_ZEPHCORE_UI_BUTTONS OR CONFIG_ZEPHCORE_UI_BUZZER OR CONFIG_ZEPHCORE_UI
|
||||
target_include_directories(app PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/helpers/ui
|
||||
)
|
||||
# Repeater and Observer builds need weak stubs for companion-only UI mesh actions
|
||||
if(CONFIG_ZEPHCORE_ROLE_REPEATER OR CONFIG_ZEPHCORE_ROLE_OBSERVER OR CONFIG_ZEPHCORE_ROLE_ROOM_SERVER)
|
||||
# Every other role needs weak stubs for the companion-only UI mesh actions
|
||||
if(NOT CONFIG_ZEPHCORE_ROLE_COMPANION)
|
||||
target_sources(app PRIVATE
|
||||
helpers/ui/ui_mesh_actions_stubs.c
|
||||
)
|
||||
|
||||
@@ -161,6 +161,14 @@ config ZEPHCORE_ROLE_ROOM_SERVER
|
||||
per-client sync cursor. No BLE — configured via USB serial CLI.
|
||||
Reuses the repeater's ACL, region filtering and CLI command set.
|
||||
|
||||
config ZEPHCORE_ROLE_KISS_MODEM
|
||||
bool "KISS modem (raw LoRa packets to a host over serial)"
|
||||
help
|
||||
KISS TNC on the USB/console serial port, as upstream MeshCore's
|
||||
kiss_modem: the host sends and receives raw packets, configures
|
||||
the radio and uses the node identity through the SetHardware
|
||||
extensions (docs/kiss_modem_protocol.md). No mesh, no BLE, no CLI.
|
||||
|
||||
endchoice
|
||||
|
||||
config ZEPHCORE_COMPANION_USB
|
||||
|
||||
@@ -81,6 +81,8 @@ public:
|
||||
|
||||
/* Extended API */
|
||||
bool isChannelActive(int threshold = 0);
|
||||
/* Instantaneous RSSI in dBm (-80 where the family cannot read it). */
|
||||
int16_t getCurrentRSSI() { return hwGetCurrentRSSI(); }
|
||||
|
||||
/* Power saving */
|
||||
void enableRxDutyCycle(bool enable);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Arduino Stream over an interrupt-driven Zephyr UART.
|
||||
*/
|
||||
|
||||
#include "ZephyrUartStream.h"
|
||||
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
|
||||
bool ZephyrUartStream::begin(const struct device *dev, void (*on_rx)(void),
|
||||
void (*on_tx_idle)(void))
|
||||
{
|
||||
if (!device_is_ready(dev)) {
|
||||
return false;
|
||||
}
|
||||
_dev = dev;
|
||||
_on_rx = on_rx;
|
||||
_on_tx_idle = on_tx_idle;
|
||||
ring_buf_init(&_rx_ring, sizeof(_rx_data), _rx_data);
|
||||
ring_buf_init(&_tx_ring, sizeof(_tx_data), _tx_data);
|
||||
k_sem_init(&_tx_drained, 0, 1);
|
||||
uart_irq_callback_user_data_set(dev, isr, this);
|
||||
uart_irq_rx_enable(dev);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ZephyrUartStream::isr(const struct device *dev, void *user_data)
|
||||
{
|
||||
ZephyrUartStream *self = static_cast<ZephyrUartStream *>(user_data);
|
||||
|
||||
for (;;) {
|
||||
uart_irq_update(dev);
|
||||
if (uart_irq_is_pending(dev) <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (uart_irq_rx_ready(dev)) {
|
||||
uint8_t buf[64];
|
||||
uint32_t room = MIN(sizeof(buf), ring_buf_space_get(&self->_rx_ring));
|
||||
|
||||
if (room == 0) {
|
||||
/* read() re-enables RX once it has made room. */
|
||||
atomic_set(&self->_rx_paused, 1);
|
||||
uart_irq_rx_disable(dev);
|
||||
} else {
|
||||
int n = uart_fifo_read(dev, buf, room);
|
||||
|
||||
if (n > 0) {
|
||||
ring_buf_put(&self->_rx_ring, buf, n);
|
||||
}
|
||||
}
|
||||
if (self->_on_rx) {
|
||||
self->_on_rx();
|
||||
}
|
||||
}
|
||||
|
||||
if (uart_irq_tx_ready(dev)) {
|
||||
uint8_t *out;
|
||||
bool empty;
|
||||
k_spinlock_key_t key = k_spin_lock(&self->_tx_lock);
|
||||
uint32_t claimed = MIN(ring_buf_get_ptr(&self->_tx_ring, &out, 0), 64U);
|
||||
|
||||
if (claimed > 0) {
|
||||
int sent = uart_fifo_fill(dev, out, claimed);
|
||||
|
||||
if (sent > 0) {
|
||||
ring_buf_consume(&self->_tx_ring, sent);
|
||||
}
|
||||
}
|
||||
empty = ring_buf_is_empty(&self->_tx_ring);
|
||||
if (empty) {
|
||||
/* Inside the lock: write() enables after its put. */
|
||||
uart_irq_tx_disable(dev);
|
||||
}
|
||||
k_spin_unlock(&self->_tx_lock, key);
|
||||
|
||||
if (empty) {
|
||||
k_sem_give(&self->_tx_drained);
|
||||
if (self->_on_tx_idle) {
|
||||
self->_on_tx_idle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t ZephyrUartStream::write(const uint8_t *buffer, size_t size)
|
||||
{
|
||||
if (!_dev || size == 0) {
|
||||
return 0;
|
||||
}
|
||||
k_spinlock_key_t key = k_spin_lock(&_tx_lock);
|
||||
uint32_t n = ring_buf_put(&_tx_ring, buffer, size);
|
||||
|
||||
k_spin_unlock(&_tx_lock, key);
|
||||
if (n > 0) {
|
||||
uart_irq_tx_enable(_dev);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int ZephyrUartStream::availableForWrite()
|
||||
{
|
||||
if (!_dev) {
|
||||
return 0;
|
||||
}
|
||||
k_spinlock_key_t key = k_spin_lock(&_tx_lock);
|
||||
uint32_t space = ring_buf_space_get(&_tx_ring);
|
||||
|
||||
k_spin_unlock(&_tx_lock, key);
|
||||
return (int)space;
|
||||
}
|
||||
|
||||
int ZephyrUartStream::available()
|
||||
{
|
||||
return _dev ? (int)ring_buf_size_get(&_rx_ring) : 0;
|
||||
}
|
||||
|
||||
int ZephyrUartStream::read()
|
||||
{
|
||||
uint8_t b;
|
||||
|
||||
if (!_dev || ring_buf_get(&_rx_ring, &b, 1) != 1) {
|
||||
return -1;
|
||||
}
|
||||
if (atomic_cas(&_rx_paused, 1, 0)) {
|
||||
uart_irq_rx_enable(_dev);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
int ZephyrUartStream::peek()
|
||||
{
|
||||
uint8_t b;
|
||||
|
||||
if (!_dev || ring_buf_peek(&_rx_ring, &b, 1) != 1) {
|
||||
return -1;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
void ZephyrUartStream::flush()
|
||||
{
|
||||
if (!_dev) {
|
||||
return;
|
||||
}
|
||||
k_sem_reset(&_tx_drained);
|
||||
k_spinlock_key_t key = k_spin_lock(&_tx_lock);
|
||||
bool empty = ring_buf_is_empty(&_tx_ring);
|
||||
|
||||
k_spin_unlock(&_tx_lock, key);
|
||||
if (!empty) {
|
||||
k_sem_take(&_tx_drained, K_MSEC(FLUSH_TIMEOUT_MS));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Arduino Stream over an interrupt-driven Zephyr UART (CDC-ACM or plain UART),
|
||||
* for code ported from upstream that talks to `Serial` (the KISS modem).
|
||||
*
|
||||
* RX and TX each go through a ring drained by the UART ISR. A full RX ring
|
||||
* stops reading the port until the main thread has made room, so the host is
|
||||
* held off instead of losing bytes. Writes take what fits (availableForWrite()).
|
||||
* The callbacks run in the ISR: post events only.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/sys/ring_buffer.h>
|
||||
#include <Stream.h>
|
||||
|
||||
class ZephyrUartStream : public Stream {
|
||||
public:
|
||||
/* on_rx: bytes arrived. on_tx_idle: the TX ring has drained. */
|
||||
bool begin(const struct device *dev, void (*on_rx)(void), void (*on_tx_idle)(void));
|
||||
|
||||
using Print::write;
|
||||
size_t write(uint8_t c) override { return write(&c, 1); }
|
||||
size_t write(const uint8_t *buffer, size_t size) override;
|
||||
int availableForWrite() override;
|
||||
|
||||
int available() override;
|
||||
int read() override;
|
||||
int peek() override;
|
||||
/* Waits for the TX ring to drain, at most FLUSH_TIMEOUT_MS. */
|
||||
void flush() override;
|
||||
|
||||
private:
|
||||
static constexpr size_t RX_BUF_SIZE = 512;
|
||||
static constexpr size_t TX_BUF_SIZE = 1024;
|
||||
static constexpr uint32_t FLUSH_TIMEOUT_MS = 200;
|
||||
|
||||
static void isr(const struct device *dev, void *user_data);
|
||||
|
||||
const struct device *_dev = nullptr;
|
||||
void (*_on_rx)(void) = nullptr;
|
||||
void (*_on_tx_idle)(void) = nullptr;
|
||||
uint8_t _rx_data[RX_BUF_SIZE];
|
||||
uint8_t _tx_data[TX_BUF_SIZE];
|
||||
struct ring_buf _rx_ring;
|
||||
struct ring_buf _tx_ring;
|
||||
struct k_spinlock _tx_lock;
|
||||
struct k_sem _tx_drained;
|
||||
atomic_t _rx_paused = ATOMIC_INIT(0);
|
||||
};
|
||||
@@ -0,0 +1,794 @@
|
||||
#include "KissModem.h"
|
||||
#include <CayenneLPP.h>
|
||||
|
||||
// ZEPHCORE: Arduino's timing calls, and the shared radio preset ranges.
|
||||
#include <math.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <helpers/NodePrefs.h>
|
||||
static inline uint32_t millis() { return k_uptime_get_32(); }
|
||||
static inline void delay(uint32_t ms) { k_msleep(ms); }
|
||||
|
||||
KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
|
||||
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors)
|
||||
: _serial(serial), _identity(identity), _rng(rng), _radio(radio), _board(board), _sensors(sensors) {
|
||||
_rx_len = 0;
|
||||
_rx_escaped = false;
|
||||
_rx_active = false;
|
||||
_has_pending_tx = false;
|
||||
_pending_tx_len = 0;
|
||||
_txdelay = KISS_DEFAULT_TXDELAY;
|
||||
_persistence = KISS_DEFAULT_PERSISTENCE;
|
||||
_slottime = KISS_DEFAULT_SLOTTIME;
|
||||
_txtail = 0;
|
||||
_fullduplex = 0;
|
||||
_tx_state = TX_IDLE;
|
||||
_tx_timer = 0;
|
||||
_tx_csma_start = 0; // ZEPHCORE
|
||||
_setRadioCallback = nullptr;
|
||||
_setTxPowerCallback = nullptr;
|
||||
_getCurrentRssiCallback = nullptr;
|
||||
_getStatsCallback = nullptr;
|
||||
_config = {0, 0, 0, 0, 0};
|
||||
_signal_report_enabled = true;
|
||||
resetOutputQueue();
|
||||
}
|
||||
|
||||
void KissModem::begin() {
|
||||
_rx_len = 0;
|
||||
_rx_escaped = false;
|
||||
_rx_active = false;
|
||||
_has_pending_tx = false;
|
||||
_tx_state = TX_IDLE;
|
||||
resetOutputQueue();
|
||||
}
|
||||
|
||||
void KissModem::resetOutputQueue() {
|
||||
_tx_frame_head = 0;
|
||||
_tx_frame_tail = 0;
|
||||
_tx_frame_count = 0;
|
||||
_tx_busy_error_pending = false;
|
||||
_tx_done_pending = false;
|
||||
_tx_done_result = 0;
|
||||
}
|
||||
|
||||
void KissModem::popTxFrame() {
|
||||
_tx_frame_head = (uint8_t)((_tx_frame_head + 1) % KISS_TX_FRAME_QUEUE_DEPTH);
|
||||
_tx_frame_count--;
|
||||
}
|
||||
|
||||
uint16_t KissModem::appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b) {
|
||||
if (b == KISS_FEND || b == KISS_FESC) {
|
||||
if (idx + 2 > max_len) {
|
||||
return 0;
|
||||
}
|
||||
dest[idx++] = KISS_FESC;
|
||||
dest[idx++] = (b == KISS_FEND) ? KISS_TFEND : KISS_TFESC;
|
||||
return idx;
|
||||
}
|
||||
if (idx + 1 > max_len) {
|
||||
return 0;
|
||||
}
|
||||
dest[idx++] = b;
|
||||
return idx;
|
||||
}
|
||||
|
||||
uint16_t KissModem::encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len) {
|
||||
if (max_len < KISS_FRAME_BOUNDARY_BYTES) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t idx = 0;
|
||||
dest[idx++] = KISS_FEND;
|
||||
|
||||
idx = appendEscapedByte(dest, idx, max_len, type);
|
||||
if (idx == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (uint16_t i = 0; i < len; i++) {
|
||||
idx = appendEscapedByte(dest, idx, max_len, data[i]);
|
||||
if (idx == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (idx + 1 > max_len) {
|
||||
return 0;
|
||||
}
|
||||
dest[idx++] = KISS_FEND;
|
||||
return idx;
|
||||
}
|
||||
|
||||
bool KissModem::tryFlushFrames() {
|
||||
while (_tx_frame_count > 0) {
|
||||
const uint8_t idx = _tx_frame_head;
|
||||
const uint16_t frame_len = _tx_frame_len[idx];
|
||||
uint16_t written_len = _tx_frame_written[idx];
|
||||
|
||||
if (written_len >= frame_len) {
|
||||
popTxFrame();
|
||||
continue;
|
||||
}
|
||||
|
||||
const int available = _serial.availableForWrite();
|
||||
if (available <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint16_t remaining = frame_len - written_len;
|
||||
const uint16_t chunk_len = (available < (int)remaining) ? (uint16_t)available : remaining;
|
||||
if (chunk_len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t chunk_written = _serial.write(_tx_frame_buf[idx] + written_len, chunk_len);
|
||||
if (chunk_written == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
written_len += (uint16_t)chunk_written;
|
||||
_tx_frame_written[idx] = written_len;
|
||||
|
||||
if (written_len < frame_len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
popTxFrame();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KissModem::queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error) {
|
||||
if (_tx_frame_count >= KISS_TX_FRAME_QUEUE_DEPTH && !tryFlushFrames()) {
|
||||
if (mark_busy_error) {
|
||||
_tx_busy_error_pending = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const uint8_t idx = _tx_frame_tail;
|
||||
uint16_t frame_len = encodeFrame(type, data, len, _tx_frame_buf[idx], sizeof(_tx_frame_buf[idx]));
|
||||
if (frame_len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_tx_frame_len[idx] = frame_len;
|
||||
_tx_frame_written[idx] = 0;
|
||||
_tx_frame_tail = (uint8_t)((_tx_frame_tail + 1) % KISS_TX_FRAME_QUEUE_DEPTH);
|
||||
_tx_frame_count++;
|
||||
tryFlushFrames();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KissModem::queuePendingBusyError() {
|
||||
if (!_tx_busy_error_pending) {
|
||||
return true;
|
||||
}
|
||||
const uint8_t err = HW_ERR_TX_BUSY;
|
||||
if (!queueHardwareFrame(HW_RESP_ERROR, &err, 1, false)) {
|
||||
return false;
|
||||
}
|
||||
_tx_busy_error_pending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KissModem::queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error) {
|
||||
if (len > KISS_MAX_FRAME_SIZE) {
|
||||
return false;
|
||||
}
|
||||
_tx_hw_payload[0] = sub_cmd;
|
||||
if (len > 0) {
|
||||
memcpy(_tx_hw_payload + 1, data, len);
|
||||
}
|
||||
return queueFrame(KISS_CMD_SETHARDWARE, _tx_hw_payload, len + 1, mark_busy_error);
|
||||
}
|
||||
|
||||
bool KissModem::queuePendingTxDone() {
|
||||
if (!_tx_done_pending) {
|
||||
return true;
|
||||
}
|
||||
if (!queueHardwareFrame(HW_RESP_TX_DONE, &_tx_done_result, 1, false)) {
|
||||
return false;
|
||||
}
|
||||
_tx_done_pending = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void KissModem::setTxDonePending(uint8_t result) {
|
||||
_tx_done_result = result;
|
||||
_tx_done_pending = true;
|
||||
_tx_state = TX_DONE_PENDING;
|
||||
}
|
||||
|
||||
bool KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
|
||||
return queueHardwareFrame(sub_cmd, data, len, true);
|
||||
}
|
||||
|
||||
void KissModem::writeHardwareError(uint8_t error_code) {
|
||||
writeHardwareFrame(HW_RESP_ERROR, &error_code, 1);
|
||||
}
|
||||
|
||||
void KissModem::loop() {
|
||||
tryFlushFrames();
|
||||
|
||||
while (_serial.available()) {
|
||||
uint8_t b = _serial.read();
|
||||
|
||||
if (b == KISS_FEND) {
|
||||
if (_rx_active && _rx_len > 0) {
|
||||
processFrame();
|
||||
}
|
||||
_rx_len = 0;
|
||||
_rx_escaped = false;
|
||||
_rx_active = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_rx_active) continue;
|
||||
|
||||
if (b == KISS_FESC) {
|
||||
_rx_escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_rx_escaped) {
|
||||
_rx_escaped = false;
|
||||
if (b == KISS_TFEND) b = KISS_FEND;
|
||||
else if (b == KISS_TFESC) b = KISS_FESC;
|
||||
else continue;
|
||||
}
|
||||
|
||||
if (_rx_len < KISS_MAX_FRAME_SIZE) {
|
||||
_rx_buf[_rx_len++] = b;
|
||||
} else {
|
||||
/* Buffer full with no FEND; reset so we don't stay stuck ignoring input. */
|
||||
_rx_len = 0;
|
||||
_rx_escaped = false;
|
||||
_rx_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
processTx();
|
||||
tryFlushFrames();
|
||||
queuePendingBusyError();
|
||||
}
|
||||
|
||||
void KissModem::processFrame() {
|
||||
if (_rx_len < 1) return;
|
||||
|
||||
uint8_t type_byte = _rx_buf[0];
|
||||
|
||||
if (type_byte == KISS_CMD_RETURN) return;
|
||||
|
||||
uint8_t port = (type_byte >> 4) & 0x0F;
|
||||
uint8_t cmd = type_byte & 0x0F;
|
||||
|
||||
if (port != 0) return;
|
||||
|
||||
const uint8_t* data = &_rx_buf[1];
|
||||
uint16_t data_len = _rx_len - 1;
|
||||
|
||||
switch (cmd) {
|
||||
case KISS_CMD_DATA:
|
||||
if (data_len > 0 && data_len <= KISS_MAX_PACKET_SIZE && !_has_pending_tx) {
|
||||
memcpy(_pending_tx, data, data_len);
|
||||
_pending_tx_len = data_len;
|
||||
_has_pending_tx = true;
|
||||
} else if (_has_pending_tx) {
|
||||
writeHardwareError(HW_ERR_TX_BUSY);
|
||||
}
|
||||
break;
|
||||
|
||||
case KISS_CMD_TXDELAY:
|
||||
if (data_len >= 1) _txdelay = data[0];
|
||||
break;
|
||||
|
||||
case KISS_CMD_PERSISTENCE:
|
||||
if (data_len >= 1) _persistence = data[0];
|
||||
break;
|
||||
|
||||
case KISS_CMD_SLOTTIME:
|
||||
if (data_len >= 1) _slottime = data[0];
|
||||
break;
|
||||
|
||||
case KISS_CMD_TXTAIL:
|
||||
if (data_len >= 1) _txtail = data[0];
|
||||
break;
|
||||
|
||||
case KISS_CMD_FULLDUPLEX:
|
||||
if (data_len >= 1) _fullduplex = data[0];
|
||||
break;
|
||||
|
||||
case KISS_CMD_SETHARDWARE:
|
||||
if (data_len >= 1) {
|
||||
handleHardwareCommand(data[0], data + 1, data_len - 1);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len) {
|
||||
switch (sub_cmd) {
|
||||
case HW_CMD_GET_IDENTITY:
|
||||
handleGetIdentity();
|
||||
break;
|
||||
case HW_CMD_GET_RANDOM:
|
||||
handleGetRandom(data, len);
|
||||
break;
|
||||
case HW_CMD_VERIFY_SIGNATURE:
|
||||
handleVerifySignature(data, len);
|
||||
break;
|
||||
case HW_CMD_SIGN_DATA:
|
||||
handleSignData(data, len);
|
||||
break;
|
||||
case HW_CMD_ENCRYPT_DATA:
|
||||
handleEncryptData(data, len);
|
||||
break;
|
||||
case HW_CMD_DECRYPT_DATA:
|
||||
handleDecryptData(data, len);
|
||||
break;
|
||||
case HW_CMD_KEY_EXCHANGE:
|
||||
handleKeyExchange(data, len);
|
||||
break;
|
||||
case HW_CMD_HASH:
|
||||
handleHash(data, len);
|
||||
break;
|
||||
case HW_CMD_SET_RADIO:
|
||||
handleSetRadio(data, len);
|
||||
break;
|
||||
case HW_CMD_SET_TX_POWER:
|
||||
handleSetTxPower(data, len);
|
||||
break;
|
||||
case HW_CMD_GET_RADIO:
|
||||
handleGetRadio();
|
||||
break;
|
||||
case HW_CMD_GET_TX_POWER:
|
||||
handleGetTxPower();
|
||||
break;
|
||||
case HW_CMD_GET_VERSION:
|
||||
handleGetVersion();
|
||||
break;
|
||||
case HW_CMD_GET_CURRENT_RSSI:
|
||||
handleGetCurrentRssi();
|
||||
break;
|
||||
case HW_CMD_IS_CHANNEL_BUSY:
|
||||
handleIsChannelBusy();
|
||||
break;
|
||||
case HW_CMD_GET_AIRTIME:
|
||||
handleGetAirtime(data, len);
|
||||
break;
|
||||
case HW_CMD_GET_NOISE_FLOOR:
|
||||
handleGetNoiseFloor();
|
||||
break;
|
||||
case HW_CMD_GET_STATS:
|
||||
handleGetStats();
|
||||
break;
|
||||
case HW_CMD_GET_BATTERY:
|
||||
handleGetBattery();
|
||||
break;
|
||||
case HW_CMD_PING:
|
||||
handlePing();
|
||||
break;
|
||||
case HW_CMD_GET_SENSORS:
|
||||
handleGetSensors(data, len);
|
||||
break;
|
||||
case HW_CMD_GET_MCU_TEMP:
|
||||
handleGetMCUTemp();
|
||||
break;
|
||||
case HW_CMD_REBOOT:
|
||||
handleReboot();
|
||||
break;
|
||||
case HW_CMD_GET_DEVICE_NAME:
|
||||
handleGetDeviceName();
|
||||
break;
|
||||
case HW_CMD_SET_SIGNAL_REPORT:
|
||||
handleSetSignalReport(data, len);
|
||||
break;
|
||||
case HW_CMD_GET_SIGNAL_REPORT:
|
||||
handleGetSignalReport();
|
||||
break;
|
||||
default:
|
||||
writeHardwareError(HW_ERR_UNKNOWN_CMD);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::processTx() {
|
||||
switch (_tx_state) {
|
||||
case TX_IDLE:
|
||||
if (_has_pending_tx) {
|
||||
_tx_csma_start = millis(); // ZEPHCORE
|
||||
if (_fullduplex) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_DELAY;
|
||||
} else {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_WAIT_CLEAR;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_WAIT_CLEAR:
|
||||
if (!_radio.isReceiving()) {
|
||||
uint8_t rand_val;
|
||||
_rng.random(&rand_val, 1);
|
||||
if (rand_val <= _persistence) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_DELAY;
|
||||
} else {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_SLOT_WAIT;
|
||||
}
|
||||
} else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(KISS_MAX_PACKET_SIZE) * KISS_TX_TIMEOUT_FACTOR) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_DELAY;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_SLOT_WAIT:
|
||||
if (millis() - _tx_timer >= (uint32_t)_slottime * 10) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_WAIT_CLEAR;
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_DELAY:
|
||||
if (millis() - _tx_timer >= (uint32_t)_txdelay * 10) {
|
||||
if (_radio.startSendRaw(_pending_tx, _pending_tx_len)) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_SENDING;
|
||||
// ZEPHCORE: the radio refuses while it hears a signal (LBT) or the last TX
|
||||
// winds down; back to CSMA until the wait-clear bound, then fail.
|
||||
} else if (millis() - _tx_csma_start < _radio.getEstAirtimeFor(KISS_MAX_PACKET_SIZE) * KISS_TX_TIMEOUT_FACTOR) {
|
||||
_tx_timer = millis();
|
||||
_tx_state = TX_SLOT_WAIT;
|
||||
} else {
|
||||
setTxDonePending(0x00);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TX_SENDING: {
|
||||
// ZEPHCORE: as Dispatcher::loop(), time out only once the radio has concluded;
|
||||
// isTxActive() is read first because completion is published before it drops.
|
||||
bool tx_active = _radio.isTxActive();
|
||||
if (_radio.isSendComplete()) {
|
||||
_radio.onSendFinished();
|
||||
setTxDonePending(0x01);
|
||||
} else if (!tx_active && millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) {
|
||||
_radio.onSendFinished();
|
||||
setTxDonePending(0x00);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TX_DONE_PENDING:
|
||||
if (queuePendingTxDone()) {
|
||||
_has_pending_tx = false;
|
||||
_tx_state = TX_IDLE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) {
|
||||
if (queueFrame(KISS_CMD_DATA, packet, len) && _signal_report_enabled) {
|
||||
uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi };
|
||||
writeHardwareFrame(HW_RESP_RX_META, meta, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::handleGetIdentity() {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_IDENTITY), _identity.pub_key, PUB_KEY_SIZE);
|
||||
}
|
||||
|
||||
void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t requested = data[0];
|
||||
if (requested < 1 || requested > 64) {
|
||||
writeHardwareError(HW_ERR_INVALID_PARAM);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t buf[64];
|
||||
_rng.random(buf, requested);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_RANDOM), buf, requested);
|
||||
}
|
||||
|
||||
void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) {
|
||||
if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
mesh::Identity signer(data);
|
||||
const uint8_t* signature = data + PUB_KEY_SIZE;
|
||||
const uint8_t* msg = data + PUB_KEY_SIZE + SIGNATURE_SIZE;
|
||||
uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE;
|
||||
|
||||
uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_VERIFY_SIGNATURE), &result, 1);
|
||||
}
|
||||
|
||||
void KissModem::handleSignData(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t signature[SIGNATURE_SIZE];
|
||||
_identity.sign(signature, data, len);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_SIGN_DATA), signature, SIGNATURE_SIZE);
|
||||
}
|
||||
|
||||
void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) {
|
||||
if (len < PUB_KEY_SIZE + 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* key = data;
|
||||
const uint8_t* plaintext = data + PUB_KEY_SIZE;
|
||||
uint16_t plaintext_len = len - PUB_KEY_SIZE;
|
||||
|
||||
uint8_t buf[KISS_MAX_FRAME_SIZE];
|
||||
int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len);
|
||||
|
||||
if (encrypted_len > 0) {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_ENCRYPT_DATA), buf, encrypted_len);
|
||||
} else {
|
||||
writeHardwareError(HW_ERR_ENCRYPT_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) {
|
||||
if (len < PUB_KEY_SIZE + CIPHER_MAC_SIZE + 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* key = data;
|
||||
const uint8_t* ciphertext = data + PUB_KEY_SIZE;
|
||||
uint16_t ciphertext_len = len - PUB_KEY_SIZE;
|
||||
|
||||
uint8_t buf[KISS_MAX_FRAME_SIZE];
|
||||
int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len);
|
||||
|
||||
if (decrypted_len > 0) {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_DECRYPT_DATA), buf, decrypted_len);
|
||||
} else {
|
||||
writeHardwareError(HW_ERR_MAC_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) {
|
||||
if (len < PUB_KEY_SIZE) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t shared_secret[PUB_KEY_SIZE];
|
||||
_identity.calcSharedSecret(shared_secret, data);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_KEY_EXCHANGE), shared_secret, PUB_KEY_SIZE);
|
||||
}
|
||||
|
||||
void KissModem::handleHash(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t hash[32];
|
||||
mesh::Utils::sha256(hash, 32, data, len);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_HASH), hash, 32);
|
||||
}
|
||||
|
||||
void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) {
|
||||
if (len < 10) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
if (!_setRadioCallback) {
|
||||
writeHardwareError(HW_ERR_NO_CALLBACK);
|
||||
return;
|
||||
}
|
||||
// ZEPHCORE: refuse a preset outside the ranges every ZephCore setter accepts (NodePrefs.h).
|
||||
{
|
||||
uint32_t freq_hz, bw_hz;
|
||||
memcpy(&freq_hz, data, 4);
|
||||
memcpy(&bw_hz, data + 4, 4);
|
||||
float mhz = freq_hz / 1000000.0f, khz = bw_hz / 1000.0f;
|
||||
if (mhz < ZC_RADIO_FREQ_MIN_MHZ || mhz > ZC_RADIO_FREQ_MAX_MHZ || khz < ZC_RADIO_BW_MIN_KHZ ||
|
||||
khz > ZC_RADIO_BW_MAX_KHZ || data[8] < 5 || data[8] > 12 || data[9] < 5 || data[9] > 8) {
|
||||
writeHardwareError(HW_ERR_INVALID_PARAM);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(&_config.freq_hz, data, 4);
|
||||
memcpy(&_config.bw_hz, data + 4, 4);
|
||||
_config.sf = data[8];
|
||||
_config.cr = data[9];
|
||||
|
||||
_setRadioCallback(_config.freq_hz / 1000000.0f, _config.bw_hz / 1000.0f, _config.sf, _config.cr);
|
||||
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
|
||||
}
|
||||
|
||||
void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
if (!_setTxPowerCallback) {
|
||||
writeHardwareError(HW_ERR_NO_CALLBACK);
|
||||
return;
|
||||
}
|
||||
|
||||
_config.tx_power = data[0];
|
||||
_setTxPowerCallback(data[0]);
|
||||
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
|
||||
}
|
||||
|
||||
void KissModem::handleGetRadio() {
|
||||
uint8_t buf[10];
|
||||
memcpy(buf, &_config.freq_hz, 4);
|
||||
memcpy(buf + 4, &_config.bw_hz, 4);
|
||||
buf[8] = _config.sf;
|
||||
buf[9] = _config.cr;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_RADIO), buf, 10);
|
||||
}
|
||||
|
||||
void KissModem::handleGetTxPower() {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_TX_POWER), &_config.tx_power, 1);
|
||||
}
|
||||
|
||||
void KissModem::handleGetVersion() {
|
||||
uint8_t buf[2];
|
||||
buf[0] = KISS_FIRMWARE_VERSION;
|
||||
buf[1] = 0;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_VERSION), buf, 2);
|
||||
}
|
||||
|
||||
void KissModem::handleGetCurrentRssi() {
|
||||
if (!_getCurrentRssiCallback) {
|
||||
writeHardwareError(HW_ERR_NO_CALLBACK);
|
||||
return;
|
||||
}
|
||||
|
||||
float rssi = _getCurrentRssiCallback();
|
||||
int8_t rssi_byte = (int8_t)rssi;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_CURRENT_RSSI), (uint8_t*)&rssi_byte, 1);
|
||||
}
|
||||
|
||||
void KissModem::handleIsChannelBusy() {
|
||||
uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_IS_CHANNEL_BUSY), &busy, 1);
|
||||
}
|
||||
|
||||
void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t packet_len = data[0];
|
||||
uint32_t airtime = _radio.getEstAirtimeFor(packet_len);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_AIRTIME), (uint8_t*)&airtime, 4);
|
||||
}
|
||||
|
||||
void KissModem::handleGetNoiseFloor() {
|
||||
int16_t noise_floor = _radio.getNoiseFloor();
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_NOISE_FLOOR), (uint8_t*)&noise_floor, 2);
|
||||
}
|
||||
|
||||
void KissModem::handleGetStats() {
|
||||
if (!_getStatsCallback) {
|
||||
writeHardwareError(HW_ERR_NO_CALLBACK);
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t rx, tx, errors;
|
||||
_getStatsCallback(&rx, &tx, &errors);
|
||||
uint8_t buf[12];
|
||||
memcpy(buf, &rx, 4);
|
||||
memcpy(buf + 4, &tx, 4);
|
||||
memcpy(buf + 8, &errors, 4);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_STATS), buf, 12);
|
||||
}
|
||||
|
||||
void KissModem::handleGetBattery() {
|
||||
uint16_t mv = _board.getBattMilliVolts();
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_BATTERY), (uint8_t*)&mv, 2);
|
||||
}
|
||||
|
||||
void KissModem::handlePing() {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_PING), nullptr, 0);
|
||||
}
|
||||
|
||||
void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t permissions = data[0];
|
||||
CayenneLPP telemetry(255);
|
||||
if (_sensors.querySensors(permissions, telemetry)) {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), telemetry.getBuffer(), telemetry.getSize());
|
||||
} else {
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_SENSORS), nullptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void KissModem::handleGetMCUTemp() {
|
||||
float temp = _board.getMCUTemperature();
|
||||
if (isnan(temp)) {
|
||||
writeHardwareError(HW_ERR_NO_CALLBACK);
|
||||
return;
|
||||
}
|
||||
int16_t temp_tenths = (int16_t)(temp * 10.0f);
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_MCU_TEMP), (uint8_t*)&temp_tenths, 2);
|
||||
}
|
||||
|
||||
void KissModem::handleReboot() {
|
||||
writeHardwareFrame(HW_RESP_OK, nullptr, 0);
|
||||
_serial.flush();
|
||||
delay(50);
|
||||
_board.reboot();
|
||||
}
|
||||
|
||||
void KissModem::handleGetDeviceName() {
|
||||
const char* name = _board.getManufacturerName();
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_DEVICE_NAME), (const uint8_t*)name, strlen(name));
|
||||
}
|
||||
|
||||
void KissModem::handleSetSignalReport(const uint8_t* data, uint16_t len) {
|
||||
if (len < 1) {
|
||||
writeHardwareError(HW_ERR_INVALID_LENGTH);
|
||||
return;
|
||||
}
|
||||
_signal_report_enabled = (data[0] != 0x00);
|
||||
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
|
||||
}
|
||||
|
||||
void KissModem::handleGetSignalReport() {
|
||||
uint8_t val = _signal_report_enabled ? 0x01 : 0x00;
|
||||
writeHardwareFrame(HW_RESP(HW_CMD_GET_SIGNAL_REPORT), &val, 1);
|
||||
}
|
||||
|
||||
// ZEPHCORE: the deadlines processTx() polls millis() for. Channel-clear is
|
||||
// rechecked once per slot (at least the 10 ms KISS unit); TX_SENDING completes on
|
||||
// the radio's TX-done event; TX_DONE_PENDING waits for host output space.
|
||||
uint32_t KissModem::msUntilNextTx() const {
|
||||
uint32_t elapsed = millis() - _tx_timer;
|
||||
uint32_t wait;
|
||||
|
||||
switch (_tx_state) {
|
||||
case TX_WAIT_CLEAR: {
|
||||
uint32_t slot = _slottime ? (uint32_t)_slottime * 10 : 10;
|
||||
return slot - elapsed % slot;
|
||||
}
|
||||
case TX_SLOT_WAIT:
|
||||
wait = (uint32_t)_slottime * 10;
|
||||
break;
|
||||
case TX_DELAY:
|
||||
wait = (uint32_t)_txdelay * 10;
|
||||
break;
|
||||
case TX_SENDING:
|
||||
wait = _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR;
|
||||
if (elapsed >= wait) {
|
||||
return UINT32_MAX; // the radio is still concluding; its TX-done event follows
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return _has_pending_tx && _tx_state == TX_IDLE ? 0 : UINT32_MAX;
|
||||
}
|
||||
return elapsed >= wait ? 0 : wait - elapsed;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <mesh/Identity.h> // ZEPHCORE: was <Identity.h>
|
||||
#include <mesh/Utils.h> // ZEPHCORE: was <Utils.h>
|
||||
#include <mesh/Mesh.h> // ZEPHCORE: was <Mesh.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
|
||||
#define KISS_FEND 0xC0
|
||||
#define KISS_FESC 0xDB
|
||||
#define KISS_TFEND 0xDC
|
||||
#define KISS_TFESC 0xDD
|
||||
|
||||
#define KISS_MAX_FRAME_SIZE 512
|
||||
#define KISS_MAX_PACKET_SIZE 255
|
||||
#define KISS_FRAME_BOUNDARY_BYTES 2
|
||||
#define KISS_TYPE_BYTES 1
|
||||
#define KISS_HW_SUBCMD_BYTES 1
|
||||
#define KISS_MAX_ESCAPABLE_BYTES (KISS_MAX_FRAME_SIZE + KISS_TYPE_BYTES + KISS_HW_SUBCMD_BYTES)
|
||||
#define KISS_MAX_ESCAPED_PAYLOAD_SIZE (2 * KISS_MAX_ESCAPABLE_BYTES)
|
||||
#define KISS_MAX_ENCODED_FRAME_SIZE (KISS_FRAME_BOUNDARY_BYTES + KISS_MAX_ESCAPED_PAYLOAD_SIZE)
|
||||
#define KISS_TX_FRAME_QUEUE_DEPTH 2
|
||||
#define KISS_HW_MAX_PAYLOAD_SIZE (KISS_MAX_FRAME_SIZE + KISS_HW_SUBCMD_BYTES)
|
||||
|
||||
#define KISS_CMD_DATA 0x00
|
||||
#define KISS_CMD_TXDELAY 0x01
|
||||
#define KISS_CMD_PERSISTENCE 0x02
|
||||
#define KISS_CMD_SLOTTIME 0x03
|
||||
#define KISS_CMD_TXTAIL 0x04
|
||||
#define KISS_CMD_FULLDUPLEX 0x05
|
||||
#define KISS_CMD_SETHARDWARE 0x06
|
||||
#define KISS_CMD_RETURN 0xFF
|
||||
|
||||
#define KISS_DEFAULT_TXDELAY 50
|
||||
#define KISS_DEFAULT_PERSISTENCE 63
|
||||
#define KISS_DEFAULT_SLOTTIME 10
|
||||
#define KISS_TX_TIMEOUT_FACTOR 3/2 // 1.5x estimated airtime
|
||||
|
||||
#define HW_CMD_GET_IDENTITY 0x01
|
||||
#define HW_CMD_GET_RANDOM 0x02
|
||||
#define HW_CMD_VERIFY_SIGNATURE 0x03
|
||||
#define HW_CMD_SIGN_DATA 0x04
|
||||
#define HW_CMD_ENCRYPT_DATA 0x05
|
||||
#define HW_CMD_DECRYPT_DATA 0x06
|
||||
#define HW_CMD_KEY_EXCHANGE 0x07
|
||||
#define HW_CMD_HASH 0x08
|
||||
#define HW_CMD_SET_RADIO 0x09
|
||||
#define HW_CMD_SET_TX_POWER 0x0A
|
||||
#define HW_CMD_GET_RADIO 0x0B
|
||||
#define HW_CMD_GET_TX_POWER 0x0C
|
||||
#define HW_CMD_GET_CURRENT_RSSI 0x0D
|
||||
#define HW_CMD_IS_CHANNEL_BUSY 0x0E
|
||||
#define HW_CMD_GET_AIRTIME 0x0F
|
||||
#define HW_CMD_GET_NOISE_FLOOR 0x10
|
||||
#define HW_CMD_GET_VERSION 0x11
|
||||
#define HW_CMD_GET_STATS 0x12
|
||||
#define HW_CMD_GET_BATTERY 0x13
|
||||
#define HW_CMD_GET_MCU_TEMP 0x14
|
||||
#define HW_CMD_GET_SENSORS 0x15
|
||||
#define HW_CMD_GET_DEVICE_NAME 0x16
|
||||
#define HW_CMD_PING 0x17
|
||||
#define HW_CMD_REBOOT 0x18
|
||||
#define HW_CMD_SET_SIGNAL_REPORT 0x19
|
||||
#define HW_CMD_GET_SIGNAL_REPORT 0x1A
|
||||
|
||||
/* Response code = command code | 0x80. Generic / unsolicited use 0xF0+. */
|
||||
#define HW_RESP(cmd) ((cmd) | 0x80)
|
||||
|
||||
/* Generic responses (shared by multiple commands) */
|
||||
#define HW_RESP_OK 0xF0
|
||||
#define HW_RESP_ERROR 0xF1
|
||||
|
||||
/* Unsolicited notifications (no corresponding request) */
|
||||
#define HW_RESP_TX_DONE 0xF8
|
||||
#define HW_RESP_RX_META 0xF9
|
||||
|
||||
#define HW_ERR_INVALID_LENGTH 0x01
|
||||
#define HW_ERR_INVALID_PARAM 0x02
|
||||
#define HW_ERR_NO_CALLBACK 0x03
|
||||
#define HW_ERR_MAC_FAILED 0x04
|
||||
#define HW_ERR_UNKNOWN_CMD 0x05
|
||||
#define HW_ERR_ENCRYPT_FAILED 0x06
|
||||
#define HW_ERR_TX_BUSY 0x07
|
||||
|
||||
#define KISS_FIRMWARE_VERSION 1
|
||||
|
||||
typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr);
|
||||
typedef void (*SetTxPowerCallback)(uint8_t power);
|
||||
typedef float (*GetCurrentRssiCallback)();
|
||||
typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors);
|
||||
|
||||
struct RadioConfig {
|
||||
uint32_t freq_hz;
|
||||
uint32_t bw_hz;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t tx_power;
|
||||
};
|
||||
|
||||
enum TxState {
|
||||
TX_IDLE,
|
||||
TX_WAIT_CLEAR,
|
||||
TX_SLOT_WAIT,
|
||||
TX_DELAY,
|
||||
TX_SENDING,
|
||||
TX_DONE_PENDING
|
||||
};
|
||||
|
||||
class KissModem {
|
||||
Stream& _serial;
|
||||
mesh::LocalIdentity& _identity;
|
||||
mesh::RNG& _rng;
|
||||
mesh::Radio& _radio;
|
||||
mesh::MainBoard& _board;
|
||||
SensorManager& _sensors;
|
||||
|
||||
uint8_t _rx_buf[KISS_MAX_FRAME_SIZE];
|
||||
uint16_t _rx_len;
|
||||
bool _rx_escaped;
|
||||
bool _rx_active;
|
||||
|
||||
uint8_t _pending_tx[KISS_MAX_PACKET_SIZE];
|
||||
uint16_t _pending_tx_len;
|
||||
bool _has_pending_tx;
|
||||
|
||||
uint8_t _txdelay;
|
||||
uint8_t _persistence;
|
||||
uint8_t _slottime;
|
||||
uint8_t _txtail;
|
||||
uint8_t _fullduplex;
|
||||
|
||||
TxState _tx_state;
|
||||
uint32_t _tx_timer;
|
||||
uint32_t _tx_csma_start; // ZEPHCORE: start of the CSMA wait, bounds LBT refusals
|
||||
|
||||
SetRadioCallback _setRadioCallback;
|
||||
SetTxPowerCallback _setTxPowerCallback;
|
||||
GetCurrentRssiCallback _getCurrentRssiCallback;
|
||||
GetStatsCallback _getStatsCallback;
|
||||
|
||||
RadioConfig _config;
|
||||
bool _signal_report_enabled;
|
||||
uint8_t _tx_frame_buf[KISS_TX_FRAME_QUEUE_DEPTH][KISS_MAX_ENCODED_FRAME_SIZE];
|
||||
uint16_t _tx_frame_len[KISS_TX_FRAME_QUEUE_DEPTH];
|
||||
uint16_t _tx_frame_written[KISS_TX_FRAME_QUEUE_DEPTH];
|
||||
uint8_t _tx_frame_head;
|
||||
uint8_t _tx_frame_tail;
|
||||
uint8_t _tx_frame_count;
|
||||
bool _tx_busy_error_pending;
|
||||
bool _tx_done_pending;
|
||||
uint8_t _tx_done_result;
|
||||
uint8_t _tx_hw_payload[KISS_HW_MAX_PAYLOAD_SIZE];
|
||||
|
||||
static uint16_t appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b);
|
||||
static uint16_t encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len);
|
||||
void resetOutputQueue();
|
||||
void popTxFrame();
|
||||
bool tryFlushFrames();
|
||||
bool queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error = true);
|
||||
bool queuePendingBusyError();
|
||||
bool queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error);
|
||||
bool queuePendingTxDone();
|
||||
void setTxDonePending(uint8_t result);
|
||||
bool writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
|
||||
void writeHardwareError(uint8_t error_code);
|
||||
void processFrame();
|
||||
void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len);
|
||||
void processTx();
|
||||
|
||||
void handleGetIdentity();
|
||||
void handleGetRandom(const uint8_t* data, uint16_t len);
|
||||
void handleVerifySignature(const uint8_t* data, uint16_t len);
|
||||
void handleSignData(const uint8_t* data, uint16_t len);
|
||||
void handleEncryptData(const uint8_t* data, uint16_t len);
|
||||
void handleDecryptData(const uint8_t* data, uint16_t len);
|
||||
void handleKeyExchange(const uint8_t* data, uint16_t len);
|
||||
void handleHash(const uint8_t* data, uint16_t len);
|
||||
void handleSetRadio(const uint8_t* data, uint16_t len);
|
||||
void handleSetTxPower(const uint8_t* data, uint16_t len);
|
||||
void handleGetRadio();
|
||||
void handleGetTxPower();
|
||||
void handleGetVersion();
|
||||
void handleGetCurrentRssi();
|
||||
void handleIsChannelBusy();
|
||||
void handleGetAirtime(const uint8_t* data, uint16_t len);
|
||||
void handleGetNoiseFloor();
|
||||
void handleGetStats();
|
||||
void handleGetBattery();
|
||||
void handlePing();
|
||||
void handleGetSensors(const uint8_t* data, uint16_t len);
|
||||
void handleGetMCUTemp();
|
||||
void handleReboot();
|
||||
void handleGetDeviceName();
|
||||
void handleSetSignalReport(const uint8_t* data, uint16_t len);
|
||||
void handleGetSignalReport();
|
||||
|
||||
public:
|
||||
KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng,
|
||||
mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors);
|
||||
|
||||
void begin();
|
||||
void loop();
|
||||
|
||||
void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; }
|
||||
void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; }
|
||||
void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; }
|
||||
void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; }
|
||||
|
||||
void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len);
|
||||
bool isTxBusy() const { return _tx_state != TX_IDLE; }
|
||||
/** True only when radio is actually transmitting; use to skip recvRaw in main loop. */
|
||||
bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; }
|
||||
bool isHostOutputBackedUp() const { return _tx_frame_count > 0 || _tx_busy_error_pending || _tx_done_pending; }
|
||||
// ZEPHCORE: ms until processTx() has a timed transition, 0 = due now,
|
||||
// UINT32_MAX = none (it waits on a radio or host event). The event loop arms its wake on it.
|
||||
uint32_t msUntilNextTx() const;
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
# KISS modem role: raw LoRa packets to a host over the USB/console serial port
|
||||
# (upstream MeshCore examples/kiss_modem, protocol in docs/kiss_modem_protocol.md).
|
||||
#
|
||||
# Build with: -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf"
|
||||
|
||||
CONFIG_ZEPHCORE_ROLE_KISS_MODEM=y
|
||||
|
||||
# No BLE; entropy explicitly, as BT no longer pulls it in.
|
||||
CONFIG_BT=n
|
||||
CONFIG_ENTROPY_GENERATOR=y
|
||||
|
||||
# The port carries the binary KISS stream: the console driver must not also
|
||||
# write to it, or boot and printk bytes land in the stream.
|
||||
CONFIG_UART_CONSOLE=n
|
||||
CONFIG_SHELL=n
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
return n;
|
||||
}
|
||||
size_t write(const char *str) { return str ? write((const uint8_t *)str, strlen(str)) : 0; }
|
||||
virtual int availableForWrite() { return 0; }
|
||||
|
||||
size_t print(char c) { return write((uint8_t)c); }
|
||||
size_t print(const char *str) { return write(str); }
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MIT
|
||||
* ZephCore - KISS modem
|
||||
*
|
||||
* Composition root of the KISS modem role (upstream examples/kiss_modem): raw
|
||||
* LoRa packets to and from a host over the USB/console serial port, plus
|
||||
* MeshCore's SetHardware extensions (KissModem). No mesh, no prefs: the host
|
||||
* configures the radio, the identity is the server roles' one.
|
||||
*
|
||||
* Event loop: serial input, LoRa RX/TX-done, host output drained and a one-shot
|
||||
* for the soonest modem or radio-maintenance deadline each wake KissModem::loop().
|
||||
*/
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
LOG_MODULE_REGISTER(zephcore_kiss_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
|
||||
#define ZEPHCORE_USB_STACK \
|
||||
(IS_ENABLED(CONFIG_USB_CDC_ACM) || IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS))
|
||||
|
||||
#if ZEPHCORE_USB_STACK && !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
|
||||
#include <ZephyrUSBCDC.h>
|
||||
#endif
|
||||
|
||||
#include <app/KissModem.h>
|
||||
#include <app/RepeaterDataStore.h>
|
||||
#include <src/RadioIncludes.h>
|
||||
#include <src/mesh_events.h>
|
||||
#include <helpers/boot_prefs.h>
|
||||
#include <helpers/NodePrefs.h>
|
||||
#include <adapters/transport/ZephyrUartStream.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
|
||||
#define MESH_EVENT_HOST_TX_IDLE BIT(MESH_EVENT_ROLE_BASE) /* host output drained */
|
||||
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | \
|
||||
MESH_EVENT_MAINTENANCE | MESH_EVENT_HOST_TX_IDLE)
|
||||
|
||||
static struct k_event mesh_events;
|
||||
|
||||
static void post_lora_rx(void *user_data)
|
||||
{
|
||||
ARG_UNUSED(user_data);
|
||||
k_event_post(&mesh_events, MESH_EVENT_LORA_RX);
|
||||
}
|
||||
|
||||
static void post_lora_tx_done(void *user_data)
|
||||
{
|
||||
ARG_UNUSED(user_data);
|
||||
k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE);
|
||||
}
|
||||
|
||||
static void post_host_rx(void)
|
||||
{
|
||||
k_event_post(&mesh_events, MESH_EVENT_CLI_RX);
|
||||
}
|
||||
|
||||
static void post_host_tx_idle(void)
|
||||
{
|
||||
k_event_post(&mesh_events, MESH_EVENT_HOST_TX_IDLE);
|
||||
}
|
||||
|
||||
static void wake_timer_fn(struct k_timer *timer)
|
||||
{
|
||||
ARG_UNUSED(timer);
|
||||
k_event_post(&mesh_events, MESH_EVENT_MAINTENANCE);
|
||||
}
|
||||
|
||||
K_TIMER_DEFINE(wake_timer, wake_timer_fn, NULL);
|
||||
|
||||
static mesh::ZephyrBoard board;
|
||||
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
|
||||
static mesh::LoRaRadio radio(lora_dev, board);
|
||||
static mesh::ZephyrRNG rng;
|
||||
static mesh::LocalIdentity identity;
|
||||
static RepeaterDataStore data_store;
|
||||
static ZephyrUartStream host;
|
||||
/* RAM only: the radio reads its parameters here, the host sets them. */
|
||||
static NodePrefs prefs;
|
||||
static KissModem modem(host, identity, rng, radio, board, sensors);
|
||||
|
||||
static void apply_cad_prefs(void)
|
||||
{
|
||||
radio.setCadParams(prefs.cad_auto != 0, prefs.cad_offset, prefs.probe_interval,
|
||||
prefs.cad_busycap, prefs.cad_base);
|
||||
}
|
||||
|
||||
static void on_set_radio(float freq, float bw, uint8_t sf, uint8_t cr)
|
||||
{
|
||||
bool new_channel = freq != prefs.freq || bw != prefs.bw || sf != prefs.sf;
|
||||
|
||||
prefs.freq = freq;
|
||||
prefs.bw = bw;
|
||||
prefs.sf = sf;
|
||||
prefs.cr = cr;
|
||||
/* The learned CAD offset belongs to the old channel, as `set radio`. */
|
||||
if (new_channel) {
|
||||
apply_cad_prefs();
|
||||
}
|
||||
radio.reconfigure();
|
||||
}
|
||||
|
||||
static void on_set_tx_power(uint8_t power)
|
||||
{
|
||||
/* The radio clamps to the board's limit when it next configures TX. */
|
||||
prefs.tx_power_dbm = (int8_t)MIN(power, 30);
|
||||
}
|
||||
|
||||
static float on_get_current_rssi(void)
|
||||
{
|
||||
return radio.getCurrentRSSI();
|
||||
}
|
||||
|
||||
static void on_get_stats(uint32_t *rx, uint32_t *tx, uint32_t *errors)
|
||||
{
|
||||
*rx = radio.getPacketsRecv();
|
||||
*tx = radio.getPacketsSent();
|
||||
*errors = radio.getPacketsRecvErrors();
|
||||
}
|
||||
|
||||
static void arm_wake(void)
|
||||
{
|
||||
uint32_t next = MIN(modem.msUntilNextTx(), radio.msUntilNextMaintenance());
|
||||
|
||||
if (next >= mesh::MAINTENANCE_IDLE) {
|
||||
k_timer_stop(&wake_timer);
|
||||
return;
|
||||
}
|
||||
k_timer_start(&wake_timer, K_MSEC(MAX(next, 1U)), K_NO_WAIT);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
board.clearBootloaderMagic();
|
||||
|
||||
#if ZEPHCORE_USB_STACK && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) && \
|
||||
!IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
|
||||
zephcore_usbd_init();
|
||||
const struct device *host_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
|
||||
#else
|
||||
const struct device *host_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
|
||||
#endif
|
||||
|
||||
if (!data_store.hasRoleData()) {
|
||||
LOG_WRN("Volume holds no data for this role - formatting before first boot");
|
||||
if (!zephcore_fs_format_all(nullptr)) {
|
||||
LOG_ERR("First-boot format failed - /lfs is not mounted");
|
||||
}
|
||||
}
|
||||
if (!data_store.begin()) {
|
||||
LOG_ERR("RepeaterDataStore init failed");
|
||||
}
|
||||
if (!data_store.loadIdentity(identity)) {
|
||||
mesh::ZephyrRNG::generateFirstBootIdentity(identity);
|
||||
data_store.saveIdentity(identity);
|
||||
}
|
||||
|
||||
initNodePrefs(&prefs);
|
||||
apply_boot_prefs(&prefs, false);
|
||||
gps_park();
|
||||
env_sensors_init();
|
||||
|
||||
k_event_init(&mesh_events);
|
||||
radio.setRxCallback(post_lora_rx, nullptr);
|
||||
radio.setTxDoneCallback(post_lora_tx_done, nullptr);
|
||||
radio.setPrefs(&prefs);
|
||||
radio.begin();
|
||||
radio.setRxBoost(prefs.rx_boost != 0);
|
||||
radio.setFemRxEnable(prefs.fem_rxgain != 0);
|
||||
apply_cad_prefs();
|
||||
|
||||
if (!host.begin(host_dev, post_host_rx, post_host_tx_idle)) {
|
||||
LOG_ERR("Host serial not ready");
|
||||
}
|
||||
|
||||
modem.setRadioCallback(on_set_radio);
|
||||
modem.setTxPowerCallback(on_set_tx_power);
|
||||
modem.setGetCurrentRssiCallback(on_get_current_rssi);
|
||||
modem.setGetStatsCallback(on_get_stats);
|
||||
modem.begin();
|
||||
|
||||
for (;;) {
|
||||
/* processTx() takes one state step per call; the second pass takes
|
||||
* the step the first one made due (a new frame, a finished TX). */
|
||||
modem.loop();
|
||||
modem.loop();
|
||||
|
||||
uint8_t rx_buf[256];
|
||||
int rx_len;
|
||||
|
||||
while (!modem.isHostOutputBackedUp() &&
|
||||
(rx_len = radio.recvRaw(rx_buf, sizeof(rx_buf))) > 0) {
|
||||
int8_t snr = (int8_t)(radio.getLastSNR() * 4);
|
||||
int8_t rssi = (int8_t)radio.getLastRSSI();
|
||||
|
||||
modem.onPacketReceived(snr, rssi, rx_buf, rx_len);
|
||||
}
|
||||
|
||||
radio.triggerNoiseFloorCalibrate(0);
|
||||
radio.cadMaintenance();
|
||||
radio.radioMaintenance();
|
||||
|
||||
arm_wake();
|
||||
uint32_t ev = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER);
|
||||
|
||||
k_event_clear(&mesh_events, ev);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user