offgrid overhaul

This commit is contained in:
liquidraver
2026-10-12 10:20:40 +02:00
parent 90370e461e
commit 448ff91590
25 changed files with 427 additions and 48 deletions
+4
View File
@@ -86,6 +86,9 @@ All of this is local behaviour. Nothing here changes the wire protocol, so mixed
- **LEDs you control.** Turn them all off with `set leds off`, or choose what each one shows: blink on
transmit, on receive or both, and a heartbeat that can signal unread messages. The receive blink tells
you at a glance whether a repeater is hearing anything.
- **Off-grid mode from the device.** A companion can relay for a group of handhelds out of reach of any
repeater. Switch it on from the display or the CLI and the node moves to the off-grid frequency of its
band and back by itself. ([OFFGRID.md](docs/OFFGRID.md))
- **2.4 GHz on LR2021 boards.** Set a 2.4 GHz frequency and the radio switches to its high-band path on
its own.
- **Runs on Linux too.** The same code runs as a normal process on a Raspberry Pi or Femtofox with a real
@@ -169,6 +172,7 @@ ZephCore is a plain west workspace application, and it tries to be a well-behave
| [adr/](docs/adr/README.md) | Decisions and why they were made |
| [BUILDING.md](docs/BUILDING.md) | Every build variant, platform notes, Kconfig options |
| [CLI_commands.md](docs/CLI_commands.md) | Every CLI command |
| [OFFGRID.md](docs/OFFGRID.md) | Off-grid mode: a companion that relays, and the frequencies it runs on |
| [supported_boards.md](docs/supported_boards.md) | Boards and build strings |
| [LINUX_NATIVE.md](docs/LINUX_NATIVE.md) | Running on a Pi or Femtofox |
| [releasenotes/](releasenotes/) | What changed in each release |
+7
View File
@@ -604,6 +604,13 @@ Handles the binary BLE protocol with ~50 command opcodes. Key features:
- **Protocol versioning**: V2/V3 frame format negotiation with phone app
- **Ed25519 signing**: 3-phase flow (start→data→finish) for signing up to 8KB
- **Flood scope**: Transport key filtering for region-scoped sends
- **Off-grid mode** (upstream "client repeat", user view in `OFFGRID.md`): the companion forwards, unfiltered, only
while `client_repeat` is set **and** `prefs.freq` is one of upstream's off-grid frequencies (433.000 / 869.495 /
918.000 MHz). `helpers/OffgridPolicy.h` holds that rule and the prefs transitions; `allowPacketForward()`, the
boot-time load, `CMD_DEVICE_QUERY` byte 80 and both device UIs go through it. The app's `CMD_SET_RADIO_PARAMS`
sets frequency and flag together, as upstream. The device toggle and the companion's `set repeat` call
`CompanionMesh::setOffgridMode()`: on = nearest off-grid frequency, the one left kept in `zc.og_home`; off = back
to it. BW, SF, CR and TX power are never touched
### 6.2.1 V-Contact (Loopback Admin Contact)
+2 -2
View File
@@ -337,7 +337,7 @@ The observer role (ESP32, `boards/common/observer.conf`) has its own CLI and, ap
|---------|---------|
| `get name` | Node name |
| `get role` | Firmware role: `repeater` or `room_server` (companion builds report `companion`) |
| `get repeat` | Forwarding enabled: `on` or `off` |
| `get repeat` | Forwarding enabled: `on` or `off`. On a companion this is off-grid mode ([OFFGRID.md](OFFGRID.md)): `on` only while repeat is set and the node is on an off-grid frequency |
| `get radio` | Radio params as `freq,bw,sf,cr` — the same comma-separated form `set radio` takes, so a reply can be edited and sent straight back |
| `get freq` | Frequency in MHz |
| `get freqerr` | *(ZephCore only)* Carrier frequency error measured on received packets: `mean N Hz, min A, max B, K pkts`. **LR2021 only** — other radios answer `not available`. Purely diagnostic; nothing acts on it. **The mean only approximates *this* node's reference error once it is averaged over many different peers** — their individual errors cancel, ours does not — so read `K` and the min/max spread before believing it: a tight spread over a handful of packets is one chatty neighbour, not a population. Small values are the expected answer and mean there is nothing to do; LoRa tolerates carrier error up to roughly a quarter of the bandwidth before sensitivity suffers, so at BW 62.5 kHz a few hundred Hz is noise. If it is kHz-scale the correction is board-dependent: XTAL parts have `SetXoscCpTrim`, but **TCXO parts have no chip-side trim at all** (DS §6.11.4: "If a TCXO is configured, this command has no effect"), leaving only a software offset to the programmed frequency. Values beyond ±200 kHz are discarded by the driver and warn once — the field is decoded from three `GetLoraPacketStatus` bytes that DS rev 2.1 does not document, so implausible readings are evidence the field is not real on that firmware rather than a genuine measurement. Reset by `clear stats`. |
@@ -421,7 +421,7 @@ four radio parameters together, since they are one interop-critical set.
| Command | Constraints | Description |
|---------|-------------|-------------|
| `set name <name>` | No `[ ] \ : , ? *` | Set node name |
| `set repeat <on\|off>` | | Enable or disable packet forwarding |
| `set repeat <on\|off>` | | Enable or disable packet forwarding. **Companion:** off-grid mode, the same as the display toggle ([OFFGRID.md](OFFGRID.md)), local only and applied at once. `on` moves the node to the nearest of 433.000 / 869.495 / 918.000 MHz and starts forwarding; `off` returns to the frequency it left. A node already on an off-grid frequency stays there both ways. BW, SF, CR and TX power are not touched; the frequency move resets the learned CAD offset. Replies `OK - repeat is now ON (869.495 MHz)`, or `Error: no off-grid frequency for this band` on a 2.4 GHz preset. `set radio` / `set freq` to any other frequency turns it off |
| `set radio <freq>,<bw>,<sf>,<cr>` | freq 150–2500, bw 7–500 (7–1000 on LR2021), sf 5–12, cr 5–8 | **Comma-separated**, not space-separated — spaces parse as a single argument and the command is rejected. Set radio params *(reboot required)*. If freq, bw or sf actually changed, this also performs a full `set cad.reset` and replies `OK - reboot to apply (CAD reset)` — the learned detPeak offset and its probe statistics belong to the old preset. The radio keeps running the old preset, at its learned offset, until the reboot; CAD adaptation is suspended in the meantime so it cannot re-dirty the reset. A cr-only change resets nothing. |
| `set freq <mhz>` | 150–2500 *(USB only)* | Set frequency alone *(reboot required)*. Same CAD reset as `set radio` when the frequency actually changes. On LR2021 boards a frequency at or above 1500 MHz also moves the radio to its 2.4 GHz front end — see the note below the table. |
| `set tx <dbm>` | −9 to board max (default 30) | Set TX power |
+49
View File
@@ -0,0 +1,49 @@
# Off-grid mode
Off-grid mode makes a companion forward packets for others, like a small repeater. It is for a group
of handhelds that is out of reach of any repeater: each node relays for the rest, so messages get
further than one radio hop.
It is MeshCore's "client repeat". ZephCore nodes and stock MeshCore nodes in off-grid mode work
together.
## It runs on its own frequencies
A companion forwards only on one of three frequencies set aside for this:
| Band | Off-grid frequency |
|---|---|
| 433 MHz | 433.000 MHz |
| EU 868 MHz | 869.495 MHz |
| US 915 MHz | 918.000 MHz |
Turning off-grid mode on therefore **moves the node off your normal mesh**. While it is on you reach
only other nodes on the same off-grid frequency with the same bandwidth, spreading factor and coding
rate. Those three, and the TX power, are left as they were, so a group that switches from the same
mesh preset ends up on matching settings.
A companion never forwards on a normal mesh frequency. It forwards everything it hears, without the
hop limits, region filters and loop detection a repeater applies, which is fine for a handful of
handhelds and harmful on a busy mesh.
## Turning it on and off
**From the app.** Choose one of the off-grid frequencies in the radio settings and enable repeat.
When you turn repeat off in the app the node stays on that frequency: select your mesh preset again
to return.
**From the display.** The Offgrid page (button boards) or System > Device > Offgrid (joystick boards)
asks for confirmation, then moves to the off-grid frequency nearest to the one you are on and starts
forwarding. Turning it off there returns to the frequency you came from. If the node was already on an
off-grid frequency when you turned it on, it stays there.
**From the CLI** (USB or the app's command line, not over the mesh): `set repeat on` and
`set repeat off` do the same as the display, and `get repeat` shows the state.
On a 2.4 GHz preset there is no off-grid frequency, and the display and the CLI refuse.
## After an update from 1.17.7 or earlier
Up to 1.17.7 the display toggle turned forwarding on without changing the frequency, so a companion
could end up repeating on the live mesh. A node left in that state stops forwarding when it boots
1.17.8 or later, and shows off-grid mode as off.
+24 -1
View File
@@ -1,6 +1,7 @@
# ZephCore 1.17.8-zephcore
Button fixes on four boards with a display: the Station G2's button now drives the pages, and
Off-grid mode from the display no longer turns a companion into a repeater on the live mesh. Button
fixes on four boards with a display: the Station G2's button now drives the pages, and
Power Off, DFU and Offgrid can be confirmed on the Heltec Mesh Node T1 and both LR2021 LoRa Plus EVK
kits.
@@ -9,6 +10,28 @@ kits.
---
## Off-grid mode: only on the off-grid frequencies
Off-grid mode makes a companion forward packets for others, for a group of handhelds out of reach of
any repeater. It belongs on three frequencies set aside for it: 433.000, 869.495 and 918.000 MHz. The
app only allows it there, but the Offgrid toggle on the display switched forwarding on without
changing the frequency, so the node repeated everything it heard on the live mesh.
- **The display toggle now moves the node.** Turning Offgrid on goes to the off-grid frequency nearest
to the one you are on and starts forwarding; turning it off returns to the frequency you came from.
Bandwidth, spreading factor, coding rate and TX power stay as they are.
- **A companion never forwards on any other frequency.** If you had switched Offgrid on from the
display on an earlier version, the node stops repeating after this update and shows Offgrid as off.
Nothing else changes; turn it on again if you want off-grid mode.
- **`set repeat on` / `set repeat off` work on a companion** and do the same as the display toggle;
`get repeat` shows the real state. Before, they answered but did nothing.
- **The display follows the app.** Offgrid switched from the app or the CLI now shows on the display
page within a few seconds.
While off-grid mode is on, the node is off your normal mesh. How it works: [OFFGRID.md](../docs/OFFGRID.md).
Thanks to N8HOP for the report.
## Station G2: the user button drives the display
Until now a press of the user button only woke the display: the pages could not be turned, and
+15
View File
@@ -352,6 +352,21 @@
"name": "Scoped unscoped and advert hop boundaries",
"source": "tests/unit/policy_tests.cpp"
},
{
"id": "UNIT-OFFGRID-001",
"name": "Off-grid frequency: the three allowed, nearest per band, none above sub-GHz",
"source": "tests/unit/policy_tests.cpp"
},
{
"id": "UNIT-OFFGRID-002",
"name": "Off-grid toggle: to the off-grid frequency and back, preset otherwise untouched",
"source": "tests/unit/policy_tests.cpp"
},
{
"id": "UNIT-OFFGRID-003",
"name": "Off-grid: a repeat flag on a mesh frequency is dropped, app and CLI edits drop the home frequency",
"source": "tests/unit/policy_tests.cpp"
},
{
"id": "UNIT-POOL-001",
"name": "Exhaust reclaim and reuse all pool slots",
+71
View File
@@ -1,5 +1,6 @@
#include "test.h"
#include <RoutingPolicy.h>
#include <OffgridPolicy.h>
#include <RateLimiter.h>
#include <mesh/Maintenance.h>
using namespace mesh;
@@ -47,3 +48,73 @@ TEST(maintenance_wrap, "UNIT-TIME-001", "Maintenance deadlines across uint32 wra
CHECK(maintenanceSooner(MAINTENANCE_IDLE, 32) == 32);
CHECK(maintenanceSooner(0, MAINTENANCE_IDLE) == 0);
}
namespace {
NodePrefs offgridPrefs(float freq, uint8_t repeat = 0, float home = 0.0f) {
NodePrefs p{};
p.freq = freq; p.bw = 62.5f; p.sf = 7; p.cr = 5; p.tx_power_dbm = 22;
p.client_repeat = repeat; p.offgrid_home_freq = home;
return p;
}
} // namespace
TEST(offgrid_freq_pick, "UNIT-OFFGRID-001", "Off-grid frequency: the three allowed, nearest per band, none above sub-GHz") {
CHECK(isOffgridFreq(433.0f) && isOffgridFreq(869.495f) && isOffgridFreq(918.0f));
CHECK(!isOffgridFreq(869.618f) && !isOffgridFreq(869.494f) && !isOffgridFreq(910.525f));
CHECK(isOffgridFreqKhz(869495) && !isOffgridFreqKhz(869496));
CHECK(nearestOffgridFreqKhz(869.618f) == 869495);
CHECK(nearestOffgridFreqKhz(868.1f) == 869495);
CHECK(nearestOffgridFreqKhz(910.525f) == 918000);
CHECK(nearestOffgridFreqKhz(927.875f) == 918000);
CHECK(nearestOffgridFreqKhz(433.875f) == 433000);
CHECK(nearestOffgridFreqKhz(150.0f) == 433000);
CHECK(nearestOffgridFreqKhz(869.495f) == 869495);
CHECK(nearestOffgridFreqKhz(2445.0f) == 0);
CHECK(nearestOffgridFreqKhz(0.0f) == 0);
}
TEST(offgrid_toggle, "UNIT-OFFGRID-002", "Off-grid toggle: to the off-grid frequency and back, preset otherwise untouched") {
NodePrefs p = offgridPrefs(869.618f);
CHECK(!offgridActive(p));
CHECK(offgridSet(p, true) == OFFGRID_RETUNED);
CHECK(offgridActive(p) && p.freq == 869.495f && p.offgrid_home_freq == 869.618f);
CHECK(p.bw == 62.5f && p.sf == 7 && p.cr == 5 && p.tx_power_dbm == 22);
CHECK(offgridSet(p, true) == OFFGRID_UNCHANGED);
CHECK(offgridSet(p, false) == OFFGRID_RETUNED);
CHECK(!offgridActive(p) && p.freq == 869.618f && p.offgrid_home_freq == 0.0f && p.client_repeat == 0);
CHECK(offgridSet(p, false) == OFFGRID_UNCHANGED);
// Already on an off-grid frequency (the app left it there): flag only, both ways.
p = offgridPrefs(918.0f);
CHECK(offgridSet(p, true) == OFFGRID_SET);
CHECK(offgridActive(p) && p.freq == 918.0f && p.offgrid_home_freq == 0.0f);
CHECK(offgridSet(p, false) == OFFGRID_SET);
CHECK(!offgridActive(p) && p.freq == 918.0f);
// No off-grid frequency on 2.4 GHz: refused, nothing changes.
p = offgridPrefs(2445.0f);
CHECK(offgridSet(p, true) == OFFGRID_NO_BAND);
CHECK(p.client_repeat == 0 && p.freq == 2445.0f && p.offgrid_home_freq == 0.0f);
}
TEST(offgrid_normalize, "UNIT-OFFGRID-003", "Off-grid: a repeat flag on a mesh frequency is dropped, app and CLI edits drop the home frequency") {
// What the pre-1.17.8 device toggle left behind.
NodePrefs p = offgridPrefs(869.618f, 1);
CHECK(!offgridActive(p));
CHECK(offgridNormalize(p) && p.client_repeat == 0 && p.freq == 869.618f);
CHECK(!offgridNormalize(p));
// Off-grid mode on is left alone, home frequency included.
p = offgridPrefs(869.495f, 1, 869.618f);
CHECK(!offgridNormalize(p) && offgridActive(p) && p.offgrid_home_freq == 869.618f);
// The app turned repeat off and stayed on the off-grid frequency.
p = offgridPrefs(869.495f, 0, 869.618f);
offgridFreqEdited(p);
CHECK(p.offgrid_home_freq == 0.0f && p.freq == 869.495f && p.client_repeat == 0);
// The app moved to another off-grid frequency with repeat on: stays on, home gone.
p = offgridPrefs(433.0f, 1, 869.618f);
offgridFreqEdited(p);
CHECK(offgridActive(p) && p.offgrid_home_freq == 0.0f);
CHECK(offgridSet(p, false) == OFFGRID_SET && p.freq == 433.0f);
// The CLI set a mesh frequency while off-grid mode was on: off, and nothing to return to.
p = offgridPrefs(868.0f, 1, 869.618f);
offgridFreqEdited(p);
CHECK(p.client_repeat == 0 && p.offgrid_home_freq == 0.0f && p.freq == 868.0f);
}
+2 -1
View File
@@ -47,7 +47,7 @@ NodePrefs sample() {
for (int i = 0; i < 16; i++) p.default_scope_key[i] = (uint8_t)(i + 1);
p.ble_disabled = 1; p.display_brightness = 50; p.wake_on_msg = 0; p.screen_off_secs = 60;
p.auto_shutdown_mv = 3300; p.v_contact_enabled = 0; p.v_battery_alert_mv = 3500;
p.v_contact_flags = 0x81; p.wifi_enabled = 0;
p.v_contact_flags = 0x81; p.wifi_enabled = 0; p.offgrid_home_freq = 869.618f;
strcpy(p.wifi_ssid, "MyNet"); strcpy(p.wifi_pwd, "secret");
return p;
}
@@ -75,6 +75,7 @@ TEST(pjson_companion_roundtrip, "UNIT-PJSON-001", "Companion prefs survive prefs
CHECK(companionBytes(q) == companionBytes(p));
// prefs.json only: not in the binary layout.
CHECK(q.led_brightness == 42);
CHECK(q.offgrid_home_freq == 869.618f);
}
TEST(pjson_server_roundtrip, "UNIT-PJSON-002", "Server prefs survive prefs.json field for field") {
+12
View File
@@ -129,6 +129,18 @@ public:
(void)freq; (void)bw; (void)sf; (void)cr; (void)timeout_mins;
}
/* `get repeat` / `set repeat`: off-grid mode, the same call as the device toggle. */
bool hasOffgridMode() const override { return true; }
bool isOffgridActive() const override { return _mesh.isOffgridActive(); }
void setOffgridMode(bool on, char* reply) override {
if (_mesh.setOffgridMode(on) == mesh::OFFGRID_NO_BAND) {
strcpy(reply, "Error: no off-grid frequency for this band");
} else {
snprintf(reply, CLI_REPLY_SIZE, "OK - repeat is now %s (%.3f MHz)",
_mesh.isOffgridActive() ? "ON" : "OFF", (double)_mesh.prefs.freq);
}
}
MeshTimeSync* getMeshTimeSync() override {
return _mesh.getMeshTimeSync();
}
+28 -28
View File
@@ -39,27 +39,6 @@ static void notify_contact_msg_ui(
}
#endif
/* Allowed client repeat frequency ranges (matches Arduino) */
struct FreqRange {
uint32_t lower_freq, upper_freq;
};
static const FreqRange repeat_freq_ranges[] = {
{ 433000, 433000 }, /* 433 MHz (ISM band) */
{ 869495, 869495 }, /* 869.495 MHz (EU 869.4-869.65 band, 10% DC, 500mW ERP) */
{ 918000, 918000 }, /* 918 MHz (US ISM band) */
};
static bool isValidClientRepeatFreq(uint32_t f)
{
for (size_t i = 0; i < sizeof(repeat_freq_ranges) / sizeof(repeat_freq_ranges[0]); i++) {
if (f >= repeat_freq_ranges[i].lower_freq && f <= repeat_freq_ranges[i].upper_freq) {
return true;
}
}
return false;
}
CompanionMesh::CompanionMesh(mesh::Radio &radio, mesh::MillisecondClock &ms, mesh::RNG &rng,
mesh::RTCClock &rtc, mesh::PacketManager &mgr, mesh::MeshTables &tables,
ZephyrDataStore &store)
@@ -143,7 +122,27 @@ void CompanionMesh::begin()
bool CompanionMesh::allowPacketForward(const mesh::Packet *packet)
{
(void)packet;
return prefs.client_repeat != 0;
return mesh::offgridActive(prefs);
}
bool CompanionMesh::isOffgridActive() const
{
return mesh::offgridActive(prefs);
}
mesh::OffgridResult CompanionMesh::setOffgridMode(bool on)
{
mesh::OffgridResult r = mesh::offgridSet(prefs, on);
if (r == mesh::OFFGRID_SET || r == mesh::OFFGRID_RETUNED) {
_store->savePrefs(prefs);
}
if (r == mesh::OFFGRID_RETUNED && _radio_reconfig_cb) {
_radio_reconfig_cb(true);
}
LOG_INF("offgrid %s: result=%d freq=%u kHz", on ? "on" : "off", (int)r,
(unsigned)mesh::offgridFreqKhz(prefs.freq));
return r;
}
void CompanionMesh::sendFloodScoped(const TransportKey &scope, mesh::Packet *pkt, uint32_t delay_millis)
@@ -1789,7 +1788,7 @@ bool CompanionMesh::handleCmdDeviceQuery(const uint8_t *data, size_t len)
memcpy(&rsp[8], fw_build, 12);
memcpy(&rsp[20], model, 40);
memcpy(&rsp[60], version, 20);
rsp[80] = prefs.client_repeat; // v9+: offgrid mode state
rsp[80] = mesh::offgridActive(prefs) ? 1 : 0; // v9+: offgrid mode state
rsp[81] = prefs.path_hash_mode; // v10+: path hash mode
writeFrame(rsp, sizeof(rsp));
return true;
@@ -2439,7 +2438,7 @@ bool CompanionMesh::handleCmdSetRadioParams(const uint8_t *data, size_t len)
repeat = data[11]; // v9+: client repeat flag
}
// If repeat requested, validate frequency against allowed bands
if (repeat && !isValidClientRepeatFreq(freq)) {
if (repeat && !mesh::isOffgridFreqKhz(freq)) {
writeErrFrame(ERR_ILLEGAL_ARG);
/* freq arrives in kHz, bw in Hz. Same limits as the USB
* CLI and the prefs loader — see NodePrefs.h. Accepting
@@ -2464,6 +2463,7 @@ bool CompanionMesh::handleCmdSetRadioParams(const uint8_t *data, size_t len)
prefs.sf = sf;
prefs.cr = cr;
prefs.client_repeat = repeat;
mesh::offgridFreqEdited(prefs);
_store->savePrefs(prefs);
if (_radio_reconfig_cb) _radio_reconfig_cb(preset_changed);
LOG_INF("SET_RADIO_PARAMS: client_repeat=%d", repeat);
@@ -3392,12 +3392,12 @@ bool CompanionMesh::handleCmdGetAutoaddConfig(const uint8_t *data, size_t len)
bool CompanionMesh::handleCmdGetAllowedRepeatFreq(const uint8_t *data, size_t len)
{
uint8_t rsp[1 + sizeof(repeat_freq_ranges)];
uint8_t rsp[1 + sizeof(mesh::OFFGRID_FREQ_RANGES)];
int i = 0;
rsp[i++] = PACKET_ALLOWED_REPEAT_FREQ;
for (size_t k = 0; k < sizeof(repeat_freq_ranges) / sizeof(repeat_freq_ranges[0]); k++) {
put_le32(&rsp[i], repeat_freq_ranges[k].lower_freq); i += 4;
put_le32(&rsp[i], repeat_freq_ranges[k].upper_freq); i += 4;
for (const mesh::OffgridFreqRange &r : mesh::OFFGRID_FREQ_RANGES) {
put_le32(&rsp[i], r.lower_khz); i += 4;
put_le32(&rsp[i], r.upper_khz); i += 4;
}
writeFrame(rsp, i);
return true;
+8
View File
@@ -11,6 +11,7 @@
#include <helpers/TransportKeyStore.h>
#include <ZephyrDataStore.h>
#include <NodePrefs.h>
#include <helpers/OffgridPolicy.h>
#include <zephyr/kernel.h>
/* Push notification codes */
@@ -201,6 +202,13 @@ public:
* zephcore_persist_before_off(). */
void flushPendingWrites() { flushDirtyContacts(); flushDirtyChannels(); }
/* Off-grid mode (upstream "client repeat"): forwarding, allowed only on the
* off-grid frequencies. setOffgridMode() is the device toggle and the CLI's
* `set repeat`: it retunes to the nearest off-grid frequency and back, saves,
* and reconfigures the radio. Main thread only. */
bool isOffgridActive() const;
mesh::OffgridResult setOffgridMode(bool on);
NodePrefs prefs;
protected:
+14 -2
View File
@@ -13,6 +13,7 @@
#include <helpers/ui/display.h>
#endif
#include <helpers/MeshTimeSync.h>
#include <helpers/OffgridPolicy.h>
#include <helpers/LoopWakeStats.h>
extern "C" void power_diag_line(int n, char *out, size_t cap); /* TEMPORARY: helpers/power_diag.c */
#include <helpers/time_sync.h>
@@ -855,6 +856,7 @@ bool CommonCLI::handleRadioCmd(uint32_t sender_timestamp, const char* command, c
_prefs->bw = bw;
_prefs->sf = sf;
_prefs->cr = cr;
mesh::offgridFreqEdited(*_prefs);
_callbacks->savePrefs();
_callbacks->freezeRadioParams(old_freq, old_bw, old_sf, old_cr);
/* Anything that moves the channel or the detPeak base table
@@ -920,6 +922,7 @@ bool CommonCLI::handleRadioCmd(uint32_t sender_timestamp, const char* command, c
f >= 150.0f && f <= 2500.0f) {
float old_freq = _prefs->freq;
_prefs->freq = f;
mesh::offgridFreqEdited(*_prefs);
savePrefs();
/* Keep _prefs->freq = f in RAM so a later savePrefs() (from any
* other "set" command before reboot) can't rewrite the old freq
@@ -1024,7 +1027,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, const char* command, cha
} else if (memcmp(config, "name", 4) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->node_name);
} else if (memcmp(config, "repeat", 6) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %s", _prefs->disable_fwd ? "off" : "on");
bool on = _callbacks->hasOffgridMode() ? _callbacks->isOffgridActive() : !_prefs->disable_fwd;
snprintf(reply, CLI_REPLY_SIZE, "> %s", on ? "on" : "off");
} else if (memcmp(config, "lat", 3) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %.6f", _prefs->node_lat);
} else if (memcmp(config, "lon", 3) == 0) {
@@ -1473,7 +1477,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, const char* command, cha
strcpy(reply, "Error: name cannot contain [ ] \\ : , ? *");
}
} else if (memcmp(config, "repeat ", 7) == 0) {
if (memcmp(&config[7], "on", 2) == 0) {
bool on = memcmp(&config[7], "on", 2) == 0;
if (_callbacks->hasOffgridMode() && (on || memcmp(&config[7], "off", 3) == 0)) {
/* Local only: it moves the node to another frequency. */
if (sender_timestamp == 0) {
_callbacks->setOffgridMode(on, reply);
} else {
strcpy(reply, "Error: local only");
}
} else if (on) {
_prefs->disable_fwd = 0;
savePrefs();
strcpy(reply, "OK - repeat is now ON");
+6
View File
@@ -79,6 +79,12 @@ public:
* _prefs takes the new ones for the next boot. No-op under tempradio. */
virtual void freezeRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) { (void)freq; (void)bw; (void)sf; (void)cr; }
/* Companion: `get repeat` / `set repeat` are off-grid mode, not the
* servers' repeat pref. setOffgridMode() writes the reply. */
virtual bool hasOffgridMode() const { return false; }
virtual bool isOffgridActive() const { return false; }
virtual void setOffgridMode(bool on, char* reply) { (void)on; (void)reply; }
// Adaptive contention window
virtual float getContentionEstimate() const { return -1.0f; }
virtual float getFloodDelayFactor() const { return 0.5f; }
+5
View File
@@ -168,6 +168,9 @@ struct NodePrefs {
uint8_t buzzer_quiet;
uint8_t autoadd_config;
uint8_t client_repeat; // 1 = offgrid mode (forward packets)
/* MHz the device toggle left when it turned off-grid mode on, 0 = none.
* Only meaningful while off-grid mode is on (helpers/OffgridPolicy.h). */
float offgrid_home_freq;
uint8_t path_hash_mode; // path mode 0-2
uint8_t autoadd_max_hops; // 0 = no limit, N = up to N-1 hops
uint8_t loop_detect; // LOOP_DETECT_{OFF,MINIMAL,MODERATE,STRICT}
@@ -277,6 +280,8 @@ static inline void sanitizeNodePrefs(NodePrefs* p) {
p->airtime_factor = sanePrefFloat(p->airtime_factor, 0.0f, 9.0f, 9.0f);
p->rx_delay_base = sanePrefFloat(p->rx_delay_base, 0.0f, 3600.0f, 0.0f);
p->adc_multiplier = sanePrefFloat(p->adc_multiplier, 0.0f, 30000.0f, 0.0f);
p->offgrid_home_freq = sanePrefFloat(p->offgrid_home_freq, ZC_RADIO_FREQ_MIN_MHZ,
ZC_RADIO_FREQ_MAX_MHZ, 0.0f);
/* Range matches the CLI (0.0-2.0). 0.0 is a legal value meaning "reactive
* backoff off" and must survive: this guard exists for NaN and corruption
* only. RepeaterDataStore used to coerce 0.0 -> 0.2 here, which silently
+132
View File
@@ -0,0 +1,132 @@
/*
* SPDX-License-Identifier: MIT
* ZephCore OffgridPolicy - companion off-grid mode (upstream "client repeat")
*
* The frequencies a companion may forward on, and the prefs transitions behind
* the app command, the device toggle and the CLI. Pure decision logic, no I/O.
* Design: devdocs/lld/06-companion.md.
*/
#pragma once
#include <stdint.h>
#include "NodePrefs.h"
namespace mesh {
struct OffgridFreqRange {
uint32_t lower_khz, upper_khz;
};
/* Upstream's allowed client-repeat frequencies; CMD_GET_ALLOWED_REPEAT_FREQ
* reports this table to the app. */
static const OffgridFreqRange OFFGRID_FREQ_RANGES[] = {
{ 433000, 433000 }, /* 433 MHz ISM */
{ 869495, 869495 }, /* EU 869.4-869.65 MHz */
{ 918000, 918000 }, /* US 902-928 MHz */
};
#define OFFGRID_FREQ_RANGE_COUNT (sizeof(mesh::OFFGRID_FREQ_RANGES) / sizeof(mesh::OFFGRID_FREQ_RANGES[0]))
/* Top of the sub-GHz LoRa range; above it no off-grid frequency applies. */
#define OFFGRID_SUBGHZ_MAX_MHZ 960.0f
inline uint32_t offgridFreqKhz(float mhz)
{
return mhz > 0.0f ? (uint32_t)(mhz * 1000.0f + 0.5f) : 0;
}
inline bool isOffgridFreqKhz(uint32_t khz)
{
for (const OffgridFreqRange &r : OFFGRID_FREQ_RANGES) {
if (khz >= r.lower_khz && khz <= r.upper_khz) return true;
}
return false;
}
inline bool isOffgridFreq(float mhz) { return isOffgridFreqKhz(offgridFreqKhz(mhz)); }
/* The off-grid frequency (kHz) nearest to `mhz`; 0 above the sub-GHz range. */
inline uint32_t nearestOffgridFreqKhz(float mhz)
{
if (!(mhz > 0.0f && mhz <= OFFGRID_SUBGHZ_MAX_MHZ)) return 0;
uint32_t khz = offgridFreqKhz(mhz);
uint32_t best = 0, best_dist = UINT32_MAX;
for (const OffgridFreqRange &r : OFFGRID_FREQ_RANGES) {
uint32_t c = khz < r.lower_khz ? r.lower_khz : (khz > r.upper_khz ? r.upper_khz : khz);
uint32_t dist = c > khz ? c - khz : khz - c;
if (dist < best_dist) {
best_dist = dist;
best = c;
}
}
return best;
}
/* Off-grid mode is on: repeat is set AND the node is on an off-grid frequency. */
inline bool offgridActive(const NodePrefs &p)
{
return p.client_repeat != 0 && isOffgridFreq(p.freq);
}
/* Drops a repeat flag left on any other frequency, and the remembered home
* frequency with it (it only means something while off-grid mode is on).
* True if prefs changed. */
inline bool offgridNormalize(NodePrefs &p)
{
if (offgridActive(p)) return false;
bool changed = p.client_repeat != 0 || p.offgrid_home_freq != 0.0f;
p.client_repeat = 0;
p.offgrid_home_freq = 0.0f;
return changed;
}
/* The frequency was set by the app or the CLI: whoever set it owns it, so the
* remembered home frequency goes. */
inline void offgridFreqEdited(NodePrefs &p)
{
p.offgrid_home_freq = 0.0f;
offgridNormalize(p);
}
enum OffgridResult : uint8_t {
OFFGRID_UNCHANGED, /* already in the requested state */
OFFGRID_SET, /* repeat flag changed, frequency kept */
OFFGRID_RETUNED, /* repeat flag and frequency changed: reconfigure the radio */
OFFGRID_NO_BAND, /* refused: no off-grid frequency for the current band */
};
/* The device toggle and `set repeat`. On: move to the nearest off-grid
* frequency and remember the one left. Off: return to it. A node already on
* an off-grid frequency stays there both ways. BW, SF, CR and TX power are
* never touched (as the app). */
inline OffgridResult offgridSet(NodePrefs &p, bool on)
{
if (on) {
if (offgridActive(p)) return OFFGRID_UNCHANGED;
if (isOffgridFreq(p.freq)) {
p.client_repeat = 1;
p.offgrid_home_freq = 0.0f;
return OFFGRID_SET;
}
uint32_t khz = nearestOffgridFreqKhz(p.freq);
if (khz == 0) return OFFGRID_NO_BAND;
p.offgrid_home_freq = p.freq;
p.freq = (float)khz / 1000.0f;
p.client_repeat = 1;
return OFFGRID_RETUNED;
}
bool was_set = p.client_repeat != 0;
float home = offgridActive(p) ? p.offgrid_home_freq : 0.0f;
p.client_repeat = 0;
p.offgrid_home_freq = 0.0f;
if (home != 0.0f) {
p.freq = home;
return OFFGRID_RETUNED;
}
return was_set ? OFFGRID_SET : OFFGRID_UNCHANGED;
}
} /* namespace mesh */
+1
View File
@@ -144,6 +144,7 @@ protected:
def("vbat_mv", _p->v_battery_alert_mv);
def("adc_mult", _p->adc_multiplier);
def("pwr_sav_en", _p->powersaving_enabled);
def("og_home", _p->offgrid_home_freq);
}
public:
+7 -4
View File
@@ -16,6 +16,7 @@
#include "ui_pages.h"
#include "ui_task.h"
#include "ui_mesh_actions.h"
#include "display.h"
#include <helpers/buzzer_gate.h>
@@ -1391,8 +1392,9 @@ static void render_offgrid_mono(void)
draw_centered(centered_row(2, 3), "Press to confirm");
} else {
draw_centered(centered_row(2, 3),
state.offgrid_enabled ? "Press to Disable"
: "Press to Enable");
state.offgrid_enabled ? "Press to Disable" :
mesh_offgrid_available() ? "Press to Enable"
: "Not on this band");
}
}
@@ -1417,8 +1419,9 @@ static void render_offgrid_color(void)
draw_centered_color(y,
state.offgrid_confirm_time != 0 ?
"Press to confirm" :
(state.offgrid_enabled ? "Press to disable"
: "Press to enable"),
(state.offgrid_enabled ? "Press to disable" :
mesh_offgrid_available() ? "Press to enable"
: "Not on this band"),
state.offgrid_confirm_time != 0 ? UI_COLOR_WARN
: UI_COLOR_VALUE);
}
+5 -1
View File
@@ -305,7 +305,7 @@ static void action_page_enter(void)
st_og->offgrid_confirm_time = 0;
mesh_set_offgrid_mode(new_state);
LOG_INF("offgrid mode %s (button)", new_state ? "on" : "off");
} else {
} else if (st_og->offgrid_enabled || mesh_offgrid_available()) {
/* First press — enter confirmation state */
st_og->offgrid_confirm_time = now_og;
}
@@ -1135,8 +1135,12 @@ void ui_set_buzzer_mode(uint8_t mode)
void ui_set_offgrid_mode(bool enabled)
{
struct ui_state *s = get_state();
bool changed = s->offgrid_enabled != enabled;
s->offgrid_enabled = enabled;
if (changed) {
schedule_render_auto();
}
}
/* Schedule a redraw for an automatic (non-interactive) state update, e.g. a
@@ -1050,7 +1050,7 @@ void JoystickUITask::disableSerial() { _ble_enabled = false; mesh_ble_set_enable
/* ===== Offgrid / LEDs / DFU ===== */
void JoystickUITask::setOffgridMode(bool enable)
{
if (_prefs) _prefs->client_repeat = enable ? 1 : 0;
/* CompanionMesh::setOffgridMode() does the prefs and the retune. */
mesh_set_offgrid_mode(enable);
}
@@ -24,6 +24,7 @@
#include <helpers/ContactInfo.h>
#include <helpers/ChannelDetails.h>
#include <helpers/NodePrefs.h>
#include <helpers/OffgridPolicy.h>
#include <adapters/clock/ZephyrRTCClock.h>
#include <zephyr/kernel.h>
@@ -125,7 +126,10 @@ public:
bool isSerialEnabled() const { return _ble_enabled; }
void disableSerial();
void enableSerial();
bool isOffgridEnabled() const { return _prefs && _prefs->client_repeat != 0; }
bool isOffgridEnabled() const { return _prefs && mesh::offgridActive(*_prefs); }
/* kHz that turning it on leads to (the current one if already off-grid);
* 0 = no off-grid frequency for this band. */
uint32_t offgridTargetKhz() const { return _prefs ? mesh::nearestOffgridFreqKhz(_prefs->freq) : 0; }
void setOffgridMode(bool enable);
bool isLedsDisabled() const { return _prefs && _prefs->leds_disabled != 0; }
void toggleLeds();
@@ -169,6 +169,8 @@ bool SystemScreen::handleInput(char c)
if (_task->isOffgridEnabled()) {
_task->setOffgridMode(false);
_task->showAlert("Offgrid: OFF", 1000);
} else if (_task->offgridTargetKhz() == 0) {
_task->showAlert("No offgrid band", 1500);
} else {
_mode = SYSMODE_OFFGRID_CONFIRM;
}
@@ -278,8 +280,12 @@ bool SystemScreen::handleInput(char c)
}
if (_mode == SYSMODE_OFFGRID_CONFIRM) {
if (c == KEY_ENTER) {
char og_alert[24];
uint32_t og_khz = _task->offgridTargetKhz();
snprintf(og_alert, sizeof(og_alert), "Offgrid ON %u.%03u",
(unsigned)(og_khz / 1000), (unsigned)(og_khz % 1000));
_task->setOffgridMode(true);
_task->showAlert("Offgrid: ON", 1000);
_task->showAlert(og_alert, 1500);
_mode = SYSMODE_DEVICE;
return true;
}
+12 -4
View File
@@ -190,9 +190,14 @@ extern "C" void mesh_set_buzzer_mode(uint8_t mode)
k_event_post(s_mesh_events, s_mesh_event_ui_action);
}
extern "C" bool mesh_offgrid_available(void)
{
return s_mesh && mesh::nearestOffgridFreqKhz(s_mesh->prefs.freq) != 0;
}
extern "C" void mesh_set_offgrid_mode(bool enable)
{
/* Defer the flash write (savePrefs) to mesh thread */
/* Defer the retune and the flash write to mesh thread */
atomic_set(&pending_offgrid_enabled, enable ? 1 : 0);
atomic_or(&pending_ui_actions, UI_ACTION_OFFGRID_TOGGLE);
k_event_post(s_mesh_events, s_mesh_event_ui_action);
@@ -254,9 +259,10 @@ extern "C" void mesh_handle_ui_actions(void)
if (actions & UI_ACTION_OFFGRID_TOGGLE) {
bool og = atomic_get(&pending_offgrid_enabled) != 0;
s_mesh->prefs.client_repeat = og ? 1 : 0;
LOG_INF("client_repeat=%d (button)", og);
need_save = true;
/* Saves and retunes by itself; the page shows what it ended up as. */
s_mesh->setOffgridMode(og);
ui_set_offgrid_mode(s_mesh->isOffgridActive());
ui_push_radio_state(*s_lora_radio);
}
if (actions & UI_ACTION_LEDS_TOGGLE) {
@@ -349,6 +355,8 @@ extern "C" void mesh_housekeeping_ui_refresh(void)
ui_set_tz(s_mesh->prefs.tz_offset);
ui_push_radio_state(*s_lora_radio);
/* The app and the CLI change it too. */
ui_set_offgrid_mode(s_mesh->isOffgridActive());
/* Update GPS satellite count even without fix. When GPS is disabled, push
* a zeroed count — gps_enable(false) already zeros the internal count, but
+2
View File
@@ -57,6 +57,8 @@ void mesh_gps_set_enabled(bool enable);
void mesh_ble_set_enabled(bool enable);
void mesh_set_buzzer_mode(uint8_t mode);
void mesh_set_offgrid_mode(bool enable);
/* False when the current band has no off-grid frequency (2.4 GHz). */
bool mesh_offgrid_available(void);
void mesh_set_leds_disabled(bool disabled);
void mesh_reboot_to_ota_dfu(void);
void mesh_save_brightness(uint8_t brightness);
@@ -19,6 +19,7 @@ __attribute__((weak)) void mesh_gps_set_enabled(bool enable) { ARG_UNUSED(enable
__attribute__((weak)) void mesh_ble_set_enabled(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) void mesh_set_buzzer_mode(uint8_t mode) { ARG_UNUSED(mode); }
__attribute__((weak)) void mesh_set_offgrid_mode(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) bool mesh_offgrid_available(void) { return false; }
__attribute__((weak)) void mesh_set_leds_disabled(bool disabled) { ARG_UNUSED(disabled); }
__attribute__((weak)) void mesh_reboot_to_ota_dfu(void) {}
/* Repeater builds persist orientation through RepeaterDataStore in
+7 -2
View File
@@ -982,6 +982,11 @@ int main(void)
companion_mesh.prefs.gps_interval = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC; /* 0 = always on */
data_store.loadPrefs(companion_mesh.prefs);
/* A repeat flag stored on a non-off-grid frequency is dropped. */
if (mesh::offgridNormalize(companion_mesh.prefs)) {
LOG_WRN("offgrid mode dropped: not on an off-grid frequency");
data_store.savePrefs(companion_mesh.prefs);
}
/* ESP32 light sleep: `powersaving off` holds a sleep lock. No-op elsewhere. */
zc_pm_set_powersaving(companion_mesh.prefs.powersaving_enabled != 0);
/* Until the first fix, the position is the configured one. */
@@ -1071,9 +1076,9 @@ int main(void)
ui_set_gps_enabled(companion_mesh.prefs.gps_enabled != 0);
ui_set_ble_enabled(companion_mesh.prefs.ble_disabled != 1); /* BLE starts advertising at boot */
ui_set_offgrid_mode(companion_mesh.prefs.client_repeat != 0);
ui_set_offgrid_mode(companion_mesh.isOffgridActive());
LOG_INF("offgrid mode: %s (from prefs)",
companion_mesh.prefs.client_repeat ? "on" : "off");
companion_mesh.isOffgridActive() ? "on" : "off");
/* buzzer_init() starts quiet, so the saved mode is always applied. */
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER)