Initial commit: Simple Sensor firmware for Heltec T114 with BME280
Some checks failed
Build and deploy Docs site to GitHub Pages / github-pages (push) Has been cancelled
PR Build Check / build (Heltec_v3_companion_radio_ble) (push) Has been cancelled
PR Build Check / build (Heltec_v3_repeater) (push) Has been cancelled
PR Build Check / build (Heltec_v3_room_server) (push) Has been cancelled
PR Build Check / build (LilyGo_Tlora_C6_repeater_) (push) Has been cancelled
PR Build Check / build (PicoW_repeater) (push) Has been cancelled
PR Build Check / build (RAK_4631_companion_radio_ble) (push) Has been cancelled
PR Build Check / build (RAK_4631_repeater) (push) Has been cancelled
PR Build Check / build (RAK_4631_room_server) (push) Has been cancelled
PR Build Check / build (wio-e5-mini_repeater) (push) Has been cancelled

- MeshCore-based Simple Sensor firmware
- Heltec T114 without display
- BME280 sensor support (temp/humidity/pressure)
- Radio: 868.856018 MHz, SF7, BW62.5 kHz, CR4/7
- 2-byte path hash
- PlatformIO project
This commit is contained in:
UA1ZBE
2026-06-01 16:19:09 +03:00
commit 42f55e12ae
725 changed files with 98374 additions and 0 deletions

389
src/Dispatcher.cpp Normal file
View File

@@ -0,0 +1,389 @@
#include "Dispatcher.h"
#if MESH_PACKET_LOGGING
#include <Arduino.h>
#endif
#include <math.h>
namespace mesh {
#define MAX_RX_DELAY_MILLIS 32000 // 32 seconds
#define MIN_TX_BUDGET_RESERVE_MS 100 // min budget (ms) required before allowing next TX
#define MIN_TX_BUDGET_AIRTIME_DIV 2 // require at least 1/N of estimated airtime as budget before TX
#ifndef NOISE_FLOOR_CALIB_INTERVAL
#define NOISE_FLOOR_CALIB_INTERVAL 2000 // 2 seconds
#endif
void Dispatcher::begin() {
n_sent_flood = n_sent_direct = 0;
n_recv_flood = n_recv_direct = 0;
_err_flags = 0;
radio_nonrx_start = _ms->getMillis();
duty_cycle_window_ms = getDutyCycleWindowMs();
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
tx_budget_ms = (unsigned long)(duty_cycle_window_ms * duty_cycle);
last_budget_update = _ms->getMillis();
_radio->begin();
prev_isrecv_mode = _radio->isInRecvMode();
}
float Dispatcher::getAirtimeBudgetFactor() const {
return 1.0;
}
void Dispatcher::updateTxBudget() {
unsigned long now = _ms->getMillis();
unsigned long elapsed = now - last_budget_update;
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long max_budget = (unsigned long)(getDutyCycleWindowMs() * duty_cycle);
unsigned long refill = (unsigned long)(elapsed * duty_cycle);
if (refill > 0) {
tx_budget_ms += refill;
if (tx_budget_ms > max_budget) {
tx_budget_ms = max_budget;
}
last_budget_update = now;
}
}
int Dispatcher::calcRxDelay(float score, uint32_t air_time) const {
return (int) ((pow(10, 0.85f - score) - 1.0) * air_time);
}
uint32_t Dispatcher::getCADFailRetryDelay() const {
return 200;
}
uint32_t Dispatcher::getCADFailMaxDuration() const {
return 4000; // 4 seconds
}
void Dispatcher::loop() {
if (millisHasNowPassed(next_floor_calib_time)) {
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL);
}
_radio->loop();
// check for radio 'stuck' in mode other than Rx
bool is_recv = _radio->isInRecvMode();
if (is_recv != prev_isrecv_mode) {
prev_isrecv_mode = is_recv;
if (!is_recv) {
radio_nonrx_start = _ms->getMillis();
}
}
if (!is_recv && _ms->getMillis() - radio_nonrx_start > 8000) { // radio has not been in Rx mode for 8 seconds!
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
}
if (outbound) { // waiting for outbound send to be completed
if (_radio->isSendComplete()) {
long t = _ms->getMillis() - outbound_start;
total_air_time += t;
//Serial.print(" airtime="); Serial.println(t);
updateTxBudget();
if (t > tx_budget_ms) {
tx_budget_ms = 0;
} else {
tx_budget_ms -= t;
}
if (tx_budget_ms < MIN_TX_BUDGET_RESERVE_MS) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = MIN_TX_BUDGET_RESERVE_MS - tx_budget_ms;
next_tx_time = futureMillis((unsigned long)(needed / duty_cycle));
} else {
next_tx_time = _ms->getMillis();
}
_radio->onSendFinished();
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
if (outbound->isRouteFlood()) {
n_sent_flood++;
} else {
n_sent_direct++;
}
releasePacket(outbound); // return to pool
outbound = NULL;
} else if (millisHasNowPassed(outbound_expiry)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime());
_radio->onSendFinished();
logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
releasePacket(outbound); // return to pool
outbound = NULL;
} else {
return; // can't do any more radio activity until send is complete or timed out
}
// going back into receive mode now...
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
_radio->resetAGC();
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
// check inbound (delayed) queue
{
Packet* pkt = _mgr->getNextInbound(_ms->getMillis());
if (pkt) {
processRecvPacket(pkt);
}
}
checkRecv();
checkSend();
}
bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) {
int i = 0;
pkt->header = raw[i++];
if (pkt->getPayloadVer() > PAYLOAD_VER_1) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime());
return false;
}
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++];
uint8_t path_mode = pkt->path_len >> 6; // upper 2 bits (legacy firmware: 00)
if (path_mode == 3) { // Reserved for future
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported path mode: 3", getLogDateTime());
return false;
}
uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize();
if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len);
return false;
}
memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len;
pkt->payload_len = len - i; // payload is remainder
if (pkt->payload_len > sizeof(pkt->payload)) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len);
return false;
}
memcpy(pkt->payload, &raw[i], pkt->payload_len);
return true; // success
}
void Dispatcher::checkRecv() {
Packet* pkt;
float score;
uint32_t air_time;
{
uint8_t raw[MAX_TRANS_UNIT+1];
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
if (len > 0) {
logRxRaw(_radio->getLastSNR(), _radio->getLastRSSI(), raw, len);
pkt = _mgr->allocNew();
if (pkt == NULL) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime());
} else {
if (tryParsePacket(pkt, raw, len)) {
pkt->_snr = _radio->getLastSNR() * 4.0f;
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
} else {
_mgr->free(pkt); // put back into pool
pkt = NULL;
}
}
} else {
pkt = NULL;
}
}
if (pkt) {
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d time=%d",
pkt->getRawLength(), pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len,
(int)pkt->getSNR(), (int)_radio->getLastRSSI(), (int)(score*1000), air_time);
static uint8_t packet_hash[MAX_HASH_SIZE];
pkt->calculatePacketHash(packet_hash);
Serial.print(" hash=");
mesh::Utils::printHex(Serial, packet_hash, MAX_HASH_SIZE);
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ
|| pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]);
} else {
Serial.printf("\n");
}
#endif
logRx(pkt, pkt->getRawLength(), score); // hook for custom logging
if (pkt->isRouteFlood()) {
n_recv_flood++;
int _delay = calcRxDelay(score, air_time);
if (_delay < 50) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(), score delay below threshold (%d)", getLogDateTime(), _delay);
processRecvPacket(pkt); // is below the score delay threshold, so process immediately
} else {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(), score delay is: %d millis", getLogDateTime(), _delay);
if (_delay > MAX_RX_DELAY_MILLIS) {
_delay = MAX_RX_DELAY_MILLIS;
}
_mgr->queueInbound(pkt, futureMillis(_delay)); // add to delayed inbound queue
}
} else {
n_recv_direct++;
processRecvPacket(pkt);
}
}
}
void Dispatcher::processRecvPacket(Packet* pkt) {
DispatcherAction action = onRecvPacket(pkt);
if (action == ACTION_RELEASE) {
_mgr->free(pkt);
} else if (action == ACTION_MANUAL_HOLD) {
// sub-class is wanting to manually hold Packet instance, and call releasePacket() at appropriate time
} else { // ACTION_RETRANSMIT*
uint8_t priority = (action >> 24) - 1;
uint32_t _delay = action & 0xFFFFFF;
_mgr->queueOutbound(pkt, priority, futureMillis(_delay));
}
}
void Dispatcher::checkSend() {
if (_mgr->getOutboundCount(_ms->getMillis()) == 0) return;
updateTxBudget();
uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
if (tx_budget_ms < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV) {
float duty_cycle = 1.0f / (1.0f + getAirtimeBudgetFactor());
unsigned long needed = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV - tx_budget_ms;
next_tx_time = futureMillis((unsigned long)(needed / duty_cycle));
return;
}
if (!millisHasNowPassed(next_tx_time)) return;
if (_radio->isReceiving()) {
if (cad_busy_start == 0) {
cad_busy_start = _ms->getMillis(); // record when CAD busy state started
}
if (_ms->getMillis() - cad_busy_start > getCADFailMaxDuration()) {
_err_flags |= ERR_EVENT_CAD_TIMEOUT;
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): CAD busy max duration reached!", getLogDateTime());
// channel activity has gone on too long... (Radio might be in a bad state)
// force the pending transmit below...
} else {
next_tx_time = futureMillis(getCADFailRetryDelay());
return;
}
}
cad_busy_start = 0; // reset busy state
outbound = _mgr->getNextOutbound(_ms->getMillis());
if (outbound) {
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
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;
len += Packet::writePath(&raw[len], outbound->path, outbound->path_len);
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len);
_mgr->free(outbound);
outbound = NULL;
} else {
memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len;
uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2;
outbound_start = _ms->getMillis();
bool success = _radio->startSendRaw(raw, len);
if (!success) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime());
logTxFail(outbound, outbound->getRawLength());
releasePacket(outbound); // return to pool
outbound = NULL;
return;
}
outbound_expiry = futureMillis(max_airtime);
#if MESH_PACKET_LOGGING
Serial.print(getLogDateTime());
Serial.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)",
len, outbound->getPayloadType(), outbound->isRouteDirect() ? "D" : "F", outbound->payload_len);
if (outbound->getPayloadType() == PAYLOAD_TYPE_PATH || outbound->getPayloadType() == PAYLOAD_TYPE_REQ
|| outbound->getPayloadType() == PAYLOAD_TYPE_RESPONSE || outbound->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) {
Serial.printf(" [%02X -> %02X]\n", (uint32_t)outbound->payload[1], (uint32_t)outbound->payload[0]);
} else {
Serial.printf("\n");
}
#endif
}
}
}
Packet* Dispatcher::obtainNewPacket() {
auto pkt = _mgr->allocNew(); // TODO: zero out all fields
if (pkt == NULL) {
_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) {
if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) {
MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
_mgr->free(packet);
} else {
_mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
}
}
// Utility function -- handles the case where millis() wraps around back to zero
// 2's complement arithmetic will handle any unsigned subtraction up to HALF the word size (32-bits in this case)
bool Dispatcher::millisHasNowPassed(unsigned long timestamp) const {
return (long)(_ms->getMillis() - timestamp) > 0;
}
unsigned long Dispatcher::futureMillis(int millis_from_now) const {
return _ms->getMillis() + millis_from_now;
}
}

202
src/Dispatcher.h Normal file
View File

@@ -0,0 +1,202 @@
#pragma once
#include <MeshCore.h>
#include <Identity.h>
#include <Packet.h>
#include <Utils.h>
#include <string.h>
namespace mesh {
/**
* \brief Abstraction of local/volatile clock with Millisecond granularity.
*/
class MillisecondClock {
public:
virtual unsigned long getMillis() = 0;
};
/**
* \brief Abstraction of this device's packet radio.
*/
class Radio {
public:
virtual void begin() { }
/**
* \brief polls for incoming raw packet.
* \param bytes destination to store incoming raw packet.
* \param sz maximum packet size allowed.
* \returns 0 if no incoming data, otherwise length of complete packet received.
*/
virtual int recvRaw(uint8_t* bytes, int sz) = 0;
/**
* \returns estimated transmit air-time needed for packet of 'len_bytes', in milliseconds.
*/
virtual uint32_t getEstAirtimeFor(int len_bytes) = 0;
virtual float packetScore(float snr, int packet_len) = 0;
/**
* \brief starts the raw packet send. (no wait)
* \param bytes the raw packet data
* \param len the length in bytes
* \returns true if successfully started
*/
virtual bool startSendRaw(const uint8_t* bytes, int len) = 0;
/**
* \returns true if the previous 'startSendRaw()' completed successfully.
*/
virtual bool isSendComplete() = 0;
/**
* \brief a hook for doing any necessary clean up after transmit.
*/
virtual void onSendFinished() = 0;
/**
* \brief do any processing needed on each loop cycle
*/
virtual void loop() { }
virtual int getNoiseFloor() const { return 0; }
virtual void triggerNoiseFloorCalibrate(int threshold) { }
virtual void resetAGC() { }
virtual bool isInRecvMode() const = 0;
/**
* \returns true if the radio is currently mid-receive of a packet.
*/
virtual bool isReceiving() { return false; }
virtual float getLastRSSI() const { return 0; }
virtual float getLastSNR() const { return 0; }
};
/**
* \brief An abstraction for managing instances of Packets (eg. in a static pool),
* and for managing the outbound packet queue.
*/
class PacketManager {
public:
virtual Packet* allocNew() = 0;
virtual void free(Packet* packet) = 0;
virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0;
virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority
virtual int getOutboundCount(uint32_t now) const = 0;
virtual int getOutboundTotal() const = 0;
virtual int getFreeCount() const = 0;
virtual Packet* getOutboundByIdx(int i) = 0;
virtual Packet* removeOutboundByIdx(int i) = 0;
virtual void queueInbound(Packet* packet, uint32_t scheduled_for) = 0;
virtual Packet* getNextInbound(uint32_t now) = 0;
};
typedef uint32_t DispatcherAction;
#define ACTION_RELEASE (0)
#define ACTION_MANUAL_HOLD (1)
#define ACTION_RETRANSMIT(pri) (((uint32_t)1 + (pri))<<24)
#define ACTION_RETRANSMIT_DELAYED(pri, _delay) ((((uint32_t)1 + (pri))<<24) | (_delay))
#define ERR_EVENT_FULL (1 << 0)
#define ERR_EVENT_CAD_TIMEOUT (1 << 1)
#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2)
/**
* \brief The low-level task that manages detecting incoming Packets, and the queueing
* and scheduling of outbound Packets.
*/
class Dispatcher {
Packet* outbound; // current outbound packet
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
unsigned long next_tx_time;
unsigned long cad_busy_start;
unsigned long radio_nonrx_start;
unsigned long next_floor_calib_time, next_agc_reset_time;
bool prev_isrecv_mode;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
unsigned long tx_budget_ms;
unsigned long last_budget_update;
unsigned long duty_cycle_window_ms;
void processRecvPacket(Packet* pkt);
void updateTxBudget();
protected:
PacketManager* _mgr;
Radio* _radio;
MillisecondClock* _ms;
uint16_t _err_flags;
Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr)
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = NULL;
total_air_time = rx_air_time = 0;
next_tx_time = ms.getMillis();
cad_busy_start = 0;
next_floor_calib_time = next_agc_reset_time = 0;
_err_flags = 0;
radio_nonrx_start = 0;
prev_isrecv_mode = true;
tx_budget_ms = 0;
last_budget_update = 0;
duty_cycle_window_ms = 3600000;
}
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { } // custom hook
virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging
virtual void logTx(Packet* packet, int len) { }
virtual void logTxFail(Packet* packet, int len) { }
virtual const char* getLogDateTime() { return ""; }
virtual float getAirtimeBudgetFactor() const;
virtual int calcRxDelay(float score, uint32_t air_time) const;
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
public:
void begin();
void loop();
Packet* obtainNewPacket();
void releasePacket(Packet* packet);
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; }
unsigned long getReceiveAirTime() const {return rx_air_time; }
unsigned long getRemainingTxBudget() const { return tx_budget_ms; }
uint32_t getNumSentFlood() const { return n_sent_flood; }
uint32_t getNumSentDirect() const { return n_sent_direct; }
uint32_t getNumRecvFlood() const { return n_recv_flood; }
uint32_t getNumRecvDirect() const { return n_recv_direct; }
void resetStats() {
n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0;
_err_flags = 0;
}
// helper methods
bool millisHasNowPassed(unsigned long timestamp) const;
unsigned long futureMillis(int millis_from_now) const;
private:
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
void checkRecv();
void checkSend();
};
}

143
src/Identity.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "Identity.h"
#include <string.h>
#define ED25519_NO_SEED 1
#include <ed_25519.h>
#include <Ed25519.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 {
#if 0
// NOTE: memory corruption bug was found in this function!!
return ed25519_verify(sig, message, msg_len, pub_key);
#else
return Ed25519::verify(sig, this->pub_key, message, msg_len);
#endif
}
bool Identity::readFrom(Stream& s) {
return (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
}
bool Identity::writeTo(Stream& s) const {
return (s.write(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
}
void Identity::printTo(Stream& s) const {
Utils::printHex(s, pub_key, PUB_KEY_SIZE);
}
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); // derive public key from given private key
// disallow 00 or FF prefixed public keys
if (pub[0] == 0x00 || pub[0] == 0xFF) return false;
// known good test client keypair
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];
// shared secret we calculte from test client pubkey and given private key
ed25519_key_exchange(ss1, test_client_pub, prv);
// shared secret they calculate from our derived public key and test client private key
ed25519_key_exchange(ss2, pub, test_client_prv);
// check that both shared secrets match
if (memcmp(ss1, ss2, 32) != 0) return false;
// reject all-zero shared secret
for (int i = 0; i < 32; i++) {
if (ss1[i] != 0) return true;
}
return false;
}
bool LocalIdentity::readFrom(Stream& s) {
bool success = (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (s.readBytes(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE);
return success;
}
bool LocalIdentity::writeTo(Stream& s) const {
bool success = (s.write(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
success = success && (s.write(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE);
return success;
}
void LocalIdentity::printTo(Stream& s) const {
s.print("pub_key: "); Utils::printHex(s, pub_key, PUB_KEY_SIZE); s.println();
s.print("prv_key: "); Utils::printHex(s, prv_key, PRV_KEY_SIZE); s.println();
}
size_t LocalIdentity::writeTo(uint8_t* dest, size_t max_len) {
if (max_len < PRV_KEY_SIZE) return 0; // not big enough
if (max_len < PRV_KEY_SIZE + PUB_KEY_SIZE) { // only room for prv_key
memcpy(dest, prv_key, PRV_KEY_SIZE);
return PRV_KEY_SIZE;
}
memcpy(dest, prv_key, PRV_KEY_SIZE); // otherwise can fit prv + pub keys
memcpy(&dest[PRV_KEY_SIZE], pub_key, PUB_KEY_SIZE);
return PRV_KEY_SIZE + PUB_KEY_SIZE;
}
void LocalIdentity::readFrom(const uint8_t* src, size_t len) {
if (len == PRV_KEY_SIZE + PUB_KEY_SIZE) { // has prv + pub keys
memcpy(prv_key, src, PRV_KEY_SIZE);
memcpy(pub_key, &src[PRV_KEY_SIZE], PUB_KEY_SIZE);
} else if (len == PRV_KEY_SIZE) {
memcpy(prv_key, src, PRV_KEY_SIZE);
// now need to re-calculate the pub_key
ed25519_derive_pub(pub_key, prv_key);
}
}
void LocalIdentity::sign(uint8_t* sig, const uint8_t* message, int msg_len) const {
ed25519_sign(sig, message, 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);
}
}

98
src/Identity.h Normal file
View File

@@ -0,0 +1,98 @@
#pragma once
#include <Utils.h>
#include <Stream.h>
namespace mesh {
/**
* \brief An identity in the mesh, with given Ed25519 public key, ie. a party whose signatures can be VERIFIED.
*/
class Identity {
public:
uint8_t pub_key[PUB_KEY_SIZE];
Identity();
Identity(const char* pub_hex);
Identity(const uint8_t* _pub) { memcpy(pub_key, _pub, PUB_KEY_SIZE); }
int copyHashTo(uint8_t* dest) const {
memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key
return PATH_HASH_SIZE;
}
int copyHashTo(uint8_t* dest, uint8_t len) const {
memcpy(dest, pub_key, len); // hash is just prefix of pub_key
return len;
}
bool isHashMatch(const uint8_t* hash) const {
return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0;
}
bool isHashMatch(const uint8_t* hash, uint8_t len) const {
return memcmp(hash, pub_key, len) == 0;
}
/**
* \brief Performs Ed25519 signature verification.
* \param sig IN - must be SIGNATURE_SIZE buffer.
* \param message IN - the original message which was signed.
* \param msg_len IN - the length in bytes of message.
* \returns true, if signature is valid.
*/
bool verify(const uint8_t* sig, const uint8_t* message, int msg_len) const;
bool matches(const Identity& other) const { return memcmp(pub_key, other.pub_key, PUB_KEY_SIZE) == 0; }
bool matches(const uint8_t* other_pubkey) const { return memcmp(pub_key, other_pubkey, PUB_KEY_SIZE) == 0; }
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;
void printTo(Stream& s) const;
};
/**
* \brief An Identity generated on THIS device, ie. with public/private Ed25519 key pair being on this device.
*/
class LocalIdentity : public Identity {
uint8_t prv_key[PRV_KEY_SIZE];
public:
LocalIdentity();
LocalIdentity(const char* prv_hex, const char* pub_hex);
LocalIdentity(RNG* rng); // create new random
/**
* \brief Ed25519 digital signature.
* \param sig OUT - must be SIGNATURE_SIZE buffer.
* \param message IN - the raw message bytes to sign.
* \param msg_len IN - the length in bytes of message.
*/
void sign(uint8_t* sig, const uint8_t* message, int msg_len) const;
/**
* \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519.
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
* \param other IN - the second party in the exchange.
*/
void calcSharedSecret(uint8_t* secret, const Identity& other) const { calcSharedSecret(secret, other.pub_key); }
/**
* \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519.
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
* \param other_pub_key IN - the public key of second party in the exchange (must be PUB_KEY_SIZE bytes)
*/
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const;
/**
* \brief Validates that a given private key can be used for ECDH / shared-secret operations.
* \param prv IN - the private key to validate (must be PRV_KEY_SIZE bytes)
* \returns true, if the private key is valid for login.
*/
static bool validatePrivateKey(const uint8_t prv[64]);
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;
void printTo(Stream& s) const;
size_t writeTo(uint8_t* dest, size_t max_len);
void readFrom(const uint8_t* src, size_t len);
};
}

729
src/Mesh.cpp Normal file
View File

@@ -0,0 +1,729 @@
#include "Mesh.h"
//#include <Arduino.h>
namespace mesh {
void Mesh::begin() {
Dispatcher::begin();
}
void Mesh::loop() {
Dispatcher::loop();
}
bool Mesh::allowPacketForward(const mesh::Packet* packet) {
return false; // by default, Transport NOT enabled
}
uint32_t Mesh::getRetransmitDelay(const mesh::Packet* packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getRawLength()) * 52 / 50) / 2;
return _rng->nextInt(0, 5)*t;
}
uint32_t Mesh::getDirectRetransmitDelay(const Packet* packet) {
return 0; // by default, no delay
}
uint8_t Mesh::getExtraAckTransmitCount() const {
return 0;
}
uint32_t Mesh::getCADFailRetryDelay() const {
return _rng->nextInt(1, 4)*120;
}
int Mesh::searchPeersByHash(const uint8_t* hash) {
return 0; // not found
}
int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int max_matches) {
return 0; // not found
}
DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) {
if (pkt->path_len < MAX_PATH_SIZE) {
uint8_t 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; // NEW v1.11+: lower 2 bits is path hash size
uint8_t len = pkt->payload_len - i;
// path_len*entry_size can exceed 255 (path_len up to 63, entry_size up to 8);
// a uint8_t offset would wrap and steer the isHashMatch() read to the wrong place.
uint16_t offset = (uint16_t)pkt->path_len << path_sz;
if (offset >= len) { // TRACE has reached end of given 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)) {
// append SNR (Not hash!)
pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(5, d); // schedule with priority 5 (for now), maybe make configurable?
}
}
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) {
if (pkt->getPathHashCount() == 0) {
onControlDataRecv(pkt);
}
// just zero-hop control packets allowed (for this subset of payloads)
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) {
// check for 'early received' ACK
if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
int i = 0;
uint32_t ack_crc;
memcpy(&ack_crc, &pkt->payload[i], 4); i += 4;
if (i <= pkt->payload_len) {
onAckRecv(pkt, ack_crc);
}
}
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) {
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
if (!_tables->hasSeen(pkt)) { // don't retransmit!
removeSelfFromPath(pkt);
routeDirectRecvAcks(pkt, 0);
}
return ACTION_RELEASE;
}
if (!_tables->hasSeen(pkt)) {
removeSelfFromPath(pkt);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
}
}
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
}
if (pkt->isRouteFlood() && filterRecvFloodPacket(pkt)) return ACTION_RELEASE;
DispatcherAction action = ACTION_RELEASE;
switch (pkt->getPayloadType()) {
case PAYLOAD_TYPE_ACK: {
int i = 0;
uint32_t ack_crc;
memcpy(&ack_crc, &pkt->payload[i], 4); i += 4;
if (i > pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime());
} else 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: {
int i = 0;
uint8_t dest_hash = pkt->payload[i++];
uint8_t src_hash = pkt->payload[i++];
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + CIPHER_MAC_SIZE >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) {
// NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins.
// For flood mode, the path may not be the 'best' in terms of hops.
// FUTURE: could send back multiple paths, using createPathReturn(), and let sender choose which to use(?)
if (self_id.isHashMatch(&dest_hash)) {
// scan contacts DB, for all matching hashes of 'src_hash' (max 4 matches supported ATM)
int num = searchPeersByHash(&src_hash);
// for each matching contact, try to decrypt data
bool found = false;
for (int j = 0; j < num; j++) {
uint8_t secret[PUB_KEY_SIZE];
getPeerSharedSecret(secret, j);
// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0;
uint8_t path_len = data[k++];
uint8_t hash_size = (path_len >> 6) + 1;
uint8_t hash_count = path_len & 63;
uint8_t* path = &data[k]; k += hash_size*hash_count;
uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use
uint8_t* extra = &data[k];
uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!)
if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) {
if (pkt->isRouteFlood()) {
// send a reciprocal return path to sender, but send DIRECTLY!
mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0);
if (rpath) sendDirect(rpath, path, path_len, 500);
}
}
} else {
onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len);
}
found = true;
break;
}
}
if (found) {
pkt->markDoNotRetransmit(); // packet was for this node, so don't retransmit
} else {
MESH_DEBUG_PRINTLN("%s recv matches no peers, src_hash=%02X", getLogDateTime(), (uint32_t)src_hash);
}
}
action = routeRecvPacket(pkt);
}
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]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} 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);
// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
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]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) {
// scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM)
GroupChannel channels[4];
int num = searchChannelsByHash(&channel_hash, channels, 4);
// for each matching channel, try to decrypt data
for (int j = 0; j < num; j++) {
// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len);
break;
}
}
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_ADVERT: {
int i = 0;
Identity id;
memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE;
uint32_t timestamp;
memcpy(&timestamp, &pkt->payload[i], 4); i += 4;
const uint8_t* signature = &pkt->payload[i]; i += SIGNATURE_SIZE;
if (i > pkt->payload_len) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete advertisement packet", getLogDateTime());
} else if (self_id.matches(id.pub_key)) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): receiving SELF advert packet", getLogDateTime());
} else if (!_tables->hasSeen(pkt)) {
uint8_t* app_data = &pkt->payload[i];
int app_data_len = pkt->payload_len - i;
if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; }
// check that signature is valid
bool is_ok;
{
uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE];
int msg_len = 0;
memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE;
memcpy(&message[msg_len], &timestamp, 4); msg_len += 4;
memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len;
is_ok = id.verify(signature, message, msg_len);
}
if (is_ok) {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): valid advertisement received!", getLogDateTime());
onAdvertRecv(pkt, id, timestamp, app_data, app_data_len);
action = routeRecvPacket(pkt);
} else {
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): received advertisement with forged signature! (app_data_len=%d)", getLogDateTime(), app_data_len);
}
}
break;
}
case PAYLOAD_TYPE_RAW_CUSTOM: {
if (pkt->isRouteDirect() && !_tables->hasSeen(pkt)) {
onRawDataRecv(pkt);
//action = routeRecvPacket(pkt); don't flood route these (yet)
}
break;
}
case PAYLOAD_TYPE_MULTIPART:
if (pkt->payload_len > 2) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = Packet::copyPath(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);
//action = routeRecvPacket(&tmp); // NOTE: currently not needed, as multipart ACKs not sent Flood
}
} else {
// FUTURE: other multipart types??
}
}
break;
default:
MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header);
// Don't flood route unknown packet types! action = routeRecvPacket(pkt);
break;
}
return action;
}
void Mesh::removeSelfFromPath(Packet* pkt) {
// remove our hash from 'path'
pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count
uint8_t sz = pkt->getPathHashSize();
for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry'
memcpy(&pkt->path[k], &pkt->path[k + sz], sz);
}
}
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
uint8_t n = packet->getPathHashCount();
if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit()
&& (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) {
// append this node's hash to 'path'
self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize());
packet->setPathHashCount(n + 1);
uint32_t d = getRetransmitDelay(packet);
// as this propagates outwards, give it lower and lower priority
return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away
}
return ACTION_RELEASE;
}
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK
Packet tmp;
tmp.header = pkt->header;
tmp.path_len = Packet::copyPath(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)) { // don't retransmit!
removeSelfFromPath(&tmp);
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
}
}
return ACTION_RELEASE;
}
void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
if (!packet->isMarkedDoNotRetransmit()) {
uint32_t crc;
memcpy(&crc, packet->payload, 4);
uint8_t extra = getExtraAckTransmitCount();
while (extra > 0) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(crc, extra);
if (a1) {
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT;
sendPacket(a1, 0, delay_millis);
}
extra--;
}
auto a2 = createAck(crc);
if (a2) {
a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len);
a2->header &= ~PH_ROUTE_MASK;
a2->header |= ROUTE_TYPE_DIRECT;
sendPacket(a2, 0, delay_millis);
}
}
}
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 NULL;
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createAdvert(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); // ROUTE_TYPE_* is set later
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; // will fill this in later
memcpy(&packet->payload[len], app_data, app_data_len); len += 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;
memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len;
id.sign(signature, message, msg_len);
}
return packet;
}
#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) {
uint8_t path_hash_size = (path_len >> 6) + 1;
uint8_t path_hash_count = path_len & 63;
if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!!
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createPathReturn(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
int len = 0;
memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; // dest hash
len += self_id.copyHashTo(&packet->payload[len]); // src hash
{
int data_len = 0;
uint8_t data[MAX_PACKET_PAYLOAD];
data[data_len++] = path_len;
memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size;
if (extra_len > 0) {
data[data_len++] = extra_type;
memcpy(&data[data_len], extra, extra_len); data_len += extra_len;
} else {
// append a timestamp, or random blob (to make packet_hash unique)
data[data_len++] = 0xFF; // dummy payload type
getRNG()->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) {
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) return NULL;
} else {
return NULL; // invalid type
}
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createDatagram(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
int len = 0;
len += dest.copyHashTo(&packet->payload[len]); // dest hash
len += self_id.copyHashTo(&packet->payload[len]); // src hash
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
packet->payload_len = 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 NULL;
} else {
return NULL; // invalid type
}
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createAnonDatagram(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
int len = 0;
if (type == PAYLOAD_TYPE_ANON_REQ) {
len += dest.copyHashTo(&packet->payload[len]); // dest hash
memcpy(&packet->payload[len], sender.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; // sender pub_key
} else {
// FUTURE:
}
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 NULL; // invalid type
if (data_len + 1 + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; // too long
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createGroupDatagram(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
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::createAck(uint32_t ack_crc) {
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createAck(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
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 == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createMultiAck(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->payload[0] = (remaining << 4) | PAYLOAD_TYPE_ACK;
memcpy(&packet->payload[1], &ack_crc, 4);
packet->payload_len = 5;
return packet;
}
Packet* Mesh::createRawData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createRawData(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_RAW_CUSTOM << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
memcpy(packet->payload, data, len);
packet->payload_len = len;
return packet;
}
Packet* Mesh::createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags) {
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createTrace(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_TRACE << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
memcpy(packet->payload, &tag, 4);
memcpy(&packet->payload[4], &auth_code, 4);
packet->payload[8] = flags;
packet->payload_len = 9; // NOTE: path will be appended to payload[] later
return packet;
}
Packet* Mesh::createControlData(const uint8_t* data, size_t len) {
if (len > sizeof(Packet::payload)) return NULL; // invalid arg
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("%s Mesh::createControlData(): error, packet pool empty", getLogDateTime());
return NULL;
}
packet->header = (PAYLOAD_TYPE_CONTROL << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
memcpy(packet->payload, data, len);
packet->payload_len = len;
return packet;
}
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return;
}
if (path_hash_size == 0 || path_hash_size > 3) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
return;
}
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_FLOOD;
packet->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 2;
} else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pri = 3; // de-prioritie these
} else {
pri = 1;
}
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) {
if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime());
return;
}
if (path_hash_size == 0 || path_hash_size > 3) {
MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime());
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->setPathHashSizeAndCount(path_hash_size, 0);
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 2;
} else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pri = 3; // de-prioritie these
} 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) { // TRACE packets are different
// for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's)
memcpy(&packet->payload[packet->payload_len], path, path_len); // NOTE: path_len here can be > 64, and NOT in the new scheme
packet->payload_len += path_len;
packet->path_len = 0;
pri = 5; // maybe make this configurable
} else {
packet->path_len = Packet::copyPath(packet->path, path, path_len);
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 1; // slightly less priority
} else {
pri = 0;
}
}
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
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; // path_len of zero means Zero Hop
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
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; // path_len of zero means Zero Hop
_tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, 0, delay_millis);
}
}

225
src/Mesh.h Normal file
View File

@@ -0,0 +1,225 @@
#pragma once
#include <Dispatcher.h>
namespace mesh {
class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
};
/**
* An abstraction of the data tables needed to be maintained
*/
class MeshTables {
public:
virtual bool hasSeen(const Packet* packet) = 0;
virtual void clear(const Packet* packet) = 0; // remove this packet hash from table
};
/**
* \brief The next layer in the basic Dispatcher task, Mesh recognises the particular Payload TYPES,
* and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets.
*/
class Mesh : public Dispatcher {
RTCClock* _rtc;
RNG* _rng;
MeshTables* _tables;
void removeSelfFromPath(Packet* packet);
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet* pkt);
protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
virtual uint32_t getCADFailRetryDelay() const override;
/**
* \brief Decide what to do with received packet, ie. discard, forward, or hold
*/
DispatcherAction routeRecvPacket(Packet* packet);
/**
* \brief Called _before_ the packet is dispatched to the on..Recv() methods.
* \returns true, if given packet should be NOT be processed.
*/
virtual bool filterRecvFloodPacket(Packet* packet) { return false; }
/**
* \brief Check whether this packet should be forwarded (re-transmitted) or not.
* Is sub-classes responsibility to make sure given packet is only transmitted ONCE (by this node)
*/
virtual bool allowPacketForward(const Packet* packet);
/**
* \returns number of milliseconds delay to apply to retransmitting the given packet.
*/
virtual uint32_t getRetransmitDelay(const Packet* packet);
/**
* \returns number of milliseconds delay to apply to retransmitting the given packet, for DIRECT mode.
*/
virtual uint32_t getDirectRetransmitDelay(const Packet* packet);
/**
* \returns number of extra (Direct) ACK transmissions wanted.
*/
virtual uint8_t getExtraAckTransmitCount() const;
/**
* \brief Perform search of local DB of peers/contacts.
* \returns Number of peers with matching hash
*/
virtual int searchPeersByHash(const uint8_t* hash);
/**
* \brief lookup the ECDH shared-secret between this node and peer by idx (calculate if necessary)
* \param dest_secret destination array to copy the secret (must be PUB_KEY_SIZE bytes)
* \param peer_idx index of peer, [0..n) where n is what searchPeersByHash() returned
*/
virtual void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { }
/**
* \brief A (now decrypted) data packet has been received (by a known peer).
* NOTE: these can be received multiple times (per sender/msg-id), via different routes
* \param type one of: PAYLOAD_TYPE_TXT_MSG, PAYLOAD_TYPE_REQ, PAYLOAD_TYPE_RESPONSE
* \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned
* \param secret the pre-calculated shared-secret (handy for sending response packet)
* \param data decrypted data from payload
*/
virtual void onPeerDataRecv(Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) { }
/**
* \brief A TRACE packet has been received. (and has reached the end of its given path)
* NOTE: this may have been initiated by another node.
* \param tag a random (unique-ish) tag set by initiator
* \param auth_code a code to authenticate the packet
* \param flags zero for now
* \param path_snrs single byte SNR*4 for each hop in the path
* \param path_hashes hashes if each repeater in the path
* \param path_len length of the path_snrs[] and path_hashes[] arrays
*/
virtual void onTraceRecv(Packet* packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t* path_snrs, const uint8_t* path_hashes, uint8_t path_len) { }
/**
* \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded)
* NOTE: these can be received multiple times (per sender), via differen routes
* \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned
* \param secret the pre-calculated shared-secret (handy for sending response packet)
* \returns true, if path was accepted and that reciprocal path should be sent
*/
virtual bool onPeerPathRecv(Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { return false; }
/**
* \brief A new incoming Advertisement has been received.
* NOTE: these can be received multiple times (per id/timestamp), via different routes
*/
virtual void onAdvertRecv(Packet* packet, const Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { }
/**
* \brief A (now decrypted) data packet has been received.
* NOTE: these can be received multiple times (per sender/contents), via different routes
* \param secret ECDH shared secret
* \param sender public key provided by sender
*/
virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret, const Identity& sender, uint8_t* data, size_t len) { }
/**
* \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded)
* NOTE: these can be received multiple times (per sender), via differen routes
*/
virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { }
/**
* \brief A control packet has been received.
*/
virtual void onControlDataRecv(Packet* packet) { }
/**
* \brief A packet with PAYLOAD_TYPE_RAW_CUSTOM has been received.
*/
virtual void onRawDataRecv(Packet* packet) { }
/**
* \brief Perform search of local DB of matching GroupChannels.
* \param channels OUT - store matching channels in this array, up to max_matches
* \returns Number of channels with matching hash
*/
virtual int searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int max_matches);
/**
* \brief An encrypted group data packet has been received.
* NOTE: the same payload can be received multiple times, via different routes
* \param type one of: PAYLOAD_TYPE_GRP_TXT, PAYLOAD_TYPE_GRP_DATA
* \param channel the matching GroupChannel
*/
virtual void onGroupDataRecv(Packet* packet, uint8_t type, const GroupChannel& channel, uint8_t* data, size_t len) { }
/**
* \brief A simple ACK packet has been received.
* NOTE: same ACK can be received multiple times, via different routes
*/
virtual void onAckRecv(Packet* packet, uint32_t ack_crc) { }
Mesh(Radio& radio, MillisecondClock& ms, RNG& rng, RTCClock& rtc, PacketManager& mgr, MeshTables& tables)
: Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc), _tables(&tables)
{
}
MeshTables* getTables() const { return _tables; }
public:
void begin();
void loop();
LocalIdentity self_id;
RNG* getRNG() const { return _rng; }
RTCClock* getRTCClock() const { return _rtc; }
Packet* createAdvert(const LocalIdentity& id, const uint8_t* app_data=NULL, size_t app_data_len=0);
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
Packet* createAck(uint32_t ack_crc);
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
Packet* 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);
Packet* 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);
Packet* createRawData(const uint8_t* data, size_t len);
Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0);
Packet* createControlData(const uint8_t* data, size_t len);
/**
* \brief send a locally-generated Packet with flood routing
*/
void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1);
/**
* \brief send a locally-generated Packet with flood routing
* \param transport_codes array of 2 codes to attach to packet
*/
void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1);
/**
* \brief send a locally-generated Packet with Direct routing
*/
void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0);
/**
* \brief send a locally-generated Packet to just neigbor nodes (zero hops)
*/
void sendZeroHop(Packet* packet, uint32_t delay_millis=0);
/**
* \brief send a locally-generated Packet to just neigbor nodes (zero hops), with specific transort codes
* \param transport_codes array of 2 codes to attach to packet
*/
void sendZeroHop(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0);
};
}

104
src/MeshCore.h Normal file
View File

@@ -0,0 +1,104 @@
#pragma once
#include <stdint.h>
#include <math.h>
#define MAX_HASH_SIZE 8
#define PUB_KEY_SIZE 32
#define PRV_KEY_SIZE 64
#define SEED_SIZE 32
#define SIGNATURE_SIZE 64
#define MAX_ADVERT_DATA_SIZE 32
#define CIPHER_KEY_SIZE 16
#define CIPHER_BLOCK_SIZE 16
// V1
#define CIPHER_MAC_SIZE 2
#define PATH_HASH_SIZE 1
#define MAX_PACKET_PAYLOAD 184
#define MAX_GROUP_DATA_LENGTH (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3)
#define MAX_PATH_SIZE 64
#define MAX_TRANS_UNIT 255
#if MESH_DEBUG && ARDUINO
#include <Arduino.h>
#define MESH_DEBUG_PRINT(F, ...) Serial.printf("DEBUG: " F, ##__VA_ARGS__)
#define MESH_DEBUG_PRINTLN(F, ...) Serial.printf("DEBUG: " F "\n", ##__VA_ARGS__)
#else
#define MESH_DEBUG_PRINT(...) {}
#define MESH_DEBUG_PRINTLN(...) {}
#endif
#if BRIDGE_DEBUG && ARDUINO
#define BRIDGE_DEBUG_PRINTLN(F, ...) Serial.printf("%s BRIDGE: " F, getLogDateTime(), ##__VA_ARGS__)
#else
#define BRIDGE_DEBUG_PRINTLN(...) {}
#endif
namespace mesh {
#define BD_STARTUP_NORMAL 0 // getStartupReason() codes
#define BD_STARTUP_RX_PACKET 1
class MainBoard {
public:
virtual uint16_t getBattMilliVolts() = 0;
virtual float getMCUTemperature() { return NAN; }
virtual bool setAdcMultiplier(float multiplier) { return false; };
virtual float getAdcMultiplier() const { return 0.0f; }
virtual const char* getManufacturerName() const = 0;
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual void powerOff() { /* no op */ }
virtual void sleep(uint32_t secs) { /* no op */ }
virtual uint32_t getGpio() { return 0; }
virtual void setGpio(uint32_t values) {}
virtual uint8_t getStartupReason() const = 0;
virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; }
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
// Power management interface (boards with power management override these)
virtual bool isExternalPowered() { return false; }
virtual uint16_t getBootVoltage() { return 0; }
virtual uint32_t getResetReason() const { return 0; }
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
virtual uint8_t getShutdownReason() const { return 0; }
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
};
/**
* An abstraction of the device's Realtime Clock.
*/
class RTCClock {
uint32_t last_unique;
protected:
RTCClock() { last_unique = 0; }
public:
/**
* \returns the current time. in UNIX epoch seconds.
*/
virtual uint32_t getCurrentTime() = 0;
/**
* \param time current time in UNIX epoch seconds.
*/
virtual void setCurrentTime(uint32_t time) = 0;
/**
* override in classes that need to periodically update internal state
*/
virtual void tick() { /* no op */}
uint32_t getCurrentTimeUnique() {
uint32_t t = getCurrentTime();
if (t <= last_unique) {
return ++last_unique;
}
return last_unique = t;
}
};
}

87
src/Packet.cpp Normal file
View File

@@ -0,0 +1,87 @@
#include "Packet.h"
#include <string.h>
#include <SHA256.h>
namespace mesh {
Packet::Packet() {
header = 0;
path_len = 0;
payload_len = 0;
}
bool Packet::isValidPathLen(uint8_t path_len) {
uint8_t hash_count = path_len & 63;
uint8_t hash_size = (path_len >> 6) + 1;
if (hash_size == 4) return false; // Reserved for future
return hash_count*hash_size <= MAX_PATH_SIZE;
}
size_t Packet::writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) {
uint8_t hash_count = path_len & 63;
uint8_t hash_size = (path_len >> 6) + 1;
size_t len = hash_count*hash_size;
if (len > MAX_PATH_SIZE) {
MESH_DEBUG_PRINTLN("Packet::copyPath, invalid path_len=%d", (uint32_t)path_len);
return 0; // Error
}
memcpy(dest, src, len);
return len;
}
uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) {
writePath(dest, src, path_len);
return path_len;
}
int Packet::getRawLength() const {
return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0);
}
void Packet::calculatePacketHash(uint8_t* hash) const {
SHA256 sha;
uint8_t t = getPayloadType();
sha.update(&t, 1);
if (t == PAYLOAD_TYPE_TRACE) {
sha.update(&path_len, sizeof(path_len)); // CAVEAT: TRACE packets can revisit same node on return path
}
sha.update(payload, payload_len);
sha.finalize(hash, MAX_HASH_SIZE);
}
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;
i += writePath(&dest[i], path, 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 (!isValidPathLen(path_len)) return false; // bad encoding
uint8_t bl = getPathByteLen();
memcpy(path, &src[i], bl); i += bl;
if (i >= len) return false; // bad encoding
payload_len = len - i;
if (payload_len > sizeof(payload)) return false; // bad encoding
memcpy(payload, &src[i], payload_len); //i += payload_len;
return true; // success
}
}

114
src/Packet.h Normal file
View File

@@ -0,0 +1,114 @@
#pragma once
#include <MeshCore.h>
namespace mesh {
// Packet::header values
#define PH_ROUTE_MASK 0x03 // 2-bits
#define PH_TYPE_SHIFT 2
#define PH_TYPE_MASK 0x0F // 4-bits
#define PH_VER_SHIFT 6
#define PH_VER_MASK 0x03 // 2-bits
#define ROUTE_TYPE_TRANSPORT_FLOOD 0x00 // flood mode + transport codes
#define ROUTE_TYPE_FLOOD 0x01 // flood mode, needs 'path' to be built up (max 64 bytes)
#define ROUTE_TYPE_DIRECT 0x02 // direct route, 'path' is supplied
#define ROUTE_TYPE_TRANSPORT_DIRECT 0x03 // direct route + transport codes
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
#define PAYLOAD_TYPE_ACK 0x03 // a simple ack
#define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity
#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: data_type(uint16), data_len, blob)
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
#define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNI for each hop
#define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets
#define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet
//...
#define PAYLOAD_TYPE_RAW_CUSTOM 0x0F // custom packet as raw bytes, for applications with custom encryption, payloads, etc
#define PAYLOAD_VER_1 0x00 // 1-byte src/dest hashes, 2-byte MAC
#define PAYLOAD_VER_2 0x01 // FUTURE (eg. 2-byte hashes, 4-byte MAC ??)
#define PAYLOAD_VER_3 0x02 // FUTURE
#define PAYLOAD_VER_4 0x03 // FUTURE
/**
* \brief The fundamental transmission unit.
*/
class Packet {
public:
Packet();
uint8_t header;
uint16_t payload_len, path_len;
uint16_t transport_codes[2];
uint8_t path[MAX_PATH_SIZE];
uint8_t payload[MAX_PACKET_PAYLOAD];
int8_t _snr;
/**
* \brief calculate the hash of payload + type
* \param dest_hash destination to store the hash (must be MAX_HASH_SIZE bytes)
*/
void calculatePacketHash(uint8_t* dest_hash) const;
/**
* \returns one of ROUTE_ values
*/
uint8_t getRouteType() const { return header & PH_ROUTE_MASK; }
bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD; }
bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; }
bool hasTransportCodes() const { return getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD || getRouteType() == ROUTE_TYPE_TRANSPORT_DIRECT; }
/**
* \returns one of PAYLOAD_TYPE_ values
*/
uint8_t getPayloadType() const { return (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; }
/**
* \returns one of PAYLOAD_VER_ values
*/
uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; }
uint8_t getPathHashSize() const { return (path_len >> 6) + 1; }
uint8_t getPathHashCount() const { return path_len & 63; }
uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); }
void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; }
void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); }
static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len); // returns path_len
static size_t writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len); // returns byte length written
static bool isValidPathLen(uint8_t path_len);
void markDoNotRetransmit() { header = 0xFF; }
bool isMarkedDoNotRetransmit() const { return header == 0xFF; }
float getSNR() const { return ((float)_snr) / 4.0f; }
/**
* \returns the encoded/wire format length of this packet
*/
int getRawLength() const;
/**
* \brief save entire packet as a blob
* \param dest (OUT) destination buffer (assumed to be MAX_MTU_SIZE)
* \returns the packet length
*/
uint8_t writeTo(uint8_t dest[]) const;
/**
* \brief restore this packet from a blob (as created using writeTo())
* \param src (IN) buffer containing blob
* \param len the packet length (as returned by writeTo())
*/
bool readFrom(const uint8_t src[], uint8_t len);
};
}

153
src/Utils.cpp Normal file
View File

@@ -0,0 +1,153 @@
#include "Utils.h"
#include <AES.h>
#include <SHA256.h>
#ifdef ARDUINO
#include <Arduino.h>
#endif
namespace mesh {
uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) {
uint32_t num;
random((uint8_t *) &num, sizeof(num));
return (num % (_max - _min)) + _min;
}
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) {
SHA256 sha;
sha.update(msg, msg_len);
sha.finalize(hash, hash_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) {
SHA256 sha;
sha.update(frag1, frag1_len);
sha.update(frag2, frag2_len);
sha.finalize(hash, hash_len);
}
int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
AES128 aes;
uint8_t* dp = dest;
const uint8_t* sp = src;
aes.setKey(shared_secret, CIPHER_KEY_SIZE);
while (sp - src < src_len) {
aes.decryptBlock(dp, sp);
dp += 16; sp += 16;
}
return sp - src; // will always be multiple of 16
}
int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
AES128 aes;
uint8_t* dp = dest;
aes.setKey(shared_secret, CIPHER_KEY_SIZE);
while (src_len >= 16) {
aes.encryptBlock(dp, src);
dp += 16; src += 16; src_len -= 16;
}
if (src_len > 0) { // remaining partial block
uint8_t tmp[16];
memset(tmp, 0, 16);
memcpy(tmp, src, src_len);
aes.encryptBlock(dp, tmp);
dp += 16;
}
return dp - dest; // will always be multiple of 16
}
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);
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(dest + CIPHER_MAC_SIZE, enc_len);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE);
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 <= CIPHER_MAC_SIZE) return 0; // invalid src bytes
uint8_t hmac[CIPHER_MAC_SIZE];
{
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE);
}
if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) {
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
}
return 0; // invalid HMAC
}
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;
}
void Utils::printHex(Stream& s, const uint8_t* src, size_t len) {
while (len > 0) {
uint8_t b = *src++;
s.print(hex_chars[b >> 4]);
s.print(hex_chars[b & 0x0F]);
len--;
}
}
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;
}
bool Utils::isHexChar(char c) {
return c == '0' || hexVal(c) > 0;
}
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
int len = strlen(src_hex);
if (len != dest_size*2) return false; // incorrect length
uint8_t* dp = dest;
while (dp - dest < dest_size) {
char ch = *src_hex++;
char cl = *src_hex++;
*dp++ = (hexVal(ch) << 4) | hexVal(cl);
}
return true;
}
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; // replace the seperator with a null, and skip past it
}
}
// if we hit the maximum parts, make sure LAST entry does NOT have separator
while (*sp && *sp != separator) sp++;
if (*sp) {
*sp = 0; // replace the separator with null
}
return num;
}
}

87
src/Utils.h Normal file
View File

@@ -0,0 +1,87 @@
#pragma once
#include <MeshCore.h>
#include <Stream.h>
#include <string.h>
namespace mesh {
class RNG {
public:
virtual void random(uint8_t* dest, size_t sz) = 0;
/**
* \returns random number between _min (inclusive) and _max (exclusive)
*/
uint32_t nextInt(uint32_t _min, uint32_t _max);
};
class Utils {
public:
/**
* \brief calculates the SHA256 hash of 'msg', storing in 'hash' and truncating the hash to 'hash_len' bytes.
*/
static void sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len);
/**
* \brief calculates the SHA256 hash of two fragments, 'frag1' and 'frag2' (in that order), storing in 'hash' and truncating.
*/
static void sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len);
/**
* \brief Encrypts the 'src' bytes using AES128 cipher, using 'shared_secret' as key, with key length fixed at CIPHER_KEY_SIZE.
* Final block is padded with zero bytes before encrypt. Result stored in 'dest'.
* \returns The length in bytes put into 'dest'. (rounded up to block size)
*/
static int encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief Decrypt the 'src' bytes using AES128 cipher, using 'shared_secret' as key, with key length fixed at CIPHER_KEY_SIZE.
* 'src_len' should be multiple of block size, as returned by 'encrypt()'.
* \returns The length in bytes put into 'dest'. (dest may contain trailing zero bytes in final block)
*/
static int decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief encrypts bytes in src, then calculates MAC on ciphertext, inserting into leading bytes of 'dest'.
* \returns total length of bytes in 'dest' (MAC + ciphertext)
*/
static int encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src.
* \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest'
*/
static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief converts 'src' bytes with given length to Hex representation, and null terminates.
*/
static void toHex(char* dest, const uint8_t* src, size_t len);
/**
* \brief converts 'src_hex' hexadecimal string (should be null term) back to raw bytes, storing in 'dest'.
* \param dest_size must be exactly the expected size in bytes.
* \returns true if successful
*/
static bool fromHex(uint8_t* dest, int dest_size, const char *src_hex);
/**
* \brief Prints the hexadecimal representation of 'src' bytes of given length, to Stream 's'.
*/
static void printHex(Stream& s, const uint8_t* src, size_t len);
/**
* \brief parse 'text' into parts separated by 'separator' char.
* \param text the text to parse (note is MODIFIED!)
* \param parts destination array to store pointers to starts of parse parts
* \param max_num max elements to store in 'parts' array
* \param separator the separator character
* \returns the number of parts parsed (in 'parts')
*/
static int parseTextParts(char* text, const char* parts[], int max_num, char separator=',');
static bool isHexChar(char c);
};
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <Mesh.h>
class AbstractBridge {
public:
virtual ~AbstractBridge() {}
/**
* @brief Initializes the bridge.
*/
virtual void begin() = 0;
/**
* @brief Stops the bridge.
*/
virtual void end() = 0;
/**
* @brief Gets the current state of the bridge.
*
* @return true if the bridge is initialized and running, false otherwise.
*/
virtual bool isRunning() const = 0;
/**
* @brief A method to be called on every main loop iteration.
* Used for tasks like checking for incoming data.
*/
virtual void loop() = 0;
/**
* @brief A callback that is triggered when the mesh transmits a packet.
* The bridge can use this to forward the packet.
*
* @param packet The packet that was transmitted.
*/
virtual void sendPacket(mesh::Packet* packet) = 0;
/**
* @brief Processes a received packet from the bridge's medium.
*
* @param packet The packet that was received.
*/
virtual void onPacketReceived(mesh::Packet* packet) = 0;
};

View File

@@ -0,0 +1,87 @@
#include <helpers/AdvertDataHelpers.h>
uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) {
app_data[0] = _type;
int i = 1;
if (_has_loc) {
app_data[0] |= ADV_LATLON_MASK;
memcpy(&app_data[i], &_lat, 4); i += 4;
memcpy(&app_data[i], &_lon, 4); i += 4;
}
if (_extra1) {
app_data[0] |= ADV_FEAT1_MASK;
memcpy(&app_data[i], &_extra1, 2); i += 2;
}
if (_extra2) {
app_data[0] |= ADV_FEAT2_MASK;
memcpy(&app_data[i], &_extra2, 2); i += 2;
}
if (_name && *_name != 0) {
app_data[0] |= ADV_NAME_MASK;
const char* sp = _name;
while (*sp && i < MAX_ADVERT_DATA_SIZE) {
app_data[i++] = *sp++;
}
}
return i;
}
AdvertDataParser::AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len) {
_name[0] = 0;
_lat = _lon = 0;
_flags = app_data[0];
_valid = false;
_extra1 = _extra2 = 0;
int i = 1;
if (_flags & ADV_LATLON_MASK) {
memcpy(&_lat, &app_data[i], 4); i += 4;
memcpy(&_lon, &app_data[i], 4); i += 4;
}
if (_flags & ADV_FEAT1_MASK) {
memcpy(&_extra1, &app_data[i], 2); i += 2;
}
if (_flags & ADV_FEAT2_MASK) {
memcpy(&_extra2, &app_data[i], 2); i += 2;
}
if (app_data_len >= i) {
int nlen = 0;
if (_flags & ADV_NAME_MASK) {
nlen = app_data_len - i; // remainder of app_data
}
if (nlen > 0) {
memcpy(_name, &app_data[i], nlen);
_name[nlen] = 0; // set null terminator
}
_valid = true;
}
}
#include <Arduino.h>
void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_now, bool short_fmt) {
const char *suffix;
if (seconds_from_now < 0) {
suffix = short_fmt ? "" : " ago";
seconds_from_now = -seconds_from_now;
} else {
suffix = short_fmt ? "" : " from now";
}
if (seconds_from_now < 60) {
sprintf(dest, "%d secs %s", seconds_from_now, suffix);
} else {
int32_t mins = seconds_from_now / 60;
if (mins < 60) {
sprintf(dest, "%d mins %s", mins, suffix);
} else {
int32_t hours = mins / 60;
if (hours < 24) {
sprintf(dest, "%d hours %s", hours, suffix);
} else {
sprintf(dest, "%d days %s", hours / 24, suffix);
}
}
}
}

View File

@@ -0,0 +1,71 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <Mesh.h>
#define ADV_TYPE_NONE 0
#define ADV_TYPE_CHAT 1
#define ADV_TYPE_REPEATER 2
#define ADV_TYPE_ROOM 3
#define ADV_TYPE_SENSOR 4
//FUTURE: 5..15
#define ADV_LATLON_MASK 0x10
#define ADV_FEAT1_MASK 0x20 // FUTURE
#define ADV_FEAT2_MASK 0x40 // FUTURE
#define ADV_NAME_MASK 0x80
class AdvertDataBuilder {
uint8_t _type;
bool _has_loc;
const char* _name;
int32_t _lat, _lon;
uint16_t _extra1 = 0;
uint16_t _extra2 = 0;
public:
AdvertDataBuilder(uint8_t adv_type) : _type(adv_type), _name(NULL), _has_loc(false) { }
AdvertDataBuilder(uint8_t adv_type, const char* name) : _type(adv_type), _name(name), _has_loc(false) { }
AdvertDataBuilder(uint8_t adv_type, const char* name, double lat, double lon) :
_type(adv_type), _name(name), _has_loc(true), _lat(lat * 1E6), _lon(lon * 1E6) { }
void setFeat1(uint16_t extra) { _extra1 = extra; }
void setFeat2(uint16_t extra) { _extra2 = extra; }
/**
* \brief encode the given advertisement data.
* \param app_data dest array, must be MAX_ADVERT_DATA_SIZE
* \returns the encoded length in bytes
*/
uint8_t encodeTo(uint8_t app_data[]);
};
class AdvertDataParser {
uint8_t _flags;
bool _valid;
char _name[MAX_ADVERT_DATA_SIZE];
int32_t _lat, _lon;
uint16_t _extra1;
uint16_t _extra2;
public:
AdvertDataParser(const uint8_t app_data[], uint8_t app_data_len);
bool isValid() const { return _valid; }
uint8_t getType() const { return _flags & 0x0F; }
uint16_t getFeat1() const { return _extra1; }
uint16_t getFeat2() const { return _extra2; }
bool hasName() const { return _name[0] != 0; }
const char* getName() const { return _name; }
bool hasLatLon() const { return (_flags & ADV_LATLON_MASK) != 0; }
int32_t getIntLat() const { return _lat; }
int32_t getIntLon() const { return _lon; }
double getLat() const { return ((double)_lat) / 1000000.0; }
double getLon() const { return ((double)_lon) / 1000000.0; }
};
class AdvertTimeHelper {
public:
static void formatRelativeTimeDiff(char dest[], int32_t seconds_from_now, bool short_fmt);
};

View File

@@ -0,0 +1,35 @@
#pragma once
#include <Mesh.h>
#include <Arduino.h>
class VolatileRTCClock : public mesh::RTCClock {
uint32_t base_time;
uint64_t accumulator;
unsigned long prev_millis;
public:
VolatileRTCClock() { base_time = 1715770351; accumulator = 0; prev_millis = millis(); } // 15 May 2024, 8:50pm
uint32_t getCurrentTime() override { return base_time + accumulator/1000; }
void setCurrentTime(uint32_t time) override { base_time = time; accumulator = 0; prev_millis = millis(); }
void tick() override {
unsigned long now = millis();
accumulator += (now - prev_millis);
prev_millis = now;
}
};
class ArduinoMillis : public mesh::MillisecondClock {
public:
unsigned long getMillis() override { return millis(); }
};
class StdRNG : public mesh::RNG {
public:
void begin(long seed) { randomSeed(seed); }
void random(uint8_t* dest, size_t sz) override {
for (int i = 0; i < sz; i++) {
dest[i] = (::random(0, 256) & 0xFF);
}
}
};

View File

@@ -0,0 +1,73 @@
#include "ArduinoSerialInterface.h"
#define RECV_STATE_IDLE 0
#define RECV_STATE_HDR_FOUND 1
#define RECV_STATE_LEN1_FOUND 2
#define RECV_STATE_LEN2_FOUND 3
void ArduinoSerialInterface::enable() {
_isEnabled = true;
_state = RECV_STATE_IDLE;
}
void ArduinoSerialInterface::disable() {
_isEnabled = false;
}
bool ArduinoSerialInterface::isConnected() const {
return true; // no way of knowing, so assume yes
}
bool ArduinoSerialInterface::isWriteBusy() const {
return false;
}
size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
// frame is too big!
return 0;
}
uint8_t hdr[3];
hdr[0] = '>';
hdr[1] = (len & 0xFF); // LSB
hdr[2] = (len >> 8); // MSB
_serial->write(hdr, 3);
return _serial->write(src, len);
}
size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) {
while (_serial->available()) {
int c = _serial->read();
if (c < 0) break;
switch (_state) {
case RECV_STATE_IDLE:
if (c == '<') {
_state = RECV_STATE_HDR_FOUND;
}
break;
case RECV_STATE_HDR_FOUND:
_frame_len = (uint8_t)c; // LSB
_state = RECV_STATE_LEN1_FOUND;
break;
case RECV_STATE_LEN1_FOUND:
_frame_len |= ((uint16_t)c) << 8; // MSB
rx_len = 0;
_state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE;
break;
default:
if (rx_len < MAX_FRAME_SIZE) {
rx_buf[rx_len] = (uint8_t)c; // rest of frame will be discarded if > MAX
}
rx_len++;
if (rx_len >= _frame_len) { // received a complete frame?
if (_frame_len > MAX_FRAME_SIZE) _frame_len = MAX_FRAME_SIZE; // truncate
memcpy(dest, rx_buf, _frame_len);
_state = RECV_STATE_IDLE; // reset state, for next frame
return _frame_len;
}
}
}
return 0;
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "BaseSerialInterface.h"
#include <Arduino.h>
class ArduinoSerialInterface : public BaseSerialInterface {
bool _isEnabled;
uint8_t _state;
uint16_t _frame_len;
uint16_t rx_len;
Stream* _serial;
uint8_t rx_buf[MAX_FRAME_SIZE];
public:
ArduinoSerialInterface() { _isEnabled = false; _state = 0; }
void begin(Stream& serial) {
_serial = &serial;
#ifdef RAK_4631
pinMode(WB_IO2, OUTPUT);
#endif
}
// BaseSerialInterface methods
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};

View File

@@ -0,0 +1,97 @@
#include "AutoDiscoverRTCClock.h"
#include "RTClib.h"
#include <Melopero_RV3028.h>
#include "RTC_RX8130CE.h"
static RTC_DS3231 rtc_3231;
static bool ds3231_success = false;
static Melopero_RV3028 rtc_rv3028;
static bool rv3028_success = false;
static RTC_PCF8563 rtc_8563;
static bool rtc_8563_success = false;
static RTC_RX8130CE rtc_8130;
static bool rtc_8130_success = false;
#define DS3231_ADDRESS 0x68
#define RV3028_ADDRESS 0x52
#define PCF8563_ADDRESS 0x51
#define RX8130CE_ADDRESS 0x32
bool AutoDiscoverRTCClock::i2c_probe(TwoWire& wire, uint8_t addr) {
wire.beginTransmission(addr);
uint8_t error = wire.endTransmission();
return (error == 0);
}
void AutoDiscoverRTCClock::begin(TwoWire& wire) {
if (i2c_probe(wire, DS3231_ADDRESS)) {
ds3231_success = rtc_3231.begin(&wire);
}
if (i2c_probe(wire, RV3028_ADDRESS)) {
rtc_rv3028.initI2C(wire);
rtc_rv3028.writeToRegister(0x35, 0x00);
rtc_rv3028.writeToRegister(0x37, 0xB4); // Direct Switching Mode (DSM): when VDD < VBACKUP, switchover occurs from VDD to VBACKUP
rtc_rv3028.set24HourMode(); // Set the device to use the 24hour format (default) instead of the 12 hour format
rv3028_success = true;
}
if (i2c_probe(wire, PCF8563_ADDRESS)) {
rtc_8563_success = rtc_8563.begin(&wire);
}
if (i2c_probe(wire, RX8130CE_ADDRESS)) {
MESH_DEBUG_PRINTLN("RX8130CE: Found");
rtc_8130.begin(&wire);
rtc_8130_success = true;
MESH_DEBUG_PRINTLN("RX8130CE: Initialized");
}
}
uint32_t AutoDiscoverRTCClock::getCurrentTime() {
if (ds3231_success) {
return rtc_3231.now().unixtime();
}
if (rv3028_success) {
return DateTime(
rtc_rv3028.getYear(),
rtc_rv3028.getMonth(),
rtc_rv3028.getDate(),
rtc_rv3028.getHour(),
rtc_rv3028.getMinute(),
rtc_rv3028.getSecond()
).unixtime();
}
if (rtc_8563_success) {
return rtc_8563.now().unixtime();
}
if (rtc_8130_success) {
MESH_DEBUG_PRINTLN("RX8130CE: Reading time");
return rtc_8130.now().unixtime();
}
return _fallback->getCurrentTime();
}
void AutoDiscoverRTCClock::setCurrentTime(uint32_t time) {
if (ds3231_success) {
rtc_3231.adjust(DateTime(time));
} else if (rv3028_success) {
auto dt = DateTime(time);
uint8_t weekday = (dt.day() + (uint16_t)((2.6 * dt.month()) - 0.2) - (2 * (dt.year() / 100)) + dt.year() + (uint16_t)(dt.year() / 4) + (uint16_t)(dt.year() / 400)) % 7;
rtc_rv3028.setTime(dt.year(), dt.month(), weekday, dt.day(), dt.hour(), dt.minute(), dt.second());
} else if (rtc_8563_success) {
rtc_8563.adjust(DateTime(time));
} else if (rtc_8130_success) {
MESH_DEBUG_PRINTLN("RX8130CE: Setting time");
rtc_8130.adjust(DateTime(time));
} else {
_fallback->setCurrentTime(time);
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include <Mesh.h>
#include <Arduino.h>
#include <Wire.h>
class AutoDiscoverRTCClock : public mesh::RTCClock {
mesh::RTCClock* _fallback;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
AutoDiscoverRTCClock(mesh::RTCClock& fallback) : _fallback(&fallback) { }
void begin(TwoWire& wire);
uint32_t getCurrentTime() override;
void setCurrentTime(uint32_t time) override;
void tick() override {
_fallback->tick(); // is typically VolatileRTCClock, which now needs tick()
}
};

View File

@@ -0,0 +1,945 @@
#include <helpers/BaseChatMesh.h>
#include <Utils.h>
#ifndef SERVER_RESPONSE_DELAY
#define SERVER_RESPONSE_DELAY 300
#endif
#ifndef TXT_ACK_DELAY
#define TXT_ACK_DELAY 200
#endif
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
}
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
{
AdvertDataBuilder builder(ADV_TYPE_CHAT, name);
app_data_len = builder.encodeTo(app_data);
}
return createAdvert(self_id, app_data, app_data_len);
}
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, double lon) {
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
uint8_t app_data_len;
{
AdvertDataBuilder builder(ADV_TYPE_CHAT, name, lat, lon);
app_data_len = builder.encodeTo(app_data);
}
return createAdvert(self_id, app_data, app_data_len);
}
void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) {
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
mesh::Packet* ack = createAck(ack_hash);
if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY);
} else {
uint32_t d = TXT_ACK_DELAY;
if (getExtraAckTransmitCount() > 0) {
mesh::Packet* a1 = createMultiAck(ack_hash, 1);
if (a1) sendDirect(a1, dest.out_path, dest.out_path_len, d);
d += 300;
}
mesh::Packet* a2 = createAck(ack_hash);
if (a2) sendDirect(a2, dest.out_path, dest.out_path_len, d);
}
}
void BaseChatMesh::bootstrapRTCfromContacts() {
uint32_t latest = 0;
for (int i = 0; i < num_contacts; i++) {
if (contacts[i].lastmod > latest) {
latest = contacts[i].lastmod;
}
}
if (latest != 0) {
getRTCClock()->setCurrentTime(latest + 1);
}
}
ContactInfo* BaseChatMesh::allocateContactSlot() {
if (num_contacts < MAX_CONTACTS) {
return &contacts[num_contacts++];
} else if (shouldOverwriteWhenFull()) {
// Find oldest non-favourite contact by oldest lastmod timestamp
int oldest_idx = -1;
uint32_t oldest_lastmod = 0xFFFFFFFF;
for (int i = 0; i < num_contacts; i++) {
bool is_favourite = (contacts[i].flags & 0x01) != 0;
if (!is_favourite && contacts[i].lastmod < oldest_lastmod) {
oldest_lastmod = contacts[i].lastmod;
oldest_idx = i;
}
}
if (oldest_idx >= 0) {
onContactOverwrite(contacts[oldest_idx].id.pub_key);
return &contacts[oldest_idx];
}
}
return NULL; // no space, no overwrite or all contacts are all favourites
}
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
memset(&ci, 0, sizeof(ci));
ci.id = id;
ci.out_path_len = OUT_PATH_UNKNOWN;
StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name));
ci.type = parser.getType();
if (parser.hasLatLon()) {
ci.gps_lat = parser.getIntLat();
ci.gps_lon = parser.getIntLon();
}
ci.last_advert_timestamp = timestamp;
ci.lastmod = getRTCClock()->getCurrentTime();
}
void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) {
AdvertDataParser parser(app_data, app_data_len);
if (!(parser.isValid() && parser.hasName())) {
MESH_DEBUG_PRINTLN("onAdvertRecv: invalid app_data, or name is missing: len=%d", app_data_len);
return;
}
ContactInfo* from = NULL;
for (int i = 0; i < num_contacts; i++) {
if (id.matches(contacts[i].id)) { // is from one of our contacts
from = &contacts[i];
if (timestamp <= from->last_advert_timestamp) { // check for replay attacks!!
MESH_DEBUG_PRINTLN("onAdvertRecv: Possible replay attack, name: %s", from->name);
return;
}
break;
}
}
// save a copy of raw advert packet (to support "Share..." function)
int plen;
{
uint8_t save = packet->header;
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_FLOOD; // make sure transport codes are NOT saved
plen = packet->writeTo(temp_buf);
packet->header = save;
}
bool is_new = false; // true = not in contacts[], false = exists in contacts[]
if (from == NULL) {
if (!shouldAutoAddContactType(parser.getType())) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
return;
}
// check hop limit for new contacts (0 = no limit, 1 = direct (0 hops), N = up to N-1 hops)
uint8_t max_hops = getAutoAddMaxHops();
if (max_hops > 0 && packet->getPathHashCount() >= max_hops) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know
return;
}
from = allocateContactSlot();
if (from == NULL) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path);
onContactsFull();
MESH_DEBUG_PRINTLN("onAdvertRecv: unable to allocate contact slot for new contact");
return;
}
populateContactFromAdvert(*from, id, parser, timestamp);
from->sync_since = 0;
from->shared_secret_valid = false;
}
// update
putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen);
StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name));
from->type = parser.getType();
if (parser.hasLatLon()) {
from->gps_lat = parser.getIntLat();
from->gps_lon = parser.getIntLon();
}
from->last_advert_timestamp = timestamp;
from->lastmod = getRTCClock()->getCurrentTime();
onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know
}
int BaseChatMesh::searchPeersByHash(const uint8_t* hash) {
int n = 0;
for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) {
if (contacts[i].id.isHashMatch(hash)) {
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
}
}
return n;
}
void BaseChatMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) {
memcpy(dest_secret, contacts[i].getSharedSecret(self_id), PUB_KEY_SIZE);
} else {
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
}
}
void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
return;
}
ContactInfo& from = contacts[i];
if (type == PAYLOAD_TYPE_TXT_MSG && len > 5) {
uint32_t timestamp;
memcpy(&timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong)
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
if (flags == TXT_TYPE_PLAIN) {
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onMessageRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), from.id.pub_key, PUB_KEY_SIZE);
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
} else {
sendAckTo(from, ack_hash);
}
} else if (flags == TXT_TYPE_CLI_DATA) {
onCommandDataRecv(from, packet, timestamp, (const char *) &data[5]); // let UI know
// NOTE: no ack expected for CLI_DATA replies
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
if (path) sendFloodScoped(from, path);
}
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
if (timestamp > from.sync_since) { // make sure 'sync_since' is up-to-date
from.sync_since = timestamp;
}
from.lastmod = getRTCClock()->getCurrentTime(); // update last heard time
onSignedMessageRecv(from, packet, timestamp, &data[5], (const char *) &data[9]); // let UI know
uint32_t ack_hash; // calc truncated hash of the message timestamp + text + OUR pub_key, to prove to sender that we got it
mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 9 + strlen((char *)&data[9]), self_id.pub_key, PUB_KEY_SIZE);
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
} else {
sendAckTo(from, ack_hash);
}
} else {
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported message type: %u", (uint32_t) flags);
}
} else if (type == PAYLOAD_TYPE_REQ && len > 4) {
uint32_t sender_timestamp;
memcpy(&sender_timestamp, data, 4);
uint8_t reply_len = onContactRequest(from, sender_timestamp, &data[4], len - 4, temp_buf);
if (reply_len > 0) {
if (packet->isRouteFlood()) {
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len);
if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY);
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
if (reply) {
if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY);
}
}
}
}
} else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) {
onContactResponse(from, data, len);
if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) {
// we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?)
handleReturnPathRetry(from, packet->path, packet->path_len);
}
}
}
bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
int i = matching_peer_indexes[sender_idx];
if (i < 0 || i >= num_contacts) {
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
return false;
}
ContactInfo& from = contacts[i];
return onContactPathRecv(from, packet->path, packet->path_len, path, path_len, extra_type, extra, extra_len);
}
bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
// NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path.
// FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?)
from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect()
from.lastmod = getRTCClock()->getCurrentTime();
onContactPathUpdated(from);
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
// also got an encoded ACK!
if (processAck(extra) != NULL) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
}
} else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
onContactResponse(from, extra, extra_len);
}
return true; // send reciprocal path if necessary
}
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
ContactInfo* from;
if ((from = processAck((uint8_t *)&ack_crc)) != NULL) {
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
// we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?)
handleReturnPathRetry(*from, packet->path, packet->path_len);
}
}
}
void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) {
// NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY)
// override this method in various firmwares, if there's a better strategy
mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0);
if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay
}
#ifdef MAX_GROUP_CHANNELS
int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) {
int n = 0;
for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) {
if (channels[i].channel.hash[0] == hash[0]) {
dest[n++] = channels[i].channel;
}
}
return n;
}
#endif
void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) {
if (type == PAYLOAD_TYPE_GRP_TXT) {
if (len < 5) {
MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping short group text payload len=%d", (uint32_t)len);
return;
}
uint8_t txt_type = data[4];
if ((txt_type >> 2) != 0) {
MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping unsupported group text type=%d", (uint32_t)txt_type);
return;
}
uint32_t timestamp;
memcpy(&timestamp, data, 4);
// len can be > original length, but 'text' will be padded with zeroes
data[len] = 0; // need to make a C string again, with null terminator
// notify UI of this new message
onChannelMessageRecv(channel, packet, timestamp, (const char *) &data[5]); // let UI know
} else if (type == PAYLOAD_TYPE_GRP_DATA) {
if (len < 3) {
MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping short group data payload len=%d", (uint32_t)len);
return;
}
uint16_t data_type = ((uint16_t)data[0]) | (((uint16_t)data[1]) << 8);
uint8_t data_len = data[2];
size_t available_len = len - 3;
if (data_len > available_len) {
MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping malformed group data type=%d len=%d available=%d",
(uint32_t)data_type, (uint32_t)data_len, (uint32_t)available_len);
return;
}
onChannelDataRecv(channel, packet, data_type, &data[3], data_len);
}
}
mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack) {
int text_len = strlen(text);
if (text_len > MAX_TEXT_LEN) return NULL;
if (attempt > 3 && text_len > MAX_TEXT_LEN-2) return NULL;
uint8_t temp[5+MAX_TEXT_LEN+1];
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (attempt & 3);
memcpy(&temp[5], text, text_len + 1);
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&expected_ack, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
int len = 5 + text_len;
if (attempt > 3) {
temp[len++] = 0; // null terminator
temp[len++] = attempt; // hide attempt number at tail end of payload
}
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len);
}
int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout) {
mesh::Packet* pkt = composeMsgPacket(recipient, timestamp, attempt, text, expected_ack);
if (pkt == NULL) return MSG_SEND_FAILED;
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc;
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
txt_send_timeout = futureMillis(est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len));
rc = MSG_SEND_SENT_DIRECT;
}
return rc;
}
int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) {
int text_len = strlen(text);
if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED;
uint8_t temp[5+MAX_TEXT_LEN+1];
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2);
memcpy(&temp[5], text, text_len + 1);
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
if (pkt == NULL) return MSG_SEND_FAILED;
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
int rc;
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t));
rc = MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
txt_send_timeout = futureMillis(est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len));
rc = MSG_SEND_SENT_DIRECT;
}
return rc;
}
bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len) {
uint8_t temp[5+MAX_TEXT_LEN+32];
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = 0; // TXT_TYPE_PLAIN
sprintf((char *) &temp[5], "%s: ", sender_name); // <sender>: <msg>
char *ep = strchr((char *) &temp[5], 0);
int prefix_len = ep - (char *) &temp[5];
if (text_len + prefix_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN - prefix_len;
memcpy(ep, text, text_len);
ep[text_len] = 0; // null terminator
auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_TXT, channel, temp, 5 + prefix_len + text_len);
if (pkt) {
sendFloodScoped(channel, pkt);
return true;
}
return false;
}
bool BaseChatMesh::sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len) {
if (data_len < 0) {
MESH_DEBUG_PRINTLN("sendGroupData: invalid negative data_len=%d", data_len);
return false;
}
if (data_len > MAX_GROUP_DATA_LENGTH) {
MESH_DEBUG_PRINTLN("sendGroupData: data_len=%d exceeds max=%d", data_len, MAX_GROUP_DATA_LENGTH);
return false;
}
uint8_t temp[3 + MAX_GROUP_DATA_LENGTH];
temp[0] = (uint8_t)(data_type & 0xFF);
temp[1] = (uint8_t)(data_type >> 8);
temp[2] = (uint8_t)data_len;
if (data_len > 0) memcpy(&temp[3], data, data_len);
auto pkt = createGroupDatagram(PAYLOAD_TYPE_GRP_DATA, channel, temp, 3 + data_len);
if (pkt == NULL) {
MESH_DEBUG_PRINTLN("sendGroupData: unable to create group datagram, data_len=%d", data_len);
return false;
}
if (path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(channel, pkt);
} else {
sendDirect(pkt, path, path_len);
}
return true;
}
bool BaseChatMesh::shareContactZeroHop(const ContactInfo& contact) {
int plen = getBlobByKey(contact.id.pub_key, PUB_KEY_SIZE, temp_buf); // retrieve last raw advert packet
if (plen == 0) return false; // not found
auto packet = obtainNewPacket();
if (packet == NULL) return false; // no Packets available
packet->readFrom(temp_buf, plen); // restore Packet from 'blob'
uint16_t codes[2];
codes[0] = codes[1] = 0; // { 0, 0 } means 'send this nowhere'
sendZeroHop(packet, codes);
return true; // success
}
uint8_t BaseChatMesh::exportContact(const ContactInfo& contact, uint8_t dest_buf[]) {
return getBlobByKey(contact.id.pub_key, PUB_KEY_SIZE, dest_buf); // retrieve last raw advert packet
}
bool BaseChatMesh::importContact(const uint8_t src_buf[], uint8_t len) {
auto pkt = obtainNewPacket();
if (pkt) {
if (pkt->readFrom(src_buf, len) && pkt->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pkt->header |= ROUTE_TYPE_FLOOD; // simulate it being received flood-mode
getTables()->clear(pkt); // remove packet hash from table, so we can receive/process it again
_pendingLoopback = pkt; // loop-back, as if received over radio
return true; // success
} else {
releasePacket(pkt); // undo the obtainNewPacket()
}
}
return false; // error
}
int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout) {
mesh::Packet* pkt;
{
int tlen;
uint8_t temp[24];
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &now, 4); // mostly an extra blob to help make packet_hash unique
if (recipient.type == ADV_TYPE_ROOM) {
memcpy(&temp[4], &recipient.sync_since, 4);
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[8], password, len);
tlen = 8 + len;
} else {
int len = strlen(password); if (len > 15) len = 15; // max 15 chars currently
memcpy(&temp[4], password, len);
tlen = 4 + len;
}
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, tlen);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout) {
mesh::Packet* pkt;
{
uint8_t temp[MAX_PACKET_PAYLOAD];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // tag to match later (also extra blob to help make packet_hash unique)
memcpy(&temp[4], data, len);
pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout) {
if (data_len > MAX_PACKET_PAYLOAD - 16) return MSG_SEND_FAILED;
mesh::Packet* pkt;
{
uint8_t temp[MAX_PACKET_PAYLOAD];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
memcpy(&temp[4], req_data, data_len);
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len);
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout) {
mesh::Packet* pkt;
{
uint8_t temp[13];
tag = getRTCClock()->getCurrentTimeUnique();
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
temp[4] = req_type;
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp));
}
if (pkt) {
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
sendFloodScoped(recipient, pkt);
est_timeout = calcFloodTimeoutMillisFor(t);
return MSG_SEND_SENT_FLOOD;
} else {
sendDirect(pkt, recipient.out_path, recipient.out_path_len);
est_timeout = calcDirectTimeoutMillisFor(t, recipient.out_path_len);
return MSG_SEND_SENT_DIRECT;
}
}
return MSG_SEND_FAILED;
}
bool BaseChatMesh::startConnection(const ContactInfo& contact, uint16_t keep_alive_secs) {
int use_idx = -1;
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis == 0) { // free slot?
use_idx = i;
} else if (connections[i].server_id.matches(contact.id)) { // already in table?
use_idx = i;
break;
}
}
if (use_idx < 0) {
return false; // table is full
}
connections[use_idx].server_id = contact.id;
uint32_t interval = connections[use_idx].keep_alive_millis = ((uint32_t)keep_alive_secs)*1000;
connections[use_idx].next_ping = futureMillis(interval);
connections[use_idx].expected_ack = 0;
connections[use_idx].last_activity = getRTCClock()->getCurrentTime();
return true; // success
}
void BaseChatMesh::stopConnection(const uint8_t* pub_key) {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].server_id.matches(pub_key)) {
connections[i].keep_alive_millis = 0; // mark slot as now free
connections[i].next_ping = 0;
connections[i].expected_ack = 0;
connections[i].last_activity = 0;
break;
}
}
}
bool BaseChatMesh::hasConnectionTo(const uint8_t* pub_key) {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis > 0 && connections[i].server_id.matches(pub_key)) return true;
}
return false;
}
void BaseChatMesh::markConnectionActive(const ContactInfo& contact) {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis > 0 && connections[i].server_id.matches(contact.id)) {
connections[i].last_activity = getRTCClock()->getCurrentTime();
// re-schedule next KEEP_ALIVE, now that we have heard from server
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
break;
}
}
}
ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) {
// yes, got an ack for our keep_alive request!
connections[i].expected_ack = 0;
connections[i].last_activity = getRTCClock()->getCurrentTime();
// re-schedule next KEEP_ALIVE, now that we have heard from server
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
auto id = &connections[i].server_id;
return lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE); // yes, a match
}
}
return NULL; /// no match
}
void BaseChatMesh::checkConnections() {
// scan connections[] table, send KEEP_ALIVE requests
for (int i = 0; i < MAX_CONNECTIONS; i++) {
if (connections[i].keep_alive_millis == 0) continue; // unused slot
uint32_t now = getRTCClock()->getCurrentTime();
uint32_t expire_secs = (connections[i].keep_alive_millis / 1000) * 5 / 2; // 2.5 x keep_alive interval
if (now >= connections[i].last_activity + expire_secs) {
// connection now lost
connections[i].keep_alive_millis = 0;
connections[i].next_ping = 0;
connections[i].expected_ack = 0;
connections[i].last_activity = 0;
continue;
}
if (millisHasNowPassed(connections[i].next_ping)) {
auto contact = lookupContactByPubKey(connections[i].server_id.pub_key, PUB_KEY_SIZE);
if (contact == NULL) {
MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!");
continue;
}
if (contact->out_path_len == OUT_PATH_UNKNOWN) {
MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!");
continue;
}
// send KEEP_ALIVE request
uint8_t data[9];
uint32_t now = getRTCClock()->getCurrentTimeUnique();
memcpy(data, &now, 4);
data[4] = REQ_TYPE_KEEP_ALIVE;
memcpy(&data[5], &contact->sync_since, 4);
// calc expected ACK reply
mesh::Utils::sha256((uint8_t *)&connections[i].expected_ack, 4, data, 9, self_id.pub_key, PUB_KEY_SIZE);
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9);
if (pkt) {
sendDirect(pkt, contact->out_path, contact->out_path_len);
}
// schedule next KEEP_ALIVE
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
}
}
}
void BaseChatMesh::resetPathTo(ContactInfo& recipient) {
recipient.out_path_len = OUT_PATH_UNKNOWN;
}
static ContactInfo* table; // pass via global :-(
static int cmp_adv_timestamp(const void *a, const void *b) {
int a_idx = *((int *)a);
int b_idx = *((int *)b);
if (table[b_idx].last_advert_timestamp > table[a_idx].last_advert_timestamp) return 1;
if (table[b_idx].last_advert_timestamp < table[a_idx].last_advert_timestamp) return -1;
return 0;
}
void BaseChatMesh::scanRecentContacts(int last_n, ContactVisitor* visitor) {
for (int i = 0; i < num_contacts; i++) { // sort the INDEXES into contacts[]
sort_array[i] = i;
}
table = contacts; // pass via global *sigh* :-(
qsort(sort_array, num_contacts, sizeof(sort_array[0]), cmp_adv_timestamp);
if (last_n == 0) {
last_n = num_contacts; // scan ALL
} else {
if (last_n > num_contacts) last_n = num_contacts;
}
for (int i = 0; i < last_n; i++) {
visitor->onContactVisit(contacts[sort_array[i]]);
}
}
ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) {
int len = strlen(name_prefix);
for (int i = 0; i < num_contacts; i++) {
auto c = &contacts[i];
if (memcmp(c->name, name_prefix, len) == 0) return c;
}
return NULL; // not found
}
ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int prefix_len) {
for (int i = 0; i < num_contacts; i++) {
auto c = &contacts[i];
if (memcmp(c->id.pub_key, pub_key, prefix_len) == 0) return c;
}
return NULL; // not found
}
bool BaseChatMesh::addContact(const ContactInfo& contact) {
ContactInfo* dest = allocateContactSlot();
if (dest) {
*dest = contact;
dest->shared_secret_valid = false; // mark shared_secret as needing calculation
return true; // success
}
return false;
}
bool BaseChatMesh::removeContact(ContactInfo& contact) {
int idx = 0;
while (idx < num_contacts && !contacts[idx].id.matches(contact.id)) {
idx++;
}
if (idx >= num_contacts) return false; // not found
// remove from contacts array
num_contacts--;
while (idx < num_contacts) {
contacts[idx] = contacts[idx + 1];
idx++;
}
return true; // Success
}
#ifdef MAX_GROUP_CHANNELS
#include <base64.hpp>
ChannelDetails* BaseChatMesh::addChannel(const char* name, const char* psk_base64) {
if (num_channels < MAX_GROUP_CHANNELS) {
auto dest = &channels[num_channels];
memset(dest->channel.secret, 0, sizeof(dest->channel.secret));
int len = decode_base64((unsigned char *) psk_base64, strlen(psk_base64), dest->channel.secret);
if (len == 32 || len == 16) {
mesh::Utils::sha256(dest->channel.hash, sizeof(dest->channel.hash), dest->channel.secret, len);
StrHelper::strncpy(dest->name, name, sizeof(dest->name));
num_channels++;
return dest;
}
}
return NULL;
}
bool BaseChatMesh::getChannel(int idx, ChannelDetails& dest) {
if (idx >= 0 && idx < MAX_GROUP_CHANNELS) {
dest = channels[idx];
return true;
}
return false;
}
bool BaseChatMesh::setChannel(int idx, const ChannelDetails& src) {
static uint8_t zeroes[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
if (idx >= 0 && idx < MAX_GROUP_CHANNELS) {
channels[idx] = src;
if (memcmp(&src.channel.secret[16], zeroes, 16) == 0) {
mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), src.channel.secret, 16); // 128-bit key
} else {
mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), src.channel.secret, 32); // 256-bit key
}
return true;
}
return false;
}
int BaseChatMesh::findChannelIdx(const mesh::GroupChannel& ch) {
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
if (memcmp(ch.secret, channels[i].channel.secret, sizeof(ch.secret)) == 0) return i;
}
return -1; // not found
}
#else
ChannelDetails* BaseChatMesh::addChannel(const char* name, const char* psk_base64) {
return NULL; // not supported
}
bool BaseChatMesh::getChannel(int idx, ChannelDetails& dest) {
return false;
}
bool BaseChatMesh::setChannel(int idx, const ChannelDetails& src) {
return false;
}
int BaseChatMesh::findChannelIdx(const mesh::GroupChannel& ch) {
return -1; // not found
}
#endif
bool BaseChatMesh::getContactByIdx(uint32_t idx, ContactInfo& contact) {
if (idx >= num_contacts) return false;
contact = contacts[idx];
return true;
}
ContactsIterator BaseChatMesh::startContactsIterator() {
return ContactsIterator();
}
bool ContactsIterator::hasNext(const BaseChatMesh* mesh, ContactInfo& dest) {
if (next_idx >= mesh->getNumContacts()) return false;
dest = mesh->contacts[next_idx++];
return true;
}
void BaseChatMesh::loop() {
Mesh::loop();
if (txt_send_timeout && millisHasNowPassed(txt_send_timeout)) {
// failed to get an ACK
onSendTimeout();
txt_send_timeout = 0;
}
if (_pendingLoopback) {
onRecvPacket(_pendingLoopback); // loop-back, as if received over radio
releasePacket(_pendingLoopback); // undo the obtainNewPacket()
_pendingLoopback = NULL;
}
}

176
src/helpers/BaseChatMesh.h Normal file
View File

@@ -0,0 +1,176 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/AdvertDataHelpers.h>
#include <helpers/TxtDataHelpers.h>
#define MAX_TEXT_LEN (10*CIPHER_BLOCK_SIZE) // must be LESS than (MAX_PACKET_PAYLOAD - 4 - CIPHER_MAC_SIZE - 1)
#include "ContactInfo.h"
#define MAX_SEARCH_RESULTS 8
#define MSG_SEND_FAILED 0
#define MSG_SEND_SENT_FLOOD 1
#define MSG_SEND_SENT_DIRECT 2
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
class ContactVisitor {
public:
virtual void onContactVisit(const ContactInfo& contact) = 0;
};
class BaseChatMesh;
class ContactsIterator {
int next_idx = 0;
public:
bool hasNext(const BaseChatMesh* mesh, ContactInfo& dest);
};
#ifndef MAX_CONTACTS
#define MAX_CONTACTS 32
#endif
#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 16
#endif
struct ConnectionInfo {
mesh::Identity server_id;
unsigned long next_ping;
uint32_t last_activity;
uint32_t keep_alive_millis;
uint32_t expected_ack;
};
#include "ChannelDetails.h"
/**
* \brief abstract Mesh class for common 'chat' client
*/
class BaseChatMesh : public mesh::Mesh {
friend class ContactsIterator;
ContactInfo contacts[MAX_CONTACTS];
int num_contacts;
int sort_array[MAX_CONTACTS];
int matching_peer_indexes[MAX_SEARCH_RESULTS];
unsigned long txt_send_timeout;
#ifdef MAX_GROUP_CHANNELS
ChannelDetails channels[MAX_GROUP_CHANNELS];
int num_channels; // only for addChannel()
#endif
mesh::Packet* _pendingLoopback;
uint8_t temp_buf[MAX_TRANS_UNIT];
ConnectionInfo connections[MAX_CONNECTIONS];
mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack);
void sendAckTo(const ContactInfo& dest, uint32_t ack_hash);
protected:
BaseChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables)
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
{
num_contacts = 0;
#ifdef MAX_GROUP_CHANNELS
memset(channels, 0, sizeof(channels));
num_channels = 0;
#endif
txt_send_timeout = 0;
_pendingLoopback = NULL;
memset(connections, 0, sizeof(connections));
}
void bootstrapRTCfromContacts();
void resetContacts() { num_contacts = 0; }
void populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp);
ContactInfo* allocateContactSlot(); // helper to find slot for new contact
// 'UI' concepts, for sub-classes to implement
virtual bool isAutoAddEnabled() const { return true; }
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {};
virtual bool shouldOverwriteWhenFull() const { return false; }
virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops
virtual void onContactOverwrite(const uint8_t* pub_key) {};
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
virtual ContactInfo* processAck(const uint8_t *data) = 0;
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onCommandDataRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
virtual void onSignedMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const uint8_t *sender_prefix, const char *text) = 0;
virtual uint32_t calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const = 0;
virtual uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const = 0;
virtual void onSendTimeout() = 0;
virtual void onChannelMessageRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t timestamp, const char *text) = 0;
virtual void onChannelDataRecv(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint16_t data_type,
const uint8_t* data, size_t data_len) {}
virtual uint8_t onContactRequest(const ContactInfo& contact, uint32_t sender_timestamp, const uint8_t* data, uint8_t len, uint8_t* reply) = 0;
virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0);
virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0);
// storage concepts, for sub-classes to override/implement
virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented
virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; }
// Mesh overrides
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;
#ifdef MAX_GROUP_CHANNELS
int searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel channels[], int max_matches) override;
#endif
void onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) override;
// Connections
bool startConnection(const ContactInfo& contact, uint16_t keep_alive_secs);
void stopConnection(const uint8_t* pub_key);
bool hasConnectionTo(const uint8_t* pub_key);
void markConnectionActive(const ContactInfo& contact);
ContactInfo* checkConnectionsAck(const uint8_t* data);
void checkConnections();
public:
mesh::Packet* createSelfAdvert(const char* name);
mesh::Packet* createSelfAdvert(const char* name, double lat, double lon);
int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout);
int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout);
bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len);
bool sendGroupData(mesh::GroupChannel& channel, uint8_t* path, uint8_t path_len, uint16_t data_type, const uint8_t* data, int data_len);
int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout);
int sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, uint8_t req_type, uint32_t& tag, uint32_t& est_timeout);
int sendRequest(const ContactInfo& recipient, const uint8_t* req_data, uint8_t data_len, uint32_t& tag, uint32_t& est_timeout);
bool shareContactZeroHop(const ContactInfo& contact);
uint8_t exportContact(const ContactInfo& contact, uint8_t dest_buf[]);
bool importContact(const uint8_t src_buf[], uint8_t len);
void resetPathTo(ContactInfo& recipient);
void scanRecentContacts(int last_n, ContactVisitor* visitor);
ContactInfo* searchContactsByPrefix(const char* name_prefix);
ContactInfo* lookupContactByPubKey(const uint8_t* pub_key, int prefix_len);
bool removeContact(ContactInfo& contact);
bool addContact(const ContactInfo& contact);
int getNumContacts() const { return num_contacts; }
bool getContactByIdx(uint32_t idx, ContactInfo& contact);
ContactsIterator startContactsIterator();
ChannelDetails* addChannel(const char* name, const char* psk_base64);
bool getChannel(int idx, ChannelDetails& dest);
bool setChannel(int idx, const ChannelDetails& src);
int findChannelIdx(const mesh::GroupChannel& ch);
void loop();
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include <Arduino.h>
#define MAX_FRAME_SIZE 172
class BaseSerialInterface {
protected:
BaseSerialInterface() { }
public:
virtual void enable() = 0;
virtual void disable() = 0;
virtual bool isEnabled() const = 0;
virtual bool isConnected() const = 0;
virtual bool isWriteBusy() const = 0;
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
};

View File

@@ -0,0 +1,9 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
struct ChannelDetails {
mesh::GroupChannel channel;
char name[32];
};

143
src/helpers/ClientACL.cpp Normal file
View File

@@ -0,0 +1,143 @@
#include "ClientACL.h"
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
_fs = fs;
num_clients = 0;
if (_fs->exists("/s_contacts")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open("/s_contacts", "r");
#else
File file = _fs->open("/s_contacts");
#endif
if (file) {
bool full = false;
while (!full) {
ClientInfo c;
uint8_t pub_key[32];
uint8_t unused[2];
memset(&c, 0, sizeof(c));
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read((uint8_t *) &c.extra.room.sync_since, 4) == 4);
success = success && (file.read(unused, 2) == 2);
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed
if (num_clients < MAX_CLIENTS) {
clients[num_clients++] = c;
} else {
full = true;
}
}
file.close();
}
}
}
void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
_fs = fs;
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[2];
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_clients; i++) {
auto c = &clients[i];
if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function
bool success = (file.write(c->id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4);
success = success && (file.write(unused, 2) == 2);
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
success = success && (file.write(c->out_path, 64) == 64);
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
if (!success) break; // write failed
}
file.close();
}
}
bool ClientACL::clear() {
if (!_fs) return false; // no filesystem, nothing to clear
if (_fs->exists("/s_contacts")) {
_fs->remove("/s_contacts");
}
memset(clients, 0, sizeof(clients));
num_clients = 0;
return true;
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
for (int i = 0; i < num_clients; i++) {
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known
}
return NULL; // not found
}
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = &clients[MAX_CLIENTS - 1];
for (int i = 0; i < num_clients; i++) {
if (id.matches(clients[i].id)) return &clients[i]; // already known
if (!clients[i].isAdmin() && clients[i].last_activity < min_time) {
oldest = &clients[i];
min_time = oldest->last_activity;
}
}
ClientInfo* c;
if (num_clients < MAX_CLIENTS) {
c = &clients[num_clients++];
} else {
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
c->permissions = init_perms;
c->id = id;
c->out_path_len = OUT_PATH_UNKNOWN;
return c;
}
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
ClientInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
c = getClient(pubkey, key_len);
if (c == NULL) return false; // partial pubkey not found
num_clients--; // delete from contacts[]
int i = c - clients;
while (i < num_clients) {
clients[i] = clients[i + 1];
i++;
}
} else {
if (key_len < PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
mesh::Identity id(pubkey);
c = putClient(id, 0);
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
}
return true;
}

60
src/helpers/ClientACL.h Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityStore.h>
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
#define PERM_ACL_GUEST 0
#define PERM_ACL_READ_ONLY 1
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
#define OUT_PATH_UNKNOWN 0xFF
struct ClientInfo {
mesh::Identity id;
uint8_t permissions;
uint8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient)
uint32_t last_activity; // by OUR clock (transient)
union {
struct {
uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock)
uint32_t pending_ack;
uint32_t push_post_timestamp;
unsigned long ack_timeout;
uint8_t push_failures;
} room;
} extra;
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 20
#endif
class ClientACL {
FILESYSTEM* _fs;
ClientInfo clients[MAX_CLIENTS];
int num_clients;
public:
ClientACL() {
memset(clients, 0, sizeof(clients));
num_clients = 0;
}
void load(FILESYSTEM* _fs, const mesh::LocalIdentity& self_id);
void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL);
bool clear();
ClientInfo* getClient(const uint8_t* pubkey, int key_len);
ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);
bool applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms);
int getNumClients() const { return num_clients; }
ClientInfo* getClientByIdx(int idx) { return &clients[idx]; }
};

1017
src/helpers/CommonCLI.cpp Normal file

File diff suppressed because it is too large Load Diff

141
src/helpers/CommonCLI.h Normal file
View File

@@ -0,0 +1,141 @@
#pragma once
#include "Mesh.h"
#include <helpers/IdentityStore.h>
#include <helpers/SensorManager.h>
#include <helpers/ClientACL.h>
#include <helpers/RegionMap.h>
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
#define WITH_BRIDGE
#endif
#define ADVERT_LOC_NONE 0
#define ADVERT_LOC_SHARE 1
#define ADVERT_LOC_PREFS 2
#define LOOP_DETECT_OFF 0
#define LOOP_DETECT_MINIMAL 1
#define LOOP_DETECT_MODERATE 2
#define LOOP_DETECT_STRICT 3
struct NodePrefs { // persisted to file
float airtime_factor;
char node_name[32];
double node_lat, node_lon;
char password[16];
float freq;
int8_t tx_power_dbm;
uint8_t disable_fwd;
uint8_t advert_interval; // minutes / 2
uint8_t flood_advert_interval; // hours
float rx_delay_base;
float tx_delay_factor;
char guest_password[16];
float direct_tx_delay_factor;
uint32_t guard;
uint8_t sf;
uint8_t cr;
uint8_t allow_read_only;
uint8_t multi_acks;
float bw;
uint8_t flood_max;
uint8_t interference_threshold;
uint8_t agc_reset_interval; // secs / 4
// Bridge settings
uint8_t bridge_enabled; // boolean
uint16_t bridge_delay; // milliseconds (default 500 ms)
uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx)
uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200)
uint8_t bridge_channel; // 1-14 (ESP-NOW only)
char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only)
// Power setting
uint8_t powersaving_enabled; // boolean
// Gps settings
uint8_t gps_enabled;
uint32_t gps_interval; // in seconds
uint8_t advert_loc_policy;
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
uint8_t rx_boosted_gain; // power settings
uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect;
};
class CommonCLICallbacks {
public:
virtual void savePrefs() = 0;
virtual const char* getFirmwareVer() = 0;
virtual const char* getBuildDate() = 0;
virtual const char* getRole() = 0;
virtual bool formatFileSystem() = 0;
virtual void sendSelfAdvertisement(int delay_millis, bool flood) = 0;
virtual void updateAdvertTimer() = 0;
virtual void updateFloodAdvertTimer() = 0;
virtual void setLoggingOn(bool enable) = 0;
virtual void eraseLogFile() = 0;
virtual void dumpLogFile() = 0;
virtual void setTxPower(int8_t power_dbm) = 0;
virtual void formatNeighborsReply(char *reply) = 0;
virtual void removeNeighbor(const uint8_t* pubkey, int key_len) {
// no op by default
};
virtual void formatStatsReply(char *reply) = 0;
virtual void formatRadioStatsReply(char *reply) = 0;
virtual void formatPacketStatsReply(char *reply) = 0;
virtual mesh::LocalIdentity& getSelfId() = 0;
virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0;
virtual void clearStats() = 0;
virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0;
virtual void startRegionsLoad() {
// no op by default
}
virtual bool saveRegions() {
return false;
}
virtual void onDefaultRegionChanged(const RegionEntry* r) {
// no op by default
}
virtual void setBridgeState(bool enable) {
// no op by default
};
virtual void restartBridge() {
// no op by default
};
virtual void setRxBoostedGain(bool enable) {
// no op by default
};
};
class CommonCLI {
mesh::RTCClock* _rtc;
NodePrefs* _prefs;
CommonCLICallbacks* _callbacks;
mesh::MainBoard* _board;
SensorManager* _sensors;
RegionMap* _region_map;
ClientACL* _acl;
char tmp[PRV_KEY_SIZE*2 + 4];
mesh::RTCClock* getRTCClock() { return _rtc; }
void savePrefs();
void loadPrefsInt(FILESYSTEM* _fs, const char* filename);
void handleRegionCmd(char* command, char* reply);
void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply);
void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply);
public:
CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks)
: _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(&region_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
void loadPrefs(FILESYSTEM* _fs);
void savePrefs(FILESYSTEM* _fs);
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
};

31
src/helpers/ContactInfo.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include <Arduino.h>
#include <Mesh.h>
#define OUT_PATH_UNKNOWN 0xFF
struct ContactInfo {
mesh::Identity id;
char name[32];
uint8_t type; // on of ADV_TYPE_*
uint8_t flags;
uint8_t out_path_len;
mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated
uint8_t out_path[MAX_PATH_SIZE];
uint32_t last_advert_timestamp; // by THEIR clock
uint32_t lastmod; // by OUR clock
int32_t gps_lat, gps_lon; // 6 dec places
uint32_t sync_since;
const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const {
if (!shared_secret_valid) {
self_id.calcSharedSecret(shared_secret, id.pub_key);
shared_secret_valid = true;
}
return shared_secret;
}
private:
mutable uint8_t shared_secret[PUB_KEY_SIZE];
};

View File

@@ -0,0 +1,47 @@
#ifdef ESP_PLATFORM
#include "ESP32Board.h"
#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <AsyncElegantOTA.h>
#include <SPIFFS.h>
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
inhibit_sleep = true; // prevent sleep during OTA
WiFi.softAP("MeshCore-OTA", NULL);
sprintf(reply, "Started: http://%s/update", WiFi.softAPIP().toString().c_str());
MESH_DEBUG_PRINTLN("startOTAUpdate: %s", reply);
static char id_buf[60];
sprintf(id_buf, "%s (%s)", id, getManufacturerName());
static char home_buf[90];
sprintf(home_buf, "<H2>Hi! I am a MeshCore Repeater. ID: %s</H2>", id);
AsyncWebServer* server = new AsyncWebServer(80);
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/html", home_buf);
});
server->on("/log", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(SPIFFS, "/packet_log", "text/plain");
});
AsyncElegantOTA.setID(id_buf);
AsyncElegantOTA.begin(server); // Start ElegantOTA
server->begin();
return true;
}
#else
bool ESP32Board::startOTAUpdate(const char* id, char reply[]) {
return false; // not supported
}
#endif
#endif

161
src/helpers/ESP32Board.h Normal file
View File

@@ -0,0 +1,161 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
#if defined(ESP_PLATFORM)
#include <rom/rtc.h>
#include <sys/time.h>
#include <Wire.h>
#include "driver/rtc_io.h"
class ESP32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
bool inhibit_sleep = false;
public:
void begin() {
// for future use, sub-classes SHOULD call this from their begin()
startup_reason = BD_STARTUP_NORMAL;
#ifdef ESP32_CPU_FREQ
setCpuFrequencyMhz(ESP32_CPU_FREQ);
#endif
#ifdef PIN_VBAT_READ
// battery read support
pinMode(PIN_VBAT_READ, INPUT);
adcAttachPin(PIN_VBAT_READ);
#endif
#ifdef P_LORA_TX_LED
pinMode(P_LORA_TX_LED, OUTPUT);
digitalWrite(P_LORA_TX_LED, LOW);
#endif
#if defined(PIN_BOARD_SDA) && defined(PIN_BOARD_SCL)
#if PIN_BOARD_SDA >= 0 && PIN_BOARD_SCL >= 0
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL);
#endif
#else
Wire.begin();
#endif
}
// Temperature from ESP32 MCU
float getMCUTemperature() override {
uint32_t raw = 0;
// To get and average the temperature so it is more accurate, especially in low temperature
for (int i = 0; i < 4; i++) {
raw += temperatureRead();
}
return raw / 4;
}
void enterLightSleep(uint32_t secs) {
#if defined(CONFIG_IDF_TARGET_ESP32S3) && defined(P_LORA_DIO_1) // Supported ESP32 variants
if (rtc_gpio_is_valid_gpio((gpio_num_t)P_LORA_DIO_1)) { // Only enter sleep mode if P_LORA_DIO_1 is RTC pin
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
esp_sleep_enable_ext1_wakeup((1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // To wake up when receiving a LoRa packet
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000); // To wake up every hour to do periodically jobs
}
esp_light_sleep_start(); // CPU enters light sleep
}
#endif
}
void sleep(uint32_t secs) override {
if (!inhibit_sleep) {
enterLightSleep(secs); // To wake up after "secs" seconds or when receiving a LoRa packet
}
}
uint8_t getStartupReason() const override { return startup_reason; }
#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off
}
#elif defined(P_LORA_TX_NEOPIXEL_LED)
#define NEOPIXEL_BRIGHTNESS 64 // white brightness (max 255)
void onBeforeTransmit() override {
neopixelWrite(P_LORA_TX_NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White)
}
void onAfterTransmit() override {
neopixelWrite(P_LORA_TX_NEOPIXEL_LED, 0, 0, 0); // turn TX neopixel off
}
#endif
uint16_t getBattMilliVolts() override {
#ifdef PIN_VBAT_READ
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 4; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 4;
return (2 * raw);
#else
return 0; // not supported
#endif
}
const char* getManufacturerName() const override {
return "Generic ESP32";
}
void reboot() override {
esp_restart();
}
bool startOTAUpdate(const char* id, char reply[]) override;
void setInhibitSleep(bool inhibit) {
inhibit_sleep = inhibit;
}
};
class ESP32RTCClock : public mesh::RTCClock {
public:
ESP32RTCClock() { }
void begin() {
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_POWERON) {
// start with some date/time in the recent past
struct timeval tv;
tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
}
uint32_t getCurrentTime() override {
time_t _now;
time(&_now);
return _now;
}
void setCurrentTime(uint32_t time) override {
struct timeval tv;
tv.tv_sec = time;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
};
#endif

View File

@@ -0,0 +1,93 @@
#include "IdentityStore.h"
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
bool loaded = false;
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
if (_fs->exists(filename)) {
#if defined(RP2040_PLATFORM)
File file = _fs->open(filename, "r");
#else
File file = _fs->open(filename);
#endif
if (file) {
loaded = id.readFrom(file);
file.close();
}
}
return loaded;
}
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz) {
bool loaded = false;
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
if (_fs->exists(filename)) {
#if defined(RP2040_PLATFORM)
File file = _fs->open(filename, "r");
#else
File file = _fs->open(filename);
#endif
if (file) {
loaded = id.readFrom(file);
int n = max_name_sz; // up to 32 bytes
if (n > 32) n = 32;
file.read((uint8_t *) display_name, n);
display_name[n - 1] = 0; // ensure null terminator
file.close();
}
}
return loaded;
}
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open(filename, "w");
#else
File file = _fs->open(filename, "w", true);
#endif
if (file) {
bool success = id.writeTo(file);
file.close();
MESH_DEBUG_PRINTLN("IdentityStore::save() write - %s", success ? "OK" : "Err");
return true;
}
MESH_DEBUG_PRINTLN("IdentityStore::save() failed");
return false;
}
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char display_name[]) {
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open(filename, "w");
#else
File file = _fs->open(filename, "w", true);
#endif
if (file) {
id.writeTo(file);
uint8_t tmp[32];
memset(tmp, 0, sizeof(tmp));
int n = strlen(display_name);
if (n > sizeof(tmp)-1) n = sizeof(tmp)-1;
memcpy(tmp, display_name, n);
file.write(tmp, sizeof(tmp));
file.close();
return true;
}
return false;
}

View File

@@ -0,0 +1,26 @@
#pragma once
#if defined(ESP32) || defined(RP2040_PLATFORM)
#include <FS.h>
#define FILESYSTEM fs::FS
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <Adafruit_LittleFS.h>
#define FILESYSTEM Adafruit_LittleFS
using namespace Adafruit_LittleFS_Namespace;
#endif
#include <Identity.h>
class IdentityStore {
FILESYSTEM* _fs;
const char* _dir;
public:
IdentityStore(FILESYSTEM& fs, const char* dir): _fs(&fs), _dir(dir) { }
void begin() {
if (_dir && _dir[0] == '/') { _fs->mkdir(_dir); } }
bool load(const char *name, mesh::LocalIdentity& id);
bool load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz);
bool save(const char *name, const mesh::LocalIdentity& id);
bool save(const char *name, const mesh::LocalIdentity& id, const char display_name[]);
};

View File

@@ -0,0 +1,81 @@
#pragma once
#include <Arduino.h>
// LoRa radio module pins for Meshadventurer
#define P_LORA_DIO_1 33
#define P_LORA_NSS 18
#define P_LORA_RESET 23
#define P_LORA_BUSY 32
#define P_LORA_SCLK 5
#define P_LORA_MISO 19
#define P_LORA_MOSI 27
#define PIN_VBAT_READ 35
#include "ESP32Board.h"
#include <driver/rtc_io.h>
class MeshadventurerBoard : public ESP32Board {
public:
void begin() {
ESP32Board::begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are held on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
void powerOff() override {
// TODO: re-enable this when there is a definite wake-up source pin:
// enterDeepSleep(0);
}
uint16_t getBattMilliVolts() override {
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 4; i++) {
raw += analogReadMilliVolts(PIN_VBAT_READ);
}
raw = raw / 4;
return (2 * raw);
}
const char* getManufacturerName() const override {
return "Meshadventurer";
}
};

366
src/helpers/NRF52Board.cpp Normal file
View File

@@ -0,0 +1,366 @@
#if defined(NRF52_PLATFORM)
#include "NRF52Board.h"
#include <bluefruit.h>
#include <nrf_soc.h>
static BLEDfu bledfu;
static void connect_callback(uint16_t conn_handle) {
(void)conn_handle;
MESH_DEBUG_PRINTLN("BLE client connected");
}
static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
(void)conn_handle;
(void)reason;
MESH_DEBUG_PRINTLN("BLE client disconnected");
}
void NRF52Board::begin() {
startup_reason = BD_STARTUP_NORMAL;
}
#ifdef NRF52_POWER_MANAGEMENT
#include "nrf.h"
// Power Management global variables
uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason
uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason
// Early constructor - runs before SystemInit() clears the registers
// Priority 101 ensures this runs before SystemInit (102) and before
// any C++ static constructors (default 65535)
static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
g_nrf52_reset_reason = NRF_POWER->RESETREAS;
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
}
void NRF52Board::initPowerMgr() {
// Copy early-captured register values
reset_reason = g_nrf52_reset_reason;
shutdown_reason = g_nrf52_shutdown_reason;
boot_voltage_mv = 0; // Will be set by checkBootVoltage()
// Clear registers for next boot
// Note: At this point SoftDevice may or may not be enabled
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_reset_reason_clr(0xFFFFFFFF);
sd_power_gpregret_clr(1, 0xFF);
} else {
NRF_POWER->RESETREAS = 0xFFFFFFFF; // Write 1s to clear
NRF_POWER->GPREGRET2 = 0;
}
// Log reset/shutdown info
if (shutdown_reason != SHUTDOWN_REASON_NONE) {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX); Shutdown = %s (0x%02X)",
getResetReasonString(reset_reason), (unsigned long)reset_reason,
getShutdownReasonString(shutdown_reason), shutdown_reason);
} else {
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
getResetReasonString(reset_reason), (unsigned long)reset_reason);
}
}
bool NRF52Board::isExternalPowered() {
// Check if SoftDevice is enabled before using its API
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
uint32_t usb_status;
sd_power_usbregstatus_get(&usb_status);
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
} else {
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
}
}
const char* NRF52Board::getResetReasonString(uint32_t reason) {
if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin";
if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog";
if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset";
if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup";
#ifdef POWER_RESETREAS_LPCOMP_Msk
if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP";
#endif
#ifdef POWER_RESETREAS_VBUS_Msk
if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS";
#endif
#ifdef POWER_RESETREAS_OFF_Msk
if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO";
#endif
#ifdef POWER_RESETREAS_DIF_Msk
if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface";
#endif
return "Cold Boot";
}
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
switch (reason) {
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
case SHUTDOWN_REASON_USER: return "User Request";
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
}
return "Unknown";
}
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
initPowerMgr();
// Read boot voltage
boot_voltage_mv = getBattMilliVolts();
if (config->voltage_bootlock == 0) return true; // Protection disabled
// Skip check if externally powered
if (isExternalPowered()) {
MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)");
boot_voltage_mv = getBattMilliVolts();
return true;
}
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
boot_voltage_mv, config->voltage_bootlock);
// Only trigger shutdown if reading is valid (>1000mV) AND below threshold
// This prevents spurious shutdowns on ADC glitches or uninitialized reads
if (boot_voltage_mv > 1000 && boot_voltage_mv < config->voltage_bootlock) {
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown");
initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
return false; // Should never reach this
}
return true;
}
void NRF52Board::initiateShutdown(uint8_t reason) {
enterSystemOff(reason);
}
void NRF52Board::enterSystemOff(uint8_t reason) {
MESH_DEBUG_PRINTLN("PWRMGT: Entering SYSTEMOFF (%s)", getShutdownReasonString(reason));
// Record shutdown reason in GPREGRET2
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_gpregret_clr(1, 0xFF);
sd_power_gpregret_set(1, reason);
} else {
NRF_POWER->GPREGRET2 = reason;
}
// Flush serial buffers
Serial.flush();
delay(100);
// Enter SYSTEMOFF
if (sd_enabled) {
uint32_t err = sd_power_system_off();
if (err == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) { //SoftDevice not enabled
sd_enabled = 0;
}
}
if (!sd_enabled) {
// SoftDevice not available; write directly to POWER->SYSTEMOFF
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
}
// If we get here, something went wrong. Reset to recover.
NVIC_SystemReset();
}
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
// LPCOMP is not managed by SoftDevice - direct register access required
// Halt and disable before reconfiguration
NRF_LPCOMP->TASKS_STOP = 1;
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
// Select analog input (AIN0-7 maps to PSEL 0-7)
NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
// Detect UP events (voltage rises above threshold for battery recovery)
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
// Enable 50mV hysteresis for noise immunity
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
// Clear stale events/interrupts before enabling wake
NRF_LPCOMP->EVENTS_READY = 0;
NRF_LPCOMP->EVENTS_DOWN = 0;
NRF_LPCOMP->EVENTS_UP = 0;
NRF_LPCOMP->EVENTS_CROSS = 0;
NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
// Enable LPCOMP
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
NRF_LPCOMP->TASKS_START = 1;
// Wait for comparator to settle before entering SYSTEMOFF
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
delayMicroseconds(50);
}
if (refsel == 7) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
} else if (refsel <= 6) {
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/8 VDD)",
ain_channel, refsel + 1);
} else {
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=%d/16 VDD)",
ain_channel, ref_num);
}
// Configure VBUS (USB power) wake alongside LPCOMP
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_usbdetected_enable(1);
} else {
NRF_POWER->EVENTS_USBDETECTED = 0;
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
}
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake configured");
}
#endif
void NRF52BoardDCDC::begin() {
NRF52Board::begin();
// Enable DC/DC converter for improved power efficiency
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
} else {
NRF_POWER->DCDCEN = 1;
}
}
void NRF52Board::sleep(uint32_t secs) {
// Clear FPU interrupt flags to avoid insomnia
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
#if (__FPU_USED == 1)
__set_FPSCR(__get_FPSCR() & ~(0x0000009F));
(void) __get_FPSCR();
NVIC_ClearPendingIRQ(FPU_IRQn);
#endif
// On nRF52, we use event-driven sleep instead of timed sleep
// The 'secs' parameter is ignored - we wake on any interrupt
uint8_t sd_enabled = 0;
sd_softdevice_is_enabled(&sd_enabled);
if (sd_enabled) {
// first call processes pending softdevice events, second call sleeps.
sd_app_evt_wait();
sd_app_evt_wait();
} else {
// softdevice is disabled, use raw WFE
__SEV();
__WFE();
__WFE();
}
}
// Temperature from NRF52 MCU
float NRF52Board::getMCUTemperature() {
NRF_TEMP->TASKS_START = 1; // Start temperature measurement
long startTime = millis();
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us
if(millis() - startTime > 5) { // To wait 5ms just in case
NRF_TEMP->TASKS_STOP = 1;
return NAN;
}
}
NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag
int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units
NRF_TEMP->TASKS_STOP = 1;
return temp * 0.25f; // Convert to *C
}
bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
static const char BOOTLOADER_MARKER[] = "UF2 Bootloader ";
const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B
for (uint32_t i = 0; i < 0x3000 - (sizeof(BOOTLOADER_MARKER) - 1); i++) {
if (memcmp(&flash[i], BOOTLOADER_MARKER, sizeof(BOOTLOADER_MARKER) - 1) == 0) {
const char* ver = (const char*)&flash[i + sizeof(BOOTLOADER_MARKER) - 1];
size_t len = 0;
while (len < max_len - 1 && ver[len] != '\0' && ver[len] != ' ' && ver[len] != '\n' && ver[len] != '\r') {
out[len] = ver[len];
len++;
}
out[len] = '\0';
return len > 0; // bootloader string is non-empty
}
}
return false;
}
bool NRF52Board::startOTAUpdate(const char *id, char reply[]) {
// Config the peripheral connection with maximum bandwidth
// more SRAM required by SoftDevice
// Note: All config***() function must be called before begin()
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16);
Bluefruit.begin(1, 0);
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
Bluefruit.setTxPower(4);
// Set the BLE device name
Bluefruit.setName(ota_name);
Bluefruit.Periph.setConnectCallback(connect_callback);
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
// To be consistent OTA DFU should be added first if it exists
bledfu.begin();
// Set up and start advertising
// Advertising packet
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addName();
/* Start Advertising
- Enable auto advertising if disconnected
- Interval: fast mode = 20 ms, slow mode = 152.5 ms
- Timeout for fast mode is 30 seconds
- Start(timeout) with timeout = 0 will advertise forever (until connected)
For recommended advertising interval
https://developer.apple.com/library/content/qa/qa1931/_index.html
*/
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
uint8_t mac_addr[6];
memset(mac_addr, 0, sizeof(mac_addr));
Bluefruit.getAddr(mac_addr);
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", mac_addr[5], mac_addr[4], mac_addr[3],
mac_addr[2], mac_addr[1], mac_addr[0]);
return true;
}
#endif

79
src/helpers/NRF52Board.h Normal file
View File

@@ -0,0 +1,79 @@
#pragma once
#include <Arduino.h>
#include <MeshCore.h>
#if defined(NRF52_PLATFORM)
#ifdef NRF52_POWER_MANAGEMENT
// Shutdown Reason Codes (stored in GPREGRET before SYSTEMOFF)
#define SHUTDOWN_REASON_NONE 0x00
#define SHUTDOWN_REASON_LOW_VOLTAGE 0x4C // 'L' - Runtime low voltage threshold
#define SHUTDOWN_REASON_USER 0x55 // 'U' - User requested powerOff()
#define SHUTDOWN_REASON_BOOT_PROTECT 0x42 // 'B' - Boot voltage protection
// Boards provide this struct with their hardware-specific settings and callbacks.
struct PowerMgtConfig {
// LPCOMP wake configuration (for voltage recovery from SYSTEMOFF)
uint8_t lpcomp_ain_channel; // AIN0-7 for voltage sensing pin
uint8_t lpcomp_refsel; // REFSEL value: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16
// Boot protection voltage threshold (millivolts)
// Set to 0 to disable boot protection
uint16_t voltage_bootlock;
};
#endif
class NRF52Board : public mesh::MainBoard {
#ifdef NRF52_POWER_MANAGEMENT
void initPowerMgr();
#endif
protected:
uint8_t startup_reason;
char *ota_name;
#ifdef NRF52_POWER_MANAGEMENT
uint32_t reset_reason; // RESETREAS register value
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
bool checkBootVoltage(const PowerMgtConfig* config);
void enterSystemOff(uint8_t reason);
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
virtual void initiateShutdown(uint8_t reason);
#endif
public:
NRF52Board(char *otaname) : ota_name(otaname) {}
virtual void begin();
virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
virtual bool getBootloaderVersion(char* version, size_t max_len) override;
virtual bool startOTAUpdate(const char *id, char reply[]) override;
virtual void sleep(uint32_t secs) override;
#ifdef NRF52_POWER_MANAGEMENT
bool isExternalPowered() override;
uint16_t getBootVoltage() override { return boot_voltage_mv; }
virtual uint32_t getResetReason() const override { return reset_reason; }
uint8_t getShutdownReason() const override { return shutdown_reason; }
const char* getResetReasonString(uint32_t reason) override;
const char* getShutdownReasonString(uint8_t reason) override;
#endif
};
/*
* The NRF52 has an internal DC/DC regulator that allows increased efficiency
* compared to the LDO regulator. For being able to use it, the module/board
* needs to have the required inductors and and capacitors populated. If the
* hardware requirements are met, this subclass can be used to enable the DC/DC
* regulator.
*/
class NRF52BoardDCDC : virtual public NRF52Board {
public:
NRF52BoardDCDC() {}
virtual void begin() override;
};
#endif

View File

@@ -0,0 +1,197 @@
#include "RTC_RX8130CE.h"
#include "RTClib.h"
bool RTC_RX8130CE::stop(bool stop) {
write_register(0x1E, stop ? 0x040 : 0x00);
return true;
}
bool RTC_RX8130CE::begin(TwoWire *wire) {
if (i2c_dev) {
delete i2c_dev;
}
i2c_dev = new Adafruit_I2CDevice(this->_addr, wire);
if (!i2c_dev->begin()) {
return false;
}
/*
* Digital offset register:
* [7] DET: 0 -> disabled
* [6:0] L7-L1: 0 -> no offset
*/
write_register(0x30, 0x00);
/*
* Extension Register register:
* [7:6] FSEL: 0 -> 0
* [5] USEL: 0 -> 0
* [4] TE: 0 ->
* [3] WADA: 0 -> 0
* [2-0] TSEL: 0 -> 0
*/
write_register(0x1C, 0x00);
/*
* Flag Register register:
* [7] VBLF: 0 -> 0
* [6] 0: 0 ->
* [5] UF: 0 ->
* [4] TF: 0 ->
* [3] AF: 0 -> 0
* [2] RSF: 0 -> 0
* [1] VLF: 0 -> 0
* [0] VBFF: 0 -> 0
*/
write_register(0x1D, 0x00);
/*
* Control Register0 register:
* [7] TEST: 0 -> 0
* [6] STOP: 0 ->
* [5] UIE: 0 ->
* [4] TIE: 0 ->
* [3] AIE: 0 -> 0
* [2] TSTP: 0 -> 0
* [1] TBKON: 0 -> 0
* [0] TBKE: 0 -> 0
*/
write_register(0x1E, 0x00);
/*
* Control Register1 register:
* [7-6] SMPTSEL: 0 -> 0
* [5] CHGEN: 0 ->
* [4] INIEN: 0 ->
* [3] 0: 0 ->
* [2] RSVSEL: 0 -> 0
* [1-0] BFVSEL: 0 -> 0
*/
write_register(0x1F, 0x00);
this->stop(false); // clear STOP bit
/*
* Function register:
* [7] 100TH: 0 -> disabled
* [6:5] Periodic interrupt: 0 -> no periodic interrupt
* [4] RTCM: 0 -> real-time clock mode
* [3] STOPM: 0 -> RTC stop is controlled by STOP bit only
* [2:0] Clock output frequency: 000 (Default value)
*/
write_register(0x28, 0x00);
// Battery switch register
write_register(0x26, 0x00); // enable battery switch feature
return true;
}
bool RTC_RX8130CE::setTime(struct tm *t) {
uint8_t buf[8];
buf[0] = 0x10;
buf[1] = bin2bcd(t->tm_sec) & 0x7F;
buf[2] = bin2bcd(t->tm_min) & 0x7F;
buf[3] = bin2bcd(t->tm_hour) & 0x3F;
buf[4] = bin2bcd(t->tm_wday) & 0x07;
buf[5] = bin2bcd(t->tm_mday) & 0x3F;
buf[6] = bin2bcd(t->tm_mon + 1) & 0x1F;
buf[7] = bin2bcd((t->tm_year - 100));
this->stop(true);
i2c_dev->write(buf, sizeof(buf));
this->stop(false);
return true;
}
void RTC_RX8130CE::adjust(DateTime dt) {
struct tm *atv;
time_t utime;
utime = (time_t)dt.unixtime();
atv = gmtime(&utime);
this->setTime(atv);
}
DateTime RTC_RX8130CE::now() {
struct tm atv;
this->getTime(&atv);
return DateTime((uint32_t)mktime(&atv));
}
uint32_t RTC_RX8130CE::unixtime() {
struct tm atv;
this->getTime(&atv);
return (uint32_t)mktime(&atv);
}
bool RTC_RX8130CE::getTime(struct tm *t) {
uint8_t buff[7];
buff[0] = 0x10;
i2c_dev->write_then_read(buff, 1, buff, 7);
t->tm_sec = bcd2bin(buff[0] & 0x7F);
t->tm_min = bcd2bin(buff[1] & 0x7F);
t->tm_hour = bcd2bin(buff[2] & 0x3F);
t->tm_wday = bcd2bin(buff[3] & 0x07);
t->tm_mday = bcd2bin(buff[4] & 0x3F);
t->tm_mon = bcd2bin(buff[5] & 0x1F) - 1;
t->tm_year = bcd2bin(buff[6]) + 100;
return true;
}
bool RTC_RX8130CE::writeRAM(uint8_t address, uint8_t value) {
return this->writeRAM(address, &value, 1);
}
size_t RTC_RX8130CE::writeRAM(uint8_t address, uint8_t *value, size_t len) {
uint8_t buf[len + 1];
if (address > 3) {
return 0;
}
if ((address + len) > 3) {
len = 3 - address;
}
buf[0] = 0x20 + address;
for (int i = 1; i <= len + 1; i++) {
buf[i] = value[i - 1];
}
i2c_dev->write(buf, len + 1);
return len;
}
bool RTC_RX8130CE::readRAM(uint8_t address, uint8_t *value, size_t len) {
uint8_t real_address = 0x20 + address;
if (address > 3) { // Oversize of 64-bytes RAM
return false;
}
if ((address + len) > 3) { // Data size over RAM size
len = 3 - address;
}
i2c_dev->write_then_read(&real_address, 1, value, len);
return true;
}
uint8_t RTC_RX8130CE::readRAM(uint8_t address) {
uint8_t value = 0xFF;
this->readRAM(address, &value, 1);
return value;
}

View File

@@ -0,0 +1,33 @@
#ifndef __RTC_RX8130CE_H__
#define __RTC_RX8130CE_H__
#include <Arduino.h>
#include <Wire.h>
#include <time.h>
#include "RTClib.h"
class RTC_RX8130CE : RTC_I2C {
private:
const uint8_t _addr = 0x32;
bool stop(bool stop);
protected:
public:
bool begin(TwoWire *wire);
bool setTime(struct tm *t);
bool getTime(struct tm *t);
void adjust(DateTime t);
DateTime now();
uint32_t unixtime();
bool writeRAM(uint8_t address, uint8_t value);
size_t writeRAM(uint8_t address, uint8_t *value, size_t len);
bool readRAM(uint8_t address, uint8_t *value, size_t len);
uint8_t readRAM(uint8_t address);
};
#endif

View File

@@ -0,0 +1,32 @@
#pragma once
#include <Arduino.h>
class RefCountedDigitalPin {
uint8_t _pin;
int8_t _claims = 0;
uint8_t _active = 0;
public:
RefCountedDigitalPin(uint8_t pin,uint8_t active=HIGH): _pin(pin), _active(active) { }
void begin() {
pinMode(_pin, OUTPUT);
digitalWrite(_pin, !_active); // initial state
}
void claim() {
_claims++;
if (_claims > 0) {
digitalWrite(_pin, _active);
}
}
void release() {
if (_claims == 0) return; // avoid negative _claims
_claims--;
if (_claims == 0) {
digitalWrite(_pin, !_active);
}
}
};

346
src/helpers/RegionMap.cpp Normal file
View File

@@ -0,0 +1,346 @@
#include "RegionMap.h"
#include <helpers/TxtDataHelpers.h>
#include <SHA256.h>
// helper class for region map exporter, we emulate Stream with a safe buffer writer.
class BufStream : public Stream {
public:
BufStream(char *buf, size_t max_len)
: _buf(buf), _max_len(max_len), _pos(0) {
if (_max_len > 0) _buf[0] = 0;
}
size_t write(uint8_t c) override {
if (_pos + 1 >= _max_len) return 0;
_buf[_pos++] = c;
_buf[_pos] = 0;
return 1;
}
size_t write(const uint8_t *buffer, size_t size) override {
size_t written = 0;
while (written < size) {
if (!write(buffer[written])) break;
written++;
}
return written;
}
int available() override { return 0; }
int read() override { return -1; }
int peek() override { return -1; }
void flush() override {}
size_t length() const { return _pos; }
private:
char *_buf;
size_t _max_len;
size_t _pos;
};
RegionMap::RegionMap(TransportKeyStore& store) : _store(&store) {
next_id = 1; num_regions = 0;
default_id = home_id = 0;
wildcard.id = wildcard.parent = 0;
wildcard.flags = 0; // default behaviour, allow flood and direct
strcpy(wildcard.name, "*");
}
bool RegionMap::is_name_char(uint8_t c) {
// accept all alpha-num or accented characters, but exclude most punctuation chars
return c == '-' || c == '$' || c == '#' || (c >= '0' && c <= '9') || c >= 'A';
}
static const char* skip_hash(const char* name) {
return *name == '#' ? name + 1 : name;
}
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
_fs->remove(filename);
return _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return _fs->open(filename, "w");
#else
return _fs->open(filename, "w", true);
#endif
}
bool RegionMap::load(FILESYSTEM* _fs, const char* path) {
if (_fs->exists(path ? path : "/regions2")) {
#if defined(RP2040_PLATFORM)
File file = _fs->open(path ? path : "/regions2", "r");
#else
File file = _fs->open(path ? path : "/regions2");
#endif
if (file) {
uint8_t pad[128];
num_regions = 0; next_id = 1;
default_id = home_id = 0;
bool success = file.read(pad, 3) == 3; // reserved header
success = success && file.read((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && file.read((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.read((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.read((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
while (num_regions < MAX_REGION_ENTRIES) {
auto r = &regions[num_regions];
success = file.read((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && file.read((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && file.read((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
success = success && file.read((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && file.read(pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // EOF
if (r->id >= next_id) { // make sure next_id is valid
next_id = r->id + 1;
}
num_regions++;
}
}
file.close();
return true;
}
}
return false; // failed
}
bool RegionMap::save(FILESYSTEM* _fs, const char* path) {
File file = openWrite(_fs, path ? path : "/regions2");
if (file) {
uint8_t pad[128];
memset(pad, 0, sizeof(pad));
bool success = file.write(pad, 3) == 3; // reserved header
success = success && file.write((uint8_t *) &default_id, sizeof(default_id)) == sizeof(default_id);
success = success && file.write((uint8_t *) &home_id, sizeof(home_id)) == sizeof(home_id);
success = success && file.write((uint8_t *) &wildcard.flags, sizeof(wildcard.flags)) == sizeof(wildcard.flags);
success = success && file.write((uint8_t *) &next_id, sizeof(next_id)) == sizeof(next_id);
if (success) {
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
success = file.write((uint8_t *) &r->id, sizeof(r->id)) == sizeof(r->id);
success = success && file.write((uint8_t *) &r->parent, sizeof(r->parent)) == sizeof(r->parent);
success = success && file.write((uint8_t *) r->name, sizeof(r->name)) == sizeof(r->name);
success = success && file.write((uint8_t *) &r->flags, sizeof(r->flags)) == sizeof(r->flags);
success = success && file.write(pad, sizeof(pad)) == sizeof(pad);
if (!success) break; // write failed
}
}
file.close();
return true;
}
return false; // failed
}
RegionEntry* RegionMap::putRegion(const char* name, uint16_t parent_id, uint16_t id) {
const char* sp = name; // check for illegal name chars
while (*sp) {
if (!is_name_char(*sp)) return NULL; // error
sp++;
}
auto region = findByName(name);
if (region) {
if (region->id == parent_id) return NULL; // ERROR: invalid parent!
region->parent = parent_id; // re-parent / move this region in the hierarchy
} else {
if (id == 0 && num_regions >= MAX_REGION_ENTRIES) return NULL; // full!
region = &regions[num_regions++]; // alloc new RegionEntry
region->flags = REGION_DENY_FLOOD; // DENY by default
region->id = id == 0 ? next_id++ : id;
StrHelper::strncpy(region->name, name, sizeof(region->name));
region->parent = parent_id;
}
return region;
}
int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num) {
int num;
if (src.name[0] == '$') { // private region
num = _store->loadKeysFor(src.id, dest, max_num);
} else if (src.name[0] == '#') { // auto hashtag region
_store->getAutoKeyFor(src.id, src.name, dest[0]);
num = 1;
} else { // new: implicit auto hashtag region
char tmp[sizeof(src.name)+1];
tmp[0] = '#';
strcpy(&tmp[1], src.name);
_store->getAutoKeyFor(src.id, tmp, dest[0]);
num = 1;
}
return num;
}
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
TransportKey keys[4];
int num = getTransportKeysFor(*region, keys, 4);
for (int j = 0; j < num; j++) {
uint16_t code = keys[j].calcTransportCode(packet);
if (packet->transport_codes[0] == code) { // a match!!
return region;
}
}
}
}
return NULL; // no matches
}
RegionEntry* RegionMap::findByName(const char* name) {
if (strcmp(name, "*") == 0) return &wildcard;
if (*name == '#') { name++; } // ignore the '#' when matching by name
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(name, skip_hash(region->name)) == 0) return region;
}
return NULL; // not found
}
RegionEntry* RegionMap::findByNamePrefix(const char* prefix) {
if (strcmp(prefix, "*") == 0) return &wildcard;
if (*prefix == '#') { prefix++; } // ignore the '#' when matching by name
RegionEntry* partial = NULL;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (strcmp(prefix, skip_hash(region->name)) == 0) return region; // is a complete match, preference this one
if (memcmp(prefix, skip_hash(region->name), strlen(prefix)) == 0) {
partial = region;
}
}
return partial;
}
RegionEntry* RegionMap::findById(uint16_t id) {
if (id == 0) return &wildcard; // special root Region
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if (region->id == id) return region;
}
return NULL; // not found
}
RegionEntry* RegionMap::getHomeRegion() {
return findById(home_id);
}
void RegionMap::setHomeRegion(const RegionEntry* home) {
home_id = home ? home->id : 0;
}
RegionEntry* RegionMap::getDefaultRegion() {
return default_id == 0 ? NULL : findById(default_id);
}
void RegionMap::setDefaultRegion(const RegionEntry* def) {
default_id = def ? def->id : 0;
}
bool RegionMap::removeRegion(const RegionEntry& region) {
if (region.id == 0) return false; // failed (cannot remove the wildcard Region)
int i; // first check region has no child regions
for (i = 0; i < num_regions; i++) {
if (regions[i].parent == region.id) return false; // failed (must remove child Regions first)
}
i = 0;
while (i < num_regions) {
if (region.id == regions[i].id) break;
i++;
}
if (i >= num_regions) return false; // failed (not found)
num_regions--; // remove from regions array
while (i < num_regions) {
regions[i] = regions[i + 1];
i++;
}
return true; // success
}
bool RegionMap::clear() {
num_regions = 0;
return true; // success
}
void RegionMap::printChildRegions(int indent, const RegionEntry* parent, Stream& out) const {
for (int i = 0; i < indent; i++) {
out.print(' ');
}
if (parent->flags & REGION_DENY_FLOOD) {
out.printf("%s%s\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
} else {
out.printf("%s%s F\n", skip_hash(parent->name), parent->id == home_id ? "^" : "");
}
for (int i = 0; i < num_regions; i++) {
auto r = &regions[i];
if (r->parent == parent->id) {
printChildRegions(indent + 1, r, out);
}
}
}
void RegionMap::exportTo(Stream& out) const {
printChildRegions(0, &wildcard, out); // recursive
}
size_t RegionMap::exportTo(char *dest, size_t max_len) const {
if (!dest || max_len == 0) return 0;
BufStream bs(dest, max_len);
exportTo(bs); // ← reuse existing logic
return bs.length();
}
int RegionMap::exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert) {
char *dp = dest;
// Check wildcard region
bool wildcard_matches = invert ? (wildcard.flags & mask) : !(wildcard.flags & mask);
if (wildcard_matches) {
*dp++ = '*';
*dp++ = ',';
}
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
// Check if region matches the filter criteria
bool region_matches = invert ? (region->flags & mask) : !(region->flags & mask);
if (region_matches) {
int len = strlen(skip_hash(region->name));
if ((dp - dest) + len + 2 < max_len) { // only append if name will fit
memcpy(dp, skip_hash(region->name), len);
dp += len;
*dp++ = ',';
}
}
}
if (dp > dest) { dp--; } // don't include trailing comma
*dp = 0; // set null terminator
return dp - dest; // return length
}

62
src/helpers/RegionMap.h Normal file
View File

@@ -0,0 +1,62 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Packet.h>
#include "TransportKeyStore.h"
#ifndef MAX_REGION_ENTRIES
#define MAX_REGION_ENTRIES 32
#endif
#define REGION_DENY_FLOOD 0x01
#define REGION_DENY_DIRECT 0x02 // reserved for future
struct RegionEntry {
uint16_t id;
uint16_t parent;
uint8_t flags;
char name[31];
bool isWildcard() const { return id == 0; }
};
class RegionMap {
TransportKeyStore* _store;
uint16_t next_id, home_id, default_id;
uint16_t num_regions;
RegionEntry regions[MAX_REGION_ENTRIES];
RegionEntry wildcard;
void printChildRegions(int indent, const RegionEntry* parent, Stream& out) const;
public:
RegionMap(TransportKeyStore& store);
static bool is_name_char(uint8_t c);
bool load(FILESYSTEM* _fs, const char* path=NULL);
bool save(FILESYSTEM* _fs, const char* path=NULL);
RegionEntry* putRegion(const char* name, uint16_t parent_id, uint16_t id = 0);
RegionEntry* findMatch(mesh::Packet* packet, uint8_t mask);
RegionEntry& getWildcard() { return wildcard; }
RegionEntry* findByName(const char* name);
RegionEntry* findByNamePrefix(const char* prefix);
RegionEntry* findById(uint16_t id);
RegionEntry* getHomeRegion(); // NOTE: can be NULL
void setHomeRegion(const RegionEntry* home);
RegionEntry* getDefaultRegion(); // NOTE: can be NULL
void setDefaultRegion(const RegionEntry* def);
bool removeRegion(const RegionEntry& region);
bool clear();
void resetFrom(const RegionMap& src) { num_regions = 0; next_id = src.next_id; }
int getCount() const { return num_regions; }
const RegionEntry* getByIdx(int i) const { return &regions[i]; }
const RegionEntry* getRoot() const { return &wildcard; }
int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false);
int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num);
void exportTo(Stream& out) const;
size_t exportTo(char *dest, size_t max_len) const;
};

View File

@@ -0,0 +1,37 @@
#pragma once
#include <CayenneLPP.h>
#include "sensors/LocationProvider.h"
#define TELEM_PERM_BASE 0x01 // 'base' permission includes battery
#define TELEM_PERM_LOCATION 0x02
#define TELEM_PERM_ENVIRONMENT 0x04 // permission to access environment sensors
#define TELEM_CHANNEL_SELF 1 // LPP data channel for 'self' device
class SensorManager {
public:
double node_lat, node_lon; // modify these, if you want to affect Advert location
double node_altitude; // altitude in meters
SensorManager() { node_lat = 0; node_lon = 0; node_altitude = 0; }
virtual bool begin() { return false; }
virtual bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) { return false; }
virtual void loop() { }
virtual int getNumSettings() const { return 0; }
virtual const char* getSettingName(int i) const { return NULL; }
virtual const char* getSettingValue(int i) const { return NULL; }
virtual bool setSettingValue(const char* name, const char* value) { return false; }
virtual LocationProvider* getLocationProvider() { return NULL; }
// Helper functions to manage setting by keys (useful in many places ...)
const char* getSettingByKey(const char* key) {
int num = getNumSettings();
for (int i = 0; i < num; i++) {
if (strcmp(getSettingName(i), key) == 0) {
return getSettingValue(i);
}
}
return NULL;
}
};

View File

@@ -0,0 +1,111 @@
#pragma once
#include <Mesh.h>
#ifdef ESP32
#include <FS.h>
#endif
#define MAX_PACKET_HASHES 128
#define MAX_PACKET_ACKS 64
class SimpleMeshTables : public mesh::MeshTables {
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
int _next_idx;
uint32_t _acks[MAX_PACKET_ACKS];
int _next_ack_idx;
uint32_t _direct_dups, _flood_dups;
public:
SimpleMeshTables() {
memset(_hashes, 0, sizeof(_hashes));
_next_idx = 0;
memset(_acks, 0, sizeof(_acks));
_next_ack_idx = 0;
_direct_dups = _flood_dups = 0;
}
#ifdef ESP32
void restoreFrom(File f) {
f.read(_hashes, sizeof(_hashes));
f.read((uint8_t *) &_next_idx, sizeof(_next_idx));
f.read((uint8_t *) &_acks[0], sizeof(_acks));
f.read((uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
}
void saveTo(File f) {
f.write(_hashes, sizeof(_hashes));
f.write((const uint8_t *) &_next_idx, sizeof(_next_idx));
f.write((const uint8_t *) &_acks[0], sizeof(_acks));
f.write((const uint8_t *) &_next_ack_idx, sizeof(_next_ack_idx));
}
#endif
bool hasSeen(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack;
memcpy(&ack, packet->payload, 4);
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (ack == _acks[i]) {
if (packet->isRouteDirect()) {
_direct_dups++; // keep some stats
} else {
_flood_dups++;
}
return true;
}
}
_acks[_next_ack_idx] = ack;
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS; // cyclic table
return false;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
const uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
if (packet->isRouteDirect()) {
_direct_dups++; // keep some stats
} else {
_flood_dups++;
}
return true;
}
}
memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table
return false;
}
void clear(const mesh::Packet* packet) override {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
uint32_t ack;
memcpy(&ack, packet->payload, 4);
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (ack == _acks[i]) {
_acks[i] = 0;
break;
}
}
} else {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
memset(sp, 0, MAX_HASH_SIZE);
break;
}
}
}
}
uint32_t getNumDirectDups() const { return _direct_dups; }
uint32_t getNumFloodDups() const { return _flood_dups; }
void resetStats() { _direct_dups = _flood_dups = 0; }
};

View File

@@ -0,0 +1,125 @@
#include "StaticPoolPacketManager.h"
PacketQueue::PacketQueue(int max_entries) {
_table = new mesh::Packet*[max_entries];
_pri_table = new uint8_t[max_entries];
_schedule_table = new uint32_t[max_entries];
_size = max_entries;
_num = 0;
}
int PacketQueue::countBefore(uint32_t now) const {
if (now == 0xFFFFFFFF) return _num; // sentinel: count all entries regardless of schedule
int n = 0;
for (int j = 0; j < _num; j++) {
if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now
n++;
}
return n;
}
mesh::Packet* PacketQueue::get(uint32_t now) {
uint8_t min_pri = 0xFF;
int best_idx = -1;
for (int j = 0; j < _num; j++) {
if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now
if (_pri_table[j] < min_pri) { // select most important priority amongst non-future entries
min_pri = _pri_table[j];
best_idx = j;
}
}
if (best_idx < 0) return NULL; // empty, or all items are still in the future
mesh::Packet* top = _table[best_idx];
int i = best_idx;
_num--;
while (i < _num) {
_table[i] = _table[i+1];
_pri_table[i] = _pri_table[i+1];
_schedule_table[i] = _schedule_table[i+1];
i++;
}
return top;
}
mesh::Packet* PacketQueue::removeByIdx(int i) {
if (i >= _num) return NULL; // invalid index
mesh::Packet* item = _table[i];
_num--;
while (i < _num) {
_table[i] = _table[i+1];
_pri_table[i] = _pri_table[i+1];
_schedule_table[i] = _schedule_table[i+1];
i++;
}
return item;
}
bool PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (_num == _size) {
return false;
}
_table[_num] = packet;
_pri_table[_num] = priority;
_schedule_table[_num] = scheduled_for;
_num++;
return true;
}
StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_size), send_queue(pool_size), rx_queue(pool_size) {
// load up our unusued Packet pool
for (int i = 0; i < pool_size; i++) {
unused.add(new mesh::Packet(), 0, 0);
}
}
mesh::Packet* StaticPoolPacketManager::allocNew() {
return unused.removeByIdx(0); // just get first one (returns NULL if empty)
}
void StaticPoolPacketManager::free(mesh::Packet* packet) {
unused.add(packet, 0, 0);
}
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (!send_queue.add(packet, priority, scheduled_for)) {
MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet");
free(packet);
}
}
mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) {
//send_queue.sort(); // sort by scheduled_for/priority first
return send_queue.get(now);
}
int StaticPoolPacketManager::getOutboundCount(uint32_t now) const {
return send_queue.countBefore(now);
}
int StaticPoolPacketManager::getOutboundTotal() const {
return send_queue.count();
}
int StaticPoolPacketManager::getFreeCount() const {
return unused.count();
}
mesh::Packet* StaticPoolPacketManager::getOutboundByIdx(int i) {
return send_queue.itemAt(i);
}
mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) {
return send_queue.removeByIdx(i);
}
void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) {
if (!rx_queue.add(packet, 0, scheduled_for)) {
MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet");
free(packet);
}
}
mesh::Packet* StaticPoolPacketManager::getNextInbound(uint32_t now) {
return rx_queue.get(now);
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <Dispatcher.h>
class PacketQueue {
mesh::Packet** _table;
uint8_t* _pri_table;
uint32_t* _schedule_table;
int _size, _num;
public:
PacketQueue(int max_entries);
mesh::Packet* get(uint32_t now);
bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
int count() const { return _num; }
int countBefore(uint32_t now) const;
mesh::Packet* itemAt(int i) const { return _table[i]; }
mesh::Packet* removeByIdx(int i);
};
class StaticPoolPacketManager : public mesh::PacketManager {
PacketQueue unused, send_queue, rx_queue;
public:
StaticPoolPacketManager(int pool_size);
mesh::Packet* allocNew() override;
void free(mesh::Packet* packet) override;
void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override;
mesh::Packet* getNextOutbound(uint32_t now) override;
int getOutboundCount(uint32_t now) const override;
int getOutboundTotal() const override;
int getFreeCount() const override;
mesh::Packet* getOutboundByIdx(int i) override;
mesh::Packet* removeOutboundByIdx(int i) override;
void queueInbound(mesh::Packet* packet, uint32_t scheduled_for) override;
mesh::Packet* getNextInbound(uint32_t now) override;
};

View File

@@ -0,0 +1,55 @@
#pragma once
#include "Mesh.h"
class StatsFormatHelper {
public:
static void formatCoreStats(char* reply,
mesh::MainBoard& board,
mesh::MillisecondClock& ms,
uint16_t err_flags,
mesh::PacketManager* mgr) {
sprintf(reply,
"{\"battery_mv\":%u,\"uptime_secs\":%u,\"errors\":%u,\"queue_len\":%u}",
board.getBattMilliVolts(),
ms.getMillis() / 1000,
err_flags,
mgr->getOutboundTotal()
);
}
template<typename RadioDriverType>
static void formatRadioStats(char* reply,
mesh::Radio* radio,
RadioDriverType& driver,
uint32_t total_air_time_ms,
uint32_t total_rx_air_time_ms) {
sprintf(reply,
"{\"noise_floor\":%d,\"last_rssi\":%d,\"last_snr\":%.2f,\"tx_air_secs\":%u,\"rx_air_secs\":%u}",
(int16_t)radio->getNoiseFloor(),
(int16_t)driver.getLastRSSI(),
driver.getLastSNR(),
total_air_time_ms / 1000,
total_rx_air_time_ms / 1000
);
}
template<typename RadioDriverType>
static void formatPacketStats(char* reply,
RadioDriverType& driver,
uint32_t n_sent_flood,
uint32_t n_sent_direct,
uint32_t n_recv_flood,
uint32_t n_recv_direct) {
sprintf(reply,
"{\"recv\":%u,\"sent\":%u,\"flood_tx\":%u,\"direct_tx\":%u,\"flood_rx\":%u,\"direct_rx\":%u,\"recv_errors\":%u}",
driver.getPacketsRecv(),
driver.getPacketsSent(),
n_sent_flood,
n_sent_direct,
n_recv_flood,
n_recv_direct,
driver.getPacketsRecvErrors()
);
}
};

View File

@@ -0,0 +1,92 @@
#include "TransportKeyStore.h"
#include <SHA256.h>
uint16_t TransportKey::calcTransportCode(const mesh::Packet* packet) const {
uint16_t code;
SHA256 sha;
sha.resetHMAC(key, sizeof(key));
uint8_t type = packet->getPayloadType();
sha.update(&type, 1);
sha.update(packet->payload, packet->payload_len);
sha.finalizeHMAC(key, sizeof(key), &code, 2);
if (code == 0) { // reserve codes 0000 and FFFF
code++;
} else if (code == 0xFFFF) {
code--;
}
return code;
}
bool TransportKey::isNull() const {
for (int i = 0; i < sizeof(key); i++) {
if (key[i]) return false;
}
return true; // key is all zeroes
}
void TransportKeyStore::putCache(uint16_t id, const TransportKey& key) {
if (num_cache < MAX_TKS_ENTRIES) {
cache_ids[num_cache] = id;
cache_keys[num_cache] = key;
num_cache++;
} else {
// TODO: evict oldest cache entry
}
}
void TransportKeyStore::getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest) {
for (int i = 0; i < num_cache; i++) { // first, check cache
if (cache_ids[i] == id) { // cache hit!
dest = cache_keys[i];
return;
}
}
// calc key for publicly-known hashtag region name
SHA256 sha;
sha.update(name, strlen(name));
sha.finalize(&dest.key, sizeof(dest.key));
putCache(id, dest);
}
int TransportKeyStore::loadKeysFor(uint16_t id, TransportKey keys[], int max_num) {
int n = 0;
for (int i = 0; i < num_cache && n < max_num; i++) { // first, check cache
if (cache_ids[i] == id) {
keys[n++] = cache_keys[i];
}
}
if (n > 0) return n; // cache hit!
// TODO: retrieve from difficult-to-copy keystore
// store in cache (if room)
for (int i = 0; i < n; i++) {
putCache(id, keys[i]);
}
return n;
}
bool TransportKeyStore::saveKeysFor(uint16_t id, const TransportKey keys[], int num) {
invalidateCache();
// TODO: update hardware keystore
return false; // failed
}
bool TransportKeyStore::removeKeys(uint16_t id) {
invalidateCache();
// TODO: remove from hardware keystore
return false; // failed
}
bool TransportKeyStore::clear() {
invalidateCache();
// TODO: clear hardware keystore
return false; // failed
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Packet.h>
#include <helpers/IdentityStore.h>
struct TransportKey {
uint8_t key[16];
uint16_t calcTransportCode(const mesh::Packet* packet) const;
bool isNull() const;
};
#define MAX_TKS_ENTRIES 16
class TransportKeyStore {
uint16_t cache_ids[MAX_TKS_ENTRIES];
TransportKey cache_keys[MAX_TKS_ENTRIES];
int num_cache;
void putCache(uint16_t id, const TransportKey& key);
void invalidateCache() { num_cache = 0; }
public:
TransportKeyStore() { num_cache = 0; }
void getAutoKeyFor(uint16_t id, const char* name, TransportKey& dest);
int loadKeysFor(uint16_t id, TransportKey keys[], int max_num);
bool saveKeysFor(uint16_t id, const TransportKey keys[], int num);
bool removeKeys(uint16_t id);
bool clear();
};

View File

@@ -0,0 +1,174 @@
#include "TxtDataHelpers.h"
void StrHelper::strncpy(char* dest, const char* src, size_t buf_sz) {
while (buf_sz > 1 && *src) {
*dest++ = *src++;
buf_sz--;
}
*dest = 0; // truncates if needed
}
void StrHelper::strzcpy(char* dest, const char* src, size_t buf_sz) {
while (buf_sz > 1 && *src) {
*dest++ = *src++;
buf_sz--;
}
while (buf_sz > 0) { // pad remaining with nulls
*dest++ = 0;
buf_sz--;
}
}
bool StrHelper::isBlank(const char* str) {
while (*str) {
if (*str++ != ' ') return false;
}
return true;
}
#include <Arduino.h>
union int32_Float_t
{
int32_t Long;
float Float;
};
#ifndef FLT_MIN_EXP
#define FLT_MIN_EXP (-999)
#endif
#ifndef FLT_MAX_EXP
#define FLT_MAX_EXP (999)
#endif
#define _FTOA_TOO_LARGE -2 // |input| > 2147483520
#define _FTOA_TOO_SMALL -1 // |input| < 0.0000001
//precision 0-9
#define PRECISION 7
//_ftoa function
static void _ftoa(float f, char *p, int *status)
{
int32_t mantissa, int_part, frac_part;
int16_t exp2;
int32_Float_t x;
*status = 0;
if (f == 0.0)
{
*p++ = '0';
*p++ = '.';
*p++ = '0';
*p = 0;
return;
}
x.Float = f;
exp2 = (unsigned char)(x.Long>>23) - 127;
mantissa = (x.Long&0xFFFFFF) | 0x800000;
frac_part = 0;
int_part = 0;
if (exp2 >= 31)
{
*status = _FTOA_TOO_LARGE;
return;
}
else if (exp2 < -23)
{
*status = _FTOA_TOO_SMALL;
return;
}
else if (exp2 >= 23)
{
int_part = mantissa<<(exp2 - 23);
}
else if (exp2 >= 0)
{
int_part = mantissa>>(23 - exp2);
frac_part = (mantissa<<(exp2 + 1))&0xFFFFFF;
}
else
{
//if (exp2 < 0)
frac_part = (mantissa&0xFFFFFF)>>-(exp2 + 1);
}
if (x.Long < 0)
*p++ = '-';
if (int_part == 0)
*p++ = '0';
else
{
ltoa(int_part, p, 10);
while (*p)
p++;
}
*p++ = '.';
if (frac_part == 0)
*p++ = '0';
else
{
char m;
for (m=0; m<PRECISION; m++)
{
//frac_part *= 10;
frac_part = (frac_part<<3) + (frac_part<<1);
*p++ = (frac_part>>24) + '0';
frac_part &= 0xFFFFFF;
}
//delete ending zeroes
for (--p; p[0] == '0' && p[-1] != '.'; --p)
;
++p;
}
*p = 0;
}
const char* StrHelper::ftoa(float f) {
static char tmp[16];
int status;
_ftoa(f, tmp, &status);
if (status) {
tmp[0] = '0'; // fallback/error value
tmp[1] = 0;
}
return tmp;
}
const char* StrHelper::ftoa3(float f) {
static char s[16];
int v = (int)(f * 1000.0f + (f >= 0 ? 0.5f : -0.5f)); // rounded ×1000
int w = v / 1000; // whole
int d = abs(v % 1000); // decimals
snprintf(s, sizeof(s), "%d.%03d", w, d);
for (int i = strlen(s) - 1; i > 0 && s[i] == '0'; i--)
s[i] = 0;
int L = strlen(s);
if (s[L - 1] == '.') s[L - 1] = 0;
return s;
}
uint32_t StrHelper::fromHex(const char* src) {
uint32_t n = 0;
while (*src) {
if (*src >= '0' && *src <= '9') {
n <<= 4;
n |= (*src - '0');
} else if (*src >= 'A' && *src <= 'F') {
n <<= 4;
n |= (*src - 'A' + 10);
} else if (*src >= 'a' && *src <= 'f') {
n <<= 4;
n |= (*src - 'a' + 10);
} else {
break; // non-hex char encountered, stop parsing
}
src++;
}
return n;
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#define TXT_TYPE_PLAIN 0 // a plain text message
#define TXT_TYPE_CLI_DATA 1 // a CLI command
#define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender
#define DATA_TYPE_RESERVED 0x0000 // reserved for future use
#define DATA_TYPE_DEV 0xFFFF // developer namespace for experimenting with group/channel datagrams and building apps
class StrHelper {
public:
static void strncpy(char* dest, const char* src, size_t buf_sz);
static void strzcpy(char* dest, const char* src, size_t buf_sz); // pads with trailing nulls
static const char* ftoa(float f);
static const char* ftoa3(float f); //Converts float to string with 3 decimal places
static bool isBlank(const char* str);
static uint32_t fromHex(const char* src);
};

View File

@@ -0,0 +1,48 @@
#include "BridgeBase.h"
#include <Arduino.h>
bool BridgeBase::isRunning() const {
return _initialized;
}
const char *BridgeBase::getLogDateTime() {
static char tmp[32];
uint32_t now = _rtc->getCurrentTime();
DateTime dt = DateTime(now);
sprintf(tmp, "%02d:%02d:%02d - %d/%d/%d U", dt.hour(), dt.minute(), dt.second(), dt.day(), dt.month(),
dt.year());
return tmp;
}
uint16_t BridgeBase::fletcher16(const uint8_t *data, size_t len) {
uint8_t sum1 = 0, sum2 = 0;
for (size_t i = 0; i < len; i++) {
sum1 = (sum1 + data[i]) % 255;
sum2 = (sum2 + sum1) % 255;
}
return (sum2 << 8) | sum1;
}
bool BridgeBase::validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum) {
uint16_t calculated_checksum = fletcher16(data, len);
return received_checksum == calculated_checksum;
}
void BridgeBase::handleReceivedPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
BRIDGE_DEBUG_PRINTLN("RX packet received before initialization\n");
_mgr->free(packet);
return;
}
if (!_seen_packets.hasSeen(packet)) {
// bridge_delay provides a buffer to prevent immediate processing conflicts in the mesh network.
_mgr->queueInbound(packet, millis() + _prefs->bridge_delay);
} else {
_mgr->free(packet);
}
}

View File

@@ -0,0 +1,120 @@
#pragma once
#include "helpers/AbstractBridge.h"
#include "helpers/CommonCLI.h"
#include "helpers/SimpleMeshTables.h"
#include <RTClib.h>
/**
* @brief Base class implementing common bridge functionality
*
* This class provides common functionality used by different bridge implementations
* like packet tracking, checksum calculation, timestamping, and duplicate detection.
*
* Features:
* - Fletcher-16 checksum calculation for data integrity
* - Packet duplicate detection using SimpleMeshTables
* - Common timestamp formatting for debug logging
* - Shared packet management and queuing logic
*/
class BridgeBase : public AbstractBridge {
public:
virtual ~BridgeBase() = default;
/**
* @brief Gets the current state of the bridge.
*
* @return true if the bridge is initialized and running, false otherwise.
*/
bool isRunning() const override;
/**
* @brief Common magic number used by all bridge implementations for packet identification
*
* This magic number is placed at the beginning of bridge packets to identify
* them as mesh bridge packets and provide frame synchronization.
*/
static constexpr uint16_t BRIDGE_PACKET_MAGIC = 0xC03E;
/**
* @brief Common field sizes used by bridge implementations
*
* These constants define the size of common packet fields used across bridges.
* BRIDGE_MAGIC_SIZE is used by all bridges for packet identification.
* BRIDGE_LENGTH_SIZE is used by bridges that need explicit length fields (like RS232).
* BRIDGE_CHECKSUM_SIZE is used by all bridges for Fletcher-16 checksums.
*/
static constexpr uint16_t BRIDGE_MAGIC_SIZE = sizeof(BRIDGE_PACKET_MAGIC);
static constexpr uint16_t BRIDGE_LENGTH_SIZE = sizeof(uint16_t);
static constexpr uint16_t BRIDGE_CHECKSUM_SIZE = sizeof(uint16_t);
protected:
/** Tracks bridge state */
bool _initialized = false;
/** Packet manager for allocating and queuing mesh packets */
mesh::PacketManager *_mgr;
/** RTC clock for timestamping debug messages */
mesh::RTCClock *_rtc;
/** Node preferences for configuration settings */
NodePrefs *_prefs;
/** Tracks seen packets to prevent loops in broadcast communications */
SimpleMeshTables _seen_packets;
/**
* @brief Constructs a BridgeBase instance
*
* @param prefs Node preferences for configuration settings
* @param mgr PacketManager for allocating and queuing packets
* @param rtc RTCClock for timestamping debug messages
*/
BridgeBase(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: _prefs(prefs), _mgr(mgr), _rtc(rtc) {}
/**
* @brief Gets formatted date/time string for logging
*
* Format: "HH:MM:SS - DD/MM/YYYY U"
*
* @return Formatted date/time string
*/
const char *getLogDateTime();
/**
* @brief Calculate Fletcher-16 checksum
*
* Based on: https://en.wikipedia.org/wiki/Fletcher%27s_checksum
* Used to verify data integrity of received packets
*
* @param data Pointer to data to calculate checksum for
* @param len Length of data in bytes
* @return Calculated Fletcher-16 checksum
*/
static uint16_t fletcher16(const uint8_t *data, size_t len);
/**
* @brief Validate received checksum against calculated checksum
*
* @param data Pointer to data to validate
* @param len Length of data in bytes
* @param received_checksum Checksum received with data
* @return true if checksum is valid, false otherwise
*/
bool validateChecksum(const uint8_t *data, size_t len, uint16_t received_checksum);
/**
* @brief Common packet handling for received packets
*
* Implements the standard pattern used by all bridges:
* - Check if packet was seen before using _seen_packets.hasSeen()
* - Queue packet for mesh processing if not seen before
* - Free packet if already seen to prevent duplicates
*
* @param packet The received mesh packet
*/
void handleReceivedPacket(mesh::Packet *packet);
};

View File

@@ -0,0 +1,219 @@
#include "ESPNowBridge.h"
#include <WiFi.h>
#include <esp_wifi.h>
#ifdef WITH_ESPNOW_BRIDGE
// Static member to handle callbacks
ESPNowBridge *ESPNowBridge::_instance = nullptr;
// Static callback wrappers
void ESPNowBridge::recv_cb(const uint8_t *mac, const uint8_t *data, int32_t len) {
if (_instance) {
_instance->onDataRecv(mac, data, len);
}
}
void ESPNowBridge::send_cb(const uint8_t *mac, esp_now_send_status_t status) {
if (_instance) {
_instance->onDataSent(mac, status);
}
}
ESPNowBridge::ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _rx_buffer_pos(0) {
_instance = this;
}
void ESPNowBridge::begin() {
BRIDGE_DEBUG_PRINTLN("Initializing...\n");
// Initialize WiFi in station mode
WiFi.mode(WIFI_STA);
// Set wifi channel
if (esp_wifi_set_channel(_prefs->bridge_channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error setting WIFI channel to %d\n", _prefs->bridge_channel);
return;
}
// Initialize ESP-NOW
if (esp_now_init() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error initializing ESP-NOW\n");
return;
}
// Register callbacks
esp_now_register_recv_cb(recv_cb);
esp_now_register_send_cb(send_cb);
// Add broadcast peer
esp_now_peer_info_t peerInfo = {};
memset(&peerInfo, 0, sizeof(peerInfo));
memset(peerInfo.peer_addr, 0xFF, ESP_NOW_ETH_ALEN); // Broadcast address
peerInfo.channel = _prefs->bridge_channel;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Failed to add broadcast peer\n");
return;
}
// Update bridge state
_initialized = true;
}
void ESPNowBridge::end() {
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
// Remove broadcast peer
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
if (esp_now_del_peer(broadcastAddress) != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error removing broadcast peer\n");
}
// Unregister callbacks
esp_now_register_recv_cb(nullptr);
esp_now_register_send_cb(nullptr);
// Deinitialize ESP-NOW
if (esp_now_deinit() != ESP_OK) {
BRIDGE_DEBUG_PRINTLN("Error deinitializing ESP-NOW\n");
}
// Turn off WiFi
WiFi.mode(WIFI_OFF);
// Update bridge state
_initialized = false;
}
void ESPNowBridge::loop() {
// Nothing to do here - ESP-NOW is callback based
}
void ESPNowBridge::xorCrypt(uint8_t *data, size_t len) {
size_t keyLen = strlen(_prefs->bridge_secret);
for (size_t i = 0; i < len; i++) {
data[i] ^= _prefs->bridge_secret[i % keyLen];
}
}
void ESPNowBridge::onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len) {
// Ignore packets that are too small to contain header + checksum
if (len < (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE)) {
BRIDGE_DEBUG_PRINTLN("RX packet too small, len=%d\n", len);
return;
}
// Validate total packet size
if (len > MAX_ESPNOW_PACKET_SIZE) {
BRIDGE_DEBUG_PRINTLN("RX packet too large, len=%d\n", len);
return;
}
// Check packet header magic
uint16_t received_magic = (data[0] << 8) | data[1];
if (received_magic != BRIDGE_PACKET_MAGIC) {
BRIDGE_DEBUG_PRINTLN("RX invalid magic 0x%04X\n", received_magic);
return;
}
// Make a copy we can decrypt
uint8_t decrypted[MAX_ESPNOW_PACKET_SIZE];
const size_t encryptedDataLen = len - BRIDGE_MAGIC_SIZE;
memcpy(decrypted, data + BRIDGE_MAGIC_SIZE, encryptedDataLen);
// Try to decrypt (checksum + payload)
xorCrypt(decrypted, encryptedDataLen);
// Validate checksum
uint16_t received_checksum = (decrypted[0] << 8) | decrypted[1];
const size_t payloadLen = encryptedDataLen - BRIDGE_CHECKSUM_SIZE;
if (!validateChecksum(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen, received_checksum)) {
// Failed to decrypt - likely from a different network
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04X\n", received_checksum);
return;
}
BRIDGE_DEBUG_PRINTLN("RX, payload_len=%d\n", payloadLen);
// Create mesh packet
mesh::Packet *pkt = _instance->_mgr->allocNew();
if (!pkt) return;
if (pkt->readFrom(decrypted + BRIDGE_CHECKSUM_SIZE, payloadLen)) {
_instance->onPacketReceived(pkt);
} else {
_instance->_mgr->free(pkt);
}
}
void ESPNowBridge::onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
// Could add transmission error handling here if needed
}
void ESPNowBridge::sendPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
// First validate the packet pointer
if (!packet) {
BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
return;
}
if (!_seen_packets.hasSeen(packet)) {
// Create a temporary buffer just for size calculation and reuse for actual writing
uint8_t sizingBuffer[MAX_PAYLOAD_SIZE];
uint16_t meshPacketLen = packet->writeTo(sizingBuffer);
// Check if packet fits within our maximum payload size
if (meshPacketLen > MAX_PAYLOAD_SIZE) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", meshPacketLen,
MAX_PAYLOAD_SIZE);
return;
}
uint8_t buffer[MAX_ESPNOW_PACKET_SIZE];
// Write magic header (2 bytes)
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF;
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF;
// Write packet payload starting after magic header and checksum
const size_t packetOffset = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE;
memcpy(buffer + packetOffset, sizingBuffer, meshPacketLen);
// Calculate and add checksum (only of the payload)
uint16_t checksum = fletcher16(buffer + packetOffset, meshPacketLen);
buffer[2] = (checksum >> 8) & 0xFF; // High byte
buffer[3] = checksum & 0xFF; // Low byte
// Encrypt payload and checksum (not including magic header)
xorCrypt(buffer + BRIDGE_MAGIC_SIZE, meshPacketLen + BRIDGE_CHECKSUM_SIZE);
// Total packet size: magic header + checksum + payload
const size_t totalPacketSize = BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE + meshPacketLen;
// Broadcast using ESP-NOW
uint8_t broadcastAddress[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
esp_err_t result = esp_now_send(broadcastAddress, buffer, totalPacketSize);
if (result == ESP_OK) {
BRIDGE_DEBUG_PRINTLN("TX, len=%d\n", meshPacketLen);
} else {
BRIDGE_DEBUG_PRINTLN("TX FAILED!\n");
}
}
}
void ESPNowBridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif

View File

@@ -0,0 +1,157 @@
#pragma once
#include "MeshCore.h"
#include "esp_now.h"
#include "helpers/bridges/BridgeBase.h"
#ifdef WITH_ESPNOW_BRIDGE
/**
* @brief Bridge implementation using ESP-NOW protocol for packet transport
*
* This bridge enables mesh packet transport over ESP-NOW, a connectionless communication
* protocol provided by Espressif that allows ESP32 devices to communicate directly
* without WiFi router infrastructure.
*
* Features:
* - Broadcast-based communication (all bridges receive all packets)
* - Network isolation using XOR encryption with shared secret
* - Duplicate packet detection using SimpleMeshTables tracking
* - Maximum packet size of 250 bytes (ESP-NOW limitation)
*
* Packet Structure:
* [2 bytes] Magic Header - Used to identify ESPNowBridge packets
* [2 bytes] Fletcher-16 checksum of encrypted payload (calculated over payload only)
* [246 bytes max] Encrypted payload containing the mesh packet
*
* The Fletcher-16 checksum is used to validate packet integrity and detect
* corrupted or tampered packets. It's calculated over the encrypted payload
* and provides a simple but effective way to verify packets are both
* uncorrupted and from the same network (since the checksum is calculated
* after encryption).
*
* Configuration:
* - Define WITH_ESPNOW_BRIDGE to enable this bridge
* - Define _prefs->bridge_secret with a string to set the network encryption key
*
* Network Isolation:
* Multiple independent mesh networks can coexist by using different
* _prefs->bridge_secret values. Packets encrypted with a different key will
* fail the checksum validation and be discarded.
*/
class ESPNowBridge : public BridgeBase {
private:
static ESPNowBridge *_instance;
static void recv_cb(const uint8_t *mac, const uint8_t *data, int32_t len);
static void send_cb(const uint8_t *mac, esp_now_send_status_t status);
/**
* ESP-NOW Protocol Structure:
* - ESP-NOW header: 20 bytes (handled by ESP-NOW protocol)
* - ESP-NOW payload: 250 bytes maximum
* Total ESP-NOW packet: 270 bytes
*
* Our Bridge Packet Structure (must fit in ESP-NOW payload):
* - Magic header: 2 bytes
* - Checksum: 2 bytes
* - Available payload: 246 bytes
*/
static const size_t MAX_ESPNOW_PACKET_SIZE = 250;
/**
* Size constants for packet parsing
*/
static const size_t MAX_PAYLOAD_SIZE = MAX_ESPNOW_PACKET_SIZE - (BRIDGE_MAGIC_SIZE + BRIDGE_CHECKSUM_SIZE);
/** Buffer for receiving ESP-NOW packets */
uint8_t _rx_buffer[MAX_ESPNOW_PACKET_SIZE];
/** Current position in receive buffer */
size_t _rx_buffer_pos;
/**
* Performs XOR encryption/decryption of data
* Used to isolate different mesh networks
*
* Uses _prefs->bridge_secret as the key in a simple XOR operation.
* The same operation is used for both encryption and decryption.
* While not cryptographically secure, it provides basic network isolation.
*
* @param data Pointer to data to encrypt/decrypt
* @param len Length of data in bytes
*/
void xorCrypt(uint8_t *data, size_t len);
/**
* ESP-NOW receive callback
* Called by ESP-NOW when a packet is received
*
* @param mac Source MAC address
* @param data Received data
* @param len Length of received data
*/
void onDataRecv(const uint8_t *mac, const uint8_t *data, int32_t len);
/**
* ESP-NOW send callback
* Called by ESP-NOW after a transmission attempt
*
* @param mac_addr Destination MAC address
* @param status Transmission status
*/
void onDataSent(const uint8_t *mac_addr, esp_now_send_status_t status);
public:
/**
* Constructs an ESPNowBridge instance
*
* @param prefs Node preferences for configuration settings
* @param mgr PacketManager for allocating and queuing packets
* @param rtc RTCClock for timestamping debug messages
*/
ESPNowBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
/**
* Initializes the ESP-NOW bridge
*
* - Configures WiFi in station mode
* - Initializes ESP-NOW protocol
* - Registers callbacks
* - Sets up broadcast peer
*/
void begin() override;
/**
* Stops the ESP-NOW bridge
*
* - Removes broadcast peer
* - Unregisters callbacks
* - Deinitializes ESP-NOW protocol
* - Turns off WiFi to release radio resources
*/
void end() override;
/**
* Main loop handler
* ESP-NOW is callback-based, so this is currently empty
*/
void loop() override;
/**
* Called when a packet is received via ESP-NOW
* Queues the packet for mesh processing if not seen before
*
* @param packet The received mesh packet
*/
void onPacketReceived(mesh::Packet *packet) override;
/**
* Called when a packet needs to be transmitted via ESP-NOW
* Encrypts and broadcasts the packet if not seen before
*
* @param packet The mesh packet to transmit
*/
void sendPacket(mesh::Packet *packet) override;
};
#endif

View File

@@ -0,0 +1,151 @@
#include "RS232Bridge.h"
#include <HardwareSerial.h>
#ifdef WITH_RS232_BRIDGE
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _serial(&serial) {}
void RS232Bridge::begin() {
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
#if !defined(WITH_RS232_BRIDGE_RX) || !defined(WITH_RS232_BRIDGE_TX)
#error "WITH_RS232_BRIDGE_RX and WITH_RS232_BRIDGE_TX must be defined"
#endif
#if defined(ESP32)
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
#elif defined(NRF52_PLATFORM)
// Tested with RAK_4631 and T114
((Uart *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
#elif defined(RP2040_PLATFORM)
((SerialUART *)_serial)->setRX(WITH_RS232_BRIDGE_RX);
((SerialUART *)_serial)->setTX(WITH_RS232_BRIDGE_TX);
#elif defined(STM32_PLATFORM)
((HardwareSerial *)_serial)->setRx(WITH_RS232_BRIDGE_RX);
((HardwareSerial *)_serial)->setTx(WITH_RS232_BRIDGE_TX);
#else
#error RS232Bridge was not tested on the current platform
#endif
((HardwareSerial *)_serial)->begin(_prefs->bridge_baud);
// Update bridge state
_initialized = true;
}
void RS232Bridge::end() {
BRIDGE_DEBUG_PRINTLN("Stopping...\n");
((HardwareSerial *)_serial)->end();
// Update bridge state
_initialized = false;
}
void RS232Bridge::loop() {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
while (_serial->available()) {
uint8_t b = _serial->read();
if (_rx_buffer_pos < 2) {
// Waiting for magic word
if ((_rx_buffer_pos == 0 && b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) ||
(_rx_buffer_pos == 1 && b == (BRIDGE_PACKET_MAGIC & 0xFF))) {
_rx_buffer[_rx_buffer_pos++] = b;
} else {
// Invalid magic byte, reset and start over
_rx_buffer_pos = 0;
// Check if this byte could be the start of a new magic word
if (b == ((BRIDGE_PACKET_MAGIC >> 8) & 0xFF)) {
_rx_buffer[_rx_buffer_pos++] = b;
}
}
} else {
// Reading length, payload, and checksum
_rx_buffer[_rx_buffer_pos++] = b;
if (_rx_buffer_pos >= 4) {
uint16_t len = (_rx_buffer[2] << 8) | _rx_buffer[3];
// Validate length field
if (len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("RX invalid length %d, resetting\n", len);
_rx_buffer_pos = 0; // Invalid length, reset
continue;
}
if (_rx_buffer_pos == len + SERIAL_OVERHEAD) { // Full packet received
uint16_t received_checksum = (_rx_buffer[4 + len] << 8) | _rx_buffer[5 + len];
if (validateChecksum(_rx_buffer + 4, len, received_checksum)) {
BRIDGE_DEBUG_PRINTLN("RX, len=%d crc=0x%04x\n", len, received_checksum);
mesh::Packet *pkt = _mgr->allocNew();
if (pkt) {
if (pkt->readFrom(_rx_buffer + 4, len)) {
onPacketReceived(pkt);
} else {
BRIDGE_DEBUG_PRINTLN("RX failed to parse packet\n");
_mgr->free(pkt);
}
} else {
BRIDGE_DEBUG_PRINTLN("RX failed to allocate packet\n");
}
} else {
BRIDGE_DEBUG_PRINTLN("RX checksum mismatch, rcv=0x%04x\n", received_checksum);
}
_rx_buffer_pos = 0; // Reset for next packet
}
}
}
}
}
void RS232Bridge::sendPacket(mesh::Packet *packet) {
// Guard against uninitialized state
if (_initialized == false) {
return;
}
// First validate the packet pointer
if (!packet) {
BRIDGE_DEBUG_PRINTLN("TX invalid packet pointer\n");
return;
}
if (!_seen_packets.hasSeen(packet)) {
uint8_t buffer[MAX_SERIAL_PACKET_SIZE];
uint16_t len = packet->writeTo(buffer + 4);
// Check if packet fits within our maximum payload size
if (len > (MAX_TRANS_UNIT + 1)) {
BRIDGE_DEBUG_PRINTLN("TX packet too large (payload=%d, max=%d)\n", len, MAX_TRANS_UNIT + 1);
return;
}
// Build packet header
buffer[0] = (BRIDGE_PACKET_MAGIC >> 8) & 0xFF; // Magic high byte
buffer[1] = BRIDGE_PACKET_MAGIC & 0xFF; // Magic low byte
buffer[2] = (len >> 8) & 0xFF; // Length high byte
buffer[3] = len & 0xFF; // Length low byte
// Calculate checksum over the payload
uint16_t checksum = fletcher16(buffer + 4, len);
buffer[4 + len] = (checksum >> 8) & 0xFF; // Checksum high byte
buffer[5 + len] = checksum & 0xFF; // Checksum low byte
// Send complete packet
_serial->write(buffer, len + SERIAL_OVERHEAD);
BRIDGE_DEBUG_PRINTLN("TX, len=%d crc=0x%04x\n", len, checksum);
}
}
void RS232Bridge::onPacketReceived(mesh::Packet *packet) {
handleReceivedPacket(packet);
}
#endif

View File

@@ -0,0 +1,148 @@
#pragma once
#include "helpers/bridges/BridgeBase.h"
#include <Stream.h>
#ifdef WITH_RS232_BRIDGE
/**
* @brief Bridge implementation using RS232/UART protocol for packet transport
*
* This bridge enables mesh packet transport over serial/UART connections,
* allowing nodes to communicate over wired serial links. It implements a simple
* packet framing protocol with checksums for reliable transfer.
*
* Features:
* - Point-to-point communication over hardware UART
* - Fletcher-16 checksum for data integrity verification
* - Magic header for packet synchronization and frame alignment
* - Duplicate packet detection using SimpleMeshTables tracking
* - Configurable RX/TX pins via build defines
* - Fixed baud rate at 115200 for consistent timing
*
* Packet Structure:
* [2 bytes] Magic Header (0xC03E) - Used to identify start of RS232Bridge packets
* [2 bytes] Payload Length - Length of the mesh packet payload
* [n bytes] Mesh Packet Payload - The actual mesh packet data
* [2 bytes] Fletcher-16 Checksum - Calculated over the payload for integrity verification
*
* The Fletcher-16 checksum is calculated over the mesh packet payload and provides
* error detection capabilities suitable for serial communication where electrical
* noise, timing issues, or hardware problems could corrupt data. The checksum
* validation ensures only valid packets are forwarded to the mesh.
*
* Configuration:
* - Define WITH_RS232_BRIDGE to enable this bridge
* - Define WITH_RS232_BRIDGE_RX with the RX pin number
* - Define WITH_RS232_BRIDGE_TX with the TX pin number
*
* Platform Support:
* Different platforms require different pin configuration methods:
* - ESP32: Uses HardwareSerial::setPins(rx, tx)
* - NRF52: Uses Uart::setPins(rx, tx)
* - RP2040: Uses SerialUART::setRX(rx) and SerialUART::setTX(tx)
* - STM32: Uses HardwareSerial::setRx(rx) and HardwareSerial::setTx(tx)
*/
class RS232Bridge : public BridgeBase {
public:
/**
* @brief Constructs an RS232Bridge instance
*
* @param prefs Node preferences for configuration settings
* @param serial The hardware serial port to use
* @param mgr PacketManager for allocating and queuing packets
* @param rtc RTCClock for timestamping debug messages
*/
RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
/**
* Initializes the RS232 bridge
*
* - Validates that RX/TX pins are defined
* - Configures UART pins based on target platform
* - Sets baud rate to 115200 for consistent communication
* - Platform-specific pin configuration methods are used
*/
void begin() override;
/**
* Stops the RS232 bridge
*
*/
void end() override;
/**
* @brief Main loop handler for processing incoming serial data
*
* Implements a state machine for packet reception:
* 1. Searches for magic header bytes for packet synchronization
* 2. Reads length field to determine expected packet size
* 3. Validates packet length against maximum allowed size
* 4. Receives complete packet payload and checksum
* 5. Validates Fletcher-16 checksum for data integrity
* 6. Creates mesh packet and forwards if valid
*/
void loop() override;
/**
* @brief Called when a packet needs to be transmitted over serial
*
* Formats the mesh packet with RS232 framing protocol:
* - Adds magic header for synchronization
* - Includes payload length field
* - Calculates Fletcher-16 checksum over payload
* - Transmits complete framed packet
* - Uses duplicate detection to prevent retransmission
*
* @param packet The mesh packet to transmit
*/
void sendPacket(mesh::Packet *packet) override;
/**
* @brief Called when a complete valid packet has been received from serial
*
* Forwards the received packet to the mesh for processing.
* The packet has already been validated for checksum integrity
* and parsed successfully at this point.
*
* @param packet The received mesh packet ready for processing
*/
void onPacketReceived(mesh::Packet *packet) override;
private:
/**
* RS232 Protocol Structure:
* - Magic header: 2 bytes (packet identification)
* - Length field: 2 bytes (payload length)
* - Payload: variable bytes (mesh packet data)
* - Checksum: 2 bytes (Fletcher-16 over payload)
* Total overhead: 6 bytes
*/
/**
* @brief The total overhead of the serial protocol in bytes.
* Includes: MAGIC_WORD (2) + LENGTH (2) + CHECKSUM (2) = 6 bytes
*/
static constexpr uint16_t SERIAL_OVERHEAD = BRIDGE_MAGIC_SIZE + BRIDGE_LENGTH_SIZE + BRIDGE_CHECKSUM_SIZE;
/**
* @brief The maximum size of a complete packet on the serial line.
*
* This is calculated as the sum of:
* - MAX_TRANS_UNIT + 1 for the maximum mesh packet size
* - SERIAL_OVERHEAD for the framing (magic + length + checksum)
*/
static constexpr uint16_t MAX_SERIAL_PACKET_SIZE = (MAX_TRANS_UNIT + 1) + SERIAL_OVERHEAD;
/** Hardware serial port interface */
Stream *_serial;
/** Buffer for building received packets */
uint8_t _rx_buffer[MAX_SERIAL_PACKET_SIZE];
/** Current position in the receive buffer */
uint16_t _rx_buffer_pos = 0;
};
#endif

View File

@@ -0,0 +1,113 @@
#include "ESPNOWRadio.h"
#include <esp_now.h>
#include <WiFi.h>
#include <esp_wifi.h>
static uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static esp_now_peer_info_t peerInfo;
static volatile bool is_send_complete = false;
static esp_err_t last_send_result;
static uint8_t rx_buf[256];
static uint8_t last_rx_len = 0;
// callback when data is sent
static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
is_send_complete = true;
ESPNOW_DEBUG_PRINTLN("Send Status: %d", (int)status);
}
static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) {
ESPNOW_DEBUG_PRINTLN("Recv: len = %d", len);
memcpy(rx_buf, data, len);
last_rx_len = len;
}
void ESPNOWRadio::init() {
// Set device as a Wi-Fi Station
WiFi.mode(WIFI_STA);
// Long Range mode
esp_wifi_set_protocol(WIFI_IF_STA, WIFI_PROTOCOL_LR);
// Init ESP-NOW
if (esp_now_init() != ESP_OK) {
ESPNOW_DEBUG_PRINTLN("Error initializing ESP-NOW");
return;
}
esp_wifi_set_max_tx_power(80); // should be 20dBm
esp_now_register_send_cb(OnDataSent);
esp_now_register_recv_cb(OnDataRecv);
// Register peer
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
is_send_complete = true;
// Add peer
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
ESPNOW_DEBUG_PRINTLN("init success");
} else {
// ESPNOW_DEBUG_PRINTLN("Failed to add peer");
}
}
void ESPNOWRadio::setTxPower(uint8_t dbm) {
esp_wifi_set_max_tx_power(dbm * 4);
}
uint32_t ESPNOWRadio::intID() {
uint8_t mac[8];
memset(mac, 0, sizeof(mac));
esp_efuse_mac_get_default(mac);
uint32_t n, m;
memcpy(&n, &mac[0], 4);
memcpy(&m, &mac[4], 4);
return n + m;
}
bool ESPNOWRadio::startSendRaw(const uint8_t* bytes, int len) {
// Send message via ESP-NOW
is_send_complete = false;
esp_err_t result = esp_now_send(broadcastAddress, bytes, len);
if (result == ESP_OK) {
n_sent++;
ESPNOW_DEBUG_PRINTLN("Send success");
return true;
}
last_send_result = result;
is_send_complete = true;
ESPNOW_DEBUG_PRINTLN("Send failed: %d", result);
return false;
}
bool ESPNOWRadio::isSendComplete() {
return is_send_complete;
}
void ESPNOWRadio::onSendFinished() {
is_send_complete = true;
}
bool ESPNOWRadio::isInRecvMode() const {
return is_send_complete; // if NO send in progress, then we're in Rx mode
}
float ESPNOWRadio::getLastRSSI() const { return 0; }
float ESPNOWRadio::getLastSNR() const { return 0; }
int ESPNOWRadio::recvRaw(uint8_t* bytes, int sz) {
int len = last_rx_len;
if (last_rx_len > 0) {
memcpy(bytes, rx_buf, last_rx_len);
last_rx_len = 0;
n_recv++;
}
return len;
}
uint32_t ESPNOWRadio::getEstAirtimeFor(int len_bytes) {
return 4; // Fast AF
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include <Mesh.h>
class ESPNOWRadio : public mesh::Radio {
protected:
uint32_t n_recv, n_sent, n_recv_errors;
public:
ESPNOWRadio() { n_recv = n_sent = n_recv_errors = 0; }
void init();
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
bool startSendRaw(const uint8_t* bytes, int len) override;
bool isSendComplete() override;
void onSendFinished() override;
bool isInRecvMode() const override;
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsSent() const { return n_sent; }
uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
virtual float getLastRSSI() const override;
virtual float getLastSNR() const override;
float packetScore(float snr, int packet_len) override { return 0; }
/**
* These two functions do nothing for ESP-NOW, but are needed for the
* Radio interface.
*/
virtual void setRxBoostedGainMode(bool) { }
virtual bool getRxBoostedGainMode() const { return false; }
uint32_t intID();
void setTxPower(uint8_t dbm);
};
#if ESPNOW_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define ESPNOW_DEBUG_PRINT(F, ...) Serial.printf("ESP-Now: " F, ##__VA_ARGS__)
#define ESPNOW_DEBUG_PRINTLN(F, ...) Serial.printf("ESP-Now: " F "\n", ##__VA_ARGS__)
#else
#define ESPNOW_DEBUG_PRINT(...) {}
#define ESPNOW_DEBUG_PRINTLN(...) {}
#endif

View File

@@ -0,0 +1,253 @@
#include "SerialBLEInterface.h"
#include "esp_mac.h"
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
#define ADVERT_RESTART_DELAY 1000 // millis
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
_pin_code = pin_code;
if (strcmp(name, "@@MAC") == 0) {
uint8_t addr[8];
memset(addr, 0, sizeof(addr));
esp_efuse_mac_get_default(addr);
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
}
char dev_name[32+16];
sprintf(dev_name, "%s%s", prefix, name);
// Create the BLE Device
BLEDevice::init(dev_name);
BLEDevice::setSecurityCallbacks(this);
BLEDevice::setMTU(MAX_FRAME_SIZE);
BLESecurity sec;
sec.setStaticPIN(pin_code);
sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
//BLEDevice::setPower(ESP_PWR_LVL_N8);
// Create the BLE Server
pServer = BLEDevice::createServer();
pServer->setCallbacks(this);
// Create the BLE Service
pService = pServer->createService(SERVICE_UUID);
// Create a BLE Characteristic
pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM);
pTxCharacteristic->addDescriptor(new BLE2902());
BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE);
pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM);
pRxCharacteristic->setCallbacks(this);
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
}
// -------- BLESecurityCallbacks methods
uint32_t SerialBLEInterface::onPassKeyRequest() {
BLE_DEBUG_PRINTLN("onPassKeyRequest()");
return _pin_code;
}
void SerialBLEInterface::onPassKeyNotify(uint32_t pass_key) {
BLE_DEBUG_PRINTLN("onPassKeyNotify(%u)", pass_key);
}
bool SerialBLEInterface::onConfirmPIN(uint32_t pass_key) {
BLE_DEBUG_PRINTLN("onConfirmPIN(%u)", pass_key);
return true;
}
bool SerialBLEInterface::onSecurityRequest() {
BLE_DEBUG_PRINTLN("onSecurityRequest()");
return true; // allow
}
void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
if (cmpl.success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
} else {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Failure*");
//pServer->removePeerDevice(pServer->getConnId(), true);
pServer->disconnect(pServer->getConnId());
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
}
}
// -------- BLEServerCallbacks methods
void SerialBLEInterface::onConnect(BLEServer* pServer) {
}
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", pServer->getPeerMTU(param->mtu.conn_id));
}
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
BLE_DEBUG_PRINTLN("onDisconnect()");
if (_isEnabled) {
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
// loop() will detect this on next loop, and set deviceConnected to false
}
}
// -------- BLECharacteristicCallbacks methods
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) {
uint8_t* rxValue = pCharacteristic->getData();
int len = pCharacteristic->getLength();
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len);
} else if (recv_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!");
} else {
recv_queue[recv_queue_len].len = len;
memcpy(recv_queue[recv_queue_len].buf, rxValue, len);
recv_queue_len++;
}
}
// ---------- public methods
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
// Start the service
pService->start();
// Start advertising
//pServer->getAdvertising()->setMinInterval(500);
//pServer->getAdvertising()->setMaxInterval(1000);
pServer->getAdvertising()->start();
adv_restart_time = 0;
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
pService->stop();
oldDeviceConnected = deviceConnected = false;
adv_restart_time = 0;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d", len);
return 0;
}
if (deviceConnected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len; // add to send queue
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 60
bool SerialBLEInterface::isWriteBusy() const {
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
if (send_queue_len > 0 // first, check send queue
&& millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart
) {
_last_write = millis();
pTxCharacteristic->setValue(send_queue[0].buf, send_queue[0].len);
pTxCharacteristic->notify();
BLE_DEBUG_PRINTLN("writeBytes: sz=%d, hdr=%d", (uint32_t)send_queue[0].len, (uint32_t) send_queue[0].buf[0]);
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
}
}
if (recv_queue_len > 0) { // check recv queue
size_t len = recv_queue[0].len; // take from top of queue
memcpy(dest, recv_queue[0].buf, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]);
recv_queue_len--;
for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue
recv_queue[i] = recv_queue[i + 1];
}
return len;
}
if (pServer->getConnectedCount() == 0) deviceConnected = false;
if (deviceConnected != oldDeviceConnected) {
if (!deviceConnected) { // disconnecting
clearBuffers();
BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
//pServer->getAdvertising()->setMinInterval(500);
//pServer->getAdvertising()->setMaxInterval(1000);
adv_restart_time = millis() + ADVERT_RESTART_DELAY;
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> stopping advertising");
BLE_DEBUG_PRINTLN("SerialBLEInterface -> connecting...");
// connecting
// do stuff here on connecting
pServer->getAdvertising()->stop();
adv_restart_time = 0;
}
oldDeviceConnected = deviceConnected;
}
if (adv_restart_time && millis() >= adv_restart_time) {
if (pServer->getConnectedCount() == 0) {
BLE_DEBUG_PRINTLN("SerialBLEInterface -> re-starting advertising");
pServer->getAdvertising()->start(); // re-Start advertising
}
adv_restart_time = 0;
}
return 0;
}
bool SerialBLEInterface::isConnected() const {
return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0;
}

View File

@@ -0,0 +1,91 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks {
BLEServer *pServer;
BLEService *pService;
BLECharacteristic * pTxCharacteristic;
bool deviceConnected;
bool oldDeviceConnected;
bool _isEnabled;
uint16_t last_conn_id;
uint32_t _pin_code;
unsigned long _last_write;
unsigned long adv_restart_time;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 4
int recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
int send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; }
protected:
// BLESecurityCallbacks methods
uint32_t onPassKeyRequest() override;
void onPassKeyNotify(uint32_t pass_key) override;
bool onConfirmPIN(uint32_t pass_key) override;
bool onSecurityRequest() override;
void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) override;
// BLEServerCallbacks methods
void onConnect(BLEServer* pServer) override;
void onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) override;
void onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) override;
void onDisconnect(BLEServer* pServer) override;
// BLECharacteristicCallbacks methods
void onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) override;
public:
SerialBLEInterface() {
pServer = NULL;
pService = NULL;
deviceConnected = false;
oldDeviceConnected = false;
adv_restart_time = 0;
_isEnabled = false;
_last_write = 0;
last_conn_id = 0;
send_queue_len = recv_queue_len = 0;
}
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
// BaseSerialInterface methods
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
#if BLE_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
#else
#define BLE_DEBUG_PRINT(...) {}
#define BLE_DEBUG_PRINTLN(...) {}
#endif

View File

@@ -0,0 +1,172 @@
#include "SerialWifiInterface.h"
#include <WiFi.h>
void SerialWifiInterface::begin(int port) {
// wifi setup is handled outside of this class, only starts the server
server.begin(port);
}
// ---------- public methods
void SerialWifiInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
}
void SerialWifiInterface::disable() {
_isEnabled = false;
}
size_t SerialWifiInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
WIFI_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len);
return 0;
}
if (deviceConnected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
WIFI_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len; // add to send queue
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
bool SerialWifiInterface::isWriteBusy() const {
return false;
}
bool SerialWifiInterface::hasReceivedFrameHeader() {
return received_frame_header.type != 0 && received_frame_header.length != 0;
}
void SerialWifiInterface::resetReceivedFrameHeader() {
received_frame_header.type = 0;
received_frame_header.length = 0;
}
size_t SerialWifiInterface::checkRecvFrame(uint8_t dest[]) {
// check if new client connected
auto newClient = server.available();
if (newClient) {
// disconnect existing client
deviceConnected = false;
client.stop();
// switch active connection to new client
client = newClient;
// forget received frame header
resetReceivedFrameHeader();
}
if (client.connected()) {
if (!deviceConnected) {
WIFI_DEBUG_PRINTLN("Got connection");
deviceConnected = true;
}
} else {
if (deviceConnected) {
deviceConnected = false;
WIFI_DEBUG_PRINTLN("Disconnected");
}
}
if (deviceConnected) {
if (send_queue_len > 0) { // first, check send queue
_last_write = millis();
int len = send_queue[0].len;
uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames
pkt[0] = '>';
pkt[1] = (len & 0xFF); // LSB
pkt[2] = (len >> 8); // MSB
memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len);
client.write(pkt, 3 + len);
send_queue_len--;
for (int i = 0; i < send_queue_len; i++) { // delete top item from queue
send_queue[i] = send_queue[i + 1];
}
} else {
// check if we are waiting for a frame header
if(!hasReceivedFrameHeader()){
// make sure we have received enough bytes for a frame header
// 3 bytes frame header = (1 byte frame type) + (2 bytes frame length as unsigned 16-bit little endian)
int frame_header_length = 3;
if(client.available() >= frame_header_length){
// read frame header
client.readBytes(&received_frame_header.type, 1);
client.readBytes((uint8_t*)&received_frame_header.length, 2);
}
}
// check if we have received a frame header
if(hasReceivedFrameHeader()){
// make sure we have received enough bytes for the required frame length
int available = client.available();
int frame_type = received_frame_header.type;
int frame_length = received_frame_header.length;
if(frame_length > available){
WIFI_DEBUG_PRINTLN("Waiting for %d more bytes", frame_length - available);
return 0;
}
// skip frames that are larger than MAX_FRAME_SIZE
if(frame_length > MAX_FRAME_SIZE){
WIFI_DEBUG_PRINTLN("Skipping frame: length=%d is larger than MAX_FRAME_SIZE=%d", frame_length, MAX_FRAME_SIZE);
while(frame_length > 0){
uint8_t skip[1];
int skipped = client.read(skip, 1);
frame_length -= skipped;
}
resetReceivedFrameHeader();
return 0;
}
// skip frames that are not expected type
// '<' is 0x3c which indicates a frame sent from app to radio
if(frame_type != '<'){
WIFI_DEBUG_PRINTLN("Skipping frame: type=0x%x is unexpected", frame_type);
while(frame_length > 0){
uint8_t skip[1];
int skipped = client.read(skip, 1);
frame_length -= skipped;
}
resetReceivedFrameHeader();
return 0;
}
// read frame data to provided buffer
client.readBytes(dest, frame_length);
// ready for next frame
resetReceivedFrameHeader();
return frame_length;
}
}
}
return 0;
}
bool SerialWifiInterface::isConnected() const {
return deviceConnected; //pServer != NULL && pServer->getConnectedCount() > 0;
}

View File

@@ -0,0 +1,71 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <WiFi.h>
class SerialWifiInterface : public BaseSerialInterface {
bool deviceConnected;
bool _isEnabled;
unsigned long _last_write;
unsigned long adv_restart_time;
WiFiServer server;
WiFiClient client;
struct FrameHeader {
uint8_t type;
uint16_t length;
};
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
FrameHeader received_frame_header;
#define FRAME_QUEUE_SIZE 4
int recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
int send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; }
protected:
public:
SerialWifiInterface() : server(WiFiServer()), client(WiFiClient()) {
deviceConnected = false;
_isEnabled = false;
_last_write = 0;
send_queue_len = recv_queue_len = 0;
received_frame_header.type = 0;
received_frame_header.length = 0;
}
void begin(int port);
// BaseSerialInterface methods
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
bool hasReceivedFrameHeader();
void resetReceivedFrameHeader();
};
#if WIFI_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define WIFI_DEBUG_PRINT(F, ...) Serial.printf("WiFi: " F, ##__VA_ARGS__)
#define WIFI_DEBUG_PRINTLN(F, ...) Serial.printf("WiFi: " F "\n", ##__VA_ARGS__)
#else
#define WIFI_DEBUG_PRINT(...) {}
#define WIFI_DEBUG_PRINTLN(...) {}
#endif

View File

@@ -0,0 +1,350 @@
#if defined(TBEAM_SUPREME_SX1262) || defined(TBEAM_SX1262) || defined(TBEAM_SX1276)
#include <Arduino.h>
#include "TBeamBoard.h"
//#include <RadioLib.h>
uint32_t deviceOnline = 0x00;
bool pmuInterrupt;
static void setPmuFlag()
{
pmuInterrupt = true;
}
void TBeamBoard::begin() {
ESP32Board::begin();
power_init();
//Configure user button
pinMode(PIN_USER_BTN, INPUT);
#ifndef TBEAM_SUPREME_SX1262
digitalWrite(P_LORA_TX_LED, HIGH); //inverted pin for SX1276 - HIGH for off
#endif
//radiotype_detect();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
#ifdef MESH_DEBUG
void TBeamBoard::scanDevices(TwoWire *w)
{
uint8_t err, addr;
int nDevices = 0;
uint32_t start = 0;
Serial.println("Scanning I2C for Devices");
for (addr = 1; addr < 127; addr++) {
start = millis();
w->beginTransmission(addr); delay(2);
err = w->endTransmission();
if (err == 0) {
nDevices++;
switch (addr) {
case 0x77:
case 0x76:
Serial.println("\tFound BME280 Sensor");
deviceOnline |= BME280_ONLINE;
break;
case 0x34:
Serial.println("\tFound AXP192/AXP2101 PMU");
deviceOnline |= POWERMANAGE_ONLINE;
break;
case 0x3C:
Serial.println("\tFound SSD1306/SH1106 display");
deviceOnline |= DISPLAY_ONLINE;
break;
case 0x51:
Serial.println("\tFound PCF8563 RTC");
deviceOnline |= PCF8563_ONLINE;
break;
case 0x1C:
Serial.println("\tFound QMC6310 MAG Sensor");
deviceOnline |= QMC6310_ONLINE;
break;
default:
Serial.print("\tI2C device found at address 0x");
if (addr < 16) {
Serial.print("0");
}
Serial.print(addr, HEX);
Serial.println(" !");
break;
}
} else if (err == 4) {
Serial.print("Unknow error at address 0x");
if (addr < 16) {
Serial.print("0");
}
Serial.println(addr, HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
Serial.println("Scan for devices is complete.");
Serial.println("\n");
Serial.printf("GPS RX pin: %d", PIN_GPS_RX);
Serial.printf(" GPS TX pin: %d", PIN_GPS_TX);
Serial.println();
}
void TBeamBoard::printPMU()
{
Serial.print("isCharging:"); Serial.println(PMU->isCharging() ? "YES" : "NO");
Serial.print("isDischarge:"); Serial.println(PMU->isDischarge() ? "YES" : "NO");
Serial.print("isVbusIn:"); Serial.println(PMU->isVbusIn() ? "YES" : "NO");
Serial.print("getBattVoltage:"); Serial.print(PMU->getBattVoltage()); Serial.println("mV");
Serial.print("getVbusVoltage:"); Serial.print(PMU->getVbusVoltage()); Serial.println("mV");
Serial.print("getSystemVoltage:"); Serial.print(PMU->getSystemVoltage()); Serial.println("mV");
// The battery percentage may be inaccurate at first use, the PMU will automatically
// learn the battery curve and will automatically calibrate the battery percentage
// after a charge and discharge cycle
if (PMU->isBatteryConnect()) {
Serial.print("getBatteryPercent:"); Serial.print(PMU->getBatteryPercent()); Serial.println("%");
}
Serial.println();
}
#endif
bool TBeamBoard::power_init()
{
if (!PMU) {
#ifdef TBEAM_SUPREME_SX1262
PMU = new XPowersAXP2101(PMU_WIRE_PORT, PIN_BOARD_SDA1, PIN_BOARD_SCL1, I2C_PMU_ADD);
#else
PMU = new XPowersAXP2101(PMU_WIRE_PORT, PIN_BOARD_SDA, PIN_BOARD_SCL, I2C_PMU_ADD);
#endif
if (!PMU->init()) {
MESH_DEBUG_PRINTLN("Warning: Failed to find AXP2101 power management");
delete PMU;
PMU = NULL;
} else {
MESH_DEBUG_PRINTLN("AXP2101 PMU init succeeded, using AXP2101 PMU");
}
}
if (!PMU) {
PMU = new XPowersAXP192(PMU_WIRE_PORT, PIN_BOARD_SDA, PIN_BOARD_SCL, I2C_PMU_ADD);
if (!PMU->init()) {
MESH_DEBUG_PRINTLN("Warning: Failed to find AXP192 power management");
delete PMU;
PMU = NULL;
} else {
MESH_DEBUG_PRINTLN("AXP192 PMU init succeeded, using AXP192 PMU");
}
}
if (!PMU) {
return false;
}
deviceOnline |= POWERMANAGE_ONLINE;
PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG);
// Set up PMU interrupts
pinMode(PIN_PMU_IRQ, INPUT_PULLUP);
attachInterrupt(PIN_PMU_IRQ, setPmuFlag, FALLING);
if (PMU->getChipModel() == XPOWERS_AXP192) {
PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); //Set up LoRa power rail
PMU->enablePowerOutput(XPOWERS_LDO2); //Enable the LoRa power rail
PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); //Set up OLED power rail
PMU->enablePowerOutput(XPOWERS_DCDC1); //Enable the OLED power rail
PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); //Set up GPS power rail
PMU->enablePowerOutput(XPOWERS_LDO3); //Enable the GPS power rail
PMU->setProtectedChannel(XPOWERS_DCDC1); //Protect the OLED power rail
PMU->setProtectedChannel(XPOWERS_DCDC3); //Protect the ESP32 power rail
PMU->disablePowerOutput(XPOWERS_DCDC2); //Disable unsused power rail DC2
PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); //Disable PMU IRQ
PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_450MA); //Set battery charging current
PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); //Set battery charge-stop voltage
}
else if(PMU->getChipModel() == XPOWERS_AXP2101){
#ifdef TBEAM_SUPREME_SX1262
//Set up the GPS power rail
PMU->setPowerChannelVoltage(XPOWERS_ALDO4, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO4);
//Set up the LoRa power rail
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO3);
//Set up power rail for the M.2 interface
PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300);
PMU->enablePowerOutput(XPOWERS_DCDC3);
if (ESP_SLEEP_WAKEUP_UNDEFINED == esp_sleep_get_wakeup_cause()) {
MESH_DEBUG_PRINTLN("Power off and restart ALDO BLDO..");
PMU->disablePowerOutput(XPOWERS_ALDO1);
PMU->disablePowerOutput(XPOWERS_ALDO2);
PMU->disablePowerOutput(XPOWERS_BLDO1);
delay(250);
}
//Set up power rail for QMC6310U
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO2);
//Set up power rail for BME280 and OLED
PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300);
PMU->enablePowerOutput(XPOWERS_ALDO1);
//Set up pwer rail for SD Card
PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
PMU->enablePowerOutput(XPOWERS_BLDO1);
//Set up power rail BLDO2 to headers
PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300);
PMU->enablePowerOutput(XPOWERS_BLDO2);
//Set up power rail DCDC4 to headers
PMU->setPowerChannelVoltage(XPOWERS_DCDC4, XPOWERS_AXP2101_DCDC4_VOL2_MAX);
PMU->enablePowerOutput(XPOWERS_DCDC4);
//Set up power rail DCDC5 to headers
PMU->setPowerChannelVoltage(XPOWERS_DCDC5, 3300);
PMU->enablePowerOutput(XPOWERS_DCDC5);
//Disable unused power rails
PMU->disablePowerOutput(XPOWERS_DCDC2);
PMU->disablePowerOutput(XPOWERS_DLDO1);
PMU->disablePowerOutput(XPOWERS_DLDO2);
PMU->disablePowerOutput(XPOWERS_VBACKUP);
#else
//Turn off unused power rails
PMU->disablePowerOutput(XPOWERS_DCDC2);
PMU->disablePowerOutput(XPOWERS_DCDC3);
PMU->disablePowerOutput(XPOWERS_DCDC4);
PMU->disablePowerOutput(XPOWERS_DCDC5);
PMU->disablePowerOutput(XPOWERS_ALDO1);
PMU->disablePowerOutput(XPOWERS_ALDO4);
PMU->disablePowerOutput(XPOWERS_BLDO1);
PMU->disablePowerOutput(XPOWERS_BLDO2);
PMU->disablePowerOutput(XPOWERS_DLDO1);
PMU->disablePowerOutput(XPOWERS_DLDO2);
//PMU->disablePowerOutput(XPOWERS_CPULDO);
PMU->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); //Set up GPS RTC power
PMU->enablePowerOutput(XPOWERS_VBACKUP); //Turn on GPS RTC power
PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); //Set up LoRa power rail
PMU->enablePowerOutput(XPOWERS_ALDO2); //Enable LoRa power rail
PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); //Set up GPS power rail
PMU->enablePowerOutput(XPOWERS_ALDO3); //Enable GPS power rail
#endif
PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); //Disable all PMU interrupts
PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); //Set battery charging current to 500mA
PMU->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); //Set battery charging cutoff voltage to 4.2V
}
PMU->clearIrqStatus(); //Clear interrupt flags
PMU->disableTSPinMeasure(); //Disable TS detection, since it is not used
//Enable voltage measurements
PMU->enableSystemVoltageMeasure();
PMU->enableVbusVoltageMeasure();
PMU->enableBattVoltageMeasure();
#ifdef MESH_DEBUG
scanDevices(&Wire);
printPMU();
#endif
// Set the power key off press time
PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S);
return true;
}
#pragma region "Debug code"
// void TBeamBoard::radiotype_detect(){
// static SPIClass spi;
// char chipTypeInfo;
// #if defined(P_LORA_SCLK)
// spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
// #endif
// for(int i = 0; i<radioVersions; i++){
// switch(i){
// case 0:
// CustomSX1262 radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
// int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8);
// if (status != RADIOLIB_ERR_NONE) {
// Serial.print("ERROR: SX1262 not found: ");
// Serial.println(status);
// //delete radio;
// radio = NULL;
// break;
// }
// else{
// MESH_DEBUG_PRINTLN("SX1262 detected");
// P_LORA_BUSY = 32;
// RADIO_CLASS = CustomSX1262;
// WRAPPER_CLASS = CustomSX1262Wrapper;
// SX126X_RX_BOOSTED_GAIN = true;
// SX126X_CURRENT_LIMIT = 140;
// //delete radio;
// radio = NULL;
// break;
// }
// case 1:
// SX1276 radio = new Module(P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1, spi);
// int status1 = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 8);
// if (status1 != RADIOLIB_ERR_NONE) {
// Serial.print("ERROR: SX1272 not found: ");
// Serial.println(status1);
// //delete radio;
// radio = NULL;
// }
// else{
// MESH_DEBUG_PRINTLN("SX1272 detected");
// P_LORA_BUSY = RADIOLIB_NC;
// P_LORA_DIO_2 = 32;
// RADIO_CLASS = CustomSX1272;
// WRAPPER_CLASS = CustomSX1272Wrapper;
// SX127X_CURRENT_LIMIT = 120;
// //delete radio;
// radio = NULL;
// return;
// }
// default:
// }
// }
// }
#pragma endregion
#endif

View File

@@ -0,0 +1,166 @@
#pragma once
#if defined(TBEAM_SUPREME_SX1262) || defined(TBEAM_SX1262) || defined(TBEAM_SX1276)
// Define pin mappings BEFORE including ESP32Board.h so sleep() can use P_LORA_DIO_1
#ifdef TBEAM_SUPREME_SX1262
// LoRa radio module pins for TBeam S3 Supreme SX1262
#define P_LORA_DIO_0 -1 //NC
#define P_LORA_DIO_1 1 //SX1262 IRQ pin
#define P_LORA_NSS 10 //SX1262 SS pin
#define P_LORA_RESET 5 //SX1262 Rest pin
#define P_LORA_BUSY 4 //SX1262 Busy pin
#define P_LORA_SCLK 12 //SX1262 SCLK pin
#define P_LORA_MISO 13 //SX1262 MISO pin
#define P_LORA_MOSI 11 //SX1262 MOSI pin
#define PIN_BOARD_SDA1 42 //SDA for PMU and PFC8563 (RTC)
#define PIN_BOARD_SCL1 41 //SCL for PMU and PFC8563 (RTC)
#define PIN_PMU_IRQ 40 //IRQ pin for PMU
// #define PIN_GPS_RX 9
// #define PIN_GPS_TX 8
// #define PIN_GPS_EN 7
#define P_BOARD_SPI_MOSI 35 //SPI for SD Card and QMI8653 (IMU)
#define P_BOARD_SPI_MISO 37 //SPI for SD Card and QMI8653 (IMU)
#define P_BOARD_SPI_SCK 36 //SPI for SD Card and QMI8653 (IMU)
#define P_BPARD_SPI_CS 47 //Pin for SD Card CS
#define P_BOARD_IMU_CS 34 //Pin for QMI8653 (IMU) CS
#define P_BOARD_IMU_INT 33 //IMU Int pin
#define P_BOARD_RTC_INT 14 //RTC Int pin
//I2C Wire addresses
#define I2C_BME280_ADD 0x76 //BME280 sensor I2C address on Wire
#define I2C_OLED_ADD 0x3C //SH1106 OLED I2C address on Wire
#define I2C_QMC6310U_ADD 0x1C //QMC6310U mag sensor I2C address on Wire
//I2C Wire1 addresses
#define I2C_RTC_ADD 0x51 //RTC I2C address on Wire1
#define I2C_PMU_ADD 0x34 //AXP2101 I2C address on Wire1
#define PMU_WIRE_PORT Wire1
#define RTC_WIRE_PORT Wire1
#endif
#ifdef TBEAM_SX1262
#define P_LORA_BUSY 32
#endif
#ifdef TBEAM_SX1276
#define P_LORA_DIO_2 32
#define P_LORA_BUSY RADIOLIB_NC
#endif
#if defined(TBEAM_SX1262) || defined(TBEAM_SX1276)
// LoRa radio module pins for TBeam
// uint32_t P_LORA_BUSY = 0; //shared, so define at run
// uint32_t P_LORA_DIO_2 = 0; //SX1276 only, so define at run
#define P_LORA_DIO_0 26
#define P_LORA_DIO_1 33
#define P_LORA_NSS 18
#define P_LORA_RESET 23
#define P_LORA_SCLK 5
#define P_LORA_MISO 19
#define P_LORA_MOSI 27
// #define PIN_GPS_RX 34
// #define PIN_GPS_TX 12
#define PIN_PMU_IRQ 35
#define PMU_WIRE_PORT Wire
#define RTC_WIRE_PORT Wire
#define I2C_PMU_ADD 0x34
#endif
// enum RadioType {
// SX1262,
// SX1276
// };
// Include headers AFTER pin definitions so ESP32Board::sleep() can use P_LORA_DIO_1
#include <Wire.h>
#include <Arduino.h>
#include "XPowersLib.h"
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
class TBeamBoard : public ESP32Board {
XPowersLibInterface *PMU = NULL;
//PhysicalLayer * pl;
//RadioType * radio = NULL;
// int radioVersions = 2;
enum {
POWERMANAGE_ONLINE = _BV(0),
DISPLAY_ONLINE = _BV(1),
RADIO_ONLINE = _BV(2),
GPS_ONLINE = _BV(3),
PSRAM_ONLINE = _BV(4),
SDCARD_ONLINE = _BV(5),
AXDL345_ONLINE = _BV(6),
BME280_ONLINE = _BV(7),
BMP280_ONLINE = _BV(8),
BME680_ONLINE = _BV(9),
QMC6310_ONLINE = _BV(10),
QMI8658_ONLINE = _BV(11),
PCF8563_ONLINE = _BV(12),
OSC32768_ONLINE = _BV(13),
};
bool power_init();
//void radiotype_detect();
public:
#ifdef MESH_DEBUG
void printPMU();
void scanDevices(TwoWire *w);
#endif
void begin();
#ifndef TBEAM_SUPREME_SX1262
void onBeforeTransmit() override{
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on - invert pin for SX1276
}
void onAfterTransmit() override{
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off - invert pin for SX1276
}
#endif
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
uint16_t getBattMilliVolts(){
return PMU->getBattVoltage();
}
const char* getManufacturerName() const{
return "LilyGo T-Beam";
}
};
#endif

View File

@@ -0,0 +1,406 @@
#include "SerialBLEInterface.h"
#include <stdio.h>
#include <string.h>
#include "ble_gap.h"
#include "ble_hci.h"
// Magic numbers came from actual testing
#define BLE_HEALTH_CHECK_INTERVAL 10000 // Advertising watchdog check every 10 seconds
#define BLE_RETRY_THROTTLE_MS 250 // Throttle retries to 250ms when queue buildup detected
// Connection parameters (units: interval=1.25ms, timeout=10ms)
#define BLE_MIN_CONN_INTERVAL 12 // 15ms
#define BLE_MAX_CONN_INTERVAL 24 // 30ms
#define BLE_SLAVE_LATENCY 4
#define BLE_CONN_SUP_TIMEOUT 200 // 2000ms
// Advertising parameters
#define BLE_ADV_INTERVAL_MIN 32 // 20ms (units: 0.625ms)
#define BLE_ADV_INTERVAL_MAX 244 // 152.5ms (units: 0.625ms)
#define BLE_ADV_FAST_TIMEOUT 30 // seconds
// RX drain buffer size for overflow protection
#define BLE_RX_DRAIN_BUF_SIZE 32
static SerialBLEInterface* instance = nullptr;
void SerialBLEInterface::onConnect(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: connected handle=0x%04X", connection_handle);
if (instance) {
instance->_conn_handle = connection_handle;
instance->_isDeviceConnected = false;
instance->clearBuffers();
}
}
void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected handle=0x%04X reason=%u", connection_handle, reason);
if (instance) {
if (instance->_conn_handle == connection_handle) {
instance->_conn_handle = BLE_CONN_HANDLE_INVALID;
instance->_isDeviceConnected = false;
instance->clearBuffers();
}
}
}
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle);
if (instance) {
if (instance->isValidConnection(connection_handle, true)) {
instance->_isDeviceConnected = true;
// Connection interval units: 1.25ms, supervision timeout units: 10ms
// Apple: "The product will not read or use the parameters in the Peripheral Preferred Connection Parameters characteristic."
// So we explicitly set it here to make Android & Apple match
ble_gap_conn_params_t conn_params;
conn_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
conn_params.max_conn_interval = BLE_MAX_CONN_INTERVAL;
conn_params.slave_latency = BLE_SLAVE_LATENCY;
conn_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT;
uint32_t err_code = sd_ble_gap_conn_param_update(connection_handle, &conn_params);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("Connection parameter update requested: %u-%ums interval, latency=%u, %ums timeout",
conn_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
conn_params.max_conn_interval * 5 / 4,
conn_params.slave_latency,
conn_params.conn_sup_timeout * 10); // convert to ms (10ms units)
} else {
BLE_DEBUG_PRINTLN("Failed to request connection parameter update: %lu", err_code);
}
} else {
BLE_DEBUG_PRINTLN("onSecured: ignoring stale/duplicate callback");
}
}
}
bool SerialBLEInterface::onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request) {
(void)connection_handle;
(void)passkey;
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing passkey request match=%d", match_request);
return true;
}
void SerialBLEInterface::onPairingComplete(uint16_t connection_handle, uint8_t auth_status) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing complete handle=0x%04X status=%u", connection_handle, auth_status);
if (instance) {
if (instance->isValidConnection(connection_handle)) {
if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing successful");
} else {
BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing failed, disconnecting");
instance->disconnect();
}
} else {
BLE_DEBUG_PRINTLN("onPairingComplete: ignoring stale callback");
}
}
}
void SerialBLEInterface::onBLEEvent(ble_evt_t* evt) {
if (!instance) return;
if (evt->header.evt_id == BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST) {
uint16_t conn_handle = evt->evt.gap_evt.conn_handle;
if (instance->isValidConnection(conn_handle)) {
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: handle=0x%04X, min_interval=%u, max_interval=%u, latency=%u, timeout=%u",
conn_handle,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.min_conn_interval,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.max_conn_interval,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.slave_latency,
evt->evt.gap_evt.params.conn_param_update_request.conn_params.conn_sup_timeout);
uint32_t err_code = sd_ble_gap_conn_param_update(conn_handle, NULL);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("Accepted CONN_PARAM_UPDATE_REQUEST (using PPCP)");
} else {
BLE_DEBUG_PRINTLN("ERROR: Failed to accept CONN_PARAM_UPDATE_REQUEST: 0x%08X", err_code);
}
} else {
BLE_DEBUG_PRINTLN("CONN_PARAM_UPDATE_REQUEST: ignoring stale callback for handle=0x%04X", conn_handle);
}
}
}
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
instance = this;
char charpin[20];
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
// If we want to control BLE LED ourselves, uncomment this:
// Bluefruit.autoConnLed(false);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
char dev_name[32+16];
if (strcmp(name, "@@MAC") == 0) {
ble_gap_addr_t addr;
if (sd_ble_gap_addr_get(&addr) == NRF_SUCCESS) {
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2], addr.addr[1], addr.addr[0]);
}
}
sprintf(dev_name, "%s%s", prefix, name);
// Connection interval units: 1.25ms, supervision timeout units: 10ms
ble_gap_conn_params_t ppcp_params;
ppcp_params.min_conn_interval = BLE_MIN_CONN_INTERVAL;
ppcp_params.max_conn_interval = BLE_MAX_CONN_INTERVAL;
ppcp_params.slave_latency = BLE_SLAVE_LATENCY;
ppcp_params.conn_sup_timeout = BLE_CONN_SUP_TIMEOUT;
uint32_t err_code = sd_ble_gap_ppcp_set(&ppcp_params);
if (err_code == NRF_SUCCESS) {
BLE_DEBUG_PRINTLN("PPCP set: %u-%ums interval, latency=%u, %ums timeout",
ppcp_params.min_conn_interval * 5 / 4, // convert to ms (1.25ms units)
ppcp_params.max_conn_interval * 5 / 4,
ppcp_params.slave_latency,
ppcp_params.conn_sup_timeout * 10); // convert to ms (10ms units)
} else {
BLE_DEBUG_PRINTLN("Failed to set PPCP: %lu", err_code);
}
Bluefruit.setTxPower(BLE_TX_POWER);
Bluefruit.setName(dev_name);
Bluefruit.Security.setMITM(true);
Bluefruit.Security.setPIN(charpin);
Bluefruit.Security.setIOCaps(true, false, false);
Bluefruit.Security.setPairPasskeyCallback(onPairingPasskey);
Bluefruit.Security.setPairCompleteCallback(onPairingComplete);
Bluefruit.Periph.setConnectCallback(onConnect);
Bluefruit.Periph.setDisconnectCallback(onDisconnect);
Bluefruit.Security.setSecuredCallback(onSecured);
Bluefruit.setEventCallback(onBLEEvent);
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
bleuart.setRxCallback(onBleUartRX);
// Register DFU on the main BLE stack so paired clients can discover it
// without switching the device into a separate OTA-only BLE mode first.
bledfu.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bledfu.begin();
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(bleuart);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.setInterval(BLE_ADV_INTERVAL_MIN, BLE_ADV_INTERVAL_MAX);
Bluefruit.Advertising.setFastTimeout(BLE_ADV_FAST_TIMEOUT);
Bluefruit.Advertising.restartOnDisconnect(true);
}
void SerialBLEInterface::clearBuffers() {
send_queue_len = 0;
recv_queue_len = 0;
_last_retry_attempt = 0;
bleuart.flush();
}
void SerialBLEInterface::shiftSendQueueLeft() {
if (send_queue_len > 0) {
send_queue_len--;
for (uint8_t i = 0; i < send_queue_len; i++) {
send_queue[i] = send_queue[i + 1];
}
}
}
void SerialBLEInterface::shiftRecvQueueLeft() {
if (recv_queue_len > 0) {
recv_queue_len--;
for (uint8_t i = 0; i < recv_queue_len; i++) {
recv_queue[i] = recv_queue[i + 1];
}
}
}
bool SerialBLEInterface::isValidConnection(uint16_t handle, bool requireWaitingForSecurity) const {
if (_conn_handle != handle) {
return false;
}
BLEConnection* conn = Bluefruit.Connection(handle);
if (conn == nullptr || !conn->connected()) {
return false;
}
if (requireWaitingForSecurity && _isDeviceConnected) {
return false;
}
return true;
}
bool SerialBLEInterface::isAdvertising() const {
ble_gap_addr_t adv_addr;
uint32_t err_code = sd_ble_gap_adv_addr_get(0, &adv_addr);
return (err_code == NRF_SUCCESS);
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
clearBuffers();
_last_health_check = millis();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.start(0);
}
void SerialBLEInterface::disconnect() {
if (_conn_handle != BLE_CONN_HANDLE_INVALID) {
sd_ble_gap_disconnect(_conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
}
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface: disable");
Bluefruit.Advertising.restartOnDisconnect(false);
Bluefruit.Advertising.stop();
disconnect();
_last_health_check = 0;
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len);
return 0;
}
bool connected = isConnected();
if (connected && len > 0) {
if (send_queue_len >= FRAME_QUEUE_SIZE) {
BLE_DEBUG_PRINTLN("writeFrame(), send_queue is full!");
return 0;
}
send_queue[send_queue_len].len = len;
memcpy(send_queue[send_queue_len].buf, src, len);
send_queue_len++;
return len;
}
return 0;
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
if (send_queue_len > 0) {
if (!isConnected()) {
BLE_DEBUG_PRINTLN("writeBytes: connection invalid, clearing send queue");
send_queue_len = 0;
} else {
unsigned long now = millis();
bool throttle_active = (_last_retry_attempt > 0 && (now - _last_retry_attempt) < BLE_RETRY_THROTTLE_MS);
if (!throttle_active) {
Frame frame_to_send = send_queue[0];
size_t written = bleuart.write(frame_to_send.buf, frame_to_send.len);
if (written == frame_to_send.len) {
BLE_DEBUG_PRINTLN("writeBytes: sz=%u, hdr=%u", (unsigned)frame_to_send.len, (unsigned)frame_to_send.buf[0]);
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else if (written > 0) {
BLE_DEBUG_PRINTLN("writeBytes: partial write, sent=%u of %u, dropping corrupted frame", (unsigned)written, (unsigned)frame_to_send.len);
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else {
if (!isConnected()) {
BLE_DEBUG_PRINTLN("writeBytes failed: connection lost, dropping frame");
_last_retry_attempt = 0;
shiftSendQueueLeft();
} else {
BLE_DEBUG_PRINTLN("writeBytes failed (buffer full), keeping frame for retry");
_last_retry_attempt = now;
}
}
}
}
}
if (recv_queue_len > 0) {
size_t len = recv_queue[0].len;
memcpy(dest, recv_queue[0].buf, len);
BLE_DEBUG_PRINTLN("readBytes: sz=%u, hdr=%u", (unsigned)len, (unsigned)dest[0]);
shiftRecvQueueLeft();
return len;
}
// Advertising watchdog: periodically check if advertising is running, restart if not
// Only run when truly disconnected (no connection handle), not during connection establishment
unsigned long now = millis();
if (_isEnabled && !isConnected() && _conn_handle == BLE_CONN_HANDLE_INVALID) {
if (now - _last_health_check >= BLE_HEALTH_CHECK_INTERVAL) {
_last_health_check = now;
if (!isAdvertising()) {
BLE_DEBUG_PRINTLN("SerialBLEInterface: advertising watchdog - advertising stopped, restarting");
Bluefruit.Advertising.start(0);
}
}
}
return 0;
}
void SerialBLEInterface::onBleUartRX(uint16_t conn_handle) {
if (!instance) {
return;
}
if (instance->_conn_handle != conn_handle || !instance->isConnected()) {
while (instance->bleuart.available() > 0) {
instance->bleuart.read();
}
return;
}
while (instance->bleuart.available() > 0) {
if (instance->recv_queue_len >= FRAME_QUEUE_SIZE) {
while (instance->bleuart.available() > 0) {
instance->bleuart.read();
}
BLE_DEBUG_PRINTLN("onBleUartRX: recv queue full, dropping data");
break;
}
int avail = instance->bleuart.available();
if (avail > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("onBleUartRX: WARN: BLE RX overflow, avail=%d, draining all", avail);
uint8_t drain_buf[BLE_RX_DRAIN_BUF_SIZE];
while (instance->bleuart.available() > 0) {
int chunk = instance->bleuart.available() > BLE_RX_DRAIN_BUF_SIZE ? BLE_RX_DRAIN_BUF_SIZE : instance->bleuart.available();
instance->bleuart.readBytes(drain_buf, chunk);
}
continue;
}
int read_len = avail;
instance->recv_queue[instance->recv_queue_len].len = read_len;
instance->bleuart.readBytes(instance->recv_queue[instance->recv_queue_len].buf, read_len);
instance->recv_queue_len++;
}
}
bool SerialBLEInterface::isConnected() const {
return _isDeviceConnected && Bluefruit.connected() > 0;
}
bool SerialBLEInterface::isWriteBusy() const {
return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3);
}

View File

@@ -0,0 +1,81 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <bluefruit.h>
#ifndef BLE_TX_POWER
#define BLE_TX_POWER 4
#endif
class SerialBLEInterface : public BaseSerialInterface {
BLEDfu bledfu;
BLEUart bleuart;
bool _isEnabled;
bool _isDeviceConnected;
uint16_t _conn_handle;
unsigned long _last_health_check;
unsigned long _last_retry_attempt;
struct Frame {
uint8_t len;
uint8_t buf[MAX_FRAME_SIZE];
};
#define FRAME_QUEUE_SIZE 12
uint8_t send_queue_len;
Frame send_queue[FRAME_QUEUE_SIZE];
uint8_t recv_queue_len;
Frame recv_queue[FRAME_QUEUE_SIZE];
void clearBuffers();
void shiftSendQueueLeft();
void shiftRecvQueueLeft();
bool isValidConnection(uint16_t handle, bool requireWaitingForSecurity = false) const;
bool isAdvertising() const;
static void onConnect(uint16_t connection_handle);
static void onDisconnect(uint16_t connection_handle, uint8_t reason);
static void onSecured(uint16_t connection_handle);
static bool onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request);
static void onPairingComplete(uint16_t connection_handle, uint8_t auth_status);
static void onBLEEvent(ble_evt_t* evt);
static void onBleUartRX(uint16_t conn_handle);
public:
SerialBLEInterface() {
_isEnabled = false;
_isDeviceConnected = false;
_conn_handle = BLE_CONN_HANDLE_INVALID;
_last_health_check = 0;
_last_retry_attempt = 0;
send_queue_len = 0;
recv_queue_len = 0;
}
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param pin_code the BLE security pin
*/
void begin(const char* prefix, char* name, uint32_t pin_code);
void disconnect();
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
bool isConnected() const override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
};
#if BLE_DEBUG_LOGGING && ARDUINO
#include <Arduino.h>
#define BLE_DEBUG_PRINT(F, ...) Serial.printf("BLE: " F, ##__VA_ARGS__)
#define BLE_DEBUG_PRINTLN(F, ...) Serial.printf("BLE: " F "\n", ##__VA_ARGS__)
#else
#define BLE_DEBUG_PRINT(...) {}
#define BLE_DEBUG_PRINTLN(...) {}
#endif

View File

@@ -0,0 +1,92 @@
#pragma once
#include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomLLCC68 : public LLCC68 {
public:
CustomLLCC68(Module *mod) : LLCC68(mod) { }
#ifdef RP2040_PLATFORM
bool std_init(SPIClassRP2040* spi = NULL)
#else
bool std_init(SPIClass* spi = NULL)
#endif
{
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
#ifdef LORA_CR
uint8_t cr = LORA_CR;
#else
uint8_t cr = 5;
#endif
#if defined(P_LORA_SCLK)
#ifdef NRF52_PLATFORM
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#elif defined(RP2040_PLATFORM)
if (spi) {
spi->setMISO(P_LORA_MISO);
//spi->setCS(P_LORA_NSS); // Setting CS results in freeze
spi->setSCK(P_LORA_SCLK);
spi->setMOSI(P_LORA_MOSI);
spi->begin();
}
#else
if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#endif
#endif
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
// if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f
if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) {
tcxo = 0.0f;
status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
}
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
setCRC(1);
#ifdef SX126X_CURRENT_LIMIT
setCurrentLimit(SX126X_CURRENT_LIMIT);
#endif
#ifdef SX126X_DIO2_AS_RF_SWITCH
setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
#if defined(SX126X_RXEN) || defined(SX126X_TXEN)
#ifndef SX1262X_RXEN
#define SX1262X_RXEN RADIOLIB_NC
#endif
#ifndef SX1262X_TXEN
#define SX1262X_TXEN RADIOLIB_NC
#endif
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
return true; // success
}
bool isReceiving() {
uint16_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
return detected;
}
bool getRxBoostedGainMode() {
uint8_t rxGain = 0;
readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1);
return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED);
}
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include "CustomLLCC68.h"
#include "RadioLibWrappers.h"
#include "SX126xReset.h"
class CustomLLCC68Wrapper : public RadioLibWrapper {
public:
CustomLLCC68Wrapper(CustomLLCC68& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override {
return ((CustomLLCC68 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomLLCC68 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomLLCC68 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLLCC68 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomLLCC68 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
void setRxBoostedGainMode(bool en) override {
((CustomLLCC68 *)_radio)->setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const override {
return ((CustomLLCC68 *)_radio)->getRxBoostedGainMode();
}
};

View File

@@ -0,0 +1,39 @@
#pragma once
#include <RadioLib.h>
#include "MeshCore.h"
class CustomLR1110 : public LR1110 {
bool _rx_boosted = false;
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
size_t getPacketLength(bool update) override {
size_t len = LR1110::getPacketLength(update);
if (len == 0 && getIrqStatus() & RADIOLIB_LR11X0_IRQ_HEADER_ERR) {
// we've just received a corrupted packet
// this may have triggered a bug causing subsequent packets to be shifted
// call standby() to return radio to known-good state
// recvRaw will call startReceive() to restart rx
MESH_DEBUG_PRINTLN("LR1110: got header err, calling standby()");
standby();
}
return len;
}
float getFreqMHz() const { return freqMHz; }
int16_t setRxBoostedGainMode(bool en) {
_rx_boosted = en;
return LR1110::setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const { return _rx_boosted; }
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED));
return detected;
}
};

View File

@@ -0,0 +1,34 @@
#pragma once
#include "CustomLR1110.h"
#include "RadioLibWrappers.h"
#include "LR11x0Reset.h"
class CustomLR1110Wrapper : public RadioLibWrapper {
public:
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); }
bool isReceivingPacket() override {
return ((CustomLR1110 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
float rssi = -110;
((CustomLR1110 *)_radio)->getRssiInst(&rssi);
return rssi;
}
void onSendFinished() override {
RadioLibWrapper::onSendFinished();
_radio->setPreambleLength(16); // overcomes weird issues with small and big pkts
}
float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); }
void setRxBoostedGainMode(bool en) override {
((CustomLR1110 *)_radio)->setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const override {
return ((CustomLR1110 *)_radio)->getRxBoostedGainMode();
}
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomSTM32WLx : public STM32WLx {
public:
CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { }
bool isReceiving() {
uint16_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
return detected;
}
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include "CustomSTM32WLx.h"
#include "RadioLibWrappers.h"
#include "SX126xReset.h"
#include <math.h>
class CustomSTM32WLxWrapper : public RadioLibWrapper {
public:
CustomSTM32WLxWrapper(CustomSTM32WLx& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override {
return ((CustomSTM32WLx *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSTM32WLx *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
};

View File

@@ -0,0 +1,100 @@
#pragma once
#include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomSX1262 : public SX1262 {
public:
CustomSX1262(Module *mod) : SX1262(mod) { }
#ifdef RP2040_PLATFORM
bool std_init(SPIClassRP2040* spi = NULL)
#else
bool std_init(SPIClass* spi = NULL)
#endif
{
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
#ifdef LORA_CR
uint8_t cr = LORA_CR;
#else
uint8_t cr = 5;
#endif
#if defined(P_LORA_SCLK)
#ifdef NRF52_PLATFORM
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#elif defined(RP2040_PLATFORM)
if (spi) {
spi->setMISO(P_LORA_MISO);
//spi->setCS(P_LORA_NSS); // Setting CS results in freeze
spi->setSCK(P_LORA_SCLK);
spi->setMOSI(P_LORA_MOSI);
spi->begin();
}
#else
if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#endif
#endif
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
// if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f
if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) {
tcxo = 0.0f;
status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
}
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
setCRC(1);
#ifdef SX126X_CURRENT_LIMIT
setCurrentLimit(SX126X_CURRENT_LIMIT);
#endif
#ifdef SX126X_DIO2_AS_RF_SWITCH
setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
#if defined(SX126X_RXEN) || defined(SX126X_TXEN)
#ifndef SX126X_RXEN
#define SX126X_RXEN RADIOLIB_NC
#endif
#ifndef SX126X_TXEN
#define SX126X_TXEN RADIOLIB_NC
#endif
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
// for improved RX with Heltec v4
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
writeRegister(0x8B5, &r_data, 1);
#endif
return true; // success
}
bool isReceiving() {
uint16_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
return detected;
}
bool getRxBoostedGainMode() {
uint8_t rxGain = 0;
readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1);
return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED);
}
};

View File

@@ -0,0 +1,39 @@
#pragma once
#include "CustomSX1262.h"
#include "RadioLibWrappers.h"
#include "SX126xReset.h"
#ifndef USE_SX1262
#define USE_SX1262
#endif
class CustomSX1262Wrapper : public RadioLibWrapper {
public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override {
return ((CustomSX1262 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1262 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomSX1262 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
virtual void powerOff() override {
((CustomSX1262 *)_radio)->sleep(false);
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
void setRxBoostedGainMode(bool en) override {
((CustomSX1262 *)_radio)->setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const override {
return ((CustomSX1262 *)_radio)->getRxBoostedGainMode();
}
};

View File

@@ -0,0 +1,92 @@
#pragma once
#include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
#define SX126X_IRQ_PREAMBLE_DETECTED 0x04
class CustomSX1268 : public SX1268 {
public:
CustomSX1268(Module *mod) : SX1268(mod) { }
#ifdef RP2040_PLATFORM
bool std_init(SPIClassRP2040* spi = NULL)
#else
bool std_init(SPIClass* spi = NULL)
#endif
{
#ifdef SX126X_DIO3_TCXO_VOLTAGE
float tcxo = SX126X_DIO3_TCXO_VOLTAGE;
#else
float tcxo = 1.6f;
#endif
#ifdef LORA_CR
uint8_t cr = LORA_CR;
#else
uint8_t cr = 5;
#endif
#if defined(P_LORA_SCLK)
#ifdef NRF52_PLATFORM
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#elif defined(RP2040_PLATFORM)
if (spi) {
spi->setMISO(P_LORA_MISO);
//spi->setCS(P_LORA_NSS); // Setting CS results in freeze
spi->setSCK(P_LORA_SCLK);
spi->setMOSI(P_LORA_MOSI);
spi->begin();
}
#else
if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#endif
#endif
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
// if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f
if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) {
tcxo = 0.0f;
status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16, tcxo);
}
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
setCRC(1);
#ifdef SX126X_CURRENT_LIMIT
setCurrentLimit(SX126X_CURRENT_LIMIT);
#endif
#ifdef SX126X_DIO2_AS_RF_SWITCH
setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
#if defined(SX126X_RXEN) || defined(SX126X_TXEN)
#ifndef SX1262X_RXEN
#define SX1262X_RXEN RADIOLIB_NC
#endif
#ifndef SX1262X_TXEN
#define SX1262X_TXEN RADIOLIB_NC
#endif
setRfSwitchPins(SX126X_RXEN, SX126X_TXEN);
#endif
return true; // success
}
bool isReceiving() {
uint16_t irq = getIrqFlags();
bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED);
return detected;
}
bool getRxBoostedGainMode() {
uint8_t rxGain = 0;
readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1);
return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED);
}
};

View File

@@ -0,0 +1,36 @@
#pragma once
#include "CustomSX1268.h"
#include "RadioLibWrappers.h"
#include "SX126xReset.h"
#ifndef USE_SX1268
#define USE_SX1268
#endif
class CustomSX1268Wrapper : public RadioLibWrapper {
public:
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override {
return ((CustomSX1268 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1268 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomSX1268 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); }
void setRxBoostedGainMode(bool en) override {
((CustomSX1268 *)_radio)->setRxBoostedGainMode(en);
}
bool getRxBoostedGainMode() const override {
return ((CustomSX1268 *)_radio)->getRxBoostedGainMode();
}
};

View File

@@ -0,0 +1,90 @@
#pragma once
#include <RadioLib.h>
#define RH_RF95_MODEM_STATUS_CLEAR 0x10
#define RH_RF95_MODEM_STATUS_HEADER_INFO_VALID 0x08
#define RH_RF95_MODEM_STATUS_RX_ONGOING 0x04
#define RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED 0x02
#define RH_RF95_MODEM_STATUS_SIGNAL_DETECTED 0x01
class CustomSX1276 : public SX1276 {
public:
CustomSX1276(Module *mod) : SX1276(mod) { }
#ifdef RP2040_PLATFORM
bool std_init(SPIClassRP2040* spi = NULL)
#else
bool std_init(SPIClass* spi = NULL)
#endif
{
#ifdef LORA_CR
uint8_t cr = LORA_CR;
#else
uint8_t cr = 5;
#endif
#if defined(P_LORA_SCLK)
#ifdef NRF52_PLATFORM
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#elif defined(RP2040_PLATFORM)
if (spi) {
spi->setMISO(P_LORA_MISO);
//spi->setCS(P_LORA_NSS); // Setting CS results in freeze
spi->setSCK(P_LORA_SCLK);
spi->setMOSI(P_LORA_MOSI);
spi->begin();
}
#else
if (spi) spi->begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI);
#endif
#endif
int status = begin(LORA_FREQ, LORA_BW, LORA_SF, cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, LORA_TX_POWER, 16);
// if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f
if (status != RADIOLIB_ERR_NONE) {
Serial.print("ERROR: radio init failed: ");
Serial.println(status);
return false; // fail
}
#ifdef SX127X_CURRENT_LIMIT
setCurrentLimit(SX127X_CURRENT_LIMIT);
#endif
#if defined(SX176X_RXEN) || defined(SX176X_TXEN)
#ifndef SX176X_RXEN
#define SX176X_RXEN RADIOLIB_NC
#endif
#ifndef SX176X_TXEN
#define SX176X_TXEN RADIOLIB_NC
#endif
setRfSwitchPins(SX176X_RXEN, SX176X_TXEN);
#endif
setCRC(1);
return true; // success
}
bool isReceiving() {
return (getModemStatus() &
(RH_RF95_MODEM_STATUS_SIGNAL_DETECTED
| RH_RF95_MODEM_STATUS_SIGNAL_SYNCHRONIZED
| RH_RF95_MODEM_STATUS_HEADER_INFO_VALID)) != 0;
}
int tryScanChannel() {
// start CAD
int16_t state = startChannelScan();
RADIOLIB_ASSERT(state);
// wait for channel activity detected or timeout
unsigned long timeout = millis() + 16;
while(!this->mod->hal->digitalRead(this->mod->getIrq()) && millis() < timeout) {
this->mod->hal->yield();
if(this->mod->hal->digitalRead(this->mod->getGpio())) {
return(RADIOLIB_PREAMBLE_DETECTED);
}
}
return 0; // timed out
}
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include "CustomSX1276.h"
#include "RadioLibWrappers.h"
#ifndef USE_SX1276
#define USE_SX1276
#endif
class CustomSX1276Wrapper : public RadioLibWrapper {
public:
CustomSX1276Wrapper(CustomSX1276& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceivingPacket() override {
return ((CustomSX1276 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1276 *)_radio)->getRSSI(false);
}
float getLastRSSI() const override { return ((CustomSX1276 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1276 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
int sf = ((CustomSX1276 *)_radio)->spreadingFactor;
return packetScoreInt(snr, sf, packet_len);
}
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include <RadioLib.h>
// Full receiver reset for LR11x0-family chips (LR1110, LR1120, LR1121).
// Warm sleep powers down analog, calibrate(0x3F) refreshes all calibration blocks,
// then re-applies RX settings that calibration may reset.
inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) {
radio->sleep(true, 0);
radio->standby(RADIOLIB_LR11X0_STANDBY_RC, true);
radio->calibrate(RADIOLIB_LR11X0_CALIBRATE_ALL);
// calibrate(0x3F) defaults image calibration to 902-928MHz band.
// Re-calibrate for the actual operating frequency (band=4MHz matches RadioLib default).
radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f);
#ifdef RX_BOOSTED_GAIN
radio->setRxBoostedGainMode(RX_BOOSTED_GAIN);
#endif
}

View File

@@ -0,0 +1,203 @@
#define RADIOLIB_STATIC_ONLY 1
#include "RadioLibWrappers.h"
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define STATE_TX_DONE 4
#define STATE_INT_READY 16
#define NUM_NOISE_FLOOR_SAMPLES 64
#define SAMPLING_THRESHOLD 14
static volatile uint8_t state = STATE_IDLE;
// this function is called when a complete packet
// is transmitted by the module
static
#if defined(ESP8266) || defined(ESP32)
ICACHE_RAM_ATTR
#endif
void setFlag(void) {
// we sent a packet, set the flag
state |= STATE_INT_READY;
}
void RadioLibWrapper::begin() {
_radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt
state = STATE_IDLE;
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
setFlag(); // LoRa packet is already received
}
_noise_floor = 0;
_threshold = 0;
// start average out some samples
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
void RadioLibWrapper::idle() {
_radio->standby();
state = STATE_IDLE; // need another startReceive()
}
void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) {
_threshold = threshold;
if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
}
void RadioLibWrapper::doResetAGC() {
_radio->sleep(); // warm sleep to reset analog frontend
}
void RadioLibWrapper::resetAGC() {
// make sure we're not mid-receive of packet!
if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return;
doResetAGC();
state = STATE_IDLE; // trigger a startReceive()
// Reset noise floor sampling so it reconverges from scratch.
// Without this, a stuck _noise_floor of -120 makes the sampling threshold
// too low (-106) to accept normal samples (~-105), self-reinforcing the
// stuck value even after the receiver has recovered.
_noise_floor = 0;
_num_floor_samples = 0;
_floor_sample_sum = 0;
}
void RadioLibWrapper::loop() {
if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) {
if (!isReceivingPacket()) {
int rssi = getCurrentRSSI();
if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD
_num_floor_samples++;
_floor_sample_sum += rssi;
}
}
} else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) {
_noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES;
if (_noise_floor < -120) {
_noise_floor = -120; // clamp to lower bound of -120dBi
}
_floor_sample_sum = 0;
MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor);
}
}
void RadioLibWrapper::startRecv() {
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
}
}
bool RadioLibWrapper::isInRecvMode() const {
return (state & ~STATE_INT_READY) == STATE_RX;
}
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
int len = 0;
if (state & STATE_INT_READY) {
len = _radio->getPacketLength();
if (len > 0) {
if (len > sz) { len = sz; }
int err = _radio->readData(bytes, len);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err);
len = 0;
n_recv_errors++;
} else {
// Serial.print(" readData() -> "); Serial.println(len);
n_recv++;
}
}
state = STATE_IDLE; // need another startReceive()
}
if (state != STATE_RX) {
int err = _radio->startReceive();
if (err == RADIOLIB_ERR_NONE) {
state = STATE_RX;
} else {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive(%d)", err);
}
}
return len;
}
uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {
return _radio->getTimeOnAir(len_bytes) / 1000;
}
bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
_board->onBeforeTransmit();
int err = _radio->startTransmit((uint8_t *) bytes, len);
if (err == RADIOLIB_ERR_NONE) {
state = STATE_TX_WAIT;
return true;
}
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startTransmit(%d)", err);
idle(); // trigger another startRecv()
_board->onAfterTransmit();
return false;
}
bool RadioLibWrapper::isSendComplete() {
if (state & STATE_INT_READY) {
state = STATE_IDLE;
n_sent++;
return true;
}
return false;
}
void RadioLibWrapper::onSendFinished() {
_radio->finishTransmit();
_board->onAfterTransmit();
state = STATE_IDLE;
}
bool RadioLibWrapper::isChannelActive() {
return _threshold == 0
? false // interference check is disabled
: getCurrentRSSI() > _noise_floor + _threshold;
}
float RadioLibWrapper::getLastRSSI() const {
return _radio->getRSSI();
}
float RadioLibWrapper::getLastSNR() const {
return _radio->getSNR();
}
// Approximate SNR threshold per SF for successful reception (based on Semtech datasheets)
static float snr_threshold[] = {
-7.5, // SF7 needs at least -7.5 dB SNR
-10, // SF8 needs at least -10 dB SNR
-12.5, // SF9 needs at least -12.5 dB SNR
-15, // SF10 needs at least -15 dB SNR
-17.5,// SF11 needs at least -17.5 dB SNR
-20 // SF12 needs at least -20 dB SNR
};
float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) {
if (sf < 7) return 0.0f;
if (snr < snr_threshold[sf - 7]) return 0.0f; // Below threshold, no chance of success
auto success_rate_based_on_snr = (snr - snr_threshold[sf - 7]) / 10.0;
auto collision_penalty = 1 - (packet_len / 256.0); // Assuming max packet of 256 bytes
return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty));
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include <Mesh.h>
#include <RadioLib.h>
class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent, n_recv_errors;
int16_t _noise_floor, _threshold;
uint16_t _num_floor_samples;
int32_t _floor_sample_sum;
void idle();
void startRecv();
float packetScoreInt(float snr, int sf, int packet_len);
virtual bool isReceivingPacket() =0;
virtual void doResetAGC();
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }
void begin() override;
virtual void powerOff() { _radio->sleep(); }
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
bool startSendRaw(const uint8_t* bytes, int len) override;
bool isSendComplete() override;
void onSendFinished() override;
bool isInRecvMode() const override;
bool isChannelActive();
bool isReceiving() override {
if (isReceivingPacket()) return true;
return isChannelActive();
}
virtual float getCurrentRSSI() =0;
int getNoiseFloor() const override { return _noise_floor; }
void triggerNoiseFloorCalibrate(int threshold) override;
void resetAGC() override;
void loop() override;
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsRecvErrors() const { return n_recv_errors; }
uint32_t getPacketsSent() const { return n_sent; }
void resetStats() { n_recv = n_sent = n_recv_errors = 0; }
virtual float getLastRSSI() const override;
virtual float getLastSNR() const override;
float packetScore(float snr, int packet_len) override { return packetScoreInt(snr, 10, packet_len); } // assume sf=10
virtual void setRxBoostedGainMode(bool) { }
virtual bool getRxBoostedGainMode() const { return false; }
};
/**
* \brief an RNG impl using the noise from the LoRa radio as entropy.
* NOTE: this is VERY SLOW! Use only for things like creating new LocalIdentity
*/
class RadioNoiseListener : public mesh::RNG {
PhysicalLayer* _radio;
public:
RadioNoiseListener(PhysicalLayer& radio): _radio(&radio) { }
void random(uint8_t* dest, size_t sz) override {
for (int i = 0; i < sz; i++) {
dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF);
}
}
};

View File

@@ -0,0 +1,37 @@
#pragma once
#include <RadioLib.h>
// Full receiver reset for all SX126x-family chips (SX1262, SX1268, LLCC68, STM32WLx).
// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration,
// then re-applies RX settings that calibration may reset.
inline void sx126xResetAGC(SX126x* radio) {
radio->sleep(true);
radio->standby(RADIOLIB_SX126X_STANDBY_RC, true);
uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL;
radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false);
radio->mod->hal->delay(5);
uint32_t start = millis();
while (radio->mod->hal->digitalRead(radio->mod->getGpio())) {
if (millis() - start > 50) break;
radio->mod->hal->yield();
}
// Calibrate(0x7F) defaults image calibration to 902-928MHz band.
// Re-calibrate for the actual operating frequency.
radio->calibrateImage(radio->freqMHz);
#ifdef SX126X_DIO2_AS_RF_SWITCH
radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH);
#endif
#ifdef SX126X_RX_BOOSTED_GAIN
radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN);
#endif
#ifdef SX126X_REGISTER_PATCH
uint8_t r_data = 0;
radio->readRegister(0x8B5, &r_data, 1);
r_data |= 0x01;
radio->writeRegister(0x8B5, &r_data, 1);
#endif
}

View File

@@ -0,0 +1,789 @@
#include "EnvironmentSensorManager.h"
#if ENV_PIN_SDA && ENV_PIN_SCL
#define TELEM_WIRE &Wire1 // Use Wire1 as the I2C bus for Environment Sensors
#else
#define TELEM_WIRE &Wire // Use default I2C bus for Environment Sensors
#endif
#ifdef ENV_INCLUDE_BME680
#ifndef TELEM_BME680_ADDRESS
#define TELEM_BME680_ADDRESS 0x76
#endif
#define TELEM_BME680_SEALEVELPRESSURE_HPA (1013.25)
#include <Adafruit_BME680.h>
static Adafruit_BME680 BME680(TELEM_WIRE);
#endif
#ifdef ENV_INCLUDE_BMP085
#define TELEM_BMP085_SEALEVELPRESSURE_HPA (1013.25)
#include <Adafruit_BMP085.h>
static Adafruit_BMP085 BMP085;
#endif
#if ENV_INCLUDE_AHTX0
#define TELEM_AHTX_ADDRESS 0x38 // AHT10, AHT20 temperature and humidity sensor I2C address
#include <Adafruit_AHTX0.h>
static Adafruit_AHTX0 AHTX0;
#endif
#if ENV_INCLUDE_BME280
#ifndef TELEM_BME280_ADDRESS
#define TELEM_BME280_ADDRESS 0x76 // BME280 environmental sensor I2C address
#endif
#define TELEM_BME280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#include <Adafruit_BME280.h>
static Adafruit_BME280 BME280;
#endif
#if ENV_INCLUDE_BMP280
#ifndef TELEM_BMP280_ADDRESS
#define TELEM_BMP280_ADDRESS 0x76 // BMP280 environmental sensor I2C address
#endif
#define TELEM_BMP280_SEALEVELPRESSURE_HPA (1013.25) // Athmospheric pressure at sea level
#include <Adafruit_BMP280.h>
static Adafruit_BMP280 BMP280(TELEM_WIRE);
#endif
#if ENV_INCLUDE_SHTC3
#include <Adafruit_SHTC3.h>
static Adafruit_SHTC3 SHTC3;
#endif
#if ENV_INCLUDE_SHT4X
#define TELEM_SHT4X_ADDRESS 0x44 //0x44 - 0x46
#include <SensirionI2cSht4x.h>
static SensirionI2cSht4x SHT4X;
#endif
#if ENV_INCLUDE_LPS22HB
#include <Arduino_LPS22HB.h>
LPS22HBClass LPS22HB(*TELEM_WIRE);
#endif
#if ENV_INCLUDE_INA3221
#ifndef TELEM_INA3221_ADDRESS
#define TELEM_INA3221_ADDRESS 0x42 // INA3221 3 channel current sensor I2C address
#endif
#ifndef TELEM_INA3221_SHUNT_VALUE
#define TELEM_INA3221_SHUNT_VALUE 0.100 // most variants will have a 0.1 ohm shunts
#endif
#ifndef TELEM_INA3221_NUM_CHANNELS
#define TELEM_INA3221_NUM_CHANNELS 3
#endif
#include <Adafruit_INA3221.h>
static Adafruit_INA3221 INA3221;
#endif
#if ENV_INCLUDE_INA219
#define TELEM_INA219_ADDRESS 0x40 // INA219 single channel current sensor I2C address
#include <Adafruit_INA219.h>
static Adafruit_INA219 INA219(TELEM_INA219_ADDRESS);
#endif
#if ENV_INCLUDE_INA260
#define TELEM_INA260_ADDRESS 0x41 // INA260 single channel current sensor I2C address
#include <Adafruit_INA260.h>
static Adafruit_INA260 INA260;
#endif
#if ENV_INCLUDE_INA226
#define TELEM_INA226_ADDRESS 0x44
#define TELEM_INA226_SHUNT_VALUE 0.100
#define TELEM_INA226_MAX_AMP 0.8
#include <INA226.h>
static INA226 INA226(TELEM_INA226_ADDRESS, TELEM_WIRE);
#endif
#if ENV_INCLUDE_MLX90614
#define TELEM_MLX90614_ADDRESS 0x5A // MLX90614 IR temperature sensor I2C address
#include <Adafruit_MLX90614.h>
static Adafruit_MLX90614 MLX90614;
#endif
#if ENV_INCLUDE_VL53L0X
#define TELEM_VL53L0X_ADDRESS 0x29 // VL53L0X time-of-flight distance sensor I2C address
#include <Adafruit_VL53L0X.h>
static Adafruit_VL53L0X VL53L0X;
#endif
#if ENV_INCLUDE_RAK12035
#define TELEM_RAK12035_ADDRESS 0x20 // RAK12035 Soil Moisture sensor I2C address
#include "RAK12035_SoilMoisture.h"
static RAK12035_SoilMoisture RAK12035;
#endif
#if ENV_INCLUDE_GPS && defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
#define RAK_WISBLOCK_GPS
#endif
#ifdef RAK_WISBLOCK_GPS
static uint32_t gpsResetPin = 0;
static bool i2cGPSFlag = false;
static bool serialGPSFlag = false;
#define TELEM_RAK12500_ADDRESS 0x42 //RAK12500 Ublox GPS via i2c
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
static SFE_UBLOX_GNSS ublox_GNSS;
class RAK12500LocationProvider : public LocationProvider {
long _lat = 0;
long _lng = 0;
long _alt = 0;
int _sats = 0;
long _epoch = 0;
bool _fix = false;
public:
long getLatitude() override { return _lat; }
long getLongitude() override { return _lng; }
long getAltitude() override { return _alt; }
long satellitesCount() override { return _sats; }
bool isValid() override { return _fix; }
long getTimestamp() override { return _epoch; }
void sendSentence(const char * sentence) override { }
void reset() override { }
void begin() override { }
void stop() override { }
void loop() override {
if (ublox_GNSS.getGnssFixOk(8)) {
_fix = true;
_lat = ublox_GNSS.getLatitude(2) / 10;
_lng = ublox_GNSS.getLongitude(2) / 10;
_alt = ublox_GNSS.getAltitude(2);
_sats = ublox_GNSS.getSIV(2);
} else {
_fix = false;
}
_epoch = ublox_GNSS.getUnixEpoch(2);
}
bool isEnabled() override { return true; }
};
static RAK12500LocationProvider RAK12500_provider;
#endif
bool EnvironmentSensorManager::begin() {
#if ENV_INCLUDE_GPS
#ifdef RAK_WISBLOCK_GPS
rakGPSInit(); //probe base board/sockets for GPS
#else
initBasicGPS();
#endif
#endif
#if ENV_PIN_SDA && ENV_PIN_SCL
#ifdef NRF52_PLATFORM
Wire1.setPins(ENV_PIN_SDA, ENV_PIN_SCL);
Wire1.setClock(100000);
Wire1.begin();
#else
Wire1.begin(ENV_PIN_SDA, ENV_PIN_SCL, 100000);
#endif
MESH_DEBUG_PRINTLN("Second I2C initialized on pins SDA: %d SCL: %d", ENV_PIN_SDA, ENV_PIN_SCL);
#endif
#if ENV_INCLUDE_AHTX0
if (AHTX0.begin(TELEM_WIRE, 0, TELEM_AHTX_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found AHT10/AHT20 at address: %02X", TELEM_AHTX_ADDRESS);
AHTX0_initialized = true;
} else {
AHTX0_initialized = false;
MESH_DEBUG_PRINTLN("AHT10/AHT20 was not found at I2C address %02X", TELEM_AHTX_ADDRESS);
}
#endif
#if ENV_INCLUDE_BME680
if (BME680.begin(TELEM_BME680_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BME680 at address: %02X", TELEM_BME680_ADDRESS);
BME680_initialized = true;
} else {
BME680_initialized = false;
MESH_DEBUG_PRINTLN("BME680 was not found at I2C address %02X", TELEM_BME680_ADDRESS);
}
#endif
#if ENV_INCLUDE_BME280
if (BME280.begin(TELEM_BME280_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found BME280 at address: %02X", TELEM_BME280_ADDRESS);
MESH_DEBUG_PRINTLN("BME sensor ID: %02X", BME280.sensorID());
// Reduce self-heating: single-shot conversions, light oversampling, long standby.
BME280.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF,
Adafruit_BME280::STANDBY_MS_1000);
BME280_initialized = true;
} else {
BME280_initialized = false;
MESH_DEBUG_PRINTLN("BME280 was not found at I2C address %02X", TELEM_BME280_ADDRESS);
}
#endif
#if ENV_INCLUDE_BMP280
if (BMP280.begin(TELEM_BMP280_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found BMP280 at address: %02X", TELEM_BMP280_ADDRESS);
MESH_DEBUG_PRINTLN("BMP sensor ID: %02X", BMP280.sensorID());
BMP280_initialized = true;
} else {
BMP280_initialized = false;
MESH_DEBUG_PRINTLN("BMP280 was not found at I2C address %02X", TELEM_BMP280_ADDRESS);
}
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor: SHTC3");
SHTC3_initialized = true;
} else {
SHTC3_initialized = false;
MESH_DEBUG_PRINTLN("SHTC3 was not found at I2C address %02X", 0x70);
}
#endif
#if ENV_INCLUDE_SHT4X
SHT4X.begin(*TELEM_WIRE, TELEM_SHT4X_ADDRESS);
uint32_t serialNumber = 0;
int16_t sht4x_error;
sht4x_error = SHT4X.serialNumber(serialNumber);
if (sht4x_error == 0) {
MESH_DEBUG_PRINTLN("Found SHT4X at address: %02X", TELEM_SHT4X_ADDRESS);
SHT4X_initialized = true;
} else {
SHT4X_initialized = false;
MESH_DEBUG_PRINTLN("SHT4X was not found at I2C address %02X", TELEM_SHT4X_ADDRESS);
}
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB.begin()) {
MESH_DEBUG_PRINTLN("Found sensor: LPS22HB");
LPS22HB_initialized = true;
} else {
LPS22HB_initialized = false;
MESH_DEBUG_PRINTLN("LPS22HB was not found at I2C address %02X", 0x5C);
}
#endif
#if ENV_INCLUDE_INA3221
if (INA3221.begin(TELEM_INA3221_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA3221 at address: %02X", TELEM_INA3221_ADDRESS);
MESH_DEBUG_PRINTLN("%04X %04X", INA3221.getDieID(), INA3221.getManufacturerID());
for(int i = 0; i < 3; i++) {
INA3221.setShuntResistance(i, TELEM_INA3221_SHUNT_VALUE);
}
INA3221_initialized = true;
} else {
INA3221_initialized = false;
MESH_DEBUG_PRINTLN("INA3221 was not found at I2C address %02X", TELEM_INA3221_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA219
if (INA219.begin(TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA219 at address: %02X", TELEM_INA219_ADDRESS);
INA219_initialized = true;
} else {
INA219_initialized = false;
MESH_DEBUG_PRINTLN("INA219 was not found at I2C address %02X", TELEM_INA219_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA260
if (INA260.begin(TELEM_INA260_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found INA260 at address: %02X", TELEM_INA260_ADDRESS);
INA260_initialized = true;
} else {
INA260_initialized = false;
MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS);
}
#endif
#if ENV_INCLUDE_INA226
if (INA226.begin()) {
MESH_DEBUG_PRINTLN("Found INA226 at address: %02X", TELEM_INA226_ADDRESS);
INA226.setMaxCurrentShunt(TELEM_INA226_MAX_AMP, TELEM_INA226_SHUNT_VALUE);
INA226_initialized = true;
} else {
INA226_initialized = false;
MESH_DEBUG_PRINTLN("INA226 was not found at I2C address %02X", TELEM_INA226_ADDRESS);
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614.begin(TELEM_MLX90614_ADDRESS, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found MLX90614 at address: %02X", TELEM_MLX90614_ADDRESS);
MLX90614_initialized = true;
} else {
MLX90614_initialized = false;
MESH_DEBUG_PRINTLN("MLX90614 was not found at I2C address %02X", TELEM_MLX90614_ADDRESS);
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X.begin(TELEM_VL53L0X_ADDRESS, false, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found VL53L0X at address: %02X", TELEM_VL53L0X_ADDRESS);
VL53L0X_initialized = true;
} else {
VL53L0X_initialized = false;
MESH_DEBUG_PRINTLN("VL53L0X was not found at I2C address %02X", TELEM_VL53L0X_ADDRESS);
}
#endif
#if ENV_INCLUDE_BMP085
// First argument is MODE (aka oversampling)
// choose ULTRALOWPOWER
if (BMP085.begin(0, TELEM_WIRE)) {
MESH_DEBUG_PRINTLN("Found sensor BMP085");
BMP085_initialized = true;
} else {
BMP085_initialized = false;
MESH_DEBUG_PRINTLN("BMP085 was not found at I2C address %02X", 0x77);
}
#endif
#if ENV_INCLUDE_RAK12035
RAK12035.setup(*TELEM_WIRE);
if (RAK12035.begin(TELEM_RAK12035_ADDRESS)) {
MESH_DEBUG_PRINTLN("Found sensor RAK12035 at address: %02X", TELEM_RAK12035_ADDRESS);
RAK12035_initialized = true;
} else {
RAK12035_initialized = false;
MESH_DEBUG_PRINTLN("RAK12035 was not found at I2C address %02X", TELEM_RAK12035_ADDRESS);
}
#endif
return true;
}
bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) {
next_available_channel = TELEM_CHANNEL_SELF + 1;
if (requester_permissions & TELEM_PERM_LOCATION && gps_active) {
telemetry.addGPS(TELEM_CHANNEL_SELF, node_lat, node_lon, node_altitude); // allow lat/lon via telemetry even if no GPS is detected
}
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
#if ENV_INCLUDE_AHTX0
if (AHTX0_initialized) {
sensors_event_t humidity, temp;
AHTX0.getEvent(&humidity, &temp);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
}
#endif
#if ENV_INCLUDE_BME680
if (BME680_initialized) {
if (BME680.performReading()) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME680.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME680.humidity);
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME680.pressure / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, 44330.0 * (1.0 - pow((BME680.pressure / 100) / TELEM_BME680_SEALEVELPRESSURE_HPA, 0.1903)));
telemetry.addAnalogInput(next_available_channel, BME680.gas_resistance);
next_available_channel++;
}
}
#endif
#if ENV_INCLUDE_BME280
if (BME280_initialized) {
if (BME280.takeForcedMeasurement()) { // trigger a fresh reading in forced mode
telemetry.addTemperature(TELEM_CHANNEL_SELF, BME280.readTemperature());
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, BME280.readHumidity());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BME280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BME280.readAltitude(TELEM_BME280_SEALEVELPRESSURE_HPA));
}
}
#endif
#if ENV_INCLUDE_BMP280
if (BMP280_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP280.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP280.readPressure()/100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP280.readAltitude(TELEM_BMP280_SEALEVELPRESSURE_HPA));
}
#endif
#if ENV_INCLUDE_SHTC3
if (SHTC3_initialized) {
sensors_event_t humidity, temp;
SHTC3.getEvent(&humidity, &temp);
telemetry.addTemperature(TELEM_CHANNEL_SELF, temp.temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, humidity.relative_humidity);
}
#endif
#if ENV_INCLUDE_SHT4X
if (SHT4X_initialized) {
float sht4x_humidity, sht4x_temperature;
int16_t sht4x_error;
sht4x_error = SHT4X.measureLowestPrecision(sht4x_temperature, sht4x_humidity);
if (sht4x_error == 0) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, sht4x_temperature);
telemetry.addRelativeHumidity(TELEM_CHANNEL_SELF, sht4x_humidity);
}
}
#endif
#if ENV_INCLUDE_LPS22HB
if (LPS22HB_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, LPS22HB.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, LPS22HB.readPressure() * 10); // convert kPa to hPa
}
#endif
#if ENV_INCLUDE_INA3221
if (INA3221_initialized) {
for(int i = 0; i < TELEM_INA3221_NUM_CHANNELS; i++) {
// add only enabled INA3221 channels to telemetry
if (INA3221.isChannelEnabled(i)) {
float voltage = INA3221.getBusVoltage(i);
float current = INA3221.getCurrentAmps(i);
telemetry.addVoltage(next_available_channel, voltage);
telemetry.addCurrent(next_available_channel, current);
telemetry.addPower(next_available_channel, voltage * current);
next_available_channel++;
}
}
}
#endif
#if ENV_INCLUDE_INA219
if (INA219_initialized) {
telemetry.addVoltage(next_available_channel, INA219.getBusVoltage_V());
telemetry.addCurrent(next_available_channel, INA219.getCurrent_mA() / 1000);
telemetry.addPower(next_available_channel, INA219.getPower_mW() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA260
if (INA260_initialized) {
telemetry.addVoltage(next_available_channel, INA260.readBusVoltage() / 1000);
telemetry.addCurrent(next_available_channel, INA260.readCurrent() / 1000);
telemetry.addPower(next_available_channel, INA260.readPower() / 1000);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_INA226
if (INA226_initialized) {
telemetry.addVoltage(next_available_channel, INA226.getBusVoltage());
telemetry.addCurrent(next_available_channel, INA226.getCurrent_mA() / 1000.0);
telemetry.addPower(next_available_channel, INA226.getPower_mW() / 1000.0);
next_available_channel++;
}
#endif
#if ENV_INCLUDE_MLX90614
if (MLX90614_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, MLX90614.readObjectTempC());
telemetry.addTemperature(TELEM_CHANNEL_SELF + 1, MLX90614.readAmbientTempC());
}
#endif
#if ENV_INCLUDE_VL53L0X
if (VL53L0X_initialized) {
VL53L0X_RangingMeasurementData_t measure;
VL53L0X.rangingTest(&measure, false); // pass in 'true' to get debug data
if (measure.RangeStatus != 4) { // phase failures
telemetry.addDistance(TELEM_CHANNEL_SELF, measure.RangeMilliMeter / 1000.0f); // convert mm to m
} else {
telemetry.addDistance(TELEM_CHANNEL_SELF, 0.0f); // no valid measurement
}
}
#endif
#if ENV_INCLUDE_BMP085
if (BMP085_initialized) {
telemetry.addTemperature(TELEM_CHANNEL_SELF, BMP085.readTemperature());
telemetry.addBarometricPressure(TELEM_CHANNEL_SELF, BMP085.readPressure() / 100);
telemetry.addAltitude(TELEM_CHANNEL_SELF, BMP085.readAltitude(TELEM_BMP085_SEALEVELPRESSURE_HPA * 100));
}
#endif
#if ENV_INCLUDE_RAK12035
if (RAK12035_initialized) {
// RAK12035 Telemetry is Channel 2
telemetry.addTemperature(2, RAK12035.get_sensor_temperature());
telemetry.addPercentage(2, RAK12035.get_sensor_moisture());
// RAK12035 CALIBRATION Telemetry is Channel 3, if enabled
#ifdef ENABLE_RAK12035_CALIBRATION
// Calibration Data Screen is Channel 3
float cap = RAK12035.get_sensor_capacitance();
float _wet = RAK12035.get_humidity_full();
float _dry = RAK12035.get_humidity_zero();
telemetry.addFrequency(3, cap);
telemetry.addTemperature(3, _wet);
telemetry.addPower(3, _dry);
if(cap > _dry){
RAK12035.set_humidity_zero(cap);
}
if(cap < _wet){
RAK12035.set_humidity_full(cap);
}
#endif
}
#endif
}
return true;
}
int EnvironmentSensorManager::getNumSettings() const {
int settings = 0;
#if ENV_INCLUDE_GPS
if (gps_detected) settings++; // only show GPS setting if GPS is detected
#endif
return settings;
}
const char* EnvironmentSensorManager::getSettingName(int i) const {
int settings = 0;
#if ENV_INCLUDE_GPS
if (gps_detected && i == settings++) {
return "gps";
}
#endif
// convenient way to add params (needed for some tests)
// if (i == settings++) return "param.2";
return NULL;
}
const char* EnvironmentSensorManager::getSettingValue(int i) const {
int settings = 0;
#if ENV_INCLUDE_GPS
if (gps_detected && i == settings++) {
return gps_active ? "1" : "0";
}
#endif
// convenient way to add params ...
// if (i == settings++) return "2";
return NULL;
}
bool EnvironmentSensorManager::setSettingValue(const char* name, const char* value) {
#if ENV_INCLUDE_GPS
if (gps_detected && strcmp(name, "gps") == 0) {
if (strcmp(value, "0") == 0) {
stop_gps();
} else {
start_gps();
}
return true;
}
if (strcmp(name, "gps_interval") == 0) {
uint32_t interval_seconds = atoi(value);
if (interval_seconds > 0) {
gps_update_interval_sec = interval_seconds;
} else {
gps_update_interval_sec = 1; // Default to 1 second if 0
}
return true;
}
#endif
return false; // not supported
}
#if ENV_INCLUDE_GPS
void EnvironmentSensorManager::initBasicGPS() {
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
#ifdef GPS_BAUD_RATE
Serial1.begin(GPS_BAUD_RATE);
#else
Serial1.begin(9600);
#endif
// Try to detect if GPS is physically connected to determine if we should expose the setting
_location->begin();
_location->reset();
#ifndef PIN_GPS_EN
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
#endif
// Give GPS a moment to power up and send data
delay(1000);
// We'll consider GPS detected if we see any data on Serial1
#ifdef ENV_SKIP_GPS_DETECT
gps_detected = true;
#else
gps_detected = (Serial1.available() > 0);
#endif
if (gps_detected) {
MESH_DEBUG_PRINTLN("GPS detected");
#ifdef PERSISTANT_GPS
gps_active = true;
return;
#endif
} else {
MESH_DEBUG_PRINTLN("No GPS detected");
}
_location->stop();
gps_active = false; //Set GPS visibility off until setting is changed
}
// gps code for rak might be moved to MicroNMEALoactionProvider
// or make a new location provider ...
#ifdef RAK_WISBLOCK_GPS
void EnvironmentSensorManager::rakGPSInit(){
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
#ifdef GPS_BAUD_RATE
Serial1.begin(GPS_BAUD_RATE);
#else
Serial1.begin(9600);
#endif
//search for the correct IO standby pin depending on socket used
if(gpsIsAwake(WB_IO2)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19007/10");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket A");
}
else if(gpsIsAwake(WB_IO4)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19003/9");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket C");
}
else if(gpsIsAwake(WB_IO5)){
// MESH_DEBUG_PRINTLN("RAK base board is RAK19001/11");
// MESH_DEBUG_PRINTLN("GPS is installed on Socket F");
}
else{
MESH_DEBUG_PRINTLN("No GPS found");
gps_active = false;
gps_detected = false;
Serial1.end();
return;
}
#ifndef FORCE_GPS_ALIVE // for use with repeaters, until GPS toggle is implimented
//Now that GPS is found and set up, set to sleep for initial state
stop_gps();
#endif
}
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin){
//set initial waking state
pinMode(ioPin,OUTPUT);
digitalWrite(ioPin,LOW);
delay(500);
digitalWrite(ioPin,HIGH);
delay(500);
//Try to init RAK12500 on I2C
if (ublox_GNSS.begin(Wire) == true){
MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i",ioPin);
ublox_GNSS.setI2COutput(COM_TYPE_UBX);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GPS);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GALILEO);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GLONASS);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_SBAS);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_BEIDOU);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_IMES);
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_QZSS);
ublox_GNSS.setMeasurementRate(1000);
ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
gpsResetPin = ioPin;
i2cGPSFlag = true;
gps_active = true;
gps_detected = true;
_location = &RAK12500_provider;
return true;
} else if (Serial1.available()) {
MESH_DEBUG_PRINTLN("Serial GPS init correctly and is turned on");
if(PIN_GPS_EN){
gpsResetPin = PIN_GPS_EN;
}
serialGPSFlag = true;
gps_active = true;
gps_detected = true;
return true;
}
pinMode(ioPin, INPUT);
MESH_DEBUG_PRINTLN("GPS did not init with this IO pin... try the next");
return false;
}
#endif
void EnvironmentSensorManager::start_gps() {
gps_active = true;
#ifdef RAK_WISBLOCK_GPS
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, HIGH);
return;
#endif
_location->begin();
_location->reset();
#ifndef PIN_GPS_EN
MESH_DEBUG_PRINTLN("Start GPS is N/A on this board. Actual GPS state unchanged");
#endif
}
void EnvironmentSensorManager::stop_gps() {
gps_active = false;
#ifdef RAK_WISBLOCK_GPS
pinMode(gpsResetPin, OUTPUT);
digitalWrite(gpsResetPin, LOW);
return;
#endif
_location->stop();
#ifndef PIN_GPS_EN
MESH_DEBUG_PRINTLN("Stop GPS is N/A on this board. Actual GPS state unchanged");
#endif
}
void EnvironmentSensorManager::loop() {
static long next_gps_update = 0;
#if ENV_INCLUDE_GPS
if (gps_active) {
_location->loop();
}
if (millis() > next_gps_update) {
if(gps_active){
#ifdef RAK_WISBLOCK_GPS
if ((i2cGPSFlag || serialGPSFlag) && _location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
}
#else
if (_location->isValid()) {
node_lat = ((double)_location->getLatitude())/1000000.;
node_lon = ((double)_location->getLongitude())/1000000.;
MESH_DEBUG_PRINTLN("lat %f lon %f", node_lat, node_lon);
node_altitude = ((double)_location->getAltitude()) / 1000.0;
MESH_DEBUG_PRINTLN("lat %f lon %f alt %f", node_lat, node_lon, node_altitude);
}
#endif
}
next_gps_update = millis() + (gps_update_interval_sec * 1000);
}
#endif
}
#endif

View File

@@ -0,0 +1,59 @@
#pragma once
#include <Mesh.h>
#include <helpers/SensorManager.h>
#include <helpers/sensors/LocationProvider.h>
class EnvironmentSensorManager : public SensorManager {
protected:
int next_available_channel = TELEM_CHANNEL_SELF + 1;
bool AHTX0_initialized = false;
bool BME280_initialized = false;
bool BMP280_initialized = false;
bool INA3221_initialized = false;
bool INA219_initialized = false;
bool INA260_initialized = false;
bool INA226_initialized = false;
bool SHTC3_initialized = false;
bool LPS22HB_initialized = false;
bool MLX90614_initialized = false;
bool VL53L0X_initialized = false;
bool SHT4X_initialized = false;
bool BME680_initialized = false;
bool BMP085_initialized = false;
bool RAK12035_initialized = false;
bool gps_detected = false;
bool gps_active = false;
uint32_t gps_update_interval_sec = 1; // Default 1 second
#if ENV_INCLUDE_GPS
LocationProvider* _location;
void start_gps();
void stop_gps();
void initBasicGPS();
#ifdef RAK_BOARD
void rakGPSInit();
bool gpsIsAwake(uint8_t ioPin);
#endif
#endif
public:
#if ENV_INCLUDE_GPS
EnvironmentSensorManager(LocationProvider &location): _location(&location){};
LocationProvider* getLocationProvider() { return _location; }
#else
EnvironmentSensorManager(){};
#endif
bool begin() override;
bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override;
#if ENV_INCLUDE_GPS
void loop() override;
#endif
int getNumSettings() const override;
const char* getSettingName(int i) const override;
const char* getSettingValue(int i) const override;
bool setSettingValue(const char* name, const char* value) override;
};

View File

@@ -0,0 +1,223 @@
#pragma once
#include <stdint.h>
#define LPP_DIGITAL_INPUT 0 // 1 byte
#define LPP_DIGITAL_OUTPUT 1 // 1 byte
#define LPP_ANALOG_INPUT 2 // 2 bytes, 0.01 signed
#define LPP_ANALOG_OUTPUT 3 // 2 bytes, 0.01 signed
#define LPP_GENERIC_SENSOR 100 // 4 bytes, unsigned
#define LPP_LUMINOSITY 101 // 2 bytes, 1 lux unsigned
#define LPP_PRESENCE 102 // 1 byte, bool
#define LPP_TEMPERATURE 103 // 2 bytes, 0.1°C signed
#define LPP_RELATIVE_HUMIDITY 104 // 1 byte, 0.5% unsigned
#define LPP_ACCELEROMETER 113 // 2 bytes per axis, 0.001G
#define LPP_BAROMETRIC_PRESSURE 115 // 2 bytes 0.1hPa unsigned
#define LPP_VOLTAGE 116 // 2 bytes 0.01V unsigned
#define LPP_CURRENT 117 // 2 bytes 0.001A unsigned
#define LPP_FREQUENCY 118 // 4 bytes 1Hz unsigned
#define LPP_PERCENTAGE 120 // 1 byte 1-100% unsigned
#define LPP_ALTITUDE 121 // 2 byte 1m signed
#define LPP_CONCENTRATION 125 // 2 bytes, 1 ppm unsigned
#define LPP_POWER 128 // 2 byte, 1W, unsigned
#define LPP_DISTANCE 130 // 4 byte, 0.001m, unsigned
#define LPP_ENERGY 131 // 4 byte, 0.001kWh, unsigned
#define LPP_DIRECTION 132 // 2 bytes, 1deg, unsigned
#define LPP_UNIXTIME 133 // 4 bytes, unsigned
#define LPP_GYROMETER 134 // 2 bytes per axis, 0.01 °/s
#define LPP_COLOUR 135 // 1 byte per RGB Color
#define LPP_GPS 136 // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
#define LPP_SWITCH 142 // 1 byte, 0/1
#define LPP_POLYLINE 240 // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
// Multipliers
#define LPP_DIGITAL_INPUT_MULT 1
#define LPP_DIGITAL_OUTPUT_MULT 1
#define LPP_ANALOG_INPUT_MULT 100
#define LPP_ANALOG_OUTPUT_MULT 100
#define LPP_GENERIC_SENSOR_MULT 1
#define LPP_LUMINOSITY_MULT 1
#define LPP_PRESENCE_MULT 1
#define LPP_TEMPERATURE_MULT 10
#define LPP_RELATIVE_HUMIDITY_MULT 2
#define LPP_ACCELEROMETER_MULT 1000
#define LPP_BAROMETRIC_PRESSURE_MULT 10
#define LPP_VOLTAGE_MULT 100
#define LPP_CURRENT_MULT 1000
#define LPP_FREQUENCY_MULT 1
#define LPP_PERCENTAGE_MULT 1
#define LPP_ALTITUDE_MULT 1
#define LPP_POWER_MULT 1
#define LPP_DISTANCE_MULT 1000
#define LPP_ENERGY_MULT 1000
#define LPP_DIRECTION_MULT 1
#define LPP_UNIXTIME_MULT 1
#define LPP_GYROMETER_MULT 100
#define LPP_GPS_LAT_LON_MULT 10000
#define LPP_GPS_ALT_MULT 100
#define LPP_SWITCH_MULT 1
#define LPP_CONCENTRATION_MULT 1
#define LPP_COLOUR_MULT 1
#define LPP_ERROR_OK 0
#define LPP_ERROR_OVERFLOW 1
#define LPP_ERROR_UNKOWN_TYPE 2
class LPPReader {
const uint8_t* _buf;
uint8_t _len;
uint8_t _pos;
float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
uint32_t value = 0;
for (uint8_t i = 0; i < size; i++) {
value = (value << 8) + buffer[i];
}
int sign = 1;
if (is_signed) {
uint32_t bit = 1ul << ((size * 8) - 1);
if ((value & bit) == bit) {
value = (bit << 1) - value;
sign = -1;
}
}
return sign * ((float) value / multiplier);
}
public:
LPPReader(const uint8_t buf[], uint8_t len) : _buf(buf), _len(len), _pos(0) { }
void reset() {
_pos = 0;
}
bool readHeader(uint8_t& channel, uint8_t& type) {
if (_pos + 2 < _len) {
channel = _buf[_pos++];
type = _buf[_pos++];
return channel != 0; // channel 0 is End-of-data
}
return false; // end-of-buffer
}
bool readGPS(float& lat, float& lon, float& alt) {
lat = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
lon = getFloat(&_buf[_pos], 3, 10000, true); _pos += 3;
alt = getFloat(&_buf[_pos], 3, 100, true); _pos += 3;
return _pos <= _len;
}
bool readVoltage(float& voltage) {
voltage = getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
return _pos <= _len;
}
bool readCurrent(float& amps) {
amps = getFloat(&_buf[_pos], 2, 1000, true); _pos += 2;
return _pos <= _len;
}
bool readPower(float& watts) {
watts = getFloat(&_buf[_pos], 2, 1, false); _pos += 2;
return _pos <= _len;
}
bool readTemperature(float& degrees_c) {
degrees_c = getFloat(&_buf[_pos], 2, 10, true); _pos += 2;
return _pos <= _len;
}
bool readPressure(float& pa) {
pa = getFloat(&_buf[_pos], 2, 10, false); _pos += 2;
return _pos <= _len;
}
bool readRelativeHumidity(float& pct) {
pct = getFloat(&_buf[_pos], 1, 2, false); _pos += 1;
return _pos <= _len;
}
bool readAltitude(float& m) {
m = getFloat(&_buf[_pos], 2, 1, true); _pos += 2;
return _pos <= _len;
}
void skipData(uint8_t type) {
switch (type) {
case LPP_GPS:
_pos += 9; break;
case LPP_POLYLINE:
_pos += 8; break; // TODO: this is MINIMIUM
case LPP_GYROMETER:
case LPP_ACCELEROMETER:
_pos += 6; break;
case LPP_GENERIC_SENSOR:
case LPP_FREQUENCY:
case LPP_DISTANCE:
case LPP_ENERGY:
case LPP_UNIXTIME:
_pos += 4; break;
case LPP_COLOUR:
_pos += 3; break;
case LPP_ANALOG_INPUT:
case LPP_ANALOG_OUTPUT:
case LPP_LUMINOSITY:
case LPP_TEMPERATURE:
case LPP_CONCENTRATION:
case LPP_BAROMETRIC_PRESSURE:
case LPP_ALTITUDE:
case LPP_VOLTAGE:
case LPP_CURRENT:
case LPP_DIRECTION:
case LPP_POWER:
_pos += 2; break;
default:
_pos++;
}
}
};
class LPPWriter {
uint8_t* _buf;
uint8_t _max_len;
uint8_t _len;
void write(uint16_t value) {
_buf[_len++] = (value >> 8) & 0xFF; // MSB
_buf[_len++] = value & 0xFF; // LSB
}
public:
LPPWriter(uint8_t buf[], uint8_t max_len): _buf(buf), _max_len(max_len), _len(0) { }
bool writeVoltage(uint8_t channel, float voltage) {
if (_len + 4 <= _max_len) {
_buf[_len++] = channel;
_buf[_len++] = LPP_VOLTAGE;
uint16_t value = voltage * 100;
write(value);
return true;
}
return false;
}
bool writeGPS(uint8_t channel, float lat, float lon, float alt) {
if (_len + 11 <= _max_len) {
_buf[_len++] = channel;
_buf[_len++] = LPP_GPS;
int32_t lati = lat * 10000; // we lose some precision :-(
int32_t loni = lon * 10000;
int32_t alti = alt * 100;
_buf[_len++] = lati >> 16;
_buf[_len++] = lati >> 8;
_buf[_len++] = lati;
_buf[_len++] = loni >> 16;
_buf[_len++] = loni >> 8;
_buf[_len++] = loni;
_buf[_len++] = alti >> 16;
_buf[_len++] = alti >> 8;
_buf[_len++] = alti;
return true;
}
return false;
}
uint8_t length() { return _len; }
};

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Mesh.h"
class LocationProvider {
protected:
bool _time_sync_needed = true;
public:
virtual void syncTime() { _time_sync_needed = true; }
virtual bool waitingTimeSync() { return _time_sync_needed; }
virtual long getLatitude() = 0;
virtual long getLongitude() = 0;
virtual long getAltitude() = 0;
virtual long satellitesCount() = 0;
virtual bool isValid() = 0;
virtual long getTimestamp() = 0;
virtual void sendSentence(const char * sentence);
virtual void reset() = 0;
virtual void begin() = 0;
virtual void stop() = 0;
virtual void loop() = 0;
virtual bool isEnabled() = 0;
};

View File

@@ -0,0 +1,164 @@
#pragma once
#include "LocationProvider.h"
#include <MicroNMEA.h>
#include <RTClib.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef GPS_EN
#ifdef PIN_GPS_EN
#define GPS_EN PIN_GPS_EN
#else
#define GPS_EN (-1)
#endif
#endif
#ifndef PIN_GPS_EN_ACTIVE
#define PIN_GPS_EN_ACTIVE HIGH
#endif
#ifndef GPS_RESET
#ifdef PIN_GPS_RESET
#define GPS_RESET PIN_GPS_RESET
#else
#define GPS_RESET (-1)
#endif
#endif
#ifndef GPS_RESET_FORCE
#ifdef PIN_GPS_RESET_ACTIVE
#define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE
#else
#define GPS_RESET_FORCE LOW
#endif
#endif
class MicroNMEALocationProvider : public LocationProvider {
char _nmeaBuffer[100];
MicroNMEA nmea;
mesh::RTCClock* _clock;
Stream* _gps_serial;
RefCountedDigitalPin* _peripher_power;
int8_t _claims = 0;
int _pin_reset;
int _pin_en;
long next_check = 0;
long time_valid = 0;
unsigned long _last_time_sync = 0;
static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes
public :
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
_gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) {
if (_pin_reset != -1) {
pinMode(_pin_reset, OUTPUT);
digitalWrite(_pin_reset, GPS_RESET_FORCE);
}
if (_pin_en != -1) {
pinMode(_pin_en, OUTPUT);
digitalWrite(_pin_en, LOW);
}
}
void claim() {
_claims++;
if (_claims > 0) {
if (_peripher_power) _peripher_power->claim();
}
}
void release() {
if (_claims == 0) return; // avoid negative _claims
_claims--;
if (_peripher_power) _peripher_power->release();
}
void begin() override {
claim();
if (_pin_en != -1) {
digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE);
}
if (_pin_reset != -1) {
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
}
}
void reset() override {
if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
delay(10);
digitalWrite(_pin_reset, !GPS_RESET_FORCE);
}
}
void stop() override {
if (_pin_en != -1) {
digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE);
}
if (_pin_reset != -1) {
digitalWrite(_pin_reset, GPS_RESET_FORCE);
}
release();
}
bool isEnabled() override {
// directly read the enable pin if present as gps can be
// activated/deactivated outside of here ...
if (_pin_en != -1) {
return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE;
} else {
return true; // no enable so must be active
}
}
void syncTime() override { nmea.clear(); LocationProvider::syncTime(); }
long getLatitude() override { return nmea.getLatitude(); }
long getLongitude() override { return nmea.getLongitude(); }
long getAltitude() override {
long alt = 0;
nmea.getAltitude(alt);
return alt;
}
long satellitesCount() override { return nmea.getNumSatellites(); }
bool isValid() override { return nmea.isValid(); }
long getTimestamp() override {
DateTime dt(nmea.getYear(), nmea.getMonth(),nmea.getDay(),nmea.getHour(),nmea.getMinute(),nmea.getSecond());
return dt.unixtime();
}
void sendSentence(const char *sentence) override {
nmea.sendSentence(*_gps_serial, sentence);
}
void loop() override {
while (_gps_serial->available()) {
char c = _gps_serial->read();
#ifdef GPS_NMEA_DEBUG
Serial.print(c);
#endif
nmea.process(c);
}
if (!isValid()) time_valid = 0;
if (millis() > next_check) {
next_check = millis() + 1000;
// Re-enable time sync periodically when GPS has valid fix
if (!_time_sync_needed && _clock != NULL && (millis() - _last_time_sync) > TIME_SYNC_INTERVAL) {
_time_sync_needed = true;
}
if (_time_sync_needed && time_valid > 2) {
if (_clock != NULL) {
_clock->setCurrentTime(getTimestamp());
_time_sync_needed = false;
_last_time_sync = millis();
}
}
if (isValid()) {
time_valid ++;
}
}
}
};

View File

@@ -0,0 +1,554 @@
/*----------------------------------------------------------------------*
* RAK12035_SoilMoistureSensor.cpp - Arduino library for the Sensor *
* version of I2C Soil Moisture Sensor version from Chrirp *
* (https://github.com/Miceuz/i2c-moisture-sensor). *
* *
* Ingo Fischer 11Nov2015 *
* https://github.com/Apollon77/I2CSoilMoistureSensor *
* *
* Ken Privitt 8Feb2026 *
* Adapted for MeshCore Firmware Stack *
* *
* MIT license *
* *
* This file contains a collection of routines to access the *
* RAK12035 Soil Moisture Sensor via I2C. The sensor provides *
* Soil Temperature and capacitance-based Soil Moisture Readings. *
* *
*----------------------------------------------------------------------*/
#include "RAK12035_SoilMoisture.h"
#include "MeshCore.h"
#include <Wire.h>
/*----------------------------------------------------------------------*
* Constructor. *
*----------------------------------------------------------------------*/
// RAK12035_SoilMoisture(uint8_t addr)
//
// Accepts the I2C Address to look for the RAK12035
// Initializes the I2C to null (will be setup later in Wire.Begin()
//
// No hardware is touched in the constructor.
// I2C communication is deferred until begin() is called.
//------------------------------------------------------------------------------
RAK12035_SoilMoisture::RAK12035_SoilMoisture(uint8_t addr)
{
_addr = addr; // Save the sensor's I2C address
_i2c = nullptr; // Bus not assigned yet; must be set in begin()
}
//------------------------------------------------------------------------------
// setup()
//------------------------------------------------------------------------------
// setup(TwoWire &i2c)
//
// Assigns the I2C bus that this driver instance will use. This allows the
// application to choose between Wire, Wire1, or any other TwoWire instance
// supported by the platform.
//
// No I2C communication occurs here; setup() simply stores the pointer so that
// begin() and all registerlevel operations know which bus to use.
//------------------------------------------------------------------------------
void RAK12035_SoilMoisture::setup(TwoWire &i2c)
{
_i2c = &i2c; // assigns the bus pointer
_i2c->begin(); // Initialize the bus to Wire or Wire1
}
//------------------------------------------------------------------------------
// RAK12035 Soil Moisture begin()
//------------------------------------------------------------------------------
//
// Performs initialization of the RAK12035 soilmoisture sensor. This
// routine assumes that the application has already selected the I2C bus via
// setup() and that the bus has been initialized externally (Wire.begin()).
// It uses the passed in I2C Address (default 0x20)
//
// *** This code does not supprt three sensors ***
// The RAK12023 has three connectors, but each of the sensors attached must
// all have a different I2C addresses.
// This code has a function to set the I2C adress of a sensor
// and currently only supports one address 0x20 (the default).
// To support three sensors, EnvironmentSensorManager would need to be modified
// to support multiple instances of the RAK12035_SoilMoisture class,
// each with a different address. (0x20, 0x21, 0x22)
// The begin() function would need to be modified to loop through the three addresses
//
// DEBUG STATEMENTS: Can be enabled by uncommenting or adding:
// File: varients/rak4631 platformio.ini
// Section example: [env:RAK_4631_companion_radio_ble]
// Enable Debug statements: -D MESH_DEBUG=1
//
//------------------------------------------------------------------------------
bool RAK12035_SoilMoisture::begin(uint8_t addr)
{
// MESH_DEBUG_PRINTLN("begin() - Start of RAK12035 initialization");
// MESH_DEBUG_PRINTLN("begin() - RAK12035 passed in Address %02X", addr);
// 1. Ensure setup() was called
if (_i2c == nullptr) {
MESH_DEBUG_PRINTLN("RAK12035 ERROR: I2C bus not set!");
return false;
}
uint16_t _dry_cal = 200;
uint16_t _wet_cal = 600;
uint8_t _version = 0;
uint8_t _addr; // The I2C address to be used (passed in parameter)
/*------------------------------------------------------------------------------------------
* Set Calibration values - This is done with custom a firmware version
*
* USE the Build Flag: -D ENABLE_RAK12035_CALIBRATION = 1
* OR
* Change the value to 1 in the RAK12035_SoilMoisture.h file
*
* Calibration Procedure:
* 1) Flash the the Calibration version of the firmware.
* 2) Leave the sensor dry, power up the device.
* 3) After detecting the RAK12035 this firmware will display calibration data on Channel 3
*
* Frequency = Current Capacitance Value
* Temperature = Current Wet calibration value
* Power = Current Dry calibration value
*
* 4) Click refresh several times. This will take a capacitance reading and if it is
* greater than the current Dry value it will store it in the sensor
* The value will bounce a little as you click refresh, but it eventually settles down (a few clicks)
* the stored value will stabalize at it's Maximum value.
*
* 5) Put the sensor in water.
*
* 6) Click refresh several times. This will take a capacitance reading and if it is
* less than the current Wet value it will store it in the sensor
* The value will bounce a little as you click refresh, but it eventually settles down (a few clicks)
* the stored value will stabalize at it's Minimum value.
*
* 7) The Sensor is now calibrated, turn off the device.
*
* 8) Reflash the device with the non-Calibration Firmware, Data will be shown on Channel 2
*
*------------------------------------------------------------------------------------------
*/
#if ENABLE_RAK12035_CALIBRATION
uint16_t _wet = 2000; // A high value the should be out of the normal Wet range
set_humidity_full(_wet);
uint16_t _dry = 50; // A low value the should be out of the normal Dry range
set_humidity_zero(_dry);
#endif
/*--------------------------------------------------------------------------------
*
* Check if a sensor is present and return true if found, false if not present
*
*--------------------------------------------------------------------------------
*/
if (query_sensor()) {
MESH_DEBUG_PRINTLN("begin() - Sensor responded with valid version");
return true;
}
else {
MESH_DEBUG_PRINTLN("begin() - Sensor version FAIL");
return false;
}
}
/*---------------------------------------------------------------------------------
*
* Below are all the routines to execute the various I2C commands supported
* by the RAK12035 sensor
*
*--------------------------------------------------------------------------------*/
uint16_t RAK12035_SoilMoisture::get_sensor_capacitance() //Command 01 - (r) 2 byte
{
uint8_t buf[2] = {0};
if (!read_rak12035(SOILMOISTURESENSOR_GET_CAPACITANCE, buf, 2)) {
MESH_DEBUG_PRINTLN("Function 1: get_capacitance() FAIL: Bad data returned = %02X %02X", buf[0], buf[1]);
return (buf[0] << 8) | buf[1]; // return raw for debugging
}
uint16_t cap = (buf[0] << 8) | buf[1];
MESH_DEBUG_PRINTLN("Function 1: get_capacitance() SUCCESS: Capacitance = %d", cap);
return cap;
}
uint8_t RAK12035_SoilMoisture::get_I2C_address() //Command 02 - (r) 1 byte
{
uint8_t addr = 0;
if (!read_rak12035(SOILMOISTURESENSOR_GET_I2C_ADDR, &addr, 1)) {
MESH_DEBUG_PRINTLN("Function 2: get_I2C_address() FAIL: Bad data returned = %02X", addr);
return addr; // return raw for debugging
}
MESH_DEBUG_PRINTLN("Function 2: get_I2C_address() SUCCESS: I2C Address = %02X", addr);
return addr;
}
bool RAK12035_SoilMoisture::set_sensor_addr(uint8_t addr) //Command 03 - (w) 1 byte
{
if (!write_rak12035(SOILMOISTURESENSOR_SET_I2C_ADDR, &addr, 1)) {
MESH_DEBUG_PRINTLN("Function 3: set_I2C_address() FAIL: Could not set new address %02X", addr);
return false;
}
MESH_DEBUG_PRINTLN("Function 3: set_I2C_address() SUCCESS: New address = %02X", addr);
return true;
}
uint8_t RAK12035_SoilMoisture::get_sensor_version() // Command 04 - 1 byte
{
uint8_t v = 0;
read_rak12035(SOILMOISTURESENSOR_GET_VERSION, &v, 1);
if (!read_rak12035(SOILMOISTURESENSOR_GET_VERSION, &v, 1)) {
MESH_DEBUG_PRINTLN("Function 4: get_sensor_version() FAIL: Bad data returned = %02X", v);
return v;
}
MESH_DEBUG_PRINTLN("Function 4: get_sensor_version() SUCCESS: Version = %02X", v);
return v;
}
float RAK12035_SoilMoisture::get_sensor_temperature() //Command 05 - (r) 2 bytes
{
uint8_t buf[2] = {0};
if (!read_rak12035(SOILMOISTURESENSOR_GET_TEMPERATURE, buf, 2)) {
MESH_DEBUG_PRINTLN("Function 5: get_temperature() FAIL: Bad data returned = %02X %02X", buf[0], buf[1]);
return (buf[0] << 8) | buf[1]; // raw data returned for debugging 0XFFFF is error
}
// Sensor returns a 16-bit signed integer (°C * 10)
int16_t raw = (buf[0] << 8) | buf[1];
float tempC = raw / 10.0f;
MESH_DEBUG_PRINTLN("Function 5: get_temperature() SUCCESS: Raw=%04X Temp=%.1f C", raw, tempC);
return tempC;
}
bool RAK12035_SoilMoisture::sensor_sleep() //Command 06 - (w) 1 byte
{
uint8_t tmp = 0;
if (!write_rak12035(SOILMOISTURESENSOR_SET_SLEEP, &tmp, 1)) {
MESH_DEBUG_PRINTLN("Function 6: sensor_sleep() FAIL: Could not send sleep command");
return false;
}
MESH_DEBUG_PRINTLN("Function 6: sensor_sleep() SUCCESS: Sensor acknowledged sleep command");
// Optional: turn off sensor power AFTER successful sleep command
// This has been commented out due to a pin name conflict with the Heltec v3
// This will need to be resolved if this funstion is to be utilized in the future
/*
digitalWrite(WB_IO2, LOW);
*/
return true;
}
bool RAK12035_SoilMoisture::set_humidity_full(uint16_t full) //Command 07 - (w) 2 bytes
{
uint8_t buf[2];
buf[0] = (full >> 8) & 0xFF; // High byte
buf[1] = full & 0xFF; // Low byte
if (!write_rak12035(SOILMOISTURESENSOR_SET_WET_CAL, buf, 2)) {
MESH_DEBUG_PRINTLN("Function 7: set_humidity_full() FAIL: Could not set wet calibration value"
);
return false;
}
MESH_DEBUG_PRINTLN("Function 7: set_humidity_full() SUCCESS: New Full = %04X", full);
return true;
}
bool RAK12035_SoilMoisture::set_humidity_zero(uint16_t zero) //Command 08 - (w) 2 bytes
{
uint8_t buf[2];
buf[0] = (zero >> 8) & 0xFF; // High byte
buf[1] = zero & 0xFF; // Low byte
if (!write_rak12035(SOILMOISTURESENSOR_SET_DRY_CAL, buf, 2)) {
MESH_DEBUG_PRINTLN("Function 8: set_humidity_zero() FAIL: Could not set dry calibration value");
return false;
}
MESH_DEBUG_PRINTLN("Function 8: set_humidity_zero() SUCCESS: New Zero = %04X", zero);
return true;
}
uint8_t RAK12035_SoilMoisture::get_sensor_moisture() //Command 09 - (r) 1 byte
{
// Load calibration values from sensor
_wet_cal = get_humidity_full();
_dry_cal = get_humidity_zero();
MESH_DEBUG_PRINTLN("Function 9: get_moisture() - Read from sensor or calculate from capacitance");
// Read sensor version
uint8_t v = get_sensor_version();
// If version > 2, read moisture directly from the sensor
if (v > 2) {
MESH_DEBUG_PRINTLN("Version > 02 - Reading moisture directly from sensor");
uint8_t moisture = get_sensor_humid();
MESH_DEBUG_PRINTLN("get_moisture() Direct Read = %d%%", moisture);
return moisture;
}
// Otherwise calculate moisture from capacitance
MESH_DEBUG_PRINTLN("Calculating moisture from capacitance");
uint16_t cap = get_sensor_capacitance();
// Clamp capacitance between calibration points
if (_dry_cal < _wet_cal) {
if (cap <= _dry_cal) cap = _dry_cal;
if (cap >= _wet_cal) cap = _wet_cal;
float pct = (_wet_cal - cap) * 100.0f / (_wet_cal - _dry_cal);
if (pct > 100.0f) pct = 100.0f;
MESH_DEBUG_PRINTLN("get_moisture Case 1() Calculated = %d%%", (uint8_t)pct);
return (uint8_t)pct;
} else {
if (cap >= _dry_cal) cap = _dry_cal;
if (cap <= _wet_cal) cap = _wet_cal;
float pct = (_dry_cal - cap) * 100.0f / (_dry_cal - _wet_cal);
if (pct > 100.0f) pct = 100.0f;
MESH_DEBUG_PRINTLN("get_moisture Case 2() Calculated = %d%%", (uint8_t)pct);
return (uint8_t)pct;
}
}
uint8_t RAK12035_SoilMoisture::get_sensor_humid() //Command 09 - (r) 1 byte
{
uint8_t moisture = 0;
if (!read_rak12035(SOILMOISTURESENSOR_GET_MOISTURE, &moisture, 1)) {
MESH_DEBUG_PRINTLN("Function 9: get_sensor_humid() FAIL: Bad data returned = %02X", moisture);
return moisture; // raw fallback
}
MESH_DEBUG_PRINTLN("Function 9: get_sensor_humid() SUCCESS: Moisture = %d%%",moisture);
return moisture;
}
uint16_t RAK12035_SoilMoisture::get_humidity_full() //Command 0A - (r) 2 bytes
{
uint8_t buf[2] = {0};
if (!read_rak12035(SOILMOISTURESENSOR_GET_WET_CAL, buf, 2)) {
MESH_DEBUG_PRINTLN("Function A: get_humidity_full() FAIL: Bad data returned = %02X%02X", buf[0], buf[1]);
return 0xFFFF; // error indicator
}
uint16_t full = (buf[0] << 8) | buf[1];
MESH_DEBUG_PRINTLN("Function A: get_humidity_full() SUCCESS: Full = %04X = %d", full, full);
return full;
}
uint16_t RAK12035_SoilMoisture::get_humidity_zero() //Command 0B - 2 bytes
{
uint8_t buf[2] = {0};
if (!read_rak12035(SOILMOISTURESENSOR_GET_DRY_CAL, buf, 2)) {
MESH_DEBUG_PRINTLN("Function B: get_humidity_zero() FAIL: Bad data returned = %02X%02X", buf[0], buf[1]);
return 0xFFFF; // error indicator
}
uint16_t zero = (buf[0] << 8) | buf[1];
MESH_DEBUG_PRINTLN("Function B: get_humidity_zero() SUCCESS: Zero = %04X = %d", zero, zero);
return zero;
}
/*------------------------------------------------------------------------------------------*
* getEvent() - High-level function to read both moisture and temperature in one call. *
*------------------------------------------------------------------------------------------*
* This function reads the moisture percentage and temperature from the sensor and returns *
* them via output parameters. This may be used for the telemerty delivery in the MeshCore *
* firmware, with a single function to get all sensor data. *
* *
* The function returns true if both readings were successfully obtained, or false if any *
* error occurred during I2C communication. *
* *
* This function is currently not used *
*------------------------------------------------------------------------------------------*/
bool RAK12035_SoilMoisture::getEvent(uint8_t *humidity, uint16_t *temp)
{
// Read moisture (0-100%)
uint8_t moist = get_sensor_moisture();
if (moist == 0xFF) //error indicator
return false;
MESH_DEBUG_PRINTLN("getEvent() - Humidity = %d", moist);
*humidity = moist;
//Read temperature (degrees C)
uint16_t t = get_sensor_temperature();
if (t == 0XFFFF) // error indicator
return false;
*temp = t;
MESH_DEBUG_PRINTLN("getEvent() - Temperature = %d", t);
return true;
}
/*------------------------------------------------------------------------------------------*
* Sensor Power Management and Reset Routines
*
* These routines manage the power and reset state of the sensor. The sensor_on() routine is
* designed to power on the sensor and wait for it to become responsive, while the reset()
* routine toggles the reset pin and waits for the sensor to respond with a valid version.
*
* They are for a future sensor power management function.
*------------------------------------------------------------------------------------------*/
bool RAK12035_SoilMoisture::sensor_on()
{
uint8_t data;
// This has been commented out due to a pin name conflict with the Heltec v3
// This will need to be resolved if this funstion is to be utilized in the future
/*
pinMode(WB_IO2, OUTPUT);
digitalWrite(WB_IO2, HIGH); //Turn on Sensor Power
pinMode(WB_IO4, OUTPUT); //Set IO4 Pin to Output (connected to *reset on sensor)
digitalWrite(WB_IO4, LOW); //*reset - Reset the Sensor
delay(1); //Wait for the minimum *reset, 1mS is longer than required minimum
digitalWrite(WB_IO4, HIGH); //Deassert Reset
delay(10); // Wait for the sensor code to complete initialization
*/
uint8_t v = 0;
time_t timeout = millis();
while ((!query_sensor())) //Wait for sensor to respond to I2C commands,
{ //indicating it is ready
if ((millis() - timeout) > 50){ //0.5 second timeout for sensor to respond
MESH_DEBUG_PRINTLN("reset() - Timeout, no response from I2C commands");
return false;
}
else {
delay(10); //delay 10mS
}
}
}
bool RAK12035_SoilMoisture::reset()
{
// This function is for a future Sensor Power Management function.
// When power is reapplied this will reset the sensor and wait for it to respond
// with a valid version.
//
// The Atmel 8495 Microcoltroller: Reset input. A low level on this pin for longer than
// the minimum pulse length will generate a reset, even if the clock is not
// running and provided the reset pin has not been disabled. The minimum pulse length is
// given in Table 25-5 on page 240. 2000ns = .002mS
// Shorter pulses are not guaranteed to generate a reset.
//
// Power is never removed so the Sensor reset was removed and is not needed,
// But might be needed if power is ever switched off. Here is tested code.
// This has been commented out due to a pin name conflict with the Heltec v3
// This will need to be resolved if this funstion is to be utilized in the future
/*
pinMode(WB_IO4, OUTPUT); //Set IO4 Pin to Output (connected to *reset on sensor)
MESH_DEBUG_PRINTLN("Assert *reset (Low) for 1 mS");
digitalWrite(WB_IO4, LOW); //Reset the Sensor
delay(1); //Wait for the minimum *reset, 1mS is longer than required minimum
MESH_DEBUG_PRINTLN("reset() - De-assert *reset (High)");
digitalWrite(WB_IO4, HIGH); // Deassert Reset
*/
MESH_DEBUG_PRINTLN("reset() - Begin poling in 100mS intervals for a non-zero version");
uint32_t start_time = millis();
MESH_DEBUG_PRINTLN("reset() - Timeout, Start Time: %d milliseconds", start_time);
const uint32_t timeout_ms = 500; // Wait for 0.5 seconds
uint32_t start = millis();
while (true) {
if (query_sensor()) {
MESH_DEBUG_PRINTLN("reset() - First Pass, Sensor responded with valid version");
uint32_t stop_time = millis();
MESH_DEBUG_PRINTLN("reset() - Timeout, Stop Time: %d mS", stop_time);
MESH_DEBUG_PRINTLN("reset() - Timeout, Duration: %d mS", (stop_time - start_time));
return true;
}
if (millis() - start > timeout_ms) {
MESH_DEBUG_PRINTLN("reset() - Timeout waiting for valid sensor version");
uint32_t stop_time = millis();
MESH_DEBUG_PRINTLN("reset() - Timeout, Stop Time: %d mS", stop_time);
MESH_DEBUG_PRINTLN("reset() - Timeout, Duration: %d mS", (stop_time - start_time));
return false;
}
delay(100);
}
}
bool RAK12035_SoilMoisture::query_sensor()
{
uint8_t v = 0;
v = get_sensor_version();
// Treat 0x00 and 0xFF as invalid / bootloader / garbage
if (v == 0x00 || v == 0xFF) {
MESH_DEBUG_PRINTLN("query_sensor() FAIL: Version value invalid: %02X", v);
return false;
}
MESH_DEBUG_PRINTLN("query_sensor() SUCCESS: Sensor Present, Version = %02X", v);
return true;
}
/*------------------------------------------------------------------------------------------*
* Below are the low-level I2C read and write functions. These handle the actual
* communication with the sensor registers. The higher-level functions call these
* to perform specific tasks.
*------------------------------------------------------------------------------------------*/
bool RAK12035_SoilMoisture::read_rak12035(uint8_t cmd, uint8_t *data, uint8_t length)
{
_i2c->beginTransmission(_addr);
_i2c->write(cmd); // <-- COMMAND, not register index
if (_i2c->endTransmission() != 0)
return false;
delay(20);
int received = _i2c->requestFrom(_addr, length);
if (received != length)
return false;
for (int i = 0; i < length; i++)
data[i] = _i2c->read();
return true;
}
bool RAK12035_SoilMoisture::write_rak12035(uint8_t cmd, uint8_t *data, uint8_t length)
{
_i2c->beginTransmission(_addr);
_i2c->write(cmd); // <-- COMMAND, not register index
for (uint8_t i = 0; i < length; i++)
_i2c->write(data[i]);
if (_i2c->endTransmission() != 0)
return false;
delay(20);
return true;
}

View File

@@ -0,0 +1,88 @@
/**
* @file RAK12035_SoilMoisture.h
* @author Bernd Giesecke (bernd.giesecke@rakwireless.com)
* @brief Header file for Class RAK12035
* @version 0.1
* @date 2021-11-20
*
* Updates for MeshCore integration
* Ken Privitt
* 2/26/2026
*
* @copyright Copyright (c) 2021
*
*/
#ifndef RAK12035_SOILMOISTURE_H
#define RAK12035_SOILMOISTURE_H
#endif
#ifndef ENABLE_RAK12025_CALIBRATION
#define ENABLE_RAK12025_CALIBRATION = 0 // Used to generate Calibration Version of Firmware
#include <Arduino.h>
#include <Wire.h>
#define RAK12035_I2C_ADDR_DEFAULT 0x20
#define RAK12035_0_ADDR 0x20
#define RAK12035_1_ADDR 0x21
#define RAK12035_2_ADDR 0x22
// Command codes used by the RAK12035 firmware
#define SOILMOISTURESENSOR_GET_CAPACITANCE 0x01 // (r) 2 bytes
#define SOILMOISTURESENSOR_GET_I2C_ADDR 0x02 // (r) 1 bytes
#define SOILMOISTURESENSOR_SET_I2C_ADDR 0x03 // (w) 1 bytes
#define SOILMOISTURESENSOR_GET_VERSION 0x04 // (r) 1 bytes
#define SOILMOISTURESENSOR_GET_TEMPERATURE 0x05 // (r) 2 bytes
#define SOILMOISTURESENSOR_SET_SLEEP 0x06 // (w) 1 bytes
#define SOILMOISTURESENSOR_SET_WET_CAL 0x07 // (w) 2 bytes
#define SOILMOISTURESENSOR_SET_DRY_CAL 0x08 // (w) 2 bytes
#define SOILMOISTURESENSOR_GET_MOISTURE 0x09 // (r) 1 bytes
#define SOILMOISTURESENSOR_GET_WET_CAL 0x0A // (r) 2 bytes
#define SOILMOISTURESENSOR_GET_DRY_CAL 0x0B // (r) 2 bytes
class RAK12035_SoilMoisture
{
public:
RAK12035_SoilMoisture(uint8_t addr = RAK12035_I2C_ADDR_DEFAULT);
void setup(TwoWire& i2c);
bool begin(uint8_t addr);
bool getEvent(uint8_t *humidity, uint16_t *temperature);
uint16_t get_sensor_capacitance(); //Command 01 - (r) 2 byte
uint8_t get_I2C_address(); //Command 02 - (r) 1 byte
bool set_sensor_addr(uint8_t addr); //Command 03 - (w) 1 byte
uint8_t get_sensor_version(); //Command 04 - (r) 1 byte
float get_sensor_temperature(); //Command 05 - (r) 2 bytes
bool sensor_sleep(); //Command 06 - (w) 1 byte
bool set_humidity_full(uint16_t hundred_val); //Command 07 - (w) 2 bytes
bool set_humidity_zero(uint16_t zero_val); //Command 08 - (w) 2 bytes
uint8_t get_sensor_moisture(); //Command 09 - (r) 1 byte
uint8_t get_sensor_humid(); //Command 09 - (r) 1 byte
uint16_t get_humidity_full(); //Command 0A - (r) 2 bytes
uint16_t get_humidity_zero(); //Command 0B - (r) 2 bytes
bool read_rak12035(uint8_t cmd, uint8_t *data, uint8_t length);
bool write_rak12035(uint8_t cmd, uint8_t *data, uint8_t length);
bool query_sensor();
bool sensor_on();
bool reset();
uint16_t _dry_cal;
uint16_t _wet_cal;
private:
bool read_reg(uint8_t reg, uint8_t *data, uint8_t len);
bool write_reg(uint8_t reg, uint8_t *data, uint8_t len);
TwoWire *_i2c = &Wire;
uint8_t _addr;
uint16_t default_dry_cal = 2000;
uint16_t default_wet_cal = 50;
uint8_t _capacitance = 0;
uint16_t _temperature = 0;
uint8_t _moisture = 0;
};
#endif

View File

@@ -0,0 +1,145 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 hathach for Adafruit Industries
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include <Arduino.h>
#include "InternalFileSystem.h"
//--------------------------------------------------------------------+
// LFS Disk IO
//--------------------------------------------------------------------+
static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size)
{
if (!buffer || !size) return LFS_ERR_INVAL;
lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * FLASH_PAGE_SIZE + off);
memcpy(buffer, (void *)address, size);
return LFS_ERR_OK;
}
// Program a region in a block. The block must have previously
// been erased. Negative error codes are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size)
{
HAL_StatusTypeDef hal_rc = HAL_OK;
lfs_block_t addr = LFS_FLASH_ADDR_BASE + (block * FLASH_PAGE_SIZE + off);
uint64_t *bufp = (uint64_t *) buffer;
if (HAL_FLASH_Unlock() != HAL_OK) return LFS_ERR_IO;
for (uint32_t i = 0; i < size/8; i++) {
if ((addr < LFS_FLASH_ADDR_BASE) || (addr > FLASH_END_ADDR)) {
HAL_FLASH_Lock();
return LFS_ERR_INVAL;
}
hal_rc = HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, *bufp);
addr += 8;
bufp += 1;
}
if (HAL_FLASH_Lock() != HAL_OK) return LFS_ERR_IO;
return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO;
}
// Erase a block. A block must be erased before being programmed.
// The state of an erased block is undefined. Negative error codes
// are propogated to the user.
// May return LFS_ERR_CORRUPT if the block should be considered bad.
static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block)
{
HAL_StatusTypeDef hal_rc;
lfs_block_t address = LFS_FLASH_ADDR_BASE + (block * FLASH_PAGE_SIZE);
uint32_t pageError = 0;
FLASH_EraseInitTypeDef EraseInitStruct = {
.TypeErase = FLASH_TYPEERASE_PAGES,
.Page = 0,
.NbPages = 1
};
if ((address < LFS_FLASH_ADDR_BASE) || (address > FLASH_END_ADDR)) {
return LFS_ERR_INVAL;
}
EraseInitStruct.Page = (address - FLASH_BASE) / FLASH_PAGE_SIZE;
HAL_FLASH_Unlock();
hal_rc = HAL_FLASHEx_Erase(&EraseInitStruct, &pageError);
HAL_FLASH_Lock();
return hal_rc == HAL_OK ? LFS_ERR_OK : LFS_ERR_IO;
}
// Sync the state of the underlying block device. Negative error codes
// are propogated to the user.
static int _internal_flash_sync(const struct lfs_config *c)
{
return LFS_ERR_OK; // don't need sync
}
struct lfs_config _InternalFSConfig = {
.context = NULL,
.read = _internal_flash_read,
.prog = _internal_flash_prog,
.erase = _internal_flash_erase,
.sync = _internal_flash_sync,
.read_size = LFS_BLOCK_SIZE,
.prog_size = LFS_BLOCK_SIZE,
.block_size = LFS_BLOCK_SIZE,
.block_count = LFS_FLASH_TOTAL_SIZE / LFS_BLOCK_SIZE,
.lookahead = 128,
.read_buffer = NULL,
.prog_buffer = NULL,
.lookahead_buffer = NULL,
.file_buffer = NULL
};
InternalFileSystem InternalFS;
//--------------------------------------------------------------------+
//
//--------------------------------------------------------------------+
InternalFileSystem::InternalFileSystem(void)
: Adafruit_LittleFS(&_InternalFSConfig)
{
}
bool InternalFileSystem::begin(void)
{
volatile bool format_fs;
#ifdef FORMAT_FS
format_fs = true;
#else
format_fs = false; // you can always use debugger to force formatting ;)
#endif
// failed to mount, erase all sector then format and mount again
if ( format_fs || !Adafruit_LittleFS::begin() )
{
// lfs format
this->format();
// mount again if still failed, give up
if ( !Adafruit_LittleFS::begin() ) return false;
}
return true;
}

View File

@@ -0,0 +1,48 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 hathach for Adafruit Industries
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifndef INTERNALFILESYSTEM_H_
#define INTERNALFILESYSTEM_H_
#include "Adafruit_LittleFS.h"
#ifndef LFS_FLASH_TOTAL_SIZE /* Flash size can be configured in platformio.ini */
#define LFS_FLASH_TOTAL_SIZE (16 * 2048) /* defaults to 32k flash */
#endif
#define LFS_BLOCK_SIZE (2048)
#define LFS_FLASH_ADDR_BASE (FLASH_END_ADDR - LFS_FLASH_TOTAL_SIZE + 1)
class InternalFileSystem : public Adafruit_LittleFS
{
public:
InternalFileSystem(void);
// overwrite to also perform low level format (sector erase of whole flash region)
bool begin(void);
};
extern InternalFileSystem InternalFS;
#endif /* INTERNALFILESYSTEM_H_ */

View File

@@ -0,0 +1,45 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
class STM32Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
public:
virtual void begin() {
startup_reason = BD_STARTUP_NORMAL;
}
uint8_t getStartupReason() const override { return startup_reason; }
uint16_t getBattMilliVolts() override {
return 0; // not supported
}
const char* getManufacturerName() const override {
return "Generic STM32";
}
void reboot() override {
NVIC_SystemReset();
}
void powerOff() override {
HAL_PWREx_DisableInternalWakeUpLine();
__disable_irq();
HAL_PWREx_EnterSHUTDOWNMode();
}
#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off
}
#endif
bool startOTAUpdate(const char* id, char reply[]) override { return false; };
};

View File

@@ -0,0 +1,100 @@
#pragma once
#include <stdint.h>
#include <string.h>
class DisplayDriver {
int _w, _h;
protected:
DisplayDriver(int w, int h) { _w = w; _h = h; }
public:
enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light
int width() const { return _w; }
int height() const { return _h; }
virtual bool isOn() = 0;
virtual void turnOn() = 0;
virtual void turnOff() = 0;
virtual void clear() = 0;
virtual void startFrame(Color bkg = DARK) = 0;
virtual void setTextSize(int sz) = 0;
virtual void setColor(Color c) = 0;
virtual void setCursor(int x, int y) = 0;
virtual void print(const char* str) = 0;
virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override
virtual void fillRect(int x, int y, int w, int h) = 0;
virtual void drawRect(int x, int y, int w, int h) = 0;
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
virtual uint16_t getTextWidth(const char* str) = 0;
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise)
int w = getTextWidth(str);
setCursor(mid_x - w/2, y);
print(str);
}
virtual void drawTextRightAlign(int x_anch, int y, const char* str) {
int w = getTextWidth(str);
setCursor(x_anch - w, y);
print(str);
}
virtual void drawTextLeftAlign(int x_anch, int y, const char* str) {
setCursor(x_anch, y);
print(str);
}
// convert UTF-8 characters to displayable block characters for compatibility
virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
size_t j = 0;
for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) {
unsigned char c = (unsigned char)src[i];
if (c >= 32 && c <= 126) {
dest[j++] = c; // ASCII printable
} else if (c >= 0x80) {
dest[j++] = '\xDB'; // CP437 full block █
while (src[i+1] && (src[i+1] & 0xC0) == 0x80)
i++; // skip UTF-8 continuation bytes
}
}
dest[j] = 0;
}
// draw text with ellipsis if it exceeds max_width
virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) {
char temp_str[256]; // reasonable buffer size
size_t len = strlen(str);
if (len >= sizeof(temp_str)) len = sizeof(temp_str) - 1;
memcpy(temp_str, str, len);
temp_str[len] = 0;
if (getTextWidth(temp_str) <= max_width) {
setCursor(x, y);
print(temp_str);
return;
}
// for variable-width fonts (GxEPD), add space after ellipsis
// for fixed-width fonts (OLED), keep tight spacing to save precious characters
const char* ellipsis;
// use a simple heuristic: if 'i' and 'l' have different widths, it's variable-width
int i_width = getTextWidth("i");
int l_width = getTextWidth("l");
if (i_width != l_width) {
ellipsis = "... "; // variable-width fonts: add space
} else {
ellipsis = "..."; // fixed-width fonts: no space
}
int ellipsis_width = getTextWidth(ellipsis);
int str_len = strlen(temp_str);
while (str_len > 0 && getTextWidth(temp_str) > max_width - ellipsis_width) {
temp_str[--str_len] = 0;
}
strcat(temp_str, ellipsis);
setCursor(x, y);
print(temp_str);
}
virtual void endFrame() = 0;
};

View File

@@ -0,0 +1,201 @@
#include "E213Display.h"
#include "../../MeshCore.h"
BaseDisplay* E213Display::detectEInk()
{
// Test 1: Logic of BUSY pin
// Determines controller IC manufacturer
// Fitipower: busy when LOW
// Solomon Systech: busy when HIGH
// Force display BUSY by holding reset pin active
pinMode(DISP_RST, OUTPUT);
digitalWrite(DISP_RST, LOW);
delay(10);
// Read whether pin is HIGH or LOW while busy
pinMode(DISP_BUSY, INPUT);
bool busyLogic = digitalRead(DISP_BUSY);
// Test complete. Release pin
pinMode(DISP_RST, INPUT);
if (busyLogic == LOW) {
#ifdef VISION_MASTER_E213
return new EInkDisplay_VisionMasterE213 ;
#else
return new EInkDisplay_WirelessPaperV1_1 ;
#endif
} else {// busy HIGH
#ifdef VISION_MASTER_E213
return new EInkDisplay_VisionMasterE213V1_1 ;
#else
return new EInkDisplay_WirelessPaperV1_1_1 ;
#endif
}
}
bool E213Display::begin() {
if (_init) return true;
powerOn();
if(display==NULL) {
display = detectEInk();
}
display->begin();
// Set to landscape mode rotated 180 degrees
display->setRotation(3);
_init = true;
_isOn = true;
clear();
display->fastmodeOn(); // Enable fast mode for quicker (partial) updates
return true;
}
void E213Display::powerOn() {
if (_periph_power) {
_periph_power->claim();
} else {
#ifdef PIN_VEXT_EN
pinMode(PIN_VEXT_EN, OUTPUT);
#ifdef PIN_VEXT_EN_ACTIVE
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
#else
digitalWrite(PIN_VEXT_EN, LOW); // Active low
#endif
#endif
}
delay(50); // Allow power to stabilize
}
void E213Display::powerOff() {
if (_periph_power) {
_periph_power->release();
} else {
#ifdef PIN_VEXT_EN
#ifdef PIN_VEXT_EN_ACTIVE
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE);
#else
digitalWrite(PIN_VEXT_EN, HIGH); // Turn off power
#endif
#endif
}
}
void E213Display::turnOn() {
if (!_init) begin();
else if (!_isOn) {
powerOn();
display->fastmodeOn(); // Reinitialize display controller after power was cut
}
_isOn = true;
}
void E213Display::turnOff() {
if (_isOn) {
powerOff();
_isOn = false;
}
}
void E213Display::clear() {
display->clear();
}
void E213Display::startFrame(Color bkg) {
display_crc.reset();
// Fill screen with white first to ensure clean background
display->fillRect(0, 0, width(), height(), WHITE);
if (bkg == LIGHT) {
// Fill with black if light background requested (inverted for e-ink)
display->fillRect(0, 0, width(), height(), BLACK);
}
}
void E213Display::setTextSize(int sz) {
display_crc.update<int>(sz);
// The library handles text size internally
display->setTextSize(sz);
}
void E213Display::setColor(Color c) {
display_crc.update<Color>(c);
// implemented in individual display methods
}
void E213Display::setCursor(int x, int y) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display->setCursor(x, y);
}
void E213Display::print(const char *str) {
display_crc.update<char>(str, strlen(str));
display->print(str);
}
void E213Display::fillRect(int x, int y, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display->fillRect(x, y, w, h, BLACK);
}
void E213Display::drawRect(int x, int y, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display->drawRect(x, y, w, h, BLACK);
}
void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(bits, w * h / 8);
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (int by = 0; by < h; by++) {
// Scan across the row bit by bit
for (int bx = 0; bx < w; bx++) {
// Get the current bit using MSB ordering (like GxEPDDisplay)
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = bits[byteOffset] & bitMask;
// If the bit is set, draw the pixel
if (bitSet) {
display->drawPixel(x + bx, y + by, BLACK);
}
}
}
}
uint16_t E213Display::getTextWidth(const char *str) {
int16_t x1, y1;
uint16_t w, h;
display->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void E213Display::endFrame() {
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display->update();
last_display_crc_value = crc;
}
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include "DisplayDriver.h"
#include <SPI.h>
#include <Wire.h>
#include <heltec-eink-modules.h>
#include <CRC32.h>
#include <helpers/RefCountedDigitalPin.h>
// Display driver for E213 e-ink display
class E213Display : public DisplayDriver {
BaseDisplay* display=NULL;
bool _init = false;
bool _isOn = false;
RefCountedDigitalPin* _periph_power;
CRC32 display_crc;
uint32_t last_display_crc_value = 0;
public:
E213Display(RefCountedDigitalPin* periph_power = NULL) : DisplayDriver(250, 122), _periph_power(periph_power) {}
~E213Display(){
if(display!=NULL) {
delete display;
}
}
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char *str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override;
void endFrame() override;
private:
BaseDisplay* detectEInk();
void powerOn();
void powerOff();
};

View File

@@ -0,0 +1,154 @@
#include "E290Display.h"
#include "../../MeshCore.h"
bool E290Display::begin() {
if (_init) return true;
powerOn();
display.begin();
// Set to landscape mode rotated 180 degrees
display.setRotation(3);
_init = true;
_isOn = true;
clear();
display.fastmodeOn(); // Enable fast mode for quicker (partial) updates
return true;
}
void E290Display::powerOn() {
if (_periph_power) {
_periph_power->claim();
} else {
#ifdef PIN_VEXT_EN
pinMode(PIN_VEXT_EN, OUTPUT);
digitalWrite(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE);
#endif
}
delay(50); // Allow power to stabilize
}
void E290Display::powerOff() {
if (_periph_power) {
_periph_power->release();
} else {
#ifdef PIN_VEXT_EN
digitalWrite(PIN_VEXT_EN, !PIN_VEXT_EN_ACTIVE); // Turn off power
#endif
}
}
void E290Display::turnOn() {
if (!_init) begin();
else if (!_isOn) {
powerOn();
display.fastmodeOn(); // Reinitialize display controller after power was cut
}
_isOn = true;
}
void E290Display::turnOff() {
if (_isOn) {
powerOff();
_isOn = false;
}
}
void E290Display::clear() {
display.clear();
}
void E290Display::startFrame(Color bkg) {
display_crc.reset();
// Fill screen with white first to ensure clean background
display.fillRect(0, 0, width(), height(), WHITE);
if (bkg == LIGHT) {
// Fill with black if light background requested (inverted for e-ink)
display.fillRect(0, 0, width(), height(), BLACK);
}
}
void E290Display::setTextSize(int sz) {
display_crc.update<int>(sz);
// The library handles text size internally
display.setTextSize(sz);
}
void E290Display::setColor(Color c) {
display_crc.update<Color>(c);
// implemented in individual display methods
}
void E290Display::setCursor(int x, int y) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display.setCursor(x, y);
}
void E290Display::print(const char *str) {
display_crc.update<char>(str, strlen(str));
display.print(str);
}
void E290Display::fillRect(int x, int y, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.fillRect(x, y, w, h, BLACK);
}
void E290Display::drawRect(int x, int y, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.drawRect(x, y, w, h, BLACK);
}
void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(bits, w * h / 8);
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (int by = 0; by < h; by++) {
// Scan across the row bit by bit
for (int bx = 0; bx < w; bx++) {
// Get the current bit using MSB ordering (like GxEPDDisplay)
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = bits[byteOffset] & bitMask;
// If the bit is set, draw the pixel
if (bitSet) {
display.drawPixel(x + bx, y + by, BLACK);
}
}
}
}
uint16_t E290Display::getTextWidth(const char *str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
void E290Display::endFrame() {
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.update();
last_display_crc_value = crc;
}
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include "DisplayDriver.h"
#include <SPI.h>
#include <Wire.h>
#include <heltec-eink-modules.h>
#include <CRC32.h>
#include <helpers/RefCountedDigitalPin.h>
// Display driver for E290 e-ink display
class E290Display : public DisplayDriver {
EInkDisplay_VisionMasterE290 display;
bool _init = false;
bool _isOn = false;
RefCountedDigitalPin* _periph_power;
CRC32 display_crc;
uint32_t last_display_crc_value = 0;
public:
E290Display(RefCountedDigitalPin* periph_power = NULL) : DisplayDriver(296, 128), _periph_power(periph_power) {}
bool begin();
bool isOn() override { return _isOn; }
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char *str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override;
void endFrame() override;
private:
void powerOn();
void powerOff();
};

View File

@@ -0,0 +1,38 @@
#ifdef PIN_VIBRATION
#include "GenericVibration.h"
void GenericVibration::begin() {
pinMode(PIN_VIBRATION, OUTPUT);
digitalWrite(PIN_VIBRATION, LOW);
duration = 0;
}
void GenericVibration::trigger() {
duration = millis();
digitalWrite(PIN_VIBRATION, HIGH);
}
void GenericVibration::loop() {
if (isVibrating()) {
if ((millis() / 1000) % 2 == 0) {
digitalWrite(PIN_VIBRATION, LOW);
} else {
digitalWrite(PIN_VIBRATION, HIGH);
}
if (millis() - duration > VIBRATION_TIMEOUT) {
stop();
}
}
}
bool GenericVibration::isVibrating() {
return duration > 0;
}
void GenericVibration::stop() {
duration = 0;
digitalWrite(PIN_VIBRATION, LOW);
}
#endif // ifdef PIN_VIBRATION

View File

@@ -0,0 +1,33 @@
#pragma once
#ifdef PIN_VIBRATION
#include <Arduino.h>
/*
* Vibration motor control class
*
* Provides vibration feedback for events like new messages and new contacts
* Features:
* - 1-second vibration pulse
* - 5-second nag timeout (cooldown between vibrations)
* - Non-blocking operation
*/
#ifndef VIBRATION_TIMEOUT
#define VIBRATION_TIMEOUT 5000 // 5 seconds default
#endif
class GenericVibration {
public:
void begin(); // set up vibration pin
void trigger(); // trigger vibration if cooldown has passed
void loop(); // non-blocking timer handling
bool isVibrating(); // returns true if currently vibrating
void stop(); // stop vibration immediately
private:
unsigned long duration;
};
#endif // ifdef PIN_VIBRATION

View File

@@ -0,0 +1,179 @@
#include "GxEPDDisplay.h"
#ifdef EXP_PIN_BACKLIGHT
#include <PCA9557.h>
extern PCA9557 expander;
#endif
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
#ifdef ESP32
SPIClass SPI1 = SPIClass(FSPI);
#endif
bool GxEPDDisplay::begin() {
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#ifdef ESP32
SPI1.begin(PIN_DISPLAY_SCLK, PIN_DISPLAY_MISO, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
#else
SPI1.begin();
#endif
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setTextSize(1); // Default to size 1
display.setPartialWindow(0, 0, display.width(), display.height());
display.fillScreen(GxEPD_WHITE);
display.display(true);
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
pinMode(DISP_BACKLIGHT, OUTPUT);
#endif
_init = true;
return true;
}
void GxEPDDisplay::turnOn() {
if (!_init) begin();
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
digitalWrite(DISP_BACKLIGHT, HIGH);
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
#endif
_isOn = true;
}
void GxEPDDisplay::turnOff() {
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
digitalWrite(DISP_BACKLIGHT, LOW);
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
#endif
_isOn = false;
}
void GxEPDDisplay::clear() {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(GxEPD_BLACK);
display_crc.reset();
}
void GxEPDDisplay::startFrame(Color bkg) {
display.fillScreen(GxEPD_WHITE);
display.setTextColor(_curr_color = GxEPD_BLACK);
display_crc.reset();
}
void GxEPDDisplay::setTextSize(int sz) {
display_crc.update<int>(sz);
switch(sz) {
case 1: // Small
display.setFont(&FreeSans9pt7b);
break;
case 2: // Medium Bold
display.setFont(&FreeSansBold12pt7b);
break;
case 3: // Large
display.setFont(&FreeSans18pt7b);
break;
default:
display.setFont(&FreeSans9pt7b);
break;
}
}
void GxEPDDisplay::setColor(Color c) {
display_crc.update<Color> (c);
// colours need to be inverted for epaper displays
if (c == DARK) {
display.setTextColor(_curr_color = GxEPD_WHITE);
} else {
display.setTextColor(_curr_color = GxEPD_BLACK);
}
}
void GxEPDDisplay::setCursor(int x, int y) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display.setCursor((x+offset_x)*scale_x, (y+offset_y)*scale_y);
}
void GxEPDDisplay::print(const char* str) {
display_crc.update<char>(str, strlen(str));
display.print(str);
}
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.fillRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color);
}
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.drawRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color);
}
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(bits, w * h / 8);
// Calculate the base position in display coordinates
uint16_t startX = x * scale_x;
uint16_t startY = y * scale_y;
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (uint16_t by = 0; by < h; by++) {
// Calculate the target y-coordinates for this logical row
int y1 = startY + (int)(by * scale_y);
int y2 = startY + (int)((by + 1) * scale_y);
int block_h = y2 - y1;
// Scan across the row bit by bit
for (uint16_t bx = 0; bx < w; bx++) {
// Calculate the target x-coordinates for this logical column
int x1 = startX + (int)(bx * scale_x);
int x2 = startX + (int)((bx + 1) * scale_x);
int block_w = x2 - x1;
// Get the current bit
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
// If the bit is set, draw a block of pixels
if (bitSet) {
// Draw the block as a filled rectangle
display.fillRect(x1, y1, block_w, block_h, _curr_color);
}
}
}
}
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return ceil((w + 1) / scale_x);
}
void GxEPDDisplay::endFrame() {
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.display(true);
last_display_crc_value = crc;
}
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include <SPI.h>
#include <Wire.h>
#define ENABLE_GxEPD2_GFX 0
#include <GxEPD2_BW.h>
#include <GxEPD2_3C.h>
#include <GxEPD2_4C.h>
#include <GxEPD2_7C.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <CRC32.h>
#include "DisplayDriver.h"
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
const float scale_x = EINK_SCALE_X;
const float scale_y = EINK_SCALE_Y;
const float offset_x = EINK_X_OFFSET;
const float offset_y = EINK_Y_OFFSET;
#else
GxEPD2_BW<GxEPD2_150_BN, 200> display;
const float scale_x = 1.5625f;
const float scale_y = 1.5625f;
const float offset_x = 0;
const float offset_y = 10;
#endif
bool _init = false;
bool _isOn = false;
uint16_t _curr_color;
CRC32 display_crc;
int last_display_crc_value = 0;
public:
#if defined(EINK_DISPLAY_MODEL)
GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
#else
GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
#endif
bool begin();
bool isOn() override {return _isOn;};
void turnOn() override;
void turnOff() override;
void clear() override;
void startFrame(Color bkg = DARK) override;
void setTextSize(int sz) override;
void setColor(Color c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
void fillRect(int x, int y, int w, int h) override;
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
void endFrame() override;
};

Some files were not shown because too many files have changed in this diff Show More