mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-23 21:12:36 +00:00
First iteration that seems to work
This commit is contained in:
commit
8d1823d0b6
205 files changed
+41989
No files matched your search
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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(×tamp, &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], ×tamp, 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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user