kiss updates

This commit is contained in:
liquidraver
2026-10-15 21:34:26 +02:00
parent 2544c24560
commit ff0e6553a0
16 changed files with 427 additions and 23 deletions
+9
View File
@@ -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
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+21 -2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
+25
View File
@@ -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"
}
]
}
+11
View File
@@ -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");
}
+5 -1
View File
@@ -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);
+254
View File
@@ -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
View File
@@ -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
+9 -3
View File
@@ -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
+1
View File
@@ -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 {
+56 -5
View File
@@ -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()) {