First iteration that seems to work

This commit is contained in:
liquidraver committed 2026-02-20 12:43:13 +01:00
commit 8d1823d0b6
205 files changed
+41989

No files matched your search

+426
View File
@@ -0,0 +1,426 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Dispatcher implementation
*/
#include <mesh/Dispatcher.h>
#include <mesh/MeshCore.h>
#include <mesh/Utils.h>
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
/* Payload types for [src -> dest] logging */
#define PAYLOAD_TYPE_REQ 0x00
#define PAYLOAD_TYPE_RESPONSE 0x01
#define PAYLOAD_TYPE_TXT_MSG 0x02
#define PAYLOAD_TYPE_PATH 0x08
#endif
namespace mesh {
#define MAX_RX_DELAY_MILLIS 32000
#define NOISE_FLOOR_CALIB_INTERVAL 2000
Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = nullptr;
total_air_time = rx_air_time = 0;
next_tx_time = 0;
cad_busy_start = 0;
next_floor_calib_time = next_agc_reset_time = 0;
_err_flags = 0;
_duty_cycle.init(0);
radio_nonrx_start = 0;
prev_isrecv_mode = true;
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
}
void Dispatcher::begin()
{
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
_err_flags = 0;
radio_nonrx_start = (uint32_t)_ms->getMillis();
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
_duty_cycle.init(getDutyCyclePercent());
}
uint8_t Dispatcher::getDutyCyclePercent() const
{
return 10;
}
bool Dispatcher::isAdminPacket(const Packet *pkt)
{
uint8_t t = pkt->getPayloadType();
return t == PAYLOAD_TYPE_REQ || t == PAYLOAD_TYPE_RESPONSE ||
t == PAYLOAD_TYPE_ANON_REQ || t == PAYLOAD_TYPE_CONTROL;
}
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const
{
/* Lookup table: (10^(0.85 - i*0.1) - 1) for i=0..10 (score 0.0 to 1.0)
* Replaces powf() to save ~1.9KB flash. Linear interpolation between entries. */
static const float lut[11] = {
6.0793f, 4.6236f, 3.4674f, 2.5489f, 1.8184f, 1.2389f,
0.7783f, 0.4125f, 0.1220f, -0.1089f, -0.2921f
};
if (score <= 0.0f) return (int)(lut[0] * (float)air_time);
if (score >= 1.0f) return (int)(lut[10] * (float)air_time);
float idx = score * 10.0f;
int i = (int)idx;
float frac = idx - (float)i;
float val = lut[i] + frac * (lut[i + 1] - lut[i]);
return (int)(val * (float)air_time);
}
uint32_t Dispatcher::getCADFailRetryDelay() const
{
return 200;
}
uint32_t Dispatcher::getCADFailMaxDuration() const
{
return 4000;
}
void Dispatcher::loop()
{
if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL);
}
bool is_recv = _radio->isInRecvMode();
if (is_recv != prev_isrecv_mode) {
prev_isrecv_mode = is_recv;
if (!is_recv) {
radio_nonrx_start = (uint32_t)_ms->getMillis();
}
}
if (!is_recv && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) {
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
}
if (outbound) {
if (_radio->isSendComplete()) {
uint32_t t = (uint32_t)_ms->getMillis() - outbound_start;
total_air_time += t;
_duty_cycle.recordTx(t, (uint32_t)_ms->getMillis());
_radio->onSendFinished();
logTx(outbound, 2 + outbound->path_len + outbound->payload_len);
if (outbound->isRouteFlood()) {
n_sent_flood++;
} else {
n_sent_direct++;
}
releasePacket(outbound);
outbound = nullptr;
} else if (millisHasNowPassed(outbound_expiry)) {
_radio->onSendFinished();
logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len);
releasePacket(outbound);
outbound = nullptr;
} else {
return;
}
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
_radio->resetAGC();
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
{
Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis());
if (pkt) {
processRecvPacket(pkt);
}
}
checkRecv();
checkSend();
}
void Dispatcher::checkRecv()
{
Packet *pkt = nullptr;
float score = 0.0f;
uint32_t air_time = 0;
{
uint8_t raw[MAX_TRANS_UNIT + 1];
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
if (len > 0) {
LOG_INF("checkRecv: got raw packet len=%d", len);
logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len);
pkt = _mgr->allocNew();
if (pkt == nullptr) {
LOG_WRN("checkRecv: packet alloc failed");
return;
}
LOG_INF("checkRecv: parsing packet");
int i = 0;
pkt->header = raw[i++];
if (pkt->hasTransportCodes()) {
memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2;
memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2;
} else {
pkt->transport_codes[0] = pkt->transport_codes[1] = 0;
}
pkt->path_len = raw[i++];
if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) {
LOG_WRN("checkRecv: bad path_len=%d", pkt->path_len);
_mgr->free(pkt);
pkt = nullptr;
} else {
memcpy(pkt->path, &raw[i], pkt->path_len);
i += pkt->path_len;
pkt->payload_len = len - i;
if (pkt->payload_len > (int)sizeof(pkt->payload)) {
LOG_WRN("checkRecv: payload too large %d", pkt->payload_len);
_mgr->free(pkt);
pkt = nullptr;
} else {
memcpy(pkt->payload, &raw[i], pkt->payload_len);
pkt->_snr = (int8_t)(_radio->getLastSNR() * 4.0f);
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
LOG_INF("checkRecv: header=0x%02x type=%d route=%s path_len=%d payload_len=%d",
pkt->header, pkt->getPayloadType(),
pkt->isRouteDirect() ? "direct" : "flood",
pkt->path_len, pkt->payload_len);
/* Log path hashes to identify forwarding nodes */
if (pkt->path_len > 0) {
LOG_INF("checkRecv: path[0]=0x%02x%s%s",
pkt->path[0],
pkt->path_len > 1 ? " path[1]=0x" : "",
pkt->path_len > 1 ? "" : "");
if (pkt->path_len > 1) {
LOG_INF(" path bytes: %02x %02x %02x %02x",
pkt->path[0],
pkt->path_len > 1 ? pkt->path[1] : 0,
pkt->path_len > 2 ? pkt->path[2] : 0,
pkt->path_len > 3 ? pkt->path[3] : 0);
}
}
}
}
}
}
if (pkt) {
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
/* Arduino-compatible packet logging - use printk to bypass log level filtering */
{
static uint8_t packet_hash[MAX_HASH_SIZE];
static char hash_hex[MAX_HASH_SIZE * 2 + 1];
pkt->calculatePacketHash(packet_hash);
Utils::toHex(hash_hex, packet_hash, MAX_HASH_SIZE);
uint8_t ptype = pkt->getPayloadType();
if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ ||
ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) {
printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s [%02X -> %02X]\n",
getLogDateTime(), pkt->getRawLength(), ptype,
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(),
(int)(score * 1000), air_time, hash_hex,
(uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
printk("%s: RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%u hash=%s\n",
getLogDateTime(), pkt->getRawLength(), ptype,
pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(),
(int)(score * 1000), air_time, hash_hex);
}
}
#endif
LOG_INF("checkRecv: processing packet");
logRx(pkt, pkt->getRawLength(), score);
if (pkt->isRouteFlood()) {
n_recv_flood++;
int delay = calcRxDelay(score, air_time);
if (delay < 50) {
LOG_INF("checkRecv: processing flood packet immediately");
processRecvPacket(pkt);
} else {
if (delay > (int)MAX_RX_DELAY_MILLIS) delay = MAX_RX_DELAY_MILLIS;
LOG_INF("checkRecv: queueing flood packet, delay=%d", delay);
_mgr->queueInbound(pkt, futureMillis(delay));
}
} else {
n_recv_direct++;
LOG_INF("checkRecv: processing direct packet");
processRecvPacket(pkt);
}
LOG_INF("checkRecv: done");
}
}
void Dispatcher::processRecvPacket(Packet *pkt)
{
DispatcherAction action = onRecvPacket(pkt);
if (action == ACTION_RELEASE) {
_mgr->free(pkt);
} else if (action == ACTION_MANUAL_HOLD) {
/* subclass holds packet */
} else {
uint8_t priority = (uint8_t)((action >> 24) - 1);
uint32_t delay = action & 0xFFFFFF;
_mgr->queueOutbound(pkt, priority, futureMillis((int)delay));
}
}
void Dispatcher::checkSend()
{
uint32_t now = (uint32_t)_ms->getMillis();
int count = _mgr->getOutboundCount(now);
if (count == 0) return;
if (!millisHasNowPassed(next_tx_time)) {
LOG_DBG("checkSend: waiting for tx_time (count=%d)", count);
return;
}
if (_radio->isReceiving()) {
if (cad_busy_start == 0) {
cad_busy_start = now;
LOG_INF("checkSend: channel busy, starting wait");
}
if (now - cad_busy_start > getCADFailMaxDuration()) {
_err_flags |= ERR_EVENT_CAD_TIMEOUT;
LOG_WRN("checkSend: CAD timeout exceeded");
} else {
next_tx_time = futureMillis((int)getCADFailRetryDelay());
return;
}
}
cad_busy_start = 0;
outbound = _mgr->getNextOutbound(now);
if (outbound) {
/* Duty cycle enforcement — exempt admin packets */
if (!isAdminPacket(outbound) && _duty_cycle.isExceeded(now)) {
LOG_WRN("checkSend: duty cycle exceeded (%u/%u ms), re-queuing type=%d",
_duty_cycle.window_airtime_ms, _duty_cycle.budgetMs(),
outbound->getPayloadType());
_mgr->queueOutbound(outbound, 0, futureMillis(5000));
outbound = nullptr;
return;
}
LOG_INF("checkSend: got outbound type=%d payload_len=%d", outbound->getPayloadType(), outbound->payload_len);
uint8_t raw[MAX_TRANS_UNIT];
int len = 0;
raw[len++] = outbound->header;
if (outbound->hasTransportCodes()) {
memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2;
memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2;
}
raw[len++] = outbound->path_len;
memcpy(&raw[len], outbound->path, outbound->path_len);
len += outbound->path_len;
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
LOG_WRN("checkSend: packet too large len=%d+%d > %d", len, outbound->payload_len, MAX_TRANS_UNIT);
_mgr->free(outbound);
outbound = nullptr;
} else {
memcpy(&raw[len], outbound->payload, outbound->payload_len);
len += outbound->payload_len;
uint32_t max_airtime = _radio->getEstAirtimeFor(len) * 3 / 2;
outbound_start = now;
#if IS_ENABLED(CONFIG_ZEPHCORE_PACKET_LOGGING)
/* Arduino-compatible packet logging - use printk to bypass log level filtering */
{
uint8_t ptype = outbound->getPayloadType();
if (ptype == PAYLOAD_TYPE_PATH || ptype == PAYLOAD_TYPE_REQ ||
ptype == PAYLOAD_TYPE_RESPONSE || ptype == PAYLOAD_TYPE_TXT_MSG) {
printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d) [%02X -> %02X]\n",
getLogDateTime(), len, ptype,
outbound->isRouteDirect() ? "D" : "F", outbound->payload_len,
(uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
} else {
printk("%s: TX, len=%d (type=%d, route=%s, payload_len=%d)\n",
getLogDateTime(), len, ptype,
outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
}
}
#endif
/* Final LBT check — close the gap between initial
* isReceiving() and actual TX start (serialisation +
* logging can take 1-5 ms). */
if (_radio->isReceiving()) {
LOG_INF("checkSend: channel busy at TX commit, re-queuing");
_mgr->queueOutbound(outbound, 0, futureMillis((int)getCADFailRetryDelay()));
outbound = nullptr;
return;
}
LOG_INF("checkSend: calling startSendRaw len=%d", len);
bool success = _radio->startSendRaw(raw, len);
if (!success) {
LOG_ERR("checkSend: startSendRaw failed!");
logTxFail(outbound, outbound->getRawLength());
releasePacket(outbound);
outbound = nullptr;
} else {
LOG_INF("checkSend: TX started, max_airtime=%u", max_airtime);
outbound_expiry = futureMillis((int)max_airtime);
}
}
}
}
Packet *Dispatcher::obtainNewPacket()
{
Packet *pkt = _mgr->allocNew();
if (pkt == nullptr) {
_err_flags |= ERR_EVENT_FULL;
} else {
pkt->payload_len = pkt->path_len = 0;
pkt->_snr = 0;
}
return pkt;
}
void Dispatcher::releasePacket(Packet *packet)
{
_mgr->free(packet);
}
void Dispatcher::sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis)
{
LOG_INF("sendPacket: type=%d payload_len=%d path_len=%d pri=%d delay=%u",
packet->getPayloadType(), packet->payload_len, packet->path_len, priority, delay_millis);
if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) {
LOG_WRN("sendPacket: rejected - path_len=%d or payload_len=%d too large",
packet->path_len, packet->payload_len);
_mgr->free(packet);
} else {
_mgr->queueOutbound(packet, priority, futureMillis((int)delay_millis));
LOG_INF("sendPacket: queued outbound count=%d", _mgr->getOutboundCount((uint32_t)_ms->getMillis()));
}
}
bool Dispatcher::millisHasNowPassed(uint32_t timestamp) const
{
return (int32_t)((uint32_t)_ms->getMillis() - timestamp) > 0;
}
uint32_t Dispatcher::futureMillis(int millis_from_now) const
{
return (uint32_t)_ms->getMillis() + millis_from_now;
}
} /* namespace mesh */
+130
View File
@@ -0,0 +1,130 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Identity - Ed25519 sign/verify
*/
#include <mesh/Identity.h>
#include <string.h>
#define ED25519_NO_SEED 1
#include <ed_25519.h>
namespace mesh {
Identity::Identity()
{
memset(pub_key, 0, sizeof(pub_key));
}
Identity::Identity(const char *pub_hex)
{
Utils::fromHex(pub_key, PUB_KEY_SIZE, pub_hex);
}
bool Identity::verify(const uint8_t *sig, const uint8_t *message, int msg_len) const
{
return ed25519_verify(sig, message, (size_t)msg_len, pub_key) == 1;
}
bool Identity::readFrom(const uint8_t *src, size_t len)
{
if (len < PUB_KEY_SIZE) return false;
memcpy(pub_key, src, PUB_KEY_SIZE);
return true;
}
bool Identity::writeTo(uint8_t *dest, size_t max_len) const
{
if (max_len < PUB_KEY_SIZE) return false;
memcpy(dest, pub_key, PUB_KEY_SIZE);
return true;
}
LocalIdentity::LocalIdentity()
{
memset(prv_key, 0, sizeof(prv_key));
}
LocalIdentity::LocalIdentity(const char *prv_hex, const char *pub_hex) : Identity(pub_hex)
{
Utils::fromHex(prv_key, PRV_KEY_SIZE, prv_hex);
}
LocalIdentity::LocalIdentity(RNG *rng)
{
uint8_t seed[SEED_SIZE];
rng->random(seed, SEED_SIZE);
ed25519_create_keypair(pub_key, prv_key, seed);
}
bool LocalIdentity::validatePrivateKey(const uint8_t prv[64])
{
uint8_t pub[32];
ed25519_derive_pub(pub, prv);
if (pub[0] == 0x00 || pub[0] == 0xFF) return false;
const uint8_t test_client_prv[64] = {
0x70, 0x65, 0xe1, 0x8f, 0xd9, 0xfa, 0xbb, 0x70,
0xc1, 0xed, 0x90, 0xdc, 0xa1, 0x99, 0x07, 0xde,
0x69, 0x8c, 0x88, 0xb7, 0x09, 0xea, 0x14, 0x6e,
0xaf, 0xd9, 0x3d, 0x9b, 0x83, 0x0c, 0x7b, 0x60,
0xc4, 0x68, 0x11, 0x93, 0xc7, 0x9b, 0xbc, 0x39,
0x94, 0x5b, 0xa8, 0x06, 0x41, 0x04, 0xbb, 0x61,
0x8f, 0x8f, 0xd7, 0xa8, 0x4a, 0x0a, 0xf6, 0xf5,
0x70, 0x33, 0xd6, 0xe8, 0xdd, 0xcd, 0x64, 0x71
};
const uint8_t test_client_pub[32] = {
0x1e, 0xc7, 0x71, 0x75, 0xb0, 0x91, 0x8e, 0xd2,
0x06, 0xf9, 0xae, 0x04, 0xec, 0x13, 0x6d, 0x6d,
0x5d, 0x43, 0x15, 0xbb, 0x26, 0x30, 0x54, 0x27,
0xf6, 0x45, 0xb4, 0x92, 0xe9, 0x35, 0x0c, 0x10
};
uint8_t ss1[32], ss2[32];
ed25519_key_exchange(ss1, test_client_pub, prv);
ed25519_key_exchange(ss2, pub, test_client_prv);
if (memcmp(ss1, ss2, 32) != 0) return false;
for (int i = 0; i < 32; i++) {
if (ss1[i] != 0) return true;
}
return false;
}
bool LocalIdentity::readFrom(const uint8_t *src, size_t len)
{
if (len == PRV_KEY_SIZE + PUB_KEY_SIZE) {
memcpy(prv_key, src, PRV_KEY_SIZE);
memcpy(pub_key, src + PRV_KEY_SIZE, PUB_KEY_SIZE);
return true;
}
if (len == PRV_KEY_SIZE) {
memcpy(prv_key, src, PRV_KEY_SIZE);
ed25519_derive_pub(pub_key, prv_key);
return true;
}
return false;
}
size_t LocalIdentity::writeTo(uint8_t *dest, size_t max_len) const
{
if (max_len < PRV_KEY_SIZE) return 0;
if (max_len < PRV_KEY_SIZE + PUB_KEY_SIZE) {
memcpy(dest, prv_key, PRV_KEY_SIZE);
return PRV_KEY_SIZE;
}
memcpy(dest, prv_key, PRV_KEY_SIZE);
memcpy(dest + PRV_KEY_SIZE, pub_key, PUB_KEY_SIZE);
return PRV_KEY_SIZE + PUB_KEY_SIZE;
}
void LocalIdentity::sign(uint8_t *sig, const uint8_t *message, int msg_len) const
{
ed25519_sign(sig, message, (size_t)msg_len, pub_key, prv_key);
}
void LocalIdentity::calcSharedSecret(uint8_t *secret, const uint8_t *other_pub_key) const
{
ed25519_key_exchange(secret, other_pub_key, prv_key);
}
} /* namespace mesh */
+668
View File
@@ -0,0 +1,668 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Mesh - minimal port for Phase 5
*/
#include <mesh/Mesh.h>
#include <mesh/Utils.h>
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_mesh, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
namespace mesh {
Mesh::Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables)
: Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables)
{
}
void Mesh::begin()
{
Dispatcher::begin();
}
void Mesh::loop()
{
Dispatcher::loop();
}
bool Mesh::allowPacketForward(const Packet *packet)
{
(void)packet;
return false;
}
uint32_t Mesh::getRetransmitDelay(const Packet *packet)
{
uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2;
return _rng->nextInt(0, 3) * t;
}
uint32_t Mesh::getCADFailRetryDelay() const
{
return _rng->nextInt(1, 4) * 120;
}
void Mesh::removeSelfFromPath(Packet *pkt)
{
pkt->path_len -= PATH_HASH_SIZE;
for (int k = 0; k < (int)pkt->path_len; k++) {
pkt->path[k] = pkt->path[k + 1];
}
}
DispatcherAction Mesh::routeRecvPacket(Packet *packet)
{
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
&& packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]);
uint32_t d = getRetransmitDelay(packet);
return ACTION_RETRANSMIT_DELAYED(packet->path_len, d);
}
return ACTION_RELEASE;
}
DispatcherAction Mesh::forwardMultipartDirect(Packet *pkt)
{
uint8_t remaining = pkt->payload[0] >> 4;
uint8_t type = pkt->payload[0] & 0x0F;
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) {
removeSelfFromPath(&tmp);
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300);
}
}
return ACTION_RELEASE;
}
void Mesh::routeDirectRecvAcks(Packet *packet, uint32_t delay_millis)
{
if (!packet->isMarkedDoNotRetransmit()) {
uint32_t crc;
memcpy(&crc, packet->payload, 4);
Packet *a2 = createAck(crc);
if (a2) {
memcpy(a2->path, packet->path, packet->path_len);
a2->path_len = packet->path_len;
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis);
}
}
}
DispatcherAction Mesh::onRecvPacket(Packet *pkt)
{
LOG_INF("onRecvPacket: header=0x%02x type=%d route=%s path_len=%d payload[0]=0x%02x",
pkt->header, pkt->getPayloadType(),
pkt->isRouteDirect() ? "direct" : "flood",
pkt->path_len, pkt->payload_len > 0 ? pkt->payload[0] : 0);
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
return ACTION_RELEASE;
}
// Handle direct TRACE packets
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) {
LOG_INF("TRACE packet received: path_len=%d payload_len=%d", pkt->path_len, pkt->payload_len);
if (pkt->path_len < MAX_PATH_SIZE) {
int i = 0;
uint32_t trace_tag;
memcpy(&trace_tag, &pkt->payload[i], 4); i += 4;
uint32_t auth_code;
memcpy(&auth_code, &pkt->payload[i], 4); i += 4;
uint8_t flags = pkt->payload[i++];
uint8_t path_sz = flags & 0x03;
uint8_t len = pkt->payload_len - i;
uint8_t offset = pkt->path_len << path_sz;
LOG_INF("TRACE: offset=%d len=%d path_sz=%d flags=0x%02x", offset, len, path_sz, flags);
if (offset >= len) {
LOG_INF("TRACE: calling onTraceRecv (reached end of path)");
onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len);
} else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) {
pkt->path[pkt->path_len++] = (int8_t)(pkt->getSNR() * 4);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(5, d);
}
}
return ACTION_RELEASE;
}
// Handle direct CONTROL packets (zero-hop only)
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
if (pkt->path_len == 0) {
onControlDataRecv(pkt);
}
return ACTION_RELEASE;
}
// Handle direct zero-hop ACKs (path_len=0)
if (pkt->isRouteDirect() && pkt->path_len == 0 && pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack_crc;
memcpy(&ack_crc, pkt->payload, 4);
LOG_INF("onRecvPacket: direct zero-hop ACK ack_crc=0x%08x", ack_crc);
onAckRecv(pkt, ack_crc);
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack_crc;
memcpy(&ack_crc, pkt->payload, 4);
onAckRecv(pkt, ack_crc);
}
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
}
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
if (!_tables->hasSeen(pkt)) {
removeSelfFromPath(pkt);
routeDirectRecvAcks(pkt, 0);
}
return ACTION_RELEASE;
}
if (!_tables->hasSeen(pkt)) {
removeSelfFromPath(pkt);
return ACTION_RETRANSMIT_DELAYED(0, getDirectRetransmitDelay(pkt));
}
}
return ACTION_RELEASE;
}
if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE;
DispatcherAction action = ACTION_RELEASE;
switch (pkt->getPayloadType()) {
case PAYLOAD_TYPE_ACK: {
uint32_t ack_crc;
memcpy(&ack_crc, pkt->payload, 4);
LOG_INF("onRecvPacket: ACK packet ack_crc=0x%08x seen=%d", ack_crc, _tables->hasSeen(pkt) ? 1 : 0);
if (!_tables->hasSeen(pkt)) {
onAckRecv(pkt, ack_crc);
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_PATH:
case PAYLOAD_TYPE_REQ:
case PAYLOAD_TYPE_RESPONSE:
case PAYLOAD_TYPE_TXT_MSG: {
LOG_INF("onRecvPacket: TXT_MSG/PATH/REQ/RESPONSE type=%d payload_len=%d",
pkt->getPayloadType(), pkt->payload_len);
int i = 0;
uint8_t dest_hash = pkt->payload[i++];
uint8_t src_hash = pkt->payload[i++];
uint8_t *macAndData = &pkt->payload[i];
if (i + CIPHER_MAC_SIZE >= (int)pkt->payload_len) {
LOG_WRN("onRecvPacket: incomplete packet (i=%d, payload_len=%d)", i, pkt->payload_len);
} else if (!_tables->hasSeen(pkt)) {
LOG_INF("onRecvPacket: checking dest_hash=0x%02x (self match=%d)",
dest_hash, self_id.isHashMatch(&dest_hash));
if (self_id.isHashMatch(&dest_hash)) {
int num = searchPeersByHash(&src_hash);
LOG_INF("onRecvPacket: found %d peers matching src_hash=0x%02x", num, src_hash);
bool found = false;
for (int j = 0; j < num; j++) {
uint8_t secret[PUB_KEY_SIZE];
getPeerSharedSecret(secret, j);
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
LOG_INF("onRecvPacket: decrypt attempt j=%d, result len=%d", j, len);
if (len > 0) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0;
uint8_t path_len = data[k++];
uint8_t *path = &data[k]; k += path_len;
uint8_t extra_type = data[k++] & 0x0F;
uint8_t *extra = &data[k];
uint8_t extra_len = len - k;
if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) {
if (pkt->isRouteFlood()) {
Packet *rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, nullptr, 0);
if (rpath) sendDirect(rpath, path, path_len, 500);
}
}
} else {
LOG_INF("onRecvPacket: calling onPeerDataRecv type=%d len=%d", pkt->getPayloadType(), len);
onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len);
}
found = true;
break;
}
}
if (found) {
pkt->markDoNotRetransmit();
} else {
LOG_WRN("onRecvPacket: no peer could decrypt message");
}
} else {
LOG_INF("onRecvPacket: dest_hash doesn't match self");
}
action = routeRecvPacket(pkt);
} else {
LOG_INF("onRecvPacket: packet already seen");
}
break;
}
case PAYLOAD_TYPE_ANON_REQ: {
int i = 0;
uint8_t dest_hash = pkt->payload[i++];
uint8_t *sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE;
uint8_t *macAndData = &pkt->payload[i];
if (i + 2 >= (int)pkt->payload_len) {
// incomplete packet
} else if (!_tables->hasSeen(pkt)) {
if (self_id.isHashMatch(&dest_hash)) {
Identity sender(sender_pub_key);
uint8_t secret[PUB_KEY_SIZE];
self_id.calcSharedSecret(secret, sender);
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) {
onAnonDataRecv(pkt, secret, sender, data, len);
pkt->markDoNotRetransmit();
}
}
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_GRP_DATA:
case PAYLOAD_TYPE_GRP_TXT: {
int i = 0;
uint8_t channel_hash = pkt->payload[i++];
uint8_t *macAndData = &pkt->payload[i];
if (i + 2 >= (int)pkt->payload_len) {
// incomplete packet
} else if (!_tables->hasSeen(pkt)) {
GroupChannel channels[4];
int num = searchChannelsByHash(&channel_hash, channels, 4);
for (int j = 0; j < num; j++) {
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i);
if (len > 0) {
onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len);
break;
}
}
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_ADVERT: {
int i = 0;
Identity id;
memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE);
i += PUB_KEY_SIZE;
uint32_t timestamp;
memcpy(&timestamp, &pkt->payload[i], 4);
i += 4;
const uint8_t *signature = &pkt->payload[i];
i += SIGNATURE_SIZE;
if (i <= (int)pkt->payload_len && !self_id.matches(id.pub_key) && !_tables->hasSeen(pkt)) {
uint8_t *app_data = (uint8_t *)&pkt->payload[i];
size_t app_data_len = pkt->payload_len - (size_t)i;
if (app_data_len > MAX_ADVERT_DATA_SIZE) app_data_len = MAX_ADVERT_DATA_SIZE;
uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE];
int msg_len = 0;
memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE;
memcpy(&message[msg_len], &timestamp, 4); msg_len += 4;
memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len;
if (id.verify(signature, message, msg_len)) {
onAdvertRecv(pkt, id, timestamp, app_data, app_data_len);
action = routeRecvPacket(pkt);
}
}
break;
}
case PAYLOAD_TYPE_RAW_CUSTOM:
if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) {
onRawDataRecv(pkt);
}
break;
case PAYLOAD_TYPE_MULTIPART:
if (pkt->payload_len > 2) {
/* uint8_t remaining = pkt->payload[0] >> 4; */ /* Reserved for future multipart support */
uint8_t type = pkt->payload[0] & 0x0F;
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) {
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = pkt->path_len;
memcpy(tmp.path, pkt->path, pkt->path_len);
tmp.payload_len = pkt->payload_len - 1;
memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len);
if (!_tables->hasSeen(&tmp)) {
uint32_t ack_crc;
memcpy(&ack_crc, tmp.payload, 4);
onAckRecv(&tmp, ack_crc);
}
}
}
break;
default:
break;
}
return action;
}
Packet *Mesh::createAdvert(const LocalIdentity &id, const uint8_t *app_data, size_t app_data_len)
{
if (app_data_len > MAX_ADVERT_DATA_SIZE) return nullptr;
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT);
int len = 0;
memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE);
len += PUB_KEY_SIZE;
uint32_t emitted_timestamp = _rtc->getCurrentTime();
memcpy(&packet->payload[len], &emitted_timestamp, 4);
len += 4;
uint8_t *signature = &packet->payload[len];
len += SIGNATURE_SIZE;
if (app_data && app_data_len > 0) {
memcpy(&packet->payload[len], app_data, app_data_len);
len += (int)app_data_len;
}
packet->payload_len = len;
uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE];
int msg_len = 0;
memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE;
memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4;
if (app_data && app_data_len > 0) {
memcpy(&message[msg_len], app_data, app_data_len); msg_len += (int)app_data_len;
}
id.sign(signature, message, msg_len);
return packet;
}
Packet *Mesh::createAck(uint32_t ack_crc)
{
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
memcpy(packet->payload, &ack_crc, 4);
packet->payload_len = 4;
return packet;
}
Packet *Mesh::createMultiAck(uint32_t ack_crc, uint8_t remaining)
{
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT);
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
memcpy(&packet->payload[1], &ack_crc, 4);
packet->payload_len = 5;
return packet;
}
Packet *Mesh::createControlData(const uint8_t *data, size_t len)
{
if (len > sizeof(Packet::payload)) return nullptr;
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT);
memcpy(packet->payload, data, len);
packet->payload_len = (uint16_t)len;
return packet;
}
void Mesh::sendFlood(Packet *packet, uint32_t delay_millis)
{
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
return;
}
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_FLOOD;
packet->path_len = 0;
_tables->hasSeen(packet);
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 2;
} else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pri = 3;
} else {
pri = 1;
}
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis)
{
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
return;
}
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD;
packet->transport_codes[0] = transport_codes[0];
packet->transport_codes[1] = transport_codes[1];
packet->path_len = 0;
_tables->hasSeen(packet);
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 2;
} else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pri = 3;
} else {
pri = 1;
}
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis)
{
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_DIRECT;
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
/* For TRACE packets, path is appended to end of PAYLOAD (used for SNRs) */
memcpy(&packet->payload[packet->payload_len], path, path_len);
packet->payload_len += path_len;
packet->path_len = 0;
pri = 5;
} else {
memcpy(packet->path, path, path_len);
packet->path_len = path_len;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 1;
} else {
pri = 0;
}
}
_tables->hasSeen(packet);
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendZeroHop(Packet *packet, uint32_t delay_millis)
{
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_DIRECT;
packet->path_len = 0;
_tables->hasSeen(packet);
sendPacket(packet, 0, delay_millis);
}
void Mesh::sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis)
{
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_TRANSPORT_DIRECT;
packet->transport_codes[0] = transport_codes[0];
packet->transport_codes[1] = transport_codes[1];
packet->path_len = 0;
_tables->hasSeen(packet);
sendPacket(packet, 0, delay_millis);
}
#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE)
Packet *Mesh::createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len,
uint8_t extra_type, const uint8_t *extra, size_t extra_len)
{
uint8_t dest_hash[PATH_HASH_SIZE];
dest.copyHashTo(dest_hash);
return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len);
}
Packet *Mesh::createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len,
uint8_t extra_type, const uint8_t *extra, size_t extra_len)
{
if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return nullptr;
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT);
int len = 0;
memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE;
len += self_id.copyHashTo(&packet->payload[len]);
{
int data_len = 0;
uint8_t data[MAX_PACKET_PAYLOAD];
data[data_len++] = path_len;
memcpy(&data[data_len], path, path_len); data_len += path_len;
if (extra_len > 0) {
data[data_len++] = extra_type;
memcpy(&data[data_len], extra, extra_len); data_len += extra_len;
} else {
data[data_len++] = 0xFF; // dummy payload type
_rng->random(&data[data_len], 4); data_len += 4;
}
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
}
packet->payload_len = len;
return packet;
}
Packet *Mesh::createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len)
{
LOG_INF("createDatagram: type=%d data_len=%u", type, (unsigned)data_len);
if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) {
if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) {
LOG_WRN("createDatagram: data too large");
return nullptr;
}
} else {
LOG_WRN("createDatagram: unsupported type %d", type);
return nullptr;
}
Packet *packet = obtainNewPacket();
if (packet == nullptr) {
LOG_WRN("createDatagram: packet alloc failed");
return nullptr;
}
packet->header = (type << PH_TYPE_SHIFT);
int len = 0;
len += dest.copyHashTo(&packet->payload[len]);
len += self_id.copyHashTo(&packet->payload[len]);
LOG_INF("createDatagram: dest_hash=0x%02x src_hash=0x%02x", packet->payload[0], packet->payload[1]);
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
packet->payload_len = len;
LOG_INF("createDatagram: created packet payload_len=%d", len);
return packet;
}
Packet *Mesh::createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest,
const uint8_t *secret, const uint8_t *data, size_t data_len)
{
if (type == PAYLOAD_TYPE_ANON_REQ) {
if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr;
} else {
return nullptr;
}
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (type << PH_TYPE_SHIFT);
int len = 0;
if (type == PAYLOAD_TYPE_ANON_REQ) {
len += dest.copyHashTo(&packet->payload[len]);
memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE;
}
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
packet->payload_len = len;
return packet;
}
Packet *Mesh::createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len)
{
if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return nullptr;
if (data_len + 1 + CIPHER_BLOCK_SIZE - 1 > MAX_PACKET_PAYLOAD) return nullptr;
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (type << PH_TYPE_SHIFT);
int len = 0;
memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE;
len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len);
packet->payload_len = len;
return packet;
}
Packet *Mesh::createRawData(const uint8_t *data, size_t len)
{
if (len > sizeof(Packet::payload)) return nullptr;
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT);
memcpy(packet->payload, data, len);
packet->payload_len = (uint16_t)len;
return packet;
}
Packet *Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags)
{
Packet *packet = obtainNewPacket();
if (packet == nullptr) return nullptr;
packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT);
memcpy(packet->payload, &tag, 4);
memcpy(&packet->payload[4], &auth_code, 4);
packet->payload[8] = flags;
packet->payload_len = 9;
return packet;
}
} /* namespace mesh */
+75
View File
@@ -0,0 +1,75 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Packet implementation
*/
#include <mesh/Packet.h>
#include <mesh/Utils.h>
#include <string.h>
namespace mesh {
Packet::Packet()
{
header = 0;
path_len = 0;
payload_len = 0;
}
int Packet::getRawLength() const
{
return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0);
}
void Packet::calculatePacketHash(uint8_t *hash) const
{
uint8_t t = getPayloadType();
if (t == PAYLOAD_TYPE_TRACE) {
uint8_t buf[2 + MAX_PACKET_PAYLOAD];
buf[0] = t;
memcpy(buf + 1, &path_len, sizeof(path_len));
memcpy(buf + 2, payload, payload_len);
Utils::sha256(hash, MAX_HASH_SIZE, buf, 2 + payload_len);
} else {
uint8_t buf[1 + MAX_PACKET_PAYLOAD];
buf[0] = t;
memcpy(buf + 1, payload, payload_len);
Utils::sha256(hash, MAX_HASH_SIZE, buf, 1 + payload_len);
}
}
uint8_t Packet::writeTo(uint8_t dest[]) const
{
uint8_t i = 0;
dest[i++] = header;
if (hasTransportCodes()) {
memcpy(&dest[i], &transport_codes[0], 2); i += 2;
memcpy(&dest[i], &transport_codes[1], 2); i += 2;
}
dest[i++] = path_len;
memcpy(&dest[i], path, path_len); i += path_len;
memcpy(&dest[i], payload, payload_len); i += payload_len;
return i;
}
bool Packet::readFrom(const uint8_t src[], uint8_t len)
{
uint8_t i = 0;
header = src[i++];
if (hasTransportCodes()) {
memcpy(&transport_codes[0], &src[i], 2); i += 2;
memcpy(&transport_codes[1], &src[i], 2); i += 2;
} else {
transport_codes[0] = transport_codes[1] = 0;
}
path_len = src[i++];
if (path_len > sizeof(path)) return false;
memcpy(path, &src[i], path_len); i += path_len;
if (i >= len) return false;
payload_len = len - i;
if (payload_len > sizeof(payload)) return false;
memcpy(payload, &src[i], payload_len);
return true;
}
} /* namespace mesh */
+145
View File
@@ -0,0 +1,145 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Static pool PacketManager - no dynamic allocation
*/
#include <mesh/StaticPoolPacketManager.h>
#include <mesh/Packet.h>
#include <string.h>
namespace mesh {
#define POOL_SIZE 16
#define QUEUE_SIZE 16
struct PacketQueue {
Packet *_table[QUEUE_SIZE];
uint8_t _pri_table[QUEUE_SIZE];
uint32_t _schedule_table[QUEUE_SIZE];
int _num;
PacketQueue() : _num(0) {
memset(_table, 0, sizeof(_table));
}
int countBefore(uint32_t now) const {
int n = 0;
for (int j = 0; j < _num; j++) {
if (_schedule_table[j] > now) continue;
n++;
}
return n;
}
Packet *get(uint32_t now) {
uint8_t min_pri = 0xFF;
int best_idx = -1;
for (int j = 0; j < _num; j++) {
if (_schedule_table[j] > now) continue;
if (_pri_table[j] < min_pri) {
min_pri = _pri_table[j];
best_idx = j;
}
}
if (best_idx < 0) return nullptr;
Packet *top = _table[best_idx];
for (int i = best_idx; i < _num - 1; i++) {
_table[i] = _table[i + 1];
_pri_table[i] = _pri_table[i + 1];
_schedule_table[i] = _schedule_table[i + 1];
}
_num--;
return top;
}
Packet *removeByIdx(int i) {
if (i >= _num) return nullptr;
Packet *item = _table[i];
for (int j = i; j < _num - 1; j++) {
_table[j] = _table[j + 1];
_pri_table[j] = _pri_table[j + 1];
_schedule_table[j] = _schedule_table[j + 1];
}
_num--;
return item;
}
void add(Packet *packet, uint8_t priority, uint32_t scheduled_for) {
if (_num >= QUEUE_SIZE) return;
_table[_num] = packet;
_pri_table[_num] = priority;
_schedule_table[_num] = scheduled_for;
_num++;
}
int count() const { return _num; }
Packet *itemAt(int i) const { return (i < _num) ? _table[i] : nullptr; }
};
static Packet _packet_pool[POOL_SIZE];
static PacketQueue _unused;
static PacketQueue _send_queue;
static PacketQueue _rx_queue;
static bool _initialized = false;
static void init_pool() {
if (_initialized) return;
_initialized = true;
for (int i = 0; i < POOL_SIZE; i++) {
_unused.add(&_packet_pool[i], 0, 0);
}
}
Packet *StaticPoolPacketManager::allocNew()
{
init_pool();
return _unused.removeByIdx(0);
}
void StaticPoolPacketManager::free(Packet *packet)
{
if (packet) _unused.add(packet, 0, 0);
}
void StaticPoolPacketManager::queueOutbound(Packet *packet, uint8_t priority, uint32_t scheduled_for)
{
_send_queue.add(packet, priority, scheduled_for);
}
Packet *StaticPoolPacketManager::getNextOutbound(uint32_t now)
{
return _send_queue.get(now);
}
int StaticPoolPacketManager::getOutboundCount(uint32_t now) const
{
return _send_queue.countBefore(now);
}
int StaticPoolPacketManager::getFreeCount() const
{
return _unused.count();
}
Packet *StaticPoolPacketManager::getOutboundByIdx(int i)
{
return _send_queue.itemAt(i);
}
Packet *StaticPoolPacketManager::removeOutboundByIdx(int i)
{
return _send_queue.removeByIdx(i);
}
void StaticPoolPacketManager::queueInbound(Packet *packet, uint32_t scheduled_for)
{
_rx_queue.add(packet, 0, scheduled_for);
}
Packet *StaticPoolPacketManager::getNextInbound(uint32_t now)
{
return _rx_queue.get(now);
}
} /* namespace mesh */
+226
View File
@@ -0,0 +1,226 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore Utils - mbedTLS backend for AES-ECB, SHA256, HMAC
*/
#include <mesh/Utils.h>
#include <mesh/MeshCore.h>
#include <mbedtls/aes.h>
#include <mbedtls/sha256.h>
#include <mbedtls/md.h>
#include <string.h>
#include <stdlib.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_utils, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
namespace mesh {
static uint8_t hexVal(char c)
{
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= '0' && c <= '9') return c - '0';
return 0;
}
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t *msg, int msg_len)
{
uint8_t full_hash[32];
mbedtls_sha256(msg, (size_t)msg_len, full_hash, 0);
size_t copy_len = (hash_len < 32) ? hash_len : 32;
memcpy(hash, full_hash, copy_len);
}
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t *frag1, int frag1_len, const uint8_t *frag2, int frag2_len)
{
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
mbedtls_sha256_update(&ctx, frag1, (size_t)frag1_len);
mbedtls_sha256_update(&ctx, frag2, (size_t)frag2_len);
uint8_t full_hash[32];
mbedtls_sha256_finish(&ctx, full_hash);
mbedtls_sha256_free(&ctx);
size_t copy_len = (hash_len < 32) ? hash_len : 32;
memcpy(hash, full_hash, copy_len);
}
/* AES-ECB using mbedTLS low-level API (PSA doesn't support ECB mode) */
static int aes_ecb_encrypt(const uint8_t *key, size_t key_len, const uint8_t *src, int src_len, uint8_t *dest)
{
if (src_len % 16 != 0) {
LOG_ERR("src_len=%d not multiple of 16", src_len);
return -1;
}
mbedtls_aes_context ctx;
mbedtls_aes_init(&ctx);
int ret = mbedtls_aes_setkey_enc(&ctx, key, key_len * 8);
if (ret != 0) {
LOG_ERR("setkey failed: %d", ret);
mbedtls_aes_free(&ctx);
return -1;
}
/* Encrypt each 16-byte block */
for (int i = 0; i < src_len; i += 16) {
ret = mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, src + i, dest + i);
if (ret != 0) {
LOG_ERR("crypt_ecb failed at block %d: %d", i/16, ret);
mbedtls_aes_free(&ctx);
return -1;
}
}
mbedtls_aes_free(&ctx);
return src_len;
}
static int aes_ecb_decrypt(const uint8_t *key, size_t key_len, const uint8_t *src, int src_len, uint8_t *dest)
{
if (src_len % 16 != 0) {
LOG_ERR("src_len=%d not multiple of 16", src_len);
return -1;
}
mbedtls_aes_context ctx;
mbedtls_aes_init(&ctx);
int ret = mbedtls_aes_setkey_dec(&ctx, key, key_len * 8);
if (ret != 0) {
LOG_ERR("setkey failed: %d", ret);
mbedtls_aes_free(&ctx);
return -1;
}
/* Decrypt each 16-byte block */
for (int i = 0; i < src_len; i += 16) {
ret = mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_DECRYPT, src + i, dest + i);
if (ret != 0) {
LOG_ERR("crypt_ecb failed at block %d: %d", i/16, ret);
mbedtls_aes_free(&ctx);
return -1;
}
}
mbedtls_aes_free(&ctx);
return src_len;
}
int Utils::encrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len)
{
uint8_t tmp[MAX_PACKET_PAYLOAD + CIPHER_BLOCK_SIZE];
const uint8_t *sp = src;
uint8_t *dp = tmp;
int rem = src_len;
while (rem >= (int)CIPHER_BLOCK_SIZE) {
memcpy(dp, sp, CIPHER_BLOCK_SIZE);
dp += CIPHER_BLOCK_SIZE;
sp += CIPHER_BLOCK_SIZE;
rem -= CIPHER_BLOCK_SIZE;
}
if (rem > 0) {
memset(dp, 0, CIPHER_BLOCK_SIZE);
memcpy(dp, sp, (size_t)rem);
dp += CIPHER_BLOCK_SIZE;
}
int total = (int)(dp - tmp);
int n = aes_ecb_encrypt(shared_secret, CIPHER_KEY_SIZE, tmp, total, dest);
return (n > 0) ? n : 0;
}
int Utils::decrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len)
{
int n = aes_ecb_decrypt(shared_secret, CIPHER_KEY_SIZE, src, src_len, dest);
return (n > 0) ? n : 0;
}
/* HMAC-SHA256 using mbedTLS */
static int compute_hmac_truncated(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t *mac_out, size_t mac_len)
{
uint8_t full_hmac[32];
const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
if (md_info == nullptr) {
LOG_ERR("compute_hmac: SHA256 not available");
return -1;
}
int ret = mbedtls_md_hmac(md_info, key, key_len, data, data_len, full_hmac);
if (ret != 0) {
LOG_ERR("compute_hmac: mbedtls_md_hmac failed: %d", ret);
return -1;
}
memcpy(mac_out, full_hmac, mac_len);
return 0;
}
int Utils::encryptThenMAC(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len)
{
int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len);
if (enc_len <= 0) return 0;
if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, dest + CIPHER_MAC_SIZE, (size_t)enc_len, dest, CIPHER_MAC_SIZE) != 0)
return 0;
return CIPHER_MAC_SIZE + enc_len;
}
int Utils::MACThenDecrypt(const uint8_t *shared_secret, uint8_t *dest, const uint8_t *src, int src_len)
{
if (src_len <= (int)CIPHER_MAC_SIZE) return 0;
uint8_t computed_mac[CIPHER_MAC_SIZE];
if (compute_hmac_truncated(shared_secret, PUB_KEY_SIZE, src + CIPHER_MAC_SIZE, (size_t)src_len - CIPHER_MAC_SIZE, computed_mac, CIPHER_MAC_SIZE) != 0)
return 0;
if (memcmp(computed_mac, src, CIPHER_MAC_SIZE) != 0) return 0;
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
}
static const char hex_chars[] = "0123456789ABCDEF";
void Utils::toHex(char *dest, const uint8_t *src, size_t len)
{
while (len > 0) {
uint8_t b = *src++;
*dest++ = hex_chars[b >> 4];
*dest++ = hex_chars[b & 0x0F];
len--;
}
*dest = 0;
}
bool Utils::fromHex(uint8_t *dest, int dest_size, const char *src_hex)
{
size_t len = strlen(src_hex);
if (len != (size_t)(dest_size * 2)) return false;
uint8_t *dp = dest;
while ((size_t)(dp - dest) < (size_t)dest_size) {
char ch = *src_hex++;
char cl = *src_hex++;
*dp++ = (uint8_t)((hexVal(ch) << 4) | hexVal(cl));
}
return true;
}
bool Utils::isHexChar(char c)
{
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
int Utils::parseTextParts(char *text, const char *parts[], int max_num, char separator)
{
int num = 0;
char *sp = text;
while (*sp && num < max_num) {
parts[num++] = sp;
while (*sp && *sp != separator) sp++;
if (*sp) {
*sp++ = 0;
}
}
while (*sp && *sp != separator) sp++;
if (*sp) *sp = 0;
return num;
}
} /* namespace mesh */
+643
View File
@@ -0,0 +1,643 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore - CompanionMesh (Event-Driven)
*
* BLE stack is in adapters/ble/ZephyrBLE.cpp.
* USB CDC transport is in adapters/usb/ZephyrUsbCompanion.cpp.
* UI ↔ mesh actions are in helpers/ui/ui_mesh_actions.cpp.
* This file handles: mesh event loop, LoRa, GPS, data storage, init.
*/
#include <stdio.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/regulator.h>
#include <zephyr/settings/settings.h>
#include <ZephyrDataStore.h>
#include <adapters/clock/ZephyrRTCClock.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/drivers/hwinfo.h>
#include <ZephyrSensorManager.h>
#include "ui_task.h"
#include "ui_mesh_actions.h"
#include "oled_power.h"
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER)
#include "buzzer.h"
#endif
#include <ZephyrBLE.h>
#if IS_ENABLED(CONFIG_LOG)
#include <ZephyrCompanionUSB.h>
#endif
/* Radio + mesh includes (shared header selects LR1110 or SX126x) */
#include <mesh/RadioIncludes.h>
#ifdef ZEPHCORE_LORA
#include <app/CompanionMesh.h>
#endif
/*
* BLE Controller assert handler — without this, a controller assert
* silently freezes the CPU at highest IRQ priority with zero log output.
* This prints the file/line and reboots so we can actually see what happened.
*/
#if IS_ENABLED(CONFIG_BT_CTLR_ASSERT_HANDLER)
#include <zephyr/sys/reboot.h>
extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
{
LOG_ERR("!!! BLE CONTROLLER ASSERT: %s:%u !!!", file ? file : "?", line);
/* Force flush logs before reboot */
k_sleep(K_MSEC(100));
sys_reboot(SYS_REBOOT_COLD);
}
#endif
/*
* Event-driven mesh loop - replaces 50ms polling with true event signaling.
* Events are signaled from ISR/callbacks, mesh loop wakes immediately.
* NO periodic timer - CPU sleeps until actual events occur.
*/
#define MESH_EVENT_LORA_RX BIT(0) /* LoRa packet received */
#define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete (event-driven!) */
#define MESH_EVENT_BLE_RX BIT(2) /* BLE frame received */
#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */
#define MESH_EVENT_UI_ACTION BIT(4) /* Button action from UI (deferred to mesh thread) */
#define MESH_EVENT_GPS_ACTION BIT(5) /* GPS state change (must run on main thread!) */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_BLE_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_UI_ACTION | MESH_EVENT_GPS_ACTION)
/* Housekeeping interval - infrequent to preserve power savings */
#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS
/* Event-driven mesh loop - k_event for signaling from ISR/callbacks */
static struct k_event mesh_events;
/* Work items for event-driven processing */
static void rx_process_work_fn(struct k_work *work);
static void contact_iter_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
K_WORK_DEFINE(rx_process_work, rx_process_work_fn);
K_WORK_DEFINE(contact_iter_work, contact_iter_work_fn);
/* Housekeeping timer for periodic tasks (noise floor calibration, etc.)
* Fires every 5 seconds to wake event loop for maintenance without
* compromising event-driven power savings. */
K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL);
/* Forward declarations */
#ifdef ZEPHCORE_LORA
static CompanionMesh *companion_mesh_ptr;
#endif
/* ========== BLE callbacks → main ========== */
/* BLE RX callback — called from BLE adapter when a frame arrives.
* Queues to recv_queue and wakes mesh event loop. */
static void ble_on_rx_frame(const uint8_t *data, uint16_t len)
{
struct {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
} f;
if (len == 0 || len > MAX_FRAME_SIZE) {
return;
}
f.len = len;
memcpy(f.buf, data, len);
if (k_msgq_put(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) != 0) {
LOG_WRN("recv queue full");
return;
}
/* Trigger RX processing and signal mesh event */
k_work_submit(&rx_process_work);
k_event_post(&mesh_events, MESH_EVENT_BLE_RX);
}
/* BLE TX idle callback — called when TX queue is empty.
* Continues contact iteration. */
static void ble_on_tx_idle(void)
{
k_work_submit(&contact_iter_work);
}
/* BLE connected callback — notify UI, clear USB state if needed */
static void ble_on_connected(void)
{
ui_notify(UI_EVENT_BLE_CONNECTED);
}
/* BLE disconnected callback — notify UI */
static void ble_on_disconnected(void)
{
#if IS_ENABLED(CONFIG_LOG)
zephcore_usb_companion_reset_rx();
#endif
ui_notify(UI_EVENT_BLE_DISCONNECTED);
}
static const struct ble_callbacks ble_cbs = {
.on_rx_frame = ble_on_rx_frame,
.on_tx_idle = ble_on_tx_idle,
.on_connected = ble_on_connected,
.on_disconnected = ble_on_disconnected,
};
/* ========== Frame send/receive (mesh ↔ BLE/USB) ========== */
static size_t write_frame(const uint8_t *src, size_t len)
{
return zephcore_ble_send(src, (uint16_t)len);
}
/**
* Push notification callback - sends push frames to BLE.
* Push frames have format: [push_code] [data...]
*/
static void push_callback(uint8_t code, const uint8_t *data, size_t len)
{
/* Push frame: code byte + optional data */
uint8_t push_buf[1 + MAX_FRAME_SIZE - 1];
size_t total_len = 1 + len;
if (total_len > MAX_FRAME_SIZE) {
LOG_WRN("data too long %u", (unsigned)len);
total_len = MAX_FRAME_SIZE;
len = MAX_FRAME_SIZE - 1;
}
push_buf[0] = code;
if (len > 0 && data != nullptr) {
memcpy(&push_buf[1], data, len);
}
LOG_INF("code=0x%02x len=%u (total frame)", code, (unsigned)total_len);
write_frame(push_buf, total_len);
}
/* RX processing work - handles received BLE/USB frames */
static void rx_process_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
struct {
uint16_t len;
uint8_t buf[MAX_FRAME_SIZE];
} f;
/* Process all queued frames */
while (k_msgq_get(zephcore_ble_get_recv_queue(), &f, K_NO_WAIT) == 0) {
#ifdef ZEPHCORE_LORA
/* Reset contact iterator when new command received */
companion_mesh_ptr->resetContactIterator();
if (!companion_mesh_ptr->handleProtocolFrame(f.buf, f.len)) {
LOG_DBG("rx_process: unknown cmd 0x%02x len=%u", f.buf[0], (unsigned)f.len);
uint8_t err_rsp[] = { 0x01, 0x01 }; /* PACKET_ERROR, ERR_UNSUPPORTED */
write_frame(err_rsp, sizeof(err_rsp));
}
#endif
}
}
/* Contact iteration work - runs when TX queue has space */
static void contact_iter_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
#ifdef ZEPHCORE_LORA
/* Check if send queue has space before continuing iteration
* Match Arduino's isWriteBusy(): busy when queue >= 2/3 full (8 of 12)
*/
uint32_t used = k_msgq_num_used_get(zephcore_ble_get_send_queue());
uint32_t queue_size = CONFIG_ZEPHCORE_BLE_QUEUE_SIZE;
bool has_space = (used < (queue_size * 2 / 3));
if (has_space && companion_mesh_ptr) {
if (companion_mesh_ptr->continueContactIteration()) {
/* More contacts to send - kick TX drain which will re-trigger us */
zephcore_ble_kick_tx();
}
}
#endif
}
/*
* Event-driven mesh loop - runs in main thread context.
* Wakes on actual events: LoRa RX, LoRa TX done, BLE RX.
* Plus a 5-second housekeeping timer for noise floor calibration, etc.
*/
static void mesh_event_loop(void)
{
LOG_INF("starting event-driven loop");
/* Start housekeeping timer for periodic maintenance tasks */
k_timer_start(&housekeeping_timer, K_MSEC(HOUSEKEEPING_INTERVAL_MS),
K_MSEC(HOUSEKEEPING_INTERVAL_MS));
for (;;) {
/* Wait for any mesh event - blocks until signaled */
uint32_t events = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER);
/* Clear the events we're handling */
k_event_clear(&mesh_events, events);
#ifdef ZEPHCORE_LORA
/* Handle deferred UI button actions (flood advert, pref saves).
* Must run in this thread — LoRa TX and flash writes block. */
if (events & MESH_EVENT_UI_ACTION) {
mesh_handle_ui_actions();
}
/* Handle deferred GPS state transitions (wake/timeout).
* MUST run here (main thread) because GNSS configuration uses
* modem_chat_run_script() which blocks on a semaphore signaled
* from the system work queue. Running it on the system work
* queue would deadlock. */
if (events & MESH_EVENT_GPS_ACTION) {
gps_process_event();
}
/* Run mesh loop - handles all pending work:
* - Process received LoRa packets
* - Check TX completion
* - Timeout handling
* - Noise floor calibration (runs when loop is called)
*/
if (companion_mesh_ptr) {
companion_mesh_ptr->loop();
}
/* Periodic UI refresh (every housekeeping cycle = 5s) */
if (events & MESH_EVENT_HOUSEKEEPING) {
mesh_housekeeping_ui_refresh();
}
#endif
}
}
/* Housekeeping timer callback - signals event to wake mesh loop periodically */
static void housekeeping_timer_fn(struct k_timer *timer)
{
ARG_UNUSED(timer);
k_event_post(&mesh_events, MESH_EVENT_HOUSEKEEPING);
}
#ifdef ZEPHCORE_LORA
/* LoRa RX callback - called from ISR context when packet received */
static void lora_rx_callback(void *user_data)
{
ARG_UNUSED(user_data);
/* Signal event to wake mesh loop immediately */
k_event_post(&mesh_events, MESH_EVENT_LORA_RX);
}
/* LoRa TX complete callback - called from work queue when async TX finishes */
static void lora_tx_done_callback(void *user_data)
{
ARG_UNUSED(user_data);
/* Signal event to wake mesh loop for TX completion handling */
k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE);
}
#endif
static mesh::ZephyrRTCClock rtc_clock;
static ZephyrDataStore data_store(rtc_clock);
#ifdef ZEPHCORE_LORA
static mesh::ZephyrBoard zephyr_board;
/* NodePrefs placeholder - will be set from CompanionMesh */
static NodePrefs temp_prefs;
#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110)
/* LR1110 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &temp_prefs);
#else
/* SX126x via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &temp_prefs);
#endif
static uint16_t get_battery_mv(void)
{
return zephyr_board.getBattMilliVolts();
}
static void radio_reconfigure(void)
{
lora_radio.reconfigure();
}
static mesh::ZephyrMillisecondClock ms_clock;
static mesh::ZephyrRNG zephyr_rng;
static mesh::SimpleMeshTables mesh_tables;
static mesh::StaticPoolPacketManager packet_mgr;
static CompanionMesh companion_mesh(lora_radio, ms_clock, zephyr_rng, rtc_clock,
packet_mgr, mesh_tables, data_store);
#endif
/* GPS enable callback - logs state changes
* Note: GPS persistence is handled in CompanionMesh.cpp CMD_SET_CUSTOM_VAR handler */
static void gps_enable_callback(bool enabled)
{
LOG_INF("GPS %s", enabled ? "enabled" : "disabled");
ui_set_gps_enabled(enabled);
}
/* GPS event callback - called when GPS work handlers need the main thread
* to process a state transition (wake from standby, timeout, etc.).
* Runs from system work queue context — just posts an event, no blocking. */
static void gps_event_callback(void)
{
k_event_post(&mesh_events, MESH_EVENT_GPS_ACTION);
}
/* GPS fix callback - called when GPS acquires a valid fix.
* Updates mesh node position and RTC. */
static void gps_fix_callback(double lat, double lon, int64_t utc_time)
{
/* Sync RTC from GPS time */
if (utc_time > 0) {
LOG_INF("GPS fix: RTC sync time=%lld", utc_time);
rtc_clock.setCurrentTime((uint32_t)utc_time);
}
#ifdef ZEPHCORE_LORA
/* Update node position for mesh advertising */
if (lat != companion_mesh.prefs.node_lat || lon != companion_mesh.prefs.node_lon) {
companion_mesh.prefs.node_lat = lat;
companion_mesh.prefs.node_lon = lon;
/* Log as integer degrees (Zephyr LOG doesn't support %f by default) */
int lat_deg = (int)lat;
int lon_deg = (int)lon;
int lat_frac = (int)((lat - lat_deg) * 1000000);
int lon_frac = (int)((lon - lon_deg) * 1000000);
if (lat_frac < 0) lat_frac = -lat_frac;
if (lon_frac < 0) lon_frac = -lon_frac;
LOG_INF("GPS fix: position updated lat=%d.%06d lon=%d.%06d",
lat_deg, lat_frac, lon_deg, lon_frac);
/* Save to storage */
data_store.savePrefs(companion_mesh.prefs);
}
/* Update UI with GPS data */
{
struct gps_position gpos;
gps_get_position(&gpos);
int32_t lat_mdeg = (int32_t)(lat * 1000.0);
int32_t lon_mdeg = (int32_t)(lon * 1000.0);
ui_set_gps_data(true, (uint8_t)gpos.satellites,
lat_mdeg, lon_mdeg, gpos.altitude_mm);
}
#else
ARG_UNUSED(lat);
ARG_UNUSED(lon);
#endif
}
/* bt_ready callback — BLE stack is up, start advertising */
static void bt_ready(int err)
{
if (err) {
LOG_ERR("bt_enable failed: %d", err);
return;
}
#ifdef ZEPHCORE_LORA
zephcore_ble_start(companion_mesh.getDeviceName());
#else
zephcore_ble_start(NULL);
#endif
}
int main(void)
{
/* Clear any stale bootloader magic from previous sessions */
zephyr_board.clearBootloaderMagic();
/* Brief settle — deferred logging will catch up once USB CDC enumerates */
k_sleep(K_MSEC(100));
LOG_INF("=== ZephCore starting ===");
if (!ZephyrDataStore::mount()) {
LOG_ERR("LittleFS mount failed");
}
data_store.begin();
/* Initialize sensor manager (GPS, environment sensors) */
sensor_manager_init();
/* Initialize UI subsystem (buttons, buzzer, display).
* If display is present, ui_init() handles display init + auto-off.
* If no display, fall back to raw OLED sleep for power saving. */
ui_init();
#if !IS_ENABLED(CONFIG_ZEPHCORE_UI_DISPLAY)
oled_sleep();
#endif
/* Register GPS callbacks (GPS state restored from prefs later) */
if (gps_is_available()) {
LOG_INF("GPS available");
gps_set_enable_callback(gps_enable_callback);
gps_set_fix_callback(gps_fix_callback);
gps_set_event_callback(gps_event_callback);
}
/* Log environment sensor availability */
if (env_sensors_available()) {
LOG_INF("Environment sensors available");
}
#ifdef ZEPHCORE_LORA
/* Initialize prefs with defaults */
memset(&companion_mesh.prefs, 0, sizeof(companion_mesh.prefs));
companion_mesh.prefs.freq = 869.618f;
companion_mesh.prefs.bw = 62.5f;
companion_mesh.prefs.sf = 8;
companion_mesh.prefs.cr = 8;
companion_mesh.prefs.tx_power_dbm = 22;
companion_mesh.prefs.rx_delay_base = 0.0f; /* Disabled for companion */
companion_mesh.prefs.airtime_factor = 10.0f; /* 10% duty cycle (EU 868 default) */
/* Generate default node name from hardware device ID */
uint8_t dev_id[8];
ssize_t id_len = hwinfo_get_device_id(dev_id, sizeof(dev_id));
if (id_len >= 4) {
snprintf(companion_mesh.prefs.node_name, sizeof(companion_mesh.prefs.node_name),
"MeshCore-%02X%02X%02X%02X", dev_id[0], dev_id[1], dev_id[2], dev_id[3]);
} else {
strncpy(companion_mesh.prefs.node_name, "MeshCore", sizeof(companion_mesh.prefs.node_name) - 1);
}
/* Load prefs from storage */
data_store.loadPrefs(companion_mesh.prefs);
/* Apply saved BLE PIN (0 = use Kconfig default) */
if (companion_mesh.prefs.ble_pin >= 100000 && companion_mesh.prefs.ble_pin <= 999999) {
zephcore_ble_set_passkey(companion_mesh.prefs.ble_pin);
LOG_INF("BLE passkey loaded from prefs: %06u", companion_mesh.prefs.ble_pin);
}
/* Copy prefs to temp_prefs for radio (radio was initialized before companion_mesh) */
temp_prefs = companion_mesh.prefs;
/* Load contacts and channels */
data_store.loadContacts(&companion_mesh);
data_store.loadChannels(&companion_mesh);
/* Add Public channel if no channels loaded (first boot) */
if (companion_mesh.getNumChannels() == 0) {
// Public channel PSK: "izOH6cXN6mrJ5e26oRXNcg==" decoded
static const uint8_t public_psk[16] = {
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72
};
companion_mesh.addChannel("Public", public_psk, 16);
data_store.saveChannels(&companion_mesh);
LOG_INF("Added default Public channel");
}
/* Load or generate identity */
mesh::LocalIdentity self_identity;
if (!data_store.loadMainIdentity(self_identity)) {
self_identity = mesh::LocalIdentity(&zephyr_rng);
/* Ensure pub_key[0] is not reserved (0x00 or 0xFF in MeshCore protocol) */
int count = 0;
while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) {
self_identity = mesh::LocalIdentity(&zephyr_rng);
count++;
}
data_store.saveMainIdentity(self_identity);
}
companion_mesh.self_id = self_identity;
/* Set callbacks */
companion_mesh.setWriteFrameCallback(write_frame);
companion_mesh.setPushCallback(push_callback);
companion_mesh.setBatteryCallback(get_battery_mv);
companion_mesh.setRadioReconfigureCallback(radio_reconfigure);
companion_mesh.setPinChangeCallback([](uint32_t new_pin) {
zephcore_ble_set_passkey(new_pin);
});
companion_mesh_ptr = &companion_mesh;
/* Set LoRa callbacks for event-driven packet processing */
lora_radio.setRxCallback(lora_rx_callback, nullptr);
lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr);
/* Start mesh */
companion_mesh.begin();
/* Push initial state to UI display */
ui_set_node_name(companion_mesh.prefs.node_name);
ui_set_radio_params(
(uint32_t)(companion_mesh.prefs.freq * 1000000.0f + 0.5f),
companion_mesh.prefs.sf,
(uint16_t)(companion_mesh.prefs.bw * 10.0f + 0.5f),
companion_mesh.prefs.cr,
companion_mesh.prefs.tx_power_dbm,
lora_radio.getNoiseFloor());
ui_set_battery(zephyr_board.getBattMilliVolts(), 0);
ui_set_gps_available(gps_is_available());
ui_set_gps_enabled(companion_mesh.prefs.gps_enabled != 0);
ui_set_ble_enabled(true); /* BLE starts advertising at boot */
/* Restore offgrid mode (client repeat) state from persisted prefs */
ui_set_offgrid_mode(companion_mesh.prefs.client_repeat != 0);
LOG_INF("offgrid mode: %s (from prefs)",
companion_mesh.prefs.client_repeat ? "on" : "off");
/* Restore buzzer mute state from persisted prefs, then play
* startup chime only if buzzer is not muted.
* buzzer_init() defaults to quiet=true, so we must always
* call buzzer_set_quiet() to apply the saved preference. */
#if IS_ENABLED(CONFIG_ZEPHCORE_UI_BUZZER)
buzzer_set_quiet(companion_mesh.prefs.buzzer_quiet);
ui_set_buzzer_quiet(companion_mesh.prefs.buzzer_quiet);
LOG_INF("buzzer: quiet=%s (from prefs)",
companion_mesh.prefs.buzzer_quiet ? "true" : "false");
ui_play_startup_chime();
#endif
/* Restore GPS state from persisted prefs.
* GPS hardware is powered at boot (bootloader/pull-up).
* First, ensure power state matches prefs (powers off if disabled).
* Then, if prefs say enabled, start the GPS state machine. */
if (gps_is_available()) {
/* Explicitly set power state at boot (handles disabled case) */
gps_ensure_power_state(companion_mesh.prefs.gps_enabled);
if (companion_mesh.prefs.gps_enabled) {
LOG_INF("GPS: Restoring enabled state from prefs");
gps_enable(true);
}
}
/* Apply RX boost setting from prefs (overrides radio default if user changed it) */
lora_radio.setRxBoost(companion_mesh.prefs.rx_boost != 0);
/* Initialize mesh event object */
k_event_init(&mesh_events);
/* Initialize UI mesh actions module (pass mesh objects for deferred actions) */
ui_mesh_actions_init(&mesh_events, MESH_EVENT_UI_ACTION,
&companion_mesh, &data_store,
&lora_radio, &zephyr_board, &rtc_clock);
#endif
/* Initialize BLE adapter (registers auth callbacks) */
zephcore_ble_init(&ble_cbs);
/*
* USB CDC ACM - only enabled when logging is enabled (debug builds).
* Production builds (CONFIG_LOG=n) skip USB entirely, saving ~2-5mA.
*/
#if IS_ENABLED(CONFIG_LOG)
zephcore_usb_companion_init(&mesh_events, &rx_process_work, MESH_EVENT_BLE_RX,
&zephyr_board);
#endif
if (bt_enable(bt_ready) != 0) {
LOG_ERR("bt_enable failed");
}
/*
* FULLY EVENT-DRIVEN architecture: main thread runs mesh event loop.
* Mesh loop wakes IMMEDIATELY on events - NO POLLING for TX completion!
*
* Event sources:
* - MESH_EVENT_LORA_RX: LoRa packet received (from RX async callback)
* - MESH_EVENT_LORA_TX_DONE: LoRa TX complete (from TX poll work -> callback)
* - MESH_EVENT_BLE_RX: BLE frame received (from NUS write)
* - MESH_EVENT_HOUSEKEEPING: Periodic maintenance (noise floor, etc.)
*
* Other processing still uses work queues:
* - BLE TX: write_frame -> tx_drain_work (event-driven via notify callback)
* - Contact iteration: contact_iter_work (on TX queue space)
*
* TX completion is now event-driven: radio polls at 5ms intervals internally
* and fires callback when done. Much better than 100ms main loop polling!
*/
#ifdef ZEPHCORE_LORA
mesh_event_loop(); /* Never returns */
#else
for (;;) {
k_sleep(K_FOREVER);
}
#endif
return 0;
}
+375
View File
@@ -0,0 +1,375 @@
/*
* SPDX-License-Identifier: Apache-2.0
* ZephCore - Repeater (USB CLI, Event-Driven)
*
* This is the main entry point for the repeater role.
* Repeaters use USB serial CLI for configuration (no BLE).
*/
#include <stdio.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_repeater_main, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/uart.h>
#include <zephyr/sys/ring_buffer.h>
#include <zephyr/drivers/hwinfo.h>
#include "oled_power.h"
/* USB CDC with 1200 baud touch detection (when not using auto-init) */
#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
#include <ZephyrRepeaterUSB.h>
#endif
#include <app/RepeaterDataStore.h>
#include <app/RepeaterMesh.h>
#include <adapters/clock/ZephyrRTCClock.h>
#include <ZephyrSensorManager.h>
/* Radio + mesh includes (shared header selects LR1110 or SX126x) */
#include <mesh/RadioIncludes.h>
/* LED configuration */
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
#define LED0_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led0 = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
#endif
#if DT_NODE_HAS_PROP(DT_ALIAS(led1), gpios)
#define LED1_NODE DT_ALIAS(led1)
static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios);
#endif
/* USB CLI configuration */
#define USB_RING_BUF_SIZE 512
#define CLI_LINE_BUF_SIZE 256
/*
* Event-driven mesh loop - replaces 50ms polling with true event signaling.
* Events are signaled from ISR/callbacks, mesh loop wakes immediately.
*/
#define MESH_EVENT_LORA_RX BIT(0) /* LoRa packet received */
#define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete */
#define MESH_EVENT_CLI_RX BIT(2) /* CLI command received */
#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING)
/* Housekeeping interval - infrequent to preserve power savings */
#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS
/* Event object for mesh loop */
static struct k_event mesh_events;
/* USB CDC state */
static const struct device *usb_dev;
static uint8_t usb_ring_buf_data[USB_RING_BUF_SIZE];
static struct ring_buf usb_ring_buf;
static char cli_line_buf[CLI_LINE_BUF_SIZE];
static char cli_reply_buf[256];
static uint16_t cli_line_idx;
/* Work items for event-driven processing */
static void cli_rx_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
K_WORK_DEFINE(cli_rx_work, cli_rx_work_fn);
/* Housekeeping timer for periodic tasks (noise floor calibration, etc.) */
K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL);
/* Forward declarations */
#ifdef ZEPHCORE_LORA
static RepeaterMesh *repeater_mesh_ptr;
#endif
/* Print string to USB serial */
static void cli_print(const char *str)
{
if (!usb_dev) return;
while (*str) {
uart_poll_out(usb_dev, *str++);
}
}
/* USB CDC UART interrupt callback */
static void cli_uart_isr(const struct device *dev, void *user_data)
{
ARG_UNUSED(user_data);
while (uart_irq_update(dev) && uart_irq_is_pending(dev)) {
if (uart_irq_rx_ready(dev)) {
uint8_t buf[64];
int recv_len = uart_fifo_read(dev, buf, sizeof(buf));
if (recv_len > 0) {
ring_buf_put(&usb_ring_buf, buf, recv_len);
k_work_submit(&cli_rx_work);
}
}
}
}
/* CLI RX work - processes line-based CLI commands
* Matches Arduino behavior: echo each char, then " -> reply" on enter
*/
static void cli_rx_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
uint8_t byte;
while (ring_buf_get(&usb_ring_buf, &byte, 1) == 1) {
/* Process command on \r OR \n (support echo from Linux) */
if (byte == '\r' || byte == '\n') {
if (cli_line_idx > 0) {
cli_line_buf[cli_line_idx] = '\0';
/* Debug: log received command */
LOG_INF("CLI cmd len=%d: %.40s%s", cli_line_idx,
cli_line_buf, cli_line_idx > 40 ? "..." : "");
/* Process CLI command */
#ifdef ZEPHCORE_LORA
if (repeater_mesh_ptr) {
cli_reply_buf[0] = '\0';
repeater_mesh_ptr->handleCommand(0, cli_line_buf, cli_reply_buf);
if (cli_reply_buf[0] != '\0') {
/* Arduino format: newline, then " -> reply" */
cli_print("\r\n -> ");
cli_print(cli_reply_buf);
}
}
#endif
cli_line_idx = 0;
}
/* New line for next command */
cli_print("\r\n");
} else if (byte == 0x7F || byte == 0x08) {
/* Backspace - echo backspace sequence */
if (cli_line_idx > 0) {
cli_line_idx--;
if (usb_dev) {
uart_poll_out(usb_dev, '\b');
uart_poll_out(usb_dev, ' ');
uart_poll_out(usb_dev, '\b');
}
}
} else if (cli_line_idx < sizeof(cli_line_buf) - 1) {
/* Echo character back (like Arduino) */
if (usb_dev) {
uart_poll_out(usb_dev, byte);
}
cli_line_buf[cli_line_idx++] = (char)byte;
}
}
}
/* Housekeeping timer callback - signals event to wake mesh loop periodically */
static void housekeeping_timer_fn(struct k_timer *timer)
{
ARG_UNUSED(timer);
k_event_post(&mesh_events, MESH_EVENT_HOUSEKEEPING);
}
#ifdef ZEPHCORE_LORA
/* LoRa RX callback - called from ISR context when packet received */
static void lora_rx_callback(void *user_data)
{
ARG_UNUSED(user_data);
k_event_post(&mesh_events, MESH_EVENT_LORA_RX);
}
/* LoRa TX complete callback */
static void lora_tx_done_callback(void *user_data)
{
ARG_UNUSED(user_data);
k_event_post(&mesh_events, MESH_EVENT_LORA_TX_DONE);
}
#endif
/* Global instances */
static mesh::ZephyrRTCClock rtc_clock;
static RepeaterDataStore data_store;
#ifdef ZEPHCORE_LORA
static mesh::ZephyrBoard zephyr_board;
/* Radio prefs — initialized with defaults in main(), updated after mesh.begin()
* loads persisted prefs. Passed to radio adapter at static construction time. */
static NodePrefs radio_prefs;
#if IS_ENABLED(CONFIG_ZEPHCORE_RADIO_LR1110)
/* LR1110 via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::LR1110Radio lora_radio(lora_dev, zephyr_board, &radio_prefs);
#else
/* SX126x via Zephyr LoRa driver */
static const struct device *const lora_dev = DEVICE_DT_GET(DT_ALIAS(lora0));
static mesh::SX126xRadio lora_radio(lora_dev, zephyr_board, &radio_prefs);
#endif
static mesh::ZephyrMillisecondClock ms_clock;
static mesh::ZephyrRNG zephyr_rng;
static mesh::SimpleMeshTables mesh_tables;
/* RepeaterMesh requires: board, radio, ms_clock, rng, rtc, tables */
static RepeaterMesh repeater_mesh(zephyr_board, lora_radio, ms_clock, zephyr_rng, rtc_clock, mesh_tables);
#endif
/* Repeater event loop */
static void repeater_event_loop(void)
{
LOG_INF("starting event-driven loop");
/* Print startup banner (no prompt - Arduino style) */
cli_print("\r\n=== ZephCore Repeater ===\r\n");
/* Start housekeeping timer for periodic maintenance tasks */
k_timer_start(&housekeeping_timer, K_MSEC(HOUSEKEEPING_INTERVAL_MS),
K_MSEC(HOUSEKEEPING_INTERVAL_MS));
for (;;) {
/* Wait for any mesh event - blocks until signaled */
uint32_t events = k_event_wait(&mesh_events, MESH_EVENT_ALL, false, K_FOREVER);
k_event_clear(&mesh_events, events);
#ifdef ZEPHCORE_LORA
if (repeater_mesh_ptr) {
repeater_mesh_ptr->loop();
}
#endif
}
}
int main(void)
{
/* Initialize radio prefs with safe defaults before anything else */
initNodePrefs(&radio_prefs);
strcpy(radio_prefs.node_name, "Repeater");
/* Wait for USB CDC to enumerate before any logging */
k_sleep(K_MSEC(2000));
LOG_INF("=== ZephCore Repeater starting ===");
/* Configure LEDs */
#if DT_NODE_HAS_PROP(DT_ALIAS(led0), gpios)
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
/* Initialize repeater data store */
if (!data_store.begin()) {
LOG_ERR("RepeaterDataStore init failed");
}
/* Initialize sensor manager */
sensor_manager_init();
/* Set GPS to repeater mode: power off now, wake every 48h for time sync only.
* This prevents GPS from draining power on boards that have it (e.g., Wio Tracker). */
if (gps_is_available()) {
gps_set_repeater_mode(true);
}
/* Put OLED display to sleep */
oled_sleep();
/* Log environment sensor availability */
if (env_sensors_available()) {
LOG_INF("Environment sensors available");
}
#ifdef ZEPHCORE_LORA
repeater_mesh_ptr = &repeater_mesh;
/* Initialize mesh event object BEFORE begin() — radio callbacks post events */
k_event_init(&mesh_events);
/* Set LoRa callbacks for event-driven packet processing */
lora_radio.setRxCallback(lora_rx_callback, nullptr);
lora_radio.setTxDoneCallback(lora_tx_done_callback, nullptr);
/* Load or generate identity BEFORE begin() */
mesh::LocalIdentity self_identity;
if (!data_store.loadIdentity(self_identity)) {
LOG_INF("No identity found, generating new keypair...");
self_identity = mesh::LocalIdentity(&zephyr_rng);
/* Ensure pub_key[0] is not reserved (0x00 or 0xFF) */
int count = 0;
while (count < 10 && (self_identity.pub_key[0] == 0x00 || self_identity.pub_key[0] == 0xFF)) {
self_identity = mesh::LocalIdentity(&zephyr_rng);
count++;
}
data_store.saveIdentity(self_identity);
LOG_INF("New identity saved");
}
repeater_mesh.self_id = self_identity;
/* Log repeater ID (first 8 bytes of public key) */
LOG_INF("Repeater ID: %02x%02x%02x%02x%02x%02x%02x%02x...",
self_identity.pub_key[0], self_identity.pub_key[1],
self_identity.pub_key[2], self_identity.pub_key[3],
self_identity.pub_key[4], self_identity.pub_key[5],
self_identity.pub_key[6], self_identity.pub_key[7]);
/* Start mesh with data store - this loads prefs, ACL, regions */
repeater_mesh.begin(&data_store);
/* Generate default node name from hardware device ID if not set */
NodePrefs* prefs = repeater_mesh.getNodePrefs();
if (strlen(prefs->node_name) == 0 || strcmp(prefs->node_name, "Repeater") == 0) {
uint8_t dev_id[8];
ssize_t id_len = hwinfo_get_device_id(dev_id, sizeof(dev_id));
if (id_len >= 4) {
snprintf(prefs->node_name, sizeof(prefs->node_name),
"Repeater-%02X%02X%02X%02X", dev_id[0], dev_id[1], dev_id[2], dev_id[3]);
}
}
/* Apply RX boost setting from prefs (overrides radio default if user changed it) */
lora_radio.setRxBoost(prefs->rx_boost != 0);
/* Send initial zero-hop advertisement after boot */
LOG_INF("Sending initial advertisement...");
repeater_mesh.sendSelfAdvertisement(500, false); /* 500ms delay, zero-hop */
#endif
/* Initialize USB CDC for CLI with 1200 baud touch detection */
#if !IS_ENABLED(CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT)
zephcore_usbd_init();
#endif
usb_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
if (device_is_ready(usb_dev)) {
LOG_INF("USB CDC device ready: %s", usb_dev->name);
ring_buf_init(&usb_ring_buf, sizeof(usb_ring_buf_data), usb_ring_buf_data);
/* Set up UART interrupt callback */
uart_irq_callback_set(usb_dev, cli_uart_isr);
uart_irq_rx_enable(usb_dev);
} else {
LOG_ERR("USB CDC device not ready");
usb_dev = NULL;
}
/*
* Event-driven architecture: main thread runs repeater event loop.
* No BLE - all configuration via USB serial CLI.
*/
#ifdef ZEPHCORE_LORA
repeater_event_loop(); /* Never returns */
#else
for (;;) {
k_sleep(K_FOREVER);
}
#endif
return 0;
}