Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
3aee296
Make table sizes overridable (MAX_ANON_CONTACTS, MAX_PACKET_HASHES, A…
kim7tin Oct 6, 2026
27ef6c3
Add CONTACT_OUT_PATH_SIZE: contacts can store shorter direct routes (…
kim7tin Oct 6, 2026
fbd55e1
Optional RX_DELAY_FAST_POW: rx delay without libm pow()
kim7tin Oct 6, 2026
35faf61
Optional DISABLE_CLI_RESCUE for serial-only boards without a filesystem
kim7tin Oct 6, 2026
9ef36c7
Optional CONFIG_SERIALIZER_NOOP for boards that persist prefs as a bi…
kim7tin Oct 6, 2026
b10e949
Optional ED25519_SMALL_BASE_TABLE: ~30 KB smaller fixed-base table fo…
kim7tin Oct 6, 2026
27ac886
Optional SERIAL_FRAME_STRICT: drop oversize frames and resync after a…
kim7tin Oct 6, 2026
c2e208a
Optional DISABLE_COMPANION_CLI: leave the text CLI out on boards shor…
kim7tin Oct 6, 2026
cba7d28
Add ASR650x platform helpers (radio, RNG seed, RTC, flash-row store, …
kim7tin Oct 6, 2026
e0bd82b
Add Heltec CubeCell AB02S variant: legacy ASR6502, USB/UART companion…
kim7tin Oct 6, 2026
5025a41
SERIAL_FRAME_STRICT: only abandon a stalled frame when nothing is buf…
kim7tin Oct 6, 2026
19d9b30
AB02S: keep the other companion settings across reboots (flash record…
kim7tin Oct 6, 2026
f8d1ca8
AB02S: keep up to 3 contacts across reboots (CONTACT_PIN_STORE)
kim7tin Oct 7, 2026
15ed6a2
ASR650x / AB02S: upstream style (clang-format, camelCase), neutral re…
kim7tin Oct 7, 2026
afb2774
build.sh and CI: ASR650x firmware artifacts, new native test envs, AB…
kim7tin Oct 7, 2026
a3142ac
AB02S: pinned contacts reloaded with lastmod=1 so GET_CONTACTS lists …
kim7tin Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/pr-build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ jobs:
- wio_wm1110_repeater
# SX1276 (ESP32)
- Tbeam_SX1276_repeater
# ASR650x (CubeCell, 128 KB flash: catches size regressions)
- Heltec_CubeCell_AB02S_companion_radio_usb

steps:
- name: Clone Repo
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-build-environment

- name: Run Unit Tests
run: pio test -e native -e native_kiss_modem -vv
run: pio test -e native -e native_kiss_modem -e native_noop_config -e native_ed25519_small -vv

- name: Upload Test Results
# Upload test results even if the test step failed.
Expand Down
8 changes: 7 additions & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ for section, options in data:
for key, value in options:
if key == 'build_flags':
for flag in value:
match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag)
match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM|ASR650X_PLATFORM)', flag)
if match:
print(match.group(1))
sys.exit(0)
Expand Down Expand Up @@ -181,6 +181,12 @@ build_firmware() {
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
fi

# for asr650x (CubeCell), copy .cyacd (CubeCellFlash / bootloader image) and .hex to out folder
if [ "$ENV_PLATFORM" == "ASR650X_PLATFORM" ]; then
cp .pio/build/$1/firmware.cyacd out/${FIRMWARE_FILENAME}.cyacd 2>/dev/null || true
cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true
fi

}

# firmwares containing $1 will be built
Expand Down
4 changes: 4 additions & 0 deletions examples/companion_radio/DataStore.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#include <Arduino.h>
#include "DataStore.h"

#if CONTACT_OUT_PATH_SIZE < MAX_PATH_SIZE
#error "this file-based contact store writes 64-byte routes: build it with the default CONTACT_OUT_PATH_SIZE"
#endif

#if defined(EXTRAFS) || defined(QSPIFLASH)
#define MAX_BLOBRECS 100
#else
Expand Down
6 changes: 6 additions & 0 deletions examples/companion_radio/DataStore.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#pragma once

#if defined(ASR650X_PLATFORM)
#include "FlashDataStore.h" // no filesystem: identity + prefs in two SFLASH rows, contacts in RAM
#else

#include <helpers/IdentityStore.h>
#include <helpers/ContactInfo.h>
#include <helpers/ChannelDetails.h>
Expand Down Expand Up @@ -53,3 +57,5 @@ class DataStore {
private:
FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;};
};

#endif
220 changes: 220 additions & 0 deletions examples/companion_radio/FlashDataStore.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#if defined(ASR650X_PLATFORM) // the other boards use DataStore.cpp (filesystem)
#include "FlashDataStore.h"

#include "target.h"

#include <helpers/asr650x/BigStack.h>
#include <helpers/asr650x/PinnedContacts.h>

namespace {
struct CommitArgs {
asr650x::PersistStore<FlashRowBackend> *store;
const asr650x::PersistState *cand;
bool ok;
};
void runCommit(void *p) {
CommitArgs *a = (CommitArgs *)p;
a->ok = a->store->commit(*a->cand);
}
} // namespace

bool DataStore::commitBig(const asr650x::PersistState &c) {
CommitArgs a = { &_store, &c, false };
asr650xCallOnBigStack(runCommit, &a);
return a.ok;
}

void DataStore::begin() {
_store.begin();
asr650xSetTxAllowed(_store.state().has_identity); // never transmit as the unprovisioned placeholder
}

// ---- pinned contacts (SFLASH row 2). Every operation runs on the crypto stack: the row, a PinSet and a
// ContactInfo together are ~600 bytes, too much for the 2 KB main stack, and none of it stays in RAM.
namespace {
const size_t PIN_ROW = 2 * asr650x::ROW_STRIDE;

enum PinOp { PIN_LOAD, PIN_REFRESH, PIN_ADD, PIN_DEL, PIN_CLEAR };

struct PinArgs {
FlashRowBackend *be;
DataStoreHost *host;
bool (*filter)(const ContactInfo &);
const ContactInfo *contact;
const uint8_t *pub;
PinOp op;
};

void runPins(void *p) {
PinArgs *a = (PinArgs *)p;
asr650x::PinSet s;
uint8_t row[asr650x::PIN_SIZE];
if (a->op != PIN_CLEAR) {
a->be->read(PIN_ROW, row, asr650x::PIN_SIZE);
asr650x::pinsDecode(row, s); // erased/corrupt -> empty set
}
bool changed = false;
if (a->op == PIN_LOAD) {
for (int i = 0; i < s.n; i++) {
ContactInfo ci;
memset(&ci, 0, sizeof(ci));
ci.id = mesh::Identity(s.e[i].pub);
memcpy(ci.name, s.e[i].name, sizeof(ci.name));
ci.type = s.e[i].type;
ci.flags = s.e[i].flags;
ci.out_path_len = OUT_PATH_UNKNOWN; // the route is learnt again (flood first)
ci.lastmod = 1; // non-zero: GET_CONTACTS only lists contacts with lastmod > since (0)
a->host->onContactLoaded(ci);
}
return;
}
if (a->op == PIN_REFRESH) {
ContactInfo ci;
for (uint32_t idx = 0; a->host->getContactForSave(idx, ci); idx++) {
if (a->filter && !a->filter(ci)) continue;
if (asr650x::pinRefresh(s, ci.id.pub_key, ci.name, ci.type, ci.flags) == asr650x::PIN_CHANGED)
changed = true;
}
} else if (a->op == PIN_ADD) {
const ContactInfo &c = *a->contact;
changed = asr650x::pinUpsert(s, c.id.pub_key, c.name, c.type, c.flags) ==
asr650x::PIN_CHANGED; // FULL: RAM only
} else if (a->op == PIN_DEL) {
changed = asr650x::pinRemove(s, a->pub);
} else {
changed = true; // clear: write the empty record
}
if (changed) {
asr650x::pinsEncode(s, row);
a->be->write(PIN_ROW, row, asr650x::PIN_SIZE);
}
}
} // namespace

void DataStore::loadContacts(DataStoreHost *host) {
PinArgs a = { &_be, host, NULL, NULL, NULL, PIN_LOAD };
asr650xCallOnBigStack(runPins, &a);
}

void DataStore::saveContacts(DataStoreHost *host, bool (*filter)(const ContactInfo &c)) {
PinArgs a = { &_be, host, filter, NULL, NULL, PIN_REFRESH };
asr650xCallOnBigStack(runPins, &a);
}

void DataStore::pinContact(const ContactInfo &c) {
PinArgs a = { &_be, NULL, NULL, &c, NULL, PIN_ADD };
asr650xCallOnBigStack(runPins, &a);
}

void DataStore::unpinContact(const uint8_t *pub_key) {
PinArgs a = { &_be, NULL, NULL, NULL, pub_key, PIN_DEL };
asr650xCallOnBigStack(runPins, &a);
}

bool DataStore::formatFileSystem() {
asr650x::PersistState empty;
memset(&empty, 0, sizeof(empty));
bool ok = commitBig(empty);
if (ok) asr650xSetTxAllowed(false);
PinArgs a = { &_be, NULL, NULL, NULL, NULL, PIN_CLEAR }; // factory reset also forgets the pinned contacts
asr650xCallOnBigStack(runPins, &a);
return ok;
}

bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
const asr650x::PersistState &s = _store.state();
if (!s.has_identity) return false;
uint8_t raw[96];
memcpy(raw, s.prv, 64);
memcpy(raw + 64, s.pub, 32);
identity.readFrom(raw, sizeof(raw));
return true;
}

bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
if (asr650xIdentityIsPlaceholder(identity)) { // never persist the placeholder ...
asr650xSetTxAllowed(false); // ... and never transmit as it (even if a real key was loaded before)
return true;
}
mesh::LocalIdentity copy = identity;
uint8_t raw[96];
if (copy.writeTo(raw, sizeof(raw)) != sizeof(raw)) return false;
asr650x::PersistState c = _store.state();
c.has_identity = true;
memcpy(c.prv, raw, 64);
memcpy(c.pub, raw + 64, 32);
bool ok = commitBig(c);
if (ok) asr650xSetTxAllowed(true);
return ok;
}

void DataStore::loadPrefs(NodePrefs &prefs) {
const asr650x::PersistState &s = _store.state();
if (!s.has_prefs) {
prefs.gps_enabled = 1;
return;
} // default: GPS on
prefs.gps_enabled = s.gps_off ? 0 : 1;
prefs.gps_interval = s.gps_interval;
strncpy(prefs.node_name, s.node_name, sizeof(prefs.node_name) - 1);
prefs.node_name[sizeof(prefs.node_name) - 1] = 0;
prefs.freq = s.freq;
prefs.bw = s.bw;
prefs.sf = s.sf;
prefs.cr = s.cr;
prefs.tx_power_dbm = s.tx_power_dbm;
if (!s.has_ext) return; // record from an older build: keep the defaults
prefs.airtime_factor = s.airtime_factor;
prefs.rx_delay_base = s.rx_delay_base;
prefs.tx_delay_factor = s.tx_delay_factor;
prefs.direct_tx_delay_factor = s.direct_tx_delay_factor;
prefs.multi_acks = s.multi_acks;
prefs.manual_add_contacts = s.manual_add_contacts;
prefs.telemetry_mode_base = s.telemetry_modes & 0x03;
prefs.telemetry_mode_loc = (s.telemetry_modes >> 2) & 0x03;
prefs.telemetry_mode_env = (s.telemetry_modes >> 4) & 0x03;
prefs.advert_loc_policy = s.advert_loc_policy;
prefs.autoadd_config = s.autoadd_config;
prefs.autoadd_max_hops = s.autoadd_max_hops;
prefs.path_hash_mode = s.path_hash_mode;
prefs.tz_offset = s.tz_offset;
prefs.cad_enabled = s.cad_enabled;
prefs.interference_threshold = s.interference_threshold;
prefs.agc_reset_interval = s.agc_reset_interval;
prefs.rx_boosted_gain = s.rx_boosted_gain;
}

bool DataStore::savePrefs(NodePrefs &prefs) {
asr650x::PersistState c = _store.state();
c.has_prefs = true;
c.gps_off = prefs.gps_enabled == 0;
c.gps_interval = prefs.gps_interval;
strncpy(c.node_name, prefs.node_name, sizeof(c.node_name) - 1);
c.node_name[sizeof(c.node_name) - 1] = 0;
c.freq = prefs.freq;
c.bw = prefs.bw;
c.sf = prefs.sf;
c.cr = prefs.cr;
c.tx_power_dbm = prefs.tx_power_dbm;
c.has_ext = true;
c.airtime_factor = prefs.airtime_factor;
c.rx_delay_base = prefs.rx_delay_base;
c.tx_delay_factor = prefs.tx_delay_factor;
c.direct_tx_delay_factor = prefs.direct_tx_delay_factor;
c.multi_acks = prefs.multi_acks;
c.manual_add_contacts = prefs.manual_add_contacts;
c.telemetry_modes =
(uint8_t)((prefs.telemetry_mode_base & 0x03) | ((prefs.telemetry_mode_loc & 0x03) << 2) |
((prefs.telemetry_mode_env & 0x03) << 4));
c.advert_loc_policy = prefs.advert_loc_policy;
c.autoadd_config = prefs.autoadd_config;
c.autoadd_max_hops = prefs.autoadd_max_hops;
c.path_hash_mode = prefs.path_hash_mode;
c.tz_offset = prefs.tz_offset;
c.cad_enabled = prefs.cad_enabled;
c.interference_threshold = prefs.interference_threshold;
c.agc_reset_interval = prefs.agc_reset_interval;
c.rx_boosted_gain = prefs.rx_boosted_gain;
return commitBig(c);
}
#endif // ASR650X_PLATFORM
46 changes: 46 additions & 0 deletions examples/companion_radio/FlashDataStore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#pragma once
#include "NodePrefs.h"

#include <Identity.h>
#include <helpers/ChannelDetails.h>
#include <helpers/ContactInfo.h>
#include <helpers/asr650x/FlashRecordStore.h>
#include <helpers/asr650x/FlashRowBackend.h>

class DataStoreHost {
public:
virtual bool onContactLoaded(const ContactInfo &contact) = 0;
virtual bool getContactForSave(uint32_t idx, ContactInfo &contact) = 0;
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails &ch) = 0;
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails &ch) = 0;
};

// Same surface MyMesh uses from MeshCore's file-based DataStore. Identity + name + radio params go to
// SFLASH rows 0/1 (FlashRecord, A/B). Up to 3 contacts added or edited by the app are pinned in SFLASH row 2
// (PinnedContacts.h); all other contacts, channels and blobs are RAM-only.
class DataStore {
FlashRowBackend _be;
asr650x::PersistStore<FlashRowBackend> _store;
bool commitBig(
const asr650x::PersistState &c); // runs the flash commit on the crypto stack (main stack is only 2 KB)
public:
DataStore() : _store(_be) {}
void begin();
bool formatFileSystem(); // factory reset: erases identity + prefs record and the pinned contacts
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs &prefs);
bool savePrefs(NodePrefs &prefs);
void loadContacts(DataStoreHost *host); // pinned contacts back into the table at boot
void saveContacts(DataStoreHost *host,
bool (*filter)(const ContactInfo &c) = NULL); // refreshes pinned copies
void pinContact(const ContactInfo &c); // the app added or edited this contact
void unpinContact(const uint8_t *pub_key); // the app removed it
void loadChannels(DataStoreHost *) {}
void saveChannels(DataStoreHost *) {}
uint8_t getBlobByKey(const uint8_t[], int, uint8_t[]) { return 0; }
bool putBlobByKey(const uint8_t[], int, const uint8_t[], uint8_t) { return false; }
bool deleteBlobByKey(const uint8_t[], int) { return false; }
uint32_t getStorageUsedKb() const { return 0; }
uint32_t getStorageTotalKb() const { return 0; }
};
Loading