mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 22:20:06 +00:00
kiss updates
This commit is contained in:
@@ -164,6 +164,11 @@ jobs:
|
||||
- name: build firmwares
|
||||
run: bash build.sh nrf
|
||||
|
||||
# Roles no manifest publishes still have to compile. Built, not packaged:
|
||||
# nothing here reaches firmware/.
|
||||
- name: compile-only roles
|
||||
run: west build -b rak4631 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf"
|
||||
|
||||
# Hit rate is the entire point of the per-job ccache namespace above, but the
|
||||
# action only prints stats at SETUP time — it runs `ccache -z -s -vv`, which
|
||||
# reports the restored cache and then zeroes the counters. Without a stats
|
||||
@@ -372,6 +377,10 @@ jobs:
|
||||
- name: build firmwares
|
||||
run: bash build.sh esp32 companions
|
||||
|
||||
# Roles no manifest publishes still have to compile (see build-nrf).
|
||||
- name: compile-only roles
|
||||
run: west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf"
|
||||
|
||||
- name: ccache stats
|
||||
if: always()
|
||||
run: ccache -s -vv
|
||||
|
||||
@@ -56,12 +56,18 @@ touch before writing code. Decisions are in `docs/adr/`.
|
||||
- Build with `--pristine` when the board or role changes; ESP32 S3/C-series repeaters need `--sysbuild`.
|
||||
- Host tests: `python3 tests/run.py run --profile quick` (Linux or WSL). A new case needs a
|
||||
`tests/catalog.json` entry. A failing pinned test is a finding: do not edit the pinned values to make it pass.
|
||||
- Logic that runs without hardware (a protocol parser, a state machine, a timing rule, a `// ZEPHCORE:` change
|
||||
to upstream behaviour) comes with host tests that pin it.
|
||||
- PR CI runs the host tests only. Build every board you touched and one you did not, and run each role you
|
||||
changed. Releases build from `zephcore.yml` alone, so nothing may depend on a hand-passed fragment.
|
||||
changed. Releases build from `zephcore.yml` alone, so nothing may depend on a hand-passed fragment. A role or
|
||||
fragment no `zephcore.yml` row builds goes into the "compile-only roles" step of `.github/workflows/build.yml`.
|
||||
- A new role or composition root puts every part the board has into a defined state, used or not: GPS parked,
|
||||
LEDs off, display blanked, as `src/server_main_common.cpp` does.
|
||||
- New board: `zephcore.yml` without `release:`, then `python zephcore/scripts/board_manifest.py check` and
|
||||
`... docs`.
|
||||
- Docs in the same PR: a clause in `docs/ARCHITECTURE.md` when behaviour, an interface or a limit changes;
|
||||
`docs/CLI_commands.md` for any CLI change; an ADR when a decision is taken or reversed.
|
||||
`docs/CLI_commands.md` for any CLI change; an ADR when a decision is taken or reversed; the next release's
|
||||
`releasenotes/RELEASE_NOTES_<version>-zephcore.md` for anything a user sees.
|
||||
|
||||
## PR description
|
||||
|
||||
|
||||
+10
-4
@@ -749,9 +749,12 @@ Listen-only node (ESP32 only): receives LoRa packets and publishes them to an MQ
|
||||
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
|
||||
CAD, AGC/drift) as the Dispatcher would. The host serial is the chosen `zephcore,kiss-uart` node (upstream's
|
||||
`KISS_UART_RX/TX`: header pins to an SBC, baud rate from its `current-speed`), else the CDC-ACM port, else the
|
||||
console UART, through `adapters/transport/ZephyrUartStream` (an Arduino `Stream` over interrupt-driven RX/TX rings).
|
||||
USB is brought up in every case, so the CDC port keeps the 1200-baud DFU touch. The console driver is off in this
|
||||
role so nothing else writes to the port; with a `zephcore,kiss-uart` other than the console it may be turned back
|
||||
on, and a build assert rejects a console on the KISS 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
|
||||
@@ -760,7 +763,10 @@ deadline. Differences from upstream:
|
||||
- `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`.
|
||||
server roles. No UI: at boot the LEDs are driven off and a display is blanked (`mc_display_off()`, or
|
||||
`oled_sleep()` without the display UI), as a server leaves them after its auto-off. Host tests: `UNIT-KISS-*`
|
||||
(`tests/unit/kiss_modem_tests.cpp`). Not released from `zephcore.yml`; the release workflow compiles it for rak4631
|
||||
and xiao_esp32c3. Build with `boards/common/kiss_modem.conf`.
|
||||
|
||||
### 6.6 CommonCLI Commands
|
||||
|
||||
|
||||
+5
-1
@@ -42,7 +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 |
|
||||
| `kiss_modem.conf` | KISS modem | Raw packets over USB or the console port, or a UART the devicetree names `zephcore,kiss-uart`; 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` |
|
||||
@@ -76,6 +76,10 @@ west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/comm
|
||||
# KISS modem
|
||||
west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf"
|
||||
|
||||
# KISS modem on header pins to an SBC: an overlay with `chosen { zephcore,kiss-uart = &uart0; };` (that UART
|
||||
# enabled, `current-speed` set); the console then stays usable on USB
|
||||
west build -b xiao_esp32c3 zephcore --pristine -- -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf" -DEXTRA_DTC_OVERLAY_FILE="kiss_uart.overlay" -DCONFIG_UART_CONSOLE=y
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ZephCore 1.18.0-zephcore
|
||||
|
||||
Safer off-grid mode, working buttons on four boards with a display, a GPS clock fix, and the
|
||||
changes from upstream MeshCore 1.18.0.
|
||||
Safer off-grid mode, working buttons on four boards with a display, a GPS clock fix, a new KISS
|
||||
modem role, and the changes from upstream MeshCore 1.18.0.
|
||||
|
||||
> [!NOTE]
|
||||
> A normal upgrade keeps your identity, settings, contacts and phone pairing.
|
||||
@@ -39,6 +39,25 @@ back. Now only satellite-solved positions count, five in a row. A fix takes abou
|
||||
This applies to every board with a GPS. Thanks to **Innocent Killer 101** for the report and the
|
||||
measurements.
|
||||
|
||||
## New: KISS modem role
|
||||
|
||||
A ZephCore node can now act as a plain LoRa modem for a program on a computer, as upstream
|
||||
MeshCore's `kiss_modem` does. The program sends and receives raw packets over USB, sets the
|
||||
radio, and uses the node's key to sign, verify, encrypt and decrypt. There is no mesh, no
|
||||
Bluetooth and no CLI on the node in this role.
|
||||
|
||||
- It speaks upstream's protocol ([kiss_modem_protocol.md](../docs/kiss_modem_protocol.md)), so
|
||||
host software written for upstream's modem works with it.
|
||||
- When the channel is busy, a packet waits for it to clear instead of failing at once.
|
||||
- Radio settings outside the ranges ZephCore supports are refused.
|
||||
- The modem can also talk on a UART of your choice, for example header pins wired to a Raspberry
|
||||
Pi, named `zephcore,kiss-uart` in the devicetree; USB then stays free for updates.
|
||||
- No prebuilt image yet: build it with `boards/common/kiss_modem.conf`
|
||||
([BUILDING.md](../docs/BUILDING.md)). It keeps the identity of a repeater, room server or
|
||||
observer on the same node; flashed over a companion, it erases the companion's data.
|
||||
|
||||
Thanks to **recrof** for the port.
|
||||
|
||||
## From upstream MeshCore 1.18.0
|
||||
|
||||
- **Acknowledgements on direct routes are relayed after a short random delay**, like other direct
|
||||
|
||||
@@ -20,6 +20,7 @@ add_executable(zephcore_unit
|
||||
unit/prefs_json_tests.cpp
|
||||
unit/lpp_tests.cpp
|
||||
unit/telemetry_text_tests.cpp
|
||||
unit/kiss_modem_tests.cpp
|
||||
support/hash_capture.cpp
|
||||
support/identity_utils_boundary.cpp
|
||||
${PRODUCTION}/src/Dispatcher.cpp
|
||||
@@ -37,6 +38,7 @@ add_executable(zephcore_unit
|
||||
${PRODUCTION}/helpers/PrefsJson.cpp
|
||||
${PRODUCTION}/helpers/ConfigSerializer.cpp
|
||||
${PRODUCTION}/helpers/ui/telemetry_text.cpp
|
||||
${PRODUCTION}/app/KissModem.cpp
|
||||
)
|
||||
target_compile_features(zephcore_unit PRIVATE cxx_std_17)
|
||||
target_compile_definitions(zephcore_unit PRIVATE CONFIG_ZEPHCORE_PACKET_LOGGING=0
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ python tests/run.py run --profile quick
|
||||
```
|
||||
|
||||
This first implementation builds **actual production C++ sources** and executes
|
||||
the named native cases listed in `catalog.json` (114 on 2026-09-25) plus the Python
|
||||
the named native cases listed in `catalog.json` (113 on 2026-10-15) plus the Python
|
||||
harness self-tests. No boards, Zephyr workspace,
|
||||
west, network access or third-party Python packages are required. This is the first
|
||||
subset of the [regression plan](../docs/REGRESSION_TEST_PLAN.md), not completion of
|
||||
@@ -73,6 +73,7 @@ and rejects missing, unexpected, renamed or duplicate cases.
|
||||
| `include/mesh/Maintenance.h` | Due/past/future deadlines, wraparound and idle composition |
|
||||
| `src/Dispatcher.cpp` | Immediate wakes, scheduled TX, completion/timeout ownership, priority-preserving refusal, LBT starvation across retry gaps, final RX gate, busy recovery, budget refill/admin exception, RX draining/hold/retransmit, maintenance, truncated headers, stats reset, clock wrap and 4,096 generated parser inputs |
|
||||
| `src/Identity.cpp` and bundled Monocypher | Two RFC8032 Ed25519 known-answer vectors, tampered signatures/messages, wrong signer, canonical/legacy/private-only storage, failed-read state preservation and bidirectional ECDH |
|
||||
| `app/KissModem.cpp` | KISS escaping both ways and oversized-frame recovery, LBT refusal back to CSMA until the wait-clear bound, TX timeout only after the radio concludes, SetRadio range refusal, `msUntilNextTx()` per state |
|
||||
|
||||
The catalog maps stable case IDs to their test source files. Production paths above
|
||||
are relative to `zephcore/`. Generated-case seeds are fixed and documented in the
|
||||
|
||||
@@ -541,6 +541,31 @@
|
||||
"id": "UNIT-UTF8-001",
|
||||
"name": "Complete codepoints only and invalid UTF-8 rejection",
|
||||
"source": "tests/unit/advert_tests.cpp"
|
||||
},
|
||||
{
|
||||
"id": "UNIT-KISS-001",
|
||||
"name": "Host frames are unescaped to the radio and received packets are escaped with RX metadata",
|
||||
"source": "tests/unit/kiss_modem_tests.cpp"
|
||||
},
|
||||
{
|
||||
"id": "UNIT-KISS-002",
|
||||
"name": "An LBT refusal returns to CSMA and fails only at the wait-clear bound",
|
||||
"source": "tests/unit/kiss_modem_tests.cpp"
|
||||
},
|
||||
{
|
||||
"id": "UNIT-KISS-003",
|
||||
"name": "A transmit times out only once the radio has concluded it",
|
||||
"source": "tests/unit/kiss_modem_tests.cpp"
|
||||
},
|
||||
{
|
||||
"id": "UNIT-KISS-004",
|
||||
"name": "SetRadio outside the ZephCore preset ranges is refused",
|
||||
"source": "tests/unit/kiss_modem_tests.cpp"
|
||||
},
|
||||
{
|
||||
"id": "UNIT-KISS-005",
|
||||
"name": "msUntilNextTx reports each timed state and nothing while idle",
|
||||
"source": "tests/unit/kiss_modem_tests.cpp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,3 +39,14 @@ void mesh::Utils::secureZeroize(void*, size_t) {
|
||||
bool mesh::Utils::constantTimeEqual(const void*, const void*, size_t) {
|
||||
throw std::runtime_error("Identity validation requires real Utils integration");
|
||||
}
|
||||
// KissModem's hash and encrypt/decrypt commands (not exercised by its tests;
|
||||
// the one-fragment sha256 is hash_capture.cpp's).
|
||||
void mesh::Utils::sha256(uint8_t*, size_t, const uint8_t*, int, const uint8_t*, int) {
|
||||
throw std::runtime_error("KISS hash command requires real Utils integration");
|
||||
}
|
||||
int mesh::Utils::encryptThenMAC(const uint8_t*, uint8_t*, const uint8_t*, int) {
|
||||
throw std::runtime_error("KISS encrypt command requires real Utils integration");
|
||||
}
|
||||
int mesh::Utils::MACThenDecrypt(const uint8_t*, uint8_t*, const uint8_t*, int) {
|
||||
throw std::runtime_error("KISS decrypt command requires real Utils integration");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
// The time-sync target uses only uptime, not a simulated Zephyr scheduler.
|
||||
// Uptime only, not a simulated Zephyr scheduler: the time-sync target uses
|
||||
// k_uptime_get(), the KISS modem k_uptime_get_32() and k_msleep(); each test
|
||||
// file that needs one defines it over its own clock.
|
||||
extern "C" int64_t k_uptime_get();
|
||||
extern "C" uint32_t k_uptime_get_32();
|
||||
extern "C" int32_t k_msleep(int32_t ms);
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "dispatcher_fixture.h"
|
||||
#include <app/KissModem.h>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
// KissModem (app/KissModem.cpp) against a scripted host stream and radio: KISS
|
||||
// framing both ways, and the ZephCore changes to its TX state machine (LBT
|
||||
// refusal back to CSMA, timeout only once the radio has concluded, SetRadio range
|
||||
// check, msUntilNextTx()).
|
||||
|
||||
static uint32_t kiss_now = 0;
|
||||
extern "C" uint32_t k_uptime_get_32() { return kiss_now; }
|
||||
extern "C" int32_t k_msleep(int32_t ms) { kiss_now += uint32_t(ms); return 0; }
|
||||
|
||||
namespace {
|
||||
struct HostStream : Stream {
|
||||
std::deque<uint8_t> in;
|
||||
std::vector<uint8_t> out;
|
||||
int available() override { return int(in.size()); }
|
||||
int read() override {
|
||||
if (in.empty()) return -1;
|
||||
uint8_t b = in.front(); in.pop_front(); return b;
|
||||
}
|
||||
int peek() override { return in.empty() ? -1 : in.front(); }
|
||||
size_t write(uint8_t c) override { out.push_back(c); return 1; }
|
||||
int availableForWrite() override { return 4096; }
|
||||
};
|
||||
struct ZeroRng : mesh::RNG {
|
||||
void random(uint8_t *dest, size_t sz) override { memset(dest, 0, sz); }
|
||||
};
|
||||
struct Board : mesh::MainBoard {
|
||||
uint16_t getBattMilliVolts() override { return 4000; }
|
||||
const char *getManufacturerName() const override { return "test"; }
|
||||
void reboot() override {}
|
||||
uint8_t getStartupReason() const override { return 0; }
|
||||
};
|
||||
|
||||
std::vector<uint8_t> escaped(uint8_t type, const std::vector<uint8_t>& data) {
|
||||
std::vector<uint8_t> f{KISS_FEND};
|
||||
auto put = [&](uint8_t b) {
|
||||
if (b == KISS_FEND) { f.push_back(KISS_FESC); f.push_back(KISS_TFEND); }
|
||||
else if (b == KISS_FESC) { f.push_back(KISS_FESC); f.push_back(KISS_TFESC); }
|
||||
else f.push_back(b);
|
||||
};
|
||||
put(type);
|
||||
for (uint8_t b : data) put(b);
|
||||
f.push_back(KISS_FEND);
|
||||
return f;
|
||||
}
|
||||
|
||||
// Splits the host output into unescaped frames (type byte first).
|
||||
std::vector<std::vector<uint8_t>> frames(const std::vector<uint8_t>& out) {
|
||||
std::vector<std::vector<uint8_t>> result;
|
||||
std::vector<uint8_t> cur;
|
||||
bool esc = false;
|
||||
for (uint8_t b : out) {
|
||||
if (b == KISS_FEND) {
|
||||
if (!cur.empty()) result.push_back(cur);
|
||||
cur.clear(); esc = false;
|
||||
} else if (b == KISS_FESC) {
|
||||
esc = true;
|
||||
} else {
|
||||
cur.push_back(esc ? (b == KISS_TFEND ? KISS_FEND : KISS_FESC) : b);
|
||||
esc = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Modem {
|
||||
HostStream host;
|
||||
test_support::Radio radio;
|
||||
ZeroRng rng;
|
||||
Board board;
|
||||
SensorManager sensors;
|
||||
mesh::LocalIdentity identity;
|
||||
KissModem modem{host, identity, rng, radio, board, sensors};
|
||||
|
||||
Modem() { kiss_now = 1000; radio.airtime = 1000; modem.begin(); }
|
||||
void send(uint8_t type, const std::vector<uint8_t>& data) {
|
||||
for (uint8_t b : escaped(type, data)) host.in.push_back(b);
|
||||
}
|
||||
// As the event loop: several passes per wake, so every step made due is taken.
|
||||
void pass() { for (int i = 0; i < 4; i++) modem.loop(); }
|
||||
void advance(uint32_t ms) { kiss_now += ms; pass(); }
|
||||
// Result byte of the TX_DONE notification, or -1 if none has been sent.
|
||||
int txDone() {
|
||||
for (auto& f : frames(host.out)) {
|
||||
if (f.size() == 3 && f[0] == KISS_CMD_SETHARDWARE && f[1] == HW_RESP_TX_DONE) return f[2];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
// Runs the modem in slot-sized steps until TX_DONE or `limit` ms have passed.
|
||||
int runUntilDone(uint32_t limit) {
|
||||
for (uint32_t t = 0; t <= limit && txDone() < 0; t += 10) advance(10);
|
||||
return txDone();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
TEST(kiss_framing_round_trip, "UNIT-KISS-001",
|
||||
"Host frames are unescaped to the radio and received packets are escaped with RX metadata") {
|
||||
Modem m;
|
||||
const std::vector<uint8_t> payload{0x01, KISS_FEND, 0x02, KISS_FESC, 0x03};
|
||||
m.send(KISS_CMD_TXDELAY, {0});
|
||||
m.send(KISS_CMD_DATA, payload);
|
||||
m.pass();
|
||||
CHECK(m.radio.sent.size() == 1);
|
||||
CHECK(m.radio.sent[0] == payload);
|
||||
|
||||
m.host.out.clear();
|
||||
m.modem.onPacketReceived(-9, -100, payload.data(), uint16_t(payload.size()));
|
||||
m.pass();
|
||||
auto out = frames(m.host.out);
|
||||
CHECK(out.size() == 2);
|
||||
std::vector<uint8_t> data{KISS_CMD_DATA};
|
||||
data.insert(data.end(), payload.begin(), payload.end());
|
||||
CHECK(out[0] == data);
|
||||
CHECK((out[1] == std::vector<uint8_t>{KISS_CMD_SETHARDWARE, HW_RESP_RX_META, uint8_t(-9), uint8_t(-100)}));
|
||||
// Every byte between the frame delimiters is escaped: no bare FEND inside a frame.
|
||||
CHECK(std::count(m.host.out.begin(), m.host.out.end(), KISS_FEND) == 4);
|
||||
|
||||
// An oversized frame without a closing FEND is dropped and the parser recovers.
|
||||
m.host.in.push_back(KISS_FEND);
|
||||
for (int i = 0; i < KISS_MAX_FRAME_SIZE + 10; i++) m.host.in.push_back(0x00);
|
||||
m.send(KISS_CMD_SETHARDWARE, {HW_CMD_PING});
|
||||
m.host.out.clear();
|
||||
m.pass();
|
||||
out = frames(m.host.out);
|
||||
CHECK(out.size() == 1);
|
||||
CHECK(out[0][0] == KISS_CMD_SETHARDWARE && out[0][1] == HW_RESP(HW_CMD_PING));
|
||||
}
|
||||
|
||||
TEST(kiss_lbt_refusal_back_to_csma, "UNIT-KISS-002",
|
||||
"An LBT refusal returns to CSMA and fails only at the wait-clear bound") {
|
||||
Modem m;
|
||||
m.radio.accept = false;
|
||||
m.send(KISS_CMD_TXDELAY, {0});
|
||||
m.send(KISS_CMD_DATA, {0xAA});
|
||||
const uint32_t start = kiss_now;
|
||||
m.pass();
|
||||
CHECK(m.txDone() < 0);
|
||||
CHECK(m.modem.msUntilNextTx() == KISS_DEFAULT_SLOTTIME * 10);
|
||||
|
||||
// Bound: 1.5 x the airtime of a full packet (1000 ms in this radio) from the start of CSMA.
|
||||
CHECK(m.runUntilDone(2000) == 0x00);
|
||||
CHECK(kiss_now - start >= 1500);
|
||||
CHECK(kiss_now - start < 1500 + KISS_DEFAULT_SLOTTIME * 10 + 20);
|
||||
CHECK(m.radio.attempts > 5);
|
||||
CHECK(!m.modem.isTxBusy());
|
||||
|
||||
// The channel clears inside the bound: the frame goes out and completes.
|
||||
Modem n;
|
||||
n.radio.accept = false;
|
||||
n.send(KISS_CMD_TXDELAY, {0});
|
||||
n.send(KISS_CMD_DATA, {0xBB});
|
||||
n.pass();
|
||||
n.advance(300);
|
||||
n.radio.accept = true;
|
||||
n.runUntilDone(200);
|
||||
CHECK(n.radio.sent.size() == 1);
|
||||
CHECK(n.modem.isActuallyTransmitting());
|
||||
n.radio.completion = true;
|
||||
n.pass();
|
||||
CHECK(n.txDone() == 0x01);
|
||||
CHECK(n.radio.finished == 1);
|
||||
}
|
||||
|
||||
TEST(kiss_tx_timeout_waits_for_radio, "UNIT-KISS-003",
|
||||
"A transmit times out only once the radio has concluded it") {
|
||||
Modem m;
|
||||
m.send(KISS_CMD_TXDELAY, {0});
|
||||
m.send(KISS_CMD_DATA, {0xCC});
|
||||
m.pass();
|
||||
CHECK(m.modem.isActuallyTransmitting());
|
||||
CHECK(m.modem.msUntilNextTx() == 1500);
|
||||
|
||||
// Past the airtime bound while the radio still transmits: no verdict, no timed wake.
|
||||
m.advance(10000);
|
||||
CHECK(m.txDone() < 0);
|
||||
CHECK(m.radio.finished == 0);
|
||||
CHECK(m.modem.msUntilNextTx() == UINT32_MAX);
|
||||
|
||||
// The radio concludes without a completion: the timeout fires on the next pass.
|
||||
m.radio.active = false;
|
||||
m.pass();
|
||||
CHECK(m.txDone() == 0x00);
|
||||
CHECK(m.radio.finished == 1);
|
||||
CHECK(!m.modem.isTxBusy());
|
||||
CHECK(m.modem.msUntilNextTx() == UINT32_MAX);
|
||||
}
|
||||
|
||||
static float set_freq, set_bw;
|
||||
static uint8_t set_sf, set_cr;
|
||||
static int set_calls;
|
||||
static void on_set_radio(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
set_freq = freq; set_bw = bw; set_sf = sf; set_cr = cr; set_calls++;
|
||||
}
|
||||
|
||||
TEST(kiss_set_radio_ranges, "UNIT-KISS-004",
|
||||
"SetRadio outside the ZephCore preset ranges is refused") {
|
||||
Modem m;
|
||||
m.modem.setRadioCallback(on_set_radio);
|
||||
set_calls = 0;
|
||||
auto setRadio = [&](uint32_t hz, uint32_t bw_hz, uint8_t sf, uint8_t cr) {
|
||||
std::vector<uint8_t> d{HW_CMD_SET_RADIO};
|
||||
for (int i = 0; i < 4; i++) d.push_back(uint8_t(hz >> (8 * i)));
|
||||
for (int i = 0; i < 4; i++) d.push_back(uint8_t(bw_hz >> (8 * i)));
|
||||
d.push_back(sf); d.push_back(cr);
|
||||
m.host.out.clear();
|
||||
m.send(KISS_CMD_SETHARDWARE, d);
|
||||
m.pass();
|
||||
auto out = frames(m.host.out);
|
||||
CHECK(out.size() == 1);
|
||||
return out[0];
|
||||
};
|
||||
const std::vector<uint8_t> ok{KISS_CMD_SETHARDWARE, HW_RESP_OK};
|
||||
const std::vector<uint8_t> refused{KISS_CMD_SETHARDWARE, HW_RESP_ERROR, HW_ERR_INVALID_PARAM};
|
||||
|
||||
CHECK(setRadio(869525000, 62500, 7, 5) == ok);
|
||||
CHECK(set_calls == 1 && set_sf == 7 && set_cr == 5);
|
||||
CHECK(set_freq > 869.52f && set_freq < 869.53f && set_bw == 62.5f);
|
||||
|
||||
CHECK(setRadio(100000000, 62500, 7, 5) == refused); // below 150 MHz
|
||||
CHECK(setRadio(869525000, 5000, 7, 5) == refused); // below 7 kHz
|
||||
CHECK(setRadio(869525000, 62500, 4, 5) == refused); // SF below 5
|
||||
CHECK(setRadio(869525000, 62500, 13, 5) == refused); // SF above 12
|
||||
CHECK(setRadio(869525000, 62500, 7, 9) == refused); // CR above 4/8
|
||||
CHECK(set_calls == 1);
|
||||
}
|
||||
|
||||
TEST(kiss_wake_deadlines, "UNIT-KISS-005",
|
||||
"msUntilNextTx reports each timed state and nothing while idle") {
|
||||
Modem m;
|
||||
CHECK(m.modem.msUntilNextTx() == UINT32_MAX);
|
||||
|
||||
// Default TXDELAY (50 x 10 ms) after a clear channel.
|
||||
m.send(KISS_CMD_DATA, {0xDD});
|
||||
m.pass();
|
||||
CHECK(m.modem.msUntilNextTx() == KISS_DEFAULT_TXDELAY * 10);
|
||||
m.advance(200);
|
||||
CHECK(m.modem.msUntilNextTx() == KISS_DEFAULT_TXDELAY * 10 - 200);
|
||||
m.advance(KISS_DEFAULT_TXDELAY * 10 - 200);
|
||||
CHECK(m.modem.isActuallyTransmitting());
|
||||
|
||||
// Busy channel: rechecked once per slot.
|
||||
Modem n;
|
||||
n.radio.receiving = true;
|
||||
n.send(KISS_CMD_DATA, {0xEE});
|
||||
n.pass();
|
||||
CHECK(n.modem.msUntilNextTx() == KISS_DEFAULT_SLOTTIME * 10);
|
||||
n.advance(30);
|
||||
CHECK(n.modem.msUntilNextTx() == KISS_DEFAULT_SLOTTIME * 10 - 30);
|
||||
}
|
||||
+6
-4
@@ -164,10 +164,12 @@ config ZEPHCORE_ROLE_ROOM_SERVER
|
||||
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.
|
||||
KISS TNC on a 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. The port is
|
||||
the chosen `zephcore,kiss-uart` node, else USB CDC-ACM, else the
|
||||
console.
|
||||
|
||||
endchoice
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# 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).
|
||||
# KISS modem role: raw LoRa packets to a host over a serial port (upstream
|
||||
# MeshCore examples/kiss_modem, protocol in docs/kiss_modem_protocol.md).
|
||||
#
|
||||
# Build with: -DEXTRA_CONF_FILE="boards/common/kiss_modem.conf"
|
||||
#
|
||||
# The host port is the chosen `zephcore,kiss-uart` node if the devicetree names
|
||||
# one (upstream's KISS_UART_RX/TX), else the USB CDC-ACM port, else the console.
|
||||
|
||||
CONFIG_ZEPHCORE_ROLE_KISS_MODEM=y
|
||||
|
||||
@@ -10,6 +13,9 @@ 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.
|
||||
# write to it, or boot and printk bytes land in the stream. With a
|
||||
# `zephcore,kiss-uart` other than the console, a build may turn the console back
|
||||
# on (-DCONFIG_UART_CONSOLE=y, plus debug.conf for logs); src/main_kiss_modem.cpp
|
||||
# fails the build if the console is the KISS port.
|
||||
CONFIG_UART_CONSOLE=n
|
||||
CONFIG_SHELL=n
|
||||
|
||||
@@ -152,6 +152,7 @@ Only `lora0` is required. Everything else enables its feature by being present.
|
||||
| `sensors-i2c.dtsi`, `rtc-i2c.dtsi` | I2C sensors (probed at runtime) and a battery-backed RTC (opt-in) |
|
||||
| `nordic,qspi-nor` + `qspi-ext.dtsi` | `/ext` on external flash for contacts, channels and blobs |
|
||||
| chosen `zephcore,companion-uart` | Companion protocol on a UART, for boards without Bluetooth |
|
||||
| chosen `zephcore,kiss-uart` | KISS modem role on its own UART (for example header pins to an SBC) instead of USB or the console; baud rate from the node's `current-speed` |
|
||||
| `zephcore,poweroff-gpios` | Pins released at shutdown on boards with a latched supply rail |
|
||||
|
||||
`board.overlay` has a worked example for each row.
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
* zephcore,gps-power regulator that powers the GPS (section 6)
|
||||
* zephcore,companion-uart companion protocol on a plain UART, for
|
||||
* boards without Bluetooth (lora_e5_mini)
|
||||
* zephcore,kiss-uart KISS modem role on its own UART (header pins
|
||||
* to a host SBC); the baud rate is the node's
|
||||
* current-speed
|
||||
* ================================================================== */
|
||||
/ {
|
||||
aliases {
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* 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.
|
||||
* LoRa packets to and from a host over a 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().
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/devicetree.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/sys/util.h>
|
||||
|
||||
#include <zephyr/logging/log.h>
|
||||
@@ -25,6 +26,24 @@ LOG_MODULE_REGISTER(zephcore_kiss_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
#include <ZephyrUSBCDC.h>
|
||||
#endif
|
||||
|
||||
/* The host port: the chosen `zephcore,kiss-uart`, else the CDC-ACM UART, else
|
||||
* the console. */
|
||||
#if DT_HAS_CHOSEN(zephcore_kiss_uart)
|
||||
#define KISS_HOST_NODE DT_CHOSEN(zephcore_kiss_uart)
|
||||
#elif ZEPHCORE_USB_STACK && DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) && \
|
||||
!IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
|
||||
#define KISS_HOST_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_cdc_acm_uart)
|
||||
#else
|
||||
#define KISS_HOST_NODE DT_CHOSEN(zephyr_console)
|
||||
#endif
|
||||
|
||||
/* The console driver writing to the host port would corrupt the KISS stream. */
|
||||
#if DT_HAS_CHOSEN(zephyr_console)
|
||||
BUILD_ASSERT(!IS_ENABLED(CONFIG_UART_CONSOLE) ||
|
||||
!DT_SAME_NODE(KISS_HOST_NODE, DT_CHOSEN(zephyr_console)),
|
||||
"CONFIG_UART_CONSOLE must be off while the console UART is the KISS port");
|
||||
#endif
|
||||
|
||||
#include <app/KissModem.h>
|
||||
#include <app/RepeaterDataStore.h>
|
||||
#include <src/RadioIncludes.h>
|
||||
@@ -34,6 +53,18 @@ LOG_MODULE_REGISTER(zephcore_kiss_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
|
||||
#include <adapters/transport/ZephyrUartStream.h>
|
||||
#include <ZephyrSensorManager.h>
|
||||
#include "../adapters/datastore/ZephyrFsFormat.h"
|
||||
#include "oled_power.h"
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY)
|
||||
#include "display.h"
|
||||
#endif
|
||||
|
||||
/* With a PWM LED, led0 is the same pin and belongs to the PWM driver. */
|
||||
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && !IS_ENABLED(CONFIG_ZEPHCORE_LED_PWM)
|
||||
static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
|
||||
#endif
|
||||
#if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios)
|
||||
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(DT_ALIAS(led1), gpios);
|
||||
#endif
|
||||
|
||||
#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 | \
|
||||
@@ -136,12 +167,32 @@ int main(void)
|
||||
{
|
||||
board.clearBootloaderMagic();
|
||||
|
||||
/* USB comes up even when the host is on another UART: the CDC port keeps
|
||||
* the 1200-baud DFU touch and any console there. */
|
||||
#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);
|
||||
#endif
|
||||
const struct device *host_dev = DEVICE_DT_GET(KISS_HOST_NODE);
|
||||
|
||||
/* No UI in this role: LEDs off, the display blanked as a server leaves
|
||||
* it after its auto-off (the panel otherwise keeps its boot state). */
|
||||
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios) && !IS_ENABLED(CONFIG_ZEPHCORE_LED_PWM)
|
||||
if (gpio_is_ready_dt(&led0)) {
|
||||
gpio_pin_configure_dt(&led0, GPIO_OUTPUT_INACTIVE);
|
||||
}
|
||||
#endif
|
||||
#if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios)
|
||||
if (gpio_is_ready_dt(&led1)) {
|
||||
gpio_pin_configure_dt(&led1, GPIO_OUTPUT_INACTIVE);
|
||||
}
|
||||
#endif
|
||||
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY)
|
||||
if (mc_display_init() == 0) {
|
||||
mc_display_off();
|
||||
}
|
||||
#else
|
||||
const struct device *host_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_console));
|
||||
oled_sleep();
|
||||
#endif
|
||||
|
||||
if (!data_store.hasRoleData()) {
|
||||
|
||||
Reference in New Issue
Block a user