sync to vanilla 1.16

This commit is contained in:
liquidraver
2026-06-06 16:42:39 +02:00
parent 4353936328
commit ffe9ea652d
15 changed files with 105 additions and 24 deletions
+8
View File
@@ -394,6 +394,14 @@ project(zephcore)
string(TIMESTAMP ZEPHCORE_BUILD_DATE "%Y %b %d" UTC)
add_definitions(-DFIRMWARE_BUILD_DATE="${ZEPHCORE_BUILD_DATE}")
# Single source of truth for the firmware version string (vMAJOR.MINOR.PATCH-zephyr).
# Injected globally like FIRMWARE_BUILD_DATE above, so every app TU sees the same
# value; the per-app `#ifndef FIRMWARE_VERSION` fallbacks only apply to builds that
# bypass this injection. NOTE: the BLE DIS value in boards/common/zephcore_common.conf
# (CONFIG_BT_DIS_FW_REV_STR) is Kconfig, not C, so it must be bumped here AND there.
set(ZEPHCORE_FIRMWARE_VERSION "v1.16.0-zephyr")
add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}")
add_subdirectory(lib/monocypher)
target_link_libraries(app PRIVATE monocypher)
+2
View File
@@ -187,6 +187,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot.
| `get backoff.multiplier` | Per-dupe reactive backoff multiplier |
| `get flood.max` | Max flood retransmit hops |
| `get flood.max.unscoped` | Max retransmit hops for un-scoped floods |
| `get flood.max.advert` | Max retransmit hops for ADVERT floods |
| `get flood.advert.interval` | Flood advertisement interval in hours |
| `get advert.interval` | Local advertisement interval in minutes |
| `get apc.margin` | Adaptive Power Control target RSSI margin in dB |
@@ -229,6 +230,7 @@ Changes are persisted immediately unless noted. Some require a reboot.
| `set backoff.multiplier <m>` | 0.02.0 | Per-dupe reactive backoff multiplier (0 = disable reactive backoff) |
| `set flood.max <count>` | 064 | Maximum flood retransmit hops |
| `set flood.max.unscoped <count>` | 064 | Hop limit for un-scoped floods only (default 64 = same as flood.max); scoped/transport floods still use flood.max |
| `set flood.max.advert <count>` | 064 | Hop limit for ADVERT floods only (default 8); curbs advert churn independent of flood.max |
| `set flood.advert.interval <hours>` | 3168 | How often the repeater floods its own advertisement |
| `set advert.interval <mins>` | min240 | How often the repeater sends local advertisements |
| `set apc.margin <db>` | 630 | Target RSSI margin for Adaptive Power Control |
@@ -7,6 +7,7 @@
*/
#include "ZephyrDataStore.h"
#include <AdvertDataHelpers.h> // ADV_TYPE_NONE (transient/anon contacts)
#include <zephyr/fs/fs.h>
#include <zephyr/fs/littlefs.h>
#include <zephyr/storage/flash_map.h>
@@ -702,18 +703,25 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host)
struct fs_file_t file;
uint8_t rec[CONTACT_DATA_SZ];
uint32_t idx = 0;
uint32_t idx = 0; // contacts iterated (incl. skipped anon)
uint32_t written = 0; // records actually written to the file
ContactInfo c;
bool write_ok = true;
auto write_contacts = [&](struct fs_file_t *dst) -> bool {
while (host->getContactForSave(idx, c)) {
// Don't persist transient/anon contacts (non-contact requests)
if (c.type == ADV_TYPE_NONE) {
idx++;
continue;
}
contact_to_record(c, rec);
if (fs_write(dst, rec, CONTACT_DATA_SZ) != (ssize_t)CONTACT_DATA_SZ) {
LOG_ERR("saveContacts: write failed at record %u", idx);
return false;
}
idx++;
written++;
}
return true;
};
@@ -749,7 +757,7 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host)
return;
}
LOG_INF("saveContacts: mode=%s saved %u contacts to %s (%u bytes)",
save_mode, idx, path, idx * CONTACT_DATA_SZ);
save_mode, written, path, written * CONTACT_DATA_SZ);
}
/* ── Channels ──────────────────────────────────────────────────────── */
+21 -4
View File
@@ -552,8 +552,9 @@ bool CompanionMesh::continueContactIteration()
if (_contact_iter_idx < getNumContacts()) {
ContactInfo c;
if (getContactByIdx(_contact_iter_idx, c)) {
// Skip transient/anon contacts (ADV_TYPE_NONE) — never synced to the app.
// Apply 'since' filter - only send contacts modified after the timestamp
if (c.lastmod > _contact_iter_since) {
if (c.type != ADV_TYPE_NONE && c.lastmod > _contact_iter_since) {
if (c.lastmod > _contact_iter_lastmod) {
_contact_iter_lastmod = c.lastmod;
}
@@ -2226,6 +2227,9 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE __DATE__
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v0.0.0-dev" // real value injected by CMakeLists.txt
#endif
/* Wire format reserves 40 bytes for the board name. Require room
* for a null terminator within those 40 bytes so a phone parsing
* it as a C-string never reads past the field. */
@@ -2233,10 +2237,10 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
"CONFIG_ZEPHCORE_BOARD_NAME must fit in 40 bytes including null terminator");
static const uint8_t fw_build[12] = FIRMWARE_BUILD_DATE;
static const uint8_t model[40] = CONFIG_ZEPHCORE_BOARD_NAME;
static const uint8_t version[20] = "v1.15.9-zephyr";
static const uint8_t version[20] = FIRMWARE_VERSION; // injected by CMakeLists.txt
uint8_t rsp[82];
rsp[0] = PACKET_DEVICE_INFO;
rsp[1] = 12; // FIRMWARE_VER_CODE - v12 = CMD_SET_FLOOD_SCOPE_KEY [0x36, 0x01] unscoped variant
rsp[1] = 13; // FIRMWARE_VER_CODE - v13 = CMD_SEND_ANON_REQ to non-contact pubkey (transient anon contacts)
rsp[2] = (MAX_CONTACTS / 2 > 255) ? 255 : (MAX_CONTACTS / 2); // protocol byte, app multiplies by 2
rsp[3] = MAX_GROUP_CHANNELS;
put_le32(&rsp[4], prefs.ble_pin ? prefs.ble_pin : 123456); // BLE PIN
@@ -2952,6 +2956,19 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
case CMD_SEND_ANON_REQ:
if (len >= 1 + PUB_KEY_SIZE + 1) {
ContactInfo *contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE);
if (contact == nullptr) {
/* FIRMWARE_VER_CODE 13+: allow requests to a non-contact pubkey by
* creating a transient "anon" contact (ADV_TYPE_NONE). These are never
* persisted or synced to the app; re-lookup to get the stable slot. */
ContactInfo anon;
memset(&anon, 0, sizeof(anon));
memcpy(anon.id.pub_key, &data[1], PUB_KEY_SIZE);
anon.out_path_len = 0; // zero-hop direct by default
anon.type = ADV_TYPE_NONE; // transient/unknown
if (addContact(anon)) {
contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE);
}
}
if (contact) {
uint32_t tag, est_timeout;
int result = sendAnonReq(*contact, &data[1 + PUB_KEY_SIZE], len - 1 - PUB_KEY_SIZE, tag, est_timeout);
@@ -2963,7 +2980,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
sendPacketError(ERR_BAD_STATE);
}
} else {
sendPacketError(ERR_NOT_FOUND);
sendPacketError(ERR_TABLE_FULL);
}
} else {
sendPacketError(ERR_ILLEGAL_ARG);
+3 -1
View File
@@ -19,7 +19,9 @@
#include "observer_creds.h"
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.9-zephyr"
// Real version injected by CMakeLists.txt (-DFIRMWARE_VERSION); this fallback
// only applies to builds that bypass that injection and should never surface.
#define FIRMWARE_VERSION "v0.0.0-dev"
#endif
#ifndef FIRMWARE_BUILD_DATE
+8 -1
View File
@@ -195,6 +195,10 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
fs_read(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
fs_read(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled));
fs_read(&file, &prefs.apc_margin, sizeof(prefs.apc_margin));
/* Flood hop-ceiling extensions (absent in <296-byte files; the no-op EOF
* read leaves the constructor defaults flood_max_unscoped=64, flood_max_advert=8). */
fs_read(&file, &prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped));
fs_read(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
fs_close(&file);
@@ -234,7 +238,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) {
prefs.path_hash_mode = 1;
prefs.loop_detect = LOOP_DETECT_MODERATE;
savePrefs(prefs);
LOG_INF("loadPrefs: upgraded prefs format (%d -> 294 bytes)", (int)entry.size);
LOG_INF("loadPrefs: upgraded prefs format (%d -> 296 bytes)", (int)entry.size);
}
/* One-time migration: disable RX duty cycle if it was previously enabled. */
@@ -313,6 +317,9 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) {
fs_write(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle));
fs_write(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled));
fs_write(&file, &prefs.apc_margin, sizeof(prefs.apc_margin));
/* Flood hop-ceiling extensions (extend the format past 294 bytes) */
fs_write(&file, &prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped));
fs_write(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert));
ret = fs_sync(&file);
fs_close(&file);
+2
View File
@@ -532,6 +532,8 @@ bool RepeaterMesh::allowPacketForward(const mesh::Packet* packet) {
if (packet->getPathHashCount() >= _prefs.flood_max) return false;
// un-scoped floods can be clamped to a lower hop limit than scoped (transport) floods
if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false;
// ADVERT floods get their own (typically tighter) hop ceiling to curb advert churn
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false;
}
if (packet->isRouteFlood() && recv_pkt_region == nullptr) return false;
if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) {
+3 -1
View File
@@ -28,7 +28,9 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.9-zephyr"
// Real version injected by CMakeLists.txt (-DFIRMWARE_VERSION); this fallback
// only applies to builds that bypass that injection and should never surface.
#define FIRMWARE_VERSION "v0.0.0-dev"
#endif
#ifndef FIRMWARE_BUILD_DATE
+3 -1
View File
@@ -27,7 +27,9 @@
#include "RepeaterDataStore.h"
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.15.9-zephyr"
// Real version injected by CMakeLists.txt (-DFIRMWARE_VERSION); this fallback
// only applies to builds that bypass that injection and should never surface.
#define FIRMWARE_VERSION "v0.0.0-dev"
#endif
#ifndef FIRMWARE_BUILD_DATE
+4 -3
View File
@@ -147,10 +147,11 @@ CONFIG_BT_DIS_MANUF_NAME_STR="ZephCore"
CONFIG_BT_DIS_MODEL_NUMBER=y
CONFIG_BT_DIS_MODEL_NUMBER_STR="ZephCore"
CONFIG_BT_DIS_FW_REV=y
# Keep in sync with the version string in CompanionMesh.cpp CMD_DEVICE_QUERY
# response (the v1.15.9-zephyr literal). Phones that read DIS and phones
# Keep in sync with ZEPHCORE_FIRMWARE_VERSION in CMakeLists.txt (the single source
# of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't
# read a C macro, so it must be bumped here too. Phones that read DIS and phones
# that query CMD_DEVICE_QUERY should see the same version.
CONFIG_BT_DIS_FW_REV_STR="v1.15.9-zephyr"
CONFIG_BT_DIS_FW_REV_STR="v1.16.0-zephyr"
CONFIG_BT_DIS_SW_REV=y
CONFIG_BT_DIS_SW_REV_STR="Zephyr"
CONFIG_BT_DIS_PNP=n
+16 -7
View File
@@ -82,18 +82,27 @@ void BaseChatMesh::bootstrapRTCfromContacts()
}
}
ContactInfo *BaseChatMesh::allocateContactSlot()
ContactInfo *BaseChatMesh::allocateContactSlot(bool transient_only)
{
if (num_contacts < MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
} else if (transient_only || shouldOverwriteWhenFull()) {
int oldest_idx = -1;
uint32_t oldest_lastmod = 0xFFFFFFFF;
for (int i = 0; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
if (transient_only) {
// transient/anon requests only ever recycle an existing anon slot
if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
} else {
// never evict favourites or transient/anon contacts
bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod && contacts[i].type != ADV_TYPE_NONE) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
}
}
if (oldest_idx >= 0) {
@@ -740,7 +749,7 @@ ContactInfo *BaseChatMesh::lookupContactByPubKey(const uint8_t *pub_key, int pre
bool BaseChatMesh::addContact(const ContactInfo &contact)
{
ContactInfo *dest = allocateContactSlot();
ContactInfo *dest = allocateContactSlot(contact.type == ADV_TYPE_NONE);
if (dest) {
*dest = contact;
dest->shared_secret_valid = false;
+8 -3
View File
@@ -19,6 +19,11 @@
#define MAX_CONTACTS 32
#endif
/* Headroom for transient "anon" contacts (type ADV_TYPE_NONE) created to service
* non-contact requests (CMD_SEND_ANON_REQ to a pubkey not in the contact list).
* These are never persisted or synced to the app. Mirrors upstream. */
#define MAX_ANON_CONTACTS 8
#ifdef CONFIG_ZEPHCORE_MAX_CHANNELS
#define MAX_GROUP_CHANNELS CONFIG_ZEPHCORE_MAX_CHANNELS
#else
@@ -78,9 +83,9 @@ public:
class BaseChatMesh : public mesh::Mesh {
friend class ContactsIterator;
ContactInfo contacts[MAX_CONTACTS];
ContactInfo contacts[MAX_CONTACTS + MAX_ANON_CONTACTS];
int num_contacts;
int sort_array[MAX_CONTACTS];
int sort_array[MAX_CONTACTS + MAX_ANON_CONTACTS];
int matching_peer_indexes[MAX_SEARCH_RESULTS];
unsigned long txt_send_timeout;
@@ -117,7 +122,7 @@ protected:
void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void populateContactFromAdvert(ContactInfo &ci, const mesh::Identity &id, const AdvertDataParser &parser, uint32_t timestamp);
ContactInfo *allocateContactSlot();
ContactInfo *allocateContactSlot(bool transient_only = false);
// UI concepts for subclasses to implement
virtual bool isAutoAddEnabled() const { return true; }
+14
View File
@@ -116,6 +116,7 @@ void CommonCLI::loadPrefs(const char* path) {
ok = ok && prefs_read(&file, &_prefs->apc_enabled, sizeof(_prefs->apc_enabled)); // 292
ok = ok && prefs_read(&file, &_prefs->apc_margin, sizeof(_prefs->apc_margin)); // 293
ok = ok && prefs_read(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 294
ok = ok && prefs_read(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295
if (!ok) {
LOG_WRN("Prefs file %s truncated, some fields use defaults", path);
@@ -160,6 +161,7 @@ void CommonCLI::loadPrefs(const char* path) {
_prefs->apc_enabled = constrain(_prefs->apc_enabled, (uint8_t)0, (uint8_t)1);
_prefs->apc_margin = constrain(_prefs->apc_margin, (uint8_t)6, (uint8_t)30);
_prefs->flood_max_unscoped = constrain(_prefs->flood_max_unscoped, (uint8_t)0, (uint8_t)64);
_prefs->flood_max_advert = constrain(_prefs->flood_max_advert, (uint8_t)0, (uint8_t)64);
LOG_INF("Loaded prefs from %s", path);
}
@@ -226,6 +228,7 @@ void CommonCLI::savePrefs(const char* path) {
fs_write(&file, &_prefs->apc_enabled, sizeof(_prefs->apc_enabled));
fs_write(&file, &_prefs->apc_margin, sizeof(_prefs->apc_margin));
fs_write(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped));
fs_write(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert));
fs_close(&file);
LOG_INF("Saved prefs to %s", path);
@@ -451,6 +454,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
(double)est, (double)ff);
} else if (memcmp(config, "apc.margin", 10) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %d dB", (int)_callbacks->getAPCTargetMargin());
} else if (memcmp(config, "flood.max.advert", 16) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max_advert);
} else if (memcmp(config, "flood.max.unscoped", 18) == 0) {
snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max_unscoped);
} else if (memcmp(config, "flood.max", 9) == 0) {
@@ -679,6 +684,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->tx_delay_factor = atof(&config[8]);
savePrefs();
strcpy(reply, "OK (ignored: txdelay is now adaptive)");
} else if (memcmp(config, "flood.max.advert ", 17) == 0) {
int m = atoi(&config[17]);
if (m >= 0 && m <= 64) {
_prefs->flood_max_advert = (uint8_t)m;
savePrefs();
strcpy(reply, "OK");
} else {
strcpy(reply, "Error: range 0-64");
}
} else if (memcmp(config, "flood.max.unscoped ", 19) == 0) {
int m = atoi(&config[19]);
if (m >= 0 && m <= 64) {
+2
View File
@@ -48,6 +48,7 @@ struct NodePrefs {
float bw;
uint8_t flood_max;
uint8_t flood_max_unscoped; // hop limit for un-scoped (ROUTE_TYPE_FLOOD) floods
uint8_t flood_max_advert; // hop limit for ADVERT floods (curbs advert churn)
uint8_t interference_threshold;
uint8_t agc_reset_interval; // stored as secs / 4
// Power saving
@@ -120,6 +121,7 @@ static inline void initNodePrefs(NodePrefs* prefs) {
prefs->multi_acks = 0;
prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!)
prefs->flood_max_unscoped = 64; // un-scoped flood hop limit (defaults to flood_max)
prefs->flood_max_advert = 8; // ADVERT flood hop limit (upstream default)
prefs->interference_threshold = 0;
prefs->agc_reset_interval = 0;
prefs->powersaving_enabled = 0;
+1 -1
View File
@@ -581,7 +581,7 @@ public:
void savePrefs() override {
data_store.savePrefs(companion_mesh.prefs);
}
const char* getFirmwareVer() override { return "v1.15.9-zephyr"; }
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return "companion"; }
bool formatFileSystem() override { return data_store.formatFileSystem(); }