mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-30 21:12:39 +00:00
Support radio2, temporary profiles and one schedule slot per command family. Preserve reply affinity, shared airtime and session expiry with guarded warm switching on SX1262 and LR1110. Validate native routing, scheduling, persistence and driver failure paths; build Companion, repeater, room and sensor roles on ESP32, nRF52 and STM32.
1122 lines
42 KiB
C++
1122 lines
42 KiB
C++
#include "SensorMesh.h"
|
|
#include <helpers/RoutingPolicy.h>
|
|
#include <helpers/sensors/LPPDataHelpers.h>
|
|
|
|
/* ------------------------------ Config -------------------------------- */
|
|
|
|
#ifndef LORA_FREQ
|
|
#define LORA_FREQ 915.0
|
|
#endif
|
|
#ifndef LORA_BW
|
|
#define LORA_BW 250
|
|
#endif
|
|
#ifndef LORA_SF
|
|
#define LORA_SF 10
|
|
#endif
|
|
#ifndef LORA_CR
|
|
#define LORA_CR 5
|
|
#endif
|
|
#ifndef LORA_TX_POWER
|
|
#define LORA_TX_POWER 20
|
|
#endif
|
|
|
|
#ifndef ADVERT_NAME
|
|
#define ADVERT_NAME "sensor"
|
|
#endif
|
|
#ifndef ADVERT_LAT
|
|
#define ADVERT_LAT 0.0
|
|
#endif
|
|
#ifndef ADVERT_LON
|
|
#define ADVERT_LON 0.0
|
|
#endif
|
|
|
|
#ifndef ADMIN_PASSWORD
|
|
#define ADMIN_PASSWORD "password"
|
|
#endif
|
|
|
|
#ifndef SERVER_RESPONSE_DELAY
|
|
#define SERVER_RESPONSE_DELAY 300
|
|
#endif
|
|
|
|
#ifndef TXT_ACK_DELAY
|
|
#define TXT_ACK_DELAY 200
|
|
#endif
|
|
|
|
#ifndef SENSOR_READ_INTERVAL_SECS
|
|
#define SENSOR_READ_INTERVAL_SECS 60
|
|
#endif
|
|
|
|
/* ------------------------------ Code -------------------------------- */
|
|
|
|
#define FIRMWARE_VER_LEVEL 1
|
|
|
|
#define REQ_TYPE_LOGIN 0x00
|
|
#define REQ_TYPE_GET_STATUS 0x01
|
|
#define REQ_TYPE_KEEP_ALIVE 0x02
|
|
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
|
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
|
#define REQ_TYPE_GET_ACCESS_LIST 0x05
|
|
#define REQ_TYPE_GET_NEIGHBOURS 0x06 // repeater only (at present)
|
|
|
|
#define REQ_TYPE_SUBSCRIBE 0x08
|
|
#define REQ_TYPE_UNSUBSCRIBE 0x09
|
|
|
|
#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ
|
|
|
|
#define CLI_REPLY_DELAY_MILLIS 1000
|
|
|
|
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
|
|
|
#define ALERT_ACK_EXPIRY_MILLIS 8000 // wait 8 secs for ACKs to alert messages
|
|
|
|
static File openAppend(FILESYSTEM* _fs, const char* fname) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
return _fs->open(fname, FILE_O_WRITE);
|
|
#elif defined(RP2040_PLATFORM)
|
|
return _fs->open(fname, "a");
|
|
#else
|
|
return _fs->open(fname, "a", true);
|
|
#endif
|
|
}
|
|
|
|
/* --------------------- Cayenne LPP helpers ----------------------------*/
|
|
|
|
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
|
|
uint8_t i = 0;
|
|
|
|
while (i + 2 < size) {
|
|
// Get channel #
|
|
uint8_t ch = buf[i++];
|
|
// Get data type
|
|
uint8_t t = buf[i++];
|
|
uint8_t sz = LPPData::getDataSize(t);
|
|
|
|
if (ch == channel && t == type) {
|
|
return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
|
}
|
|
i += sz; // skip
|
|
}
|
|
return def_value; // not found
|
|
}
|
|
|
|
/* ------------------ end Cayenne LPP helpers ----------------------*/
|
|
|
|
bool SensorMesh::telemHasChanged(ClientInfo* c) {
|
|
auto buf = telemetry.getBuffer();
|
|
uint8_t size = telemetry.getSize();
|
|
uint8_t i = 0;
|
|
bool changed = false;
|
|
|
|
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
|
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
|
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
|
uint8_t sz = LPPData::getDataSize(t);
|
|
|
|
float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
|
float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
|
|
|
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
|
if (abs(v - pv) > min_delta) changed = true; // Yes, has changed
|
|
|
|
i += 2 + sz; // skip
|
|
}
|
|
if (changed) {
|
|
// take snapshot of all _monitored_ telem values, for next cycle
|
|
i = 0;
|
|
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
|
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
|
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
|
uint8_t sz = LPPData::getDataSize(t);
|
|
|
|
c->extra.sensor.prev_telem[i] = ch;
|
|
c->extra.sensor.prev_telem[i + 1] = t;
|
|
|
|
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
|
LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
|
|
|
i += 2 + sz; // skip
|
|
}
|
|
}
|
|
return changed;
|
|
}
|
|
|
|
uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp, uint8_t req_type, uint8_t* payload, size_t payload_len) {
|
|
uint8_t perms = from->isAdmin() ? 0xFF : from->permissions;
|
|
|
|
memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag')
|
|
|
|
if (req_type == REQ_TYPE_GET_TELEMETRY_DATA) { // allow all
|
|
uint8_t perm_mask = ~(payload[0]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions
|
|
|
|
telemetry.reset();
|
|
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
|
// query other sensors -- target specific
|
|
sensors.querySensors(0xFF & perm_mask, telemetry); // allow all telemetry permissions for admin or guest
|
|
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
|
float temperature = board.getMCUTemperature();
|
|
if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
|
|
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
|
|
}
|
|
|
|
uint8_t tlen = telemetry.getSize();
|
|
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
|
return 4 + tlen; // reply_len
|
|
}
|
|
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
|
uint32_t start_secs_ago, end_secs_ago;
|
|
memcpy(&start_secs_ago, &payload[0], 4);
|
|
memcpy(&end_secs_ago, &payload[4], 4);
|
|
uint8_t res1 = payload[8]; // reserved for future (extra query params)
|
|
uint8_t res2 = payload[9];
|
|
|
|
MinMaxAvg data[8];
|
|
int n;
|
|
if (res1 == 0 && res2 == 0) {
|
|
n = querySeriesData(start_secs_ago, end_secs_ago, data, 8);
|
|
} else {
|
|
n = 0;
|
|
}
|
|
|
|
uint8_t ofs = 4;
|
|
{
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
memcpy(&reply_data[ofs], &now, 4); ofs += 4;
|
|
}
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
auto d = &data[i];
|
|
reply_data[ofs++] = d->_channel;
|
|
reply_data[ofs++] = d->_lpp_type;
|
|
uint8_t sz = LPPData::getDataSize(d->_lpp_type);
|
|
uint32_t mult = LPPData::getMultiplier(d->_lpp_type);
|
|
bool is_signed = LPPData::isSigned(d->_lpp_type);
|
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_max, sz, mult, is_signed);
|
|
ofs += LPPData::putFloat(&reply_data[ofs], d->_avg, sz, mult, is_signed);
|
|
}
|
|
return ofs;
|
|
}
|
|
if (req_type == REQ_TYPE_GET_ACCESS_LIST && (perms & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN) {
|
|
uint8_t res1 = payload[0]; // reserved for future (extra query params)
|
|
uint8_t res2 = payload[1];
|
|
if (res1 == 0 && res2 == 0) {
|
|
uint8_t ofs = 4;
|
|
for (int i = 0; i < acl.getNumClients() && ofs + 7 <= sizeof(reply_data) - 4; i++) {
|
|
auto c = acl.getClientByIdx(i);
|
|
if (c->permissions == 0) continue; // skip deleted entries
|
|
memcpy(&reply_data[ofs], c->id.pub_key, 6); ofs += 6; // just 6-byte pub_key prefix
|
|
reply_data[ofs++] = c->permissions;
|
|
}
|
|
return ofs;
|
|
}
|
|
}
|
|
if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
|
memcpy(&from->extra.sensor.push_tag, &payload[0], 4);
|
|
uint16_t timeout_secs;
|
|
memcpy(&timeout_secs, &payload[4], 2);
|
|
uint8_t reserved = payload[6];
|
|
uint8_t min_deltas_len = payload[7];
|
|
RegionEntry* r;
|
|
if (recv_pkt_region && !recv_pkt_region->isWildcard()) { // use request scope
|
|
r = recv_pkt_region;
|
|
} else { // use default scope
|
|
r = region_map.getDefaultRegion();
|
|
}
|
|
uint8_t reply_len;
|
|
if (r && min_deltas_len >= 3 && min_deltas_len <= sizeof(from->extra.sensor.min_deltas)) {
|
|
from->extra.sensor.scope_region_id = r->id;
|
|
from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs;
|
|
from->extra.sensor.min_deltas_len = min_deltas_len;
|
|
memcpy(from->extra.sensor.min_deltas, &payload[8], min_deltas_len);
|
|
// reply with actual expiry seconds (we could modify/impose restriction)
|
|
memcpy(&reply_data[4], &timeout_secs, 2);
|
|
memset(&reply_data[6], 0, 2); // reserved
|
|
strcpy((char *)&reply_data[8], r ? r->name : ""); // reply with name of scope that will be used
|
|
reply_len = 8 + strlen((char *)&reply_data[8]);
|
|
} else {
|
|
memset(&reply_data[4], 0, 4); // expiry secs (0 for error)
|
|
reply_len = 8;
|
|
}
|
|
return reply_len;
|
|
}
|
|
if (req_type == REQ_TYPE_UNSUBSCRIBE && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
|
from->extra.sensor.scope_region_id = 0;
|
|
from->extra.sensor.push_tag = 0;
|
|
from->extra.sensor.expiry_timestamp = 0;
|
|
from->extra.sensor.min_deltas_len = 0;
|
|
// REVISIT: maybe return some stats, eg total number of telemetry pushes since SUBSCRIBE?
|
|
memset(&reply_data[4], 0, 8); // success
|
|
getRNG()->random(&reply_data[12], 2); // just some entropy for better packet-hash uniqueness
|
|
return 12 + 2;
|
|
}
|
|
return 0; // unknown command
|
|
}
|
|
|
|
mesh::Packet* SensorMesh::createSelfAdvert() {
|
|
uint8_t app_data[MAX_ADVERT_DATA_SIZE];
|
|
uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_SENSOR, app_data);
|
|
|
|
return createAdvert(self_id, app_data, app_data_len);
|
|
}
|
|
|
|
void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) {
|
|
int text_len = strlen(t->text);
|
|
|
|
uint8_t data[MAX_PACKET_PAYLOAD];
|
|
memcpy(data, &t->timestamp, 4);
|
|
data[4] = (TXT_TYPE_PLAIN << 2) | t->attempt; // attempt and flags
|
|
memcpy(&data[5], t->text, text_len);
|
|
|
|
// calc expected ACK reply
|
|
mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE);
|
|
t->attempt++;
|
|
|
|
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len);
|
|
if (pkt) {
|
|
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
|
sendDirect(pkt, c->out_path, c->out_path_len);
|
|
} else {
|
|
unsigned long delay_millis = 0;
|
|
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
|
}
|
|
}
|
|
t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS);
|
|
}
|
|
|
|
void SensorMesh::alertIf(bool condition, Trigger& t, AlertPriority pri, const char* text) {
|
|
if (condition) {
|
|
if (!t.isTriggered() && num_alert_tasks < MAX_CONCURRENT_ALERTS) {
|
|
StrHelper::strncpy(t.text, text, sizeof(t.text));
|
|
t.pri = pri;
|
|
t.send_expiry = 0; // signal that initial send is needed
|
|
t.attempt = 4;
|
|
t.curr_contact_idx = -1; // start iterating thru contacts[]
|
|
|
|
alert_tasks[num_alert_tasks++] = &t; // add to queue
|
|
}
|
|
} else {
|
|
if (t.isTriggered()) {
|
|
t.text[0] = 0;
|
|
// remove 't' from alert queue
|
|
int i = 0;
|
|
while (i < num_alert_tasks && alert_tasks[i] != &t) i++;
|
|
|
|
if (i < num_alert_tasks) { // found, now delete from array
|
|
num_alert_tasks--;
|
|
while (i < num_alert_tasks) {
|
|
alert_tasks[i] = alert_tasks[i + 1];
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float SensorMesh::getAirtimeBudgetFactor() const {
|
|
return _prefs.airtime_factor;
|
|
}
|
|
|
|
bool SensorMesh::allowPacketForward(const mesh::Packet* packet) {
|
|
if (_prefs.disable_fwd) return false;
|
|
if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false;
|
|
return true;
|
|
}
|
|
|
|
int SensorMesh::calcRxDelay(float score, uint32_t air_time) const {
|
|
if (_prefs.rx_delay_base <= 0.0f) return 0;
|
|
return (int) ((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time);
|
|
}
|
|
|
|
uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) {
|
|
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
|
return getRNG()->nextInt(0, 6)*t;
|
|
}
|
|
uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) {
|
|
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
|
return getRNG()->nextInt(0, 6)*t;
|
|
}
|
|
int SensorMesh::getInterferenceThreshold() const {
|
|
return _prefs.interference_threshold;
|
|
}
|
|
bool SensorMesh::getCADEnabled() const {
|
|
return _prefs.cad_enabled;
|
|
}
|
|
int SensorMesh::getAGCResetInterval() const {
|
|
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
|
}
|
|
|
|
void SensorMesh::startRegionsLoad() {
|
|
temp_map.resetFrom(region_map); // rebuild regions in a temp instance
|
|
memset(load_stack, 0, sizeof(load_stack));
|
|
load_stack[0] = &temp_map.getWildcard();
|
|
region_load_active = true;
|
|
}
|
|
|
|
bool SensorMesh::saveRegions() {
|
|
return region_map.save(_fs);
|
|
}
|
|
|
|
void SensorMesh::onDefaultRegionChanged(const RegionEntry* r) {
|
|
if (r) {
|
|
region_map.getTransportKeysFor(*r, &default_scope, 1);
|
|
} else {
|
|
memset(default_scope.key, 0, sizeof(default_scope.key));
|
|
}
|
|
}
|
|
|
|
uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) {
|
|
ClientInfo* client;
|
|
if (data[0] == 0) { // blank password, just check if sender is in ACL
|
|
client = acl.getClient(sender.pub_key, PUB_KEY_SIZE);
|
|
if (client == NULL) {
|
|
#if MESH_DEBUG
|
|
MESH_DEBUG_PRINTLN("Login, sender not in ACL");
|
|
#endif
|
|
return 0;
|
|
}
|
|
} else {
|
|
if (strcmp((char *) data, _prefs.password) != 0) { // check for valid admin password
|
|
#if MESH_DEBUG
|
|
MESH_DEBUG_PRINTLN("Invalid password: %s", &data[0]);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
client = acl.putClient(sender, PERM_RECV_ALERTS_HI | PERM_RECV_ALERTS_LO); // add to contacts (if not already known)
|
|
if (sender_timestamp <= client->last_timestamp) {
|
|
MESH_DEBUG_PRINTLN("Possible login replay attack!");
|
|
return 0; // FATAL: client table is full -OR- replay attack
|
|
}
|
|
|
|
MESH_DEBUG_PRINTLN("Login success!");
|
|
client->last_timestamp = sender_timestamp;
|
|
client->last_activity = getRTCClock()->getCurrentTime();
|
|
client->permissions |= PERM_ACL_ADMIN;
|
|
memcpy(client->shared_secret, secret, PUB_KEY_SIZE);
|
|
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
}
|
|
|
|
if (is_flood) {
|
|
client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path
|
|
}
|
|
|
|
uint32_t now = getRTCClock()->getCurrentTimeUnique();
|
|
memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp
|
|
reply_data[4] = RESP_SERVER_LOGIN_OK;
|
|
reply_data[5] = 0;
|
|
reply_data[6] = client->isAdmin() ? 1 : 0;
|
|
reply_data[7] = client->permissions;
|
|
getRNG()->random(&reply_data[8], 4); // random blob to help packet-hash uniqueness
|
|
reply_data[12] = FIRMWARE_VER_LEVEL;
|
|
|
|
return 13; // reply length
|
|
}
|
|
|
|
void SensorMesh::handleCommand(ClientInfo* from, uint32_t sender_timestamp, char* command, char* reply) {
|
|
if (region_load_active) {
|
|
if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation
|
|
region_map = temp_map; // copy over the temp instance as new current map
|
|
region_load_active = false;
|
|
|
|
sprintf(reply, "OK - loaded %d regions", region_map.getCount());
|
|
} else {
|
|
char *np = command;
|
|
while (*np == ' ') np++; // skip indent
|
|
int indent = np - command;
|
|
|
|
char *ep = np;
|
|
while (RegionMap::is_name_char(*ep)) ep++;
|
|
if (*ep) { *ep++ = 0; } // set null terminator for end of name
|
|
|
|
while (*ep && *ep != 'F') ep++; // look for (optional) flags
|
|
|
|
if (indent > 0 && indent < 8 && strlen(np) > 0) {
|
|
auto parent = load_stack[indent - 1];
|
|
if (parent) {
|
|
auto old = region_map.findByName(np);
|
|
auto nw = temp_map.putRegion(np, parent->id, old ? old->id : 0); // carry-over the current ID (if name already exists)
|
|
if (nw) {
|
|
nw->flags = old ? old->flags : (*ep == 'F' ? 0 : REGION_DENY_FLOOD); // carry-over flags from curr
|
|
|
|
load_stack[indent] = nw; // keep pointers to parent regions, to resolve parent_id's
|
|
}
|
|
}
|
|
}
|
|
reply[0] = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
while (*command == ' ') command++; // skip leading spaces
|
|
|
|
if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI)
|
|
memcpy(reply, command, 3); // reflect the prefix back
|
|
reply += 3;
|
|
command += 3;
|
|
}
|
|
|
|
// first, see if this is a custom-handled CLI command (ie. in main.cpp)
|
|
if (handleCustomCommand(sender_timestamp, command, reply)) {
|
|
return; // command has been handled
|
|
}
|
|
|
|
// handle sensor-specific CLI commands
|
|
if (memcmp(command, "setperm ", 8) == 0) { // format: setperm {pubkey-hex} {permissions-int8}
|
|
char* hex = &command[8];
|
|
char* sp = strchr(hex, ' '); // look for separator char
|
|
if (sp == NULL) {
|
|
strcpy(reply, "Err - bad params");
|
|
} else {
|
|
int hex_len = min(sp - hex, PUB_KEY_SIZE*2);
|
|
uint8_t pubkey[PUB_KEY_SIZE];
|
|
|
|
*sp++ = 0; // replace space with null terminator
|
|
|
|
if (mesh::Utils::fromHex(pubkey, hex_len / 2, hex)) {
|
|
uint8_t perms = atoi(sp);
|
|
if (acl.applyPermissions(self_id, pubkey, hex_len / 2, perms)) {
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // trigger acl.save()
|
|
strcpy(reply, "OK");
|
|
} else {
|
|
strcpy(reply, "Err - invalid params");
|
|
}
|
|
} else {
|
|
strcpy(reply, "Err - bad pubkey");
|
|
}
|
|
}
|
|
} else if (sender_timestamp == 0 && strcmp(command, "get acl") == 0) {
|
|
Serial.println("ACL:");
|
|
for (int i = 0; i < acl.getNumClients(); i++) {
|
|
auto c = acl.getClientByIdx(i);
|
|
if (c->permissions == 0) continue; // skip deleted entries
|
|
|
|
Serial.printf("%02X ", c->permissions);
|
|
mesh::Utils::printHex(Serial, c->id.pub_key, PUB_KEY_SIZE);
|
|
Serial.printf("\n");
|
|
}
|
|
reply[0] = 0;
|
|
} else if (memcmp(command, "io ", 2) == 0) { // io {value}: write, io: read
|
|
if (command[2] == ' ') { // it's a write
|
|
uint32_t val;
|
|
uint32_t g = board.getGpio();
|
|
if (command[3] == 'r') { // reset bits
|
|
sscanf(&command[4], "%x", &val);
|
|
val = g & ~val;
|
|
} else if (command[3] == 's') { // set bits
|
|
sscanf(&command[4], "%x", &val);
|
|
val |= g;
|
|
} else if (command[3] == 't') { // toggle bits
|
|
sscanf(&command[4], "%x", &val);
|
|
val ^= g;
|
|
} else { // set value
|
|
sscanf(&command[3], "%x", &val);
|
|
}
|
|
board.setGpio(val);
|
|
}
|
|
sprintf(reply, "%x", board.getGpio());
|
|
} else{
|
|
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
|
|
}
|
|
}
|
|
|
|
mesh::DispatcherAction SensorMesh::onRecvPacket(mesh::Packet* pkt) {
|
|
if (pkt->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD) {
|
|
recv_pkt_region = region_map.findMatch(pkt, REGION_DENY_FLOOD);
|
|
} else if (pkt->getRouteType() == ROUTE_TYPE_FLOOD) {
|
|
if (region_map.getWildcard().flags & REGION_DENY_FLOOD) {
|
|
recv_pkt_region = NULL;
|
|
} else {
|
|
recv_pkt_region = ®ion_map.getWildcard();
|
|
}
|
|
} else {
|
|
recv_pkt_region = NULL;
|
|
}
|
|
return Mesh::onRecvPacket(pkt);
|
|
}
|
|
|
|
void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) {
|
|
if (packet->getPayloadType() == PAYLOAD_TYPE_ANON_REQ) { // received an initial request by a possible admin client (unknown at this stage)
|
|
uint32_t timestamp;
|
|
memcpy(×tamp, data, 4);
|
|
|
|
data[len] = 0; // ensure null terminator
|
|
uint8_t reply_len;
|
|
if (data[4] == 0 || data[4] >= ' ') { // is password, ie. a login request
|
|
reply_len = handleLoginReq(sender, secret, timestamp, &data[4], packet->isRouteFlood());
|
|
//} else if (data[4] == ANON_REQ_TYPE_*) { // future type codes
|
|
// TODO
|
|
} else {
|
|
reply_len = 0; // unknown request type
|
|
}
|
|
|
|
if (reply_len == 0) return; // invalid request
|
|
|
|
if (packet->isRouteFlood()) {
|
|
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
|
|
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
|
|
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
|
|
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
|
} else {
|
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
|
|
if (reply) sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
|
}
|
|
}
|
|
}
|
|
|
|
int SensorMesh::searchPeersByHash(const uint8_t* hash) {
|
|
int n = 0;
|
|
for (int i = 0; i < acl.getNumClients() && n < MAX_SEARCH_RESULTS; i++) {
|
|
if (acl.getClientByIdx(i)->id.isHashMatch(hash)) {
|
|
matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods)
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
|
|
int i = matching_peer_indexes[peer_idx];
|
|
if (i >= 0 && i < acl.getNumClients()) {
|
|
// lookup pre-calculated shared_secret
|
|
memcpy(dest_secret, acl.getClientByIdx(i)->shared_secret, PUB_KEY_SIZE);
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("getPeerSharedSecret: Invalid peer idx: %d", i);
|
|
}
|
|
}
|
|
|
|
void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) {
|
|
if (dest.out_path_len == OUT_PATH_UNKNOWN) {
|
|
mesh::Packet* ack = createAck(ack_hash);
|
|
if (ack) sendFloodScoped(default_scope, ack, TXT_ACK_DELAY, path_hash_size);
|
|
} 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 SensorMesh::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 >= acl.getNumClients()) {
|
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: Invalid sender idx: %d", i);
|
|
return;
|
|
}
|
|
|
|
ClientInfo* from = acl.getClientByIdx(i);
|
|
|
|
if (type == PAYLOAD_TYPE_REQ) { // request (from a known contact)
|
|
uint32_t timestamp;
|
|
memcpy(×tamp, data, 4);
|
|
|
|
if (timestamp > from->last_timestamp) { // prevent replay attacks
|
|
uint8_t reply_len = handleRequest(from, timestamp, data[4], &data[5], len - 5);
|
|
if (reply_len == 0) return; // invalid command
|
|
|
|
from->last_timestamp = timestamp;
|
|
from->last_activity = getRTCClock()->getCurrentTime();
|
|
|
|
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, reply_data, reply_len);
|
|
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
|
} else {
|
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, 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 {
|
|
sendFloodReply(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
|
}
|
|
} else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && from->isAdmin()) { // a CLI command
|
|
uint32_t sender_timestamp;
|
|
memcpy(&sender_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
|
|
|
|
if (sender_timestamp > from->last_timestamp) { // prevent replay attacks
|
|
if (flags == TXT_TYPE_PLAIN) {
|
|
bool handled = handleIncomingMsg(*from, sender_timestamp, &data[5], flags, len - 5);
|
|
if (handled) { // if msg was handled then send an ack
|
|
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) sendFloodReply(path, TXT_ACK_DELAY, packet->getPathHashSize());
|
|
} else {
|
|
sendAckTo(*from, ack_hash, packet->getPathHashSize());
|
|
}
|
|
}
|
|
} else if (flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND) {
|
|
from->last_timestamp = sender_timestamp;
|
|
from->last_activity = getRTCClock()->getCurrentTime();
|
|
|
|
// 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
|
|
|
|
uint8_t temp[166];
|
|
char *command = (char *) &data[5];
|
|
char *reply = (char *) &temp[5];
|
|
handleCommand(from, sender_timestamp, command, reply);
|
|
|
|
int text_len = strlen(reply);
|
|
if (text_len > 0) {
|
|
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
|
if (timestamp == sender_timestamp) {
|
|
// WORKAROUND: the two timestamps need to be different, in the CLI view
|
|
timestamp++;
|
|
}
|
|
memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique
|
|
temp[4] = (TXT_TYPE_CLI_DATA << 2);
|
|
|
|
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len);
|
|
_prefs.getRadioPrefs()->profileCLI().attachReply(reply);
|
|
if (reply) {
|
|
if (from->out_path_len == OUT_PATH_UNKNOWN) {
|
|
sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
|
|
} else {
|
|
sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags);
|
|
}
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected");
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SensorMesh::handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len) {
|
|
MESH_DEBUG_PRINT("handleIncomingMsg: unhandled msg from ");
|
|
#ifdef MESH_DEBUG
|
|
mesh::Utils::printHex(Serial, from.id.pub_key, PUB_KEY_SIZE);
|
|
Serial.printf(": %s\n", data);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
#define CTL_TYPE_NODE_DISCOVER_REQ 0x80
|
|
#define CTL_TYPE_NODE_DISCOVER_RESP 0x90
|
|
|
|
void SensorMesh::onControlDataRecv(mesh::Packet* packet) {
|
|
uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits
|
|
if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6) {
|
|
// TODO: apply rate limiting to these!
|
|
int i = 1;
|
|
uint8_t filter = packet->payload[i++];
|
|
uint32_t tag;
|
|
memcpy(&tag, &packet->payload[i], 4); i += 4;
|
|
uint32_t since;
|
|
if (packet->payload_len >= i+4) { // optional since field
|
|
memcpy(&since, &packet->payload[i], 4); i += 4;
|
|
} else {
|
|
since = 0;
|
|
}
|
|
|
|
if ((filter & (1 << ADV_TYPE_SENSOR)) != 0 && _prefs.discovery_mod_timestamp >= since) {
|
|
bool prefix_only = packet->payload[0] & 1;
|
|
uint8_t data[6 + PUB_KEY_SIZE];
|
|
data[0] = CTL_TYPE_NODE_DISCOVER_RESP | ADV_TYPE_SENSOR; // low 4-bits for node type
|
|
data[1] = packet->_snr; // let sender know the inbound SNR ( x 4)
|
|
memcpy(&data[2], &tag, 4); // include tag from request, for client to match to
|
|
memcpy(&data[6], self_id.pub_key, PUB_KEY_SIZE);
|
|
auto resp = createControlData(data, prefix_only ? 6 + 8 : 6 + PUB_KEY_SIZE);
|
|
if (resp) {
|
|
sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SensorMesh::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 >= acl.getNumClients()) {
|
|
MESH_DEBUG_PRINTLN("onPeerPathRecv: Invalid sender idx: %d", i);
|
|
return false;
|
|
}
|
|
|
|
ClientInfo* from = acl.getClientByIdx(i);
|
|
|
|
MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len);
|
|
// NOTE: for this 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, path, path_len); // store a copy of path, for sendDirect()
|
|
from->last_activity = getRTCClock()->getCurrentTime();
|
|
|
|
// REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes??
|
|
if (from->isAdmin()) {
|
|
// only do saveContacts() (of this out_path change) if this is an admin
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
}
|
|
|
|
// NOTE: no reciprocal path send!!
|
|
return false;
|
|
}
|
|
|
|
void SensorMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
|
if (num_alert_tasks > 0) {
|
|
auto t = alert_tasks[0]; // check current alert task
|
|
for (int i = 0; i < t->attempt; i++) {
|
|
if (ack_crc == t->expected_acks[i]) { // matching ACK!
|
|
t->attempt = 4; // signal to move to next contact
|
|
t->send_expiry = 0;
|
|
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables)
|
|
: mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables),
|
|
region_map(key_store), temp_map(key_store),
|
|
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
|
|
telemetry(MAX_PACKET_PAYLOAD - 4)
|
|
{
|
|
next_local_advert = next_flood_advert = 0;
|
|
dirty_contacts_expiry = 0;
|
|
last_read_time = 0;
|
|
num_alert_tasks = 0;
|
|
set_radio_at = revert_radio_at = 0;
|
|
recv_pkt_region = NULL;
|
|
region_load_active = false;
|
|
|
|
// defaults
|
|
_prefs.airtime_factor = 1.0;
|
|
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
|
|
_prefs.tx_delay_factor = 0.5f; // was 0.25f
|
|
_prefs.direct_tx_delay_factor = 0.2f; // was zero
|
|
StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name));
|
|
_prefs.node_lat = ADVERT_LAT;
|
|
_prefs.node_lon = ADVERT_LON;
|
|
StrHelper::strncpy(_prefs.password, ADMIN_PASSWORD, sizeof(_prefs.password));
|
|
_prefs.freq = LORA_FREQ;
|
|
_prefs.sf = LORA_SF;
|
|
_prefs.bw = LORA_BW;
|
|
_prefs.cr = LORA_CR;
|
|
_prefs.tx_power_dbm = LORA_TX_POWER;
|
|
_prefs.advert_interval = 1; // default to 2 minutes for NEW installs
|
|
_prefs.flood_advert_interval = 0; // disabled
|
|
_prefs.disable_fwd = true;
|
|
_prefs.flood_max = 64;
|
|
_prefs.interference_threshold = 0; // disabled
|
|
_prefs.cad_enabled = 0; // hardware CAD before TX (off by default; 'set cad on')
|
|
|
|
// GPS defaults
|
|
_prefs.gps_enabled = 0;
|
|
_prefs.gps_interval = 0;
|
|
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
|
|
_prefs.radio_fem_rxgain = 1;
|
|
_prefs.radio_fem_txgain = 0;
|
|
|
|
memset(default_scope.key, 0, sizeof(default_scope.key));
|
|
}
|
|
|
|
void SensorMesh::begin(FILESYSTEM* fs) {
|
|
mesh::Mesh::begin();
|
|
_fs = fs;
|
|
// load persisted prefs
|
|
_cli.loadPrefs(_fs);
|
|
|
|
acl.load(_fs, self_id);
|
|
region_map.load(_fs);
|
|
|
|
// establish default-scope
|
|
{
|
|
RegionEntry* r = region_map.getDefaultRegion();
|
|
if (r) {
|
|
region_map.getTransportKeysFor(*r, &default_scope, 1);
|
|
} else {
|
|
#ifdef DEFAULT_FLOOD_SCOPE_NAME
|
|
r = region_map.findByName(DEFAULT_FLOOD_SCOPE_NAME);
|
|
if (r == NULL) {
|
|
r = region_map.putRegion(DEFAULT_FLOOD_SCOPE_NAME, 0); // auto-create the default scope region
|
|
if (r) { r->flags = 0; } // Allow-flood
|
|
}
|
|
if (r) {
|
|
region_map.setDefaultRegion(r);
|
|
region_map.getTransportKeysFor(*r, &default_scope, 1);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
|
_prefs.getRadioPrefs()->profileCLI().begin(*_prefs.getRadioPrefs(), radio_driver, *getRTCClock(), this);
|
|
radio_driver.setTxPower(_prefs.tx_power_dbm);
|
|
|
|
board.attachDynamicPrefs(_prefs.getCustom());
|
|
|
|
updateAdvertTimer();
|
|
updateFloodAdvertTimer();
|
|
|
|
board.setAdcMultiplier(_prefs.adc_multiplier);
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
applyGpsPrefs();
|
|
#endif
|
|
}
|
|
|
|
bool SensorMesh::formatFileSystem() {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
return InternalFS.format();
|
|
#elif defined(RP2040_PLATFORM)
|
|
return LittleFS.format();
|
|
#elif defined(ESP32)
|
|
return SPIFFS.format();
|
|
#else
|
|
#error "need to implement file system erase"
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void SensorMesh::saveIdentity(const mesh::LocalIdentity& new_id) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
IdentityStore store(*_fs, "");
|
|
#elif defined(ESP32)
|
|
IdentityStore store(*_fs, "/identity");
|
|
#elif defined(RP2040_PLATFORM)
|
|
IdentityStore store(*_fs, "/identity");
|
|
#else
|
|
#error "need to define saveIdentity()"
|
|
#endif
|
|
store.save("_main", new_id);
|
|
}
|
|
|
|
void SensorMesh::applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) {
|
|
set_radio_at = futureMillis(2000); // give CLI reply some time to be sent back, before applying temp radio params
|
|
pending_freq = freq;
|
|
pending_bw = bw;
|
|
pending_sf = sf;
|
|
pending_cr = cr;
|
|
|
|
revert_radio_at = futureMillis(2000 + timeout_mins*60*1000); // schedule when to revert radio params
|
|
}
|
|
|
|
void SensorMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size) {
|
|
if (scope.isNull()) {
|
|
sendFlood(pkt, delay_millis, path_hash_size);
|
|
} else {
|
|
uint16_t codes[2];
|
|
codes[0] = scope.calcTransportCode(pkt);
|
|
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
|
|
sendFlood(pkt, codes, delay_millis, path_hash_size);
|
|
}
|
|
}
|
|
|
|
void SensorMesh::sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size) {
|
|
TransportKey req_scope;
|
|
bool is_wildcard = recv_pkt_region != NULL && recv_pkt_region->isWildcard();
|
|
bool req_scope_known = recv_pkt_region != NULL && !is_wildcard
|
|
&& region_map.getTransportKeysFor(*recv_pkt_region, &req_scope, 1) > 0;
|
|
|
|
switch (mesh::chooseReplyScope(req_scope_known, is_wildcard, !default_scope.isNull())) {
|
|
case mesh::REPLY_SCOPE_REQUEST:
|
|
sendFloodScoped(req_scope, packet, delay_millis, path_hash_size); // reply with same scope as request
|
|
break;
|
|
case mesh::REPLY_SCOPE_DEFAULT:
|
|
// requester's scope is unknown: DIRECT request (no transport codes), or code matched no Region.
|
|
// un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0
|
|
sendFloodScoped(default_scope, packet, delay_millis, path_hash_size);
|
|
break;
|
|
case mesh::REPLY_SCOPE_NONE:
|
|
sendFlood(packet, delay_millis, path_hash_size); // send un-scoped
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) {
|
|
mesh::Packet* pkt = createSelfAdvert();
|
|
if (pkt) {
|
|
if (flood) {
|
|
sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
|
} else {
|
|
sendZeroHop(pkt, delay_millis);
|
|
}
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
|
|
}
|
|
}
|
|
|
|
void SensorMesh::updateAdvertTimer() {
|
|
if (_prefs.advert_interval > 0) { // schedule local advert timer
|
|
next_local_advert = futureMillis( ((uint32_t)_prefs.advert_interval) * 2 * 60 * 1000);
|
|
} else {
|
|
next_local_advert = 0; // stop the timer
|
|
}
|
|
}
|
|
void SensorMesh::updateFloodAdvertTimer() {
|
|
if (_prefs.flood_advert_interval > 0) { // schedule flood advert timer
|
|
next_flood_advert = futureMillis( ((uint32_t)_prefs.flood_advert_interval) * 60 * 60 * 1000);
|
|
} else {
|
|
next_flood_advert = 0; // stop the timer
|
|
}
|
|
}
|
|
|
|
void SensorMesh::setTxPower(int8_t power_dbm) {
|
|
radio_driver.setTxPower(power_dbm);
|
|
}
|
|
|
|
void SensorMesh::formatStatsReply(char *reply) {
|
|
StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr);
|
|
}
|
|
|
|
void SensorMesh::formatRadioStatsReply(char *reply) {
|
|
StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime());
|
|
}
|
|
|
|
void SensorMesh::formatPacketStatsReply(char *reply) {
|
|
StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(),
|
|
getNumRecvFlood(), getNumRecvDirect());
|
|
}
|
|
|
|
float SensorMesh::getTelemValue(uint8_t channel, uint8_t type) {
|
|
return findTelemValue(telemetry.getBuffer(), telemetry.getSize(), channel, type, 0.0f);
|
|
}
|
|
|
|
bool SensorMesh::getGPS(uint8_t channel, float& lat, float& lon, float& alt) {
|
|
if (channel == TELEM_CHANNEL_SELF) {
|
|
lat = sensors.node_lat;
|
|
lon = sensors.node_lon;
|
|
alt = sensors.node_altitude;
|
|
return true;
|
|
}
|
|
// REVISIT: custom GPS channels??
|
|
return false;
|
|
}
|
|
|
|
void SensorMesh::loop() {
|
|
auto& profiles = _prefs.getRadioPrefs()->profileCLI();
|
|
profiles.loop(!hasPendingTransmissions());
|
|
mesh::Mesh::loop();
|
|
if (profiles.saveDue()) savePrefs();
|
|
|
|
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
|
|
mesh::Packet* pkt = createSelfAdvert();
|
|
unsigned long delay_millis = 0;
|
|
if (pkt) sendFloodScoped(default_scope, pkt, delay_millis, _prefs.path_hash_mode + 1);
|
|
|
|
updateFloodAdvertTimer(); // schedule next flood advert
|
|
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
|
|
} else if (next_local_advert && millisHasNowPassed(next_local_advert)) {
|
|
mesh::Packet* pkt = createSelfAdvert();
|
|
if (pkt) sendZeroHop(pkt);
|
|
|
|
updateAdvertTimer(); // schedule next local advert
|
|
}
|
|
|
|
if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params
|
|
set_radio_at = 0; // clear timer
|
|
radio_driver.setParams(pending_freq, pending_bw, pending_sf, pending_cr);
|
|
MESH_DEBUG_PRINTLN("Temp radio params");
|
|
}
|
|
|
|
if (revert_radio_at && millisHasNowPassed(revert_radio_at)) { // revert radio params to orig
|
|
revert_radio_at = 0; // clear timer
|
|
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
|
MESH_DEBUG_PRINTLN("Radio params restored");
|
|
}
|
|
|
|
uint32_t curr = getRTCClock()->getCurrentTime();
|
|
if (curr >= last_read_time + SENSOR_READ_INTERVAL_SECS) {
|
|
telemetry.reset();
|
|
telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
|
// query other sensors -- target specific
|
|
sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions
|
|
// This MCU temperature will be overridden by external sensors (if any)
|
|
float temperature = board.getMCUTemperature();
|
|
if (!isnan(temperature)) { // Supported boards with built-in temperature sensor. ESP32-C3 may return NAN
|
|
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
|
|
}
|
|
|
|
// compare with previous telemetry, check if any deltas are greater than subscriber minimums
|
|
for (int i = 0; i < acl.getNumClients(); i++) {
|
|
auto c = acl.getClientByIdx(i);
|
|
if (c->permissions == 0 || c->extra.sensor.scope_region_id == 0 || c->extra.sensor.min_deltas_len == 0) continue; // skip deleted entries, or Not subscribed to deltas
|
|
RegionEntry* r = region_map.findById(c->extra.sensor.scope_region_id);
|
|
if (r == NULL) continue; // unknown region scope
|
|
if (curr > c->extra.sensor.expiry_timestamp) continue; // subscription now expired
|
|
if (telemHasChanged(c)) {
|
|
TransportKey scope;
|
|
if (region_map.getTransportKeysFor(*r, &scope, 1) > 0) {
|
|
uint8_t tlen = telemetry.getSize();
|
|
memcpy(reply_data, &c->extra.sensor.push_tag, 4);
|
|
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
|
memcpy(&reply_data[4], ×tamp, 4);
|
|
memcpy(&reply_data[8], telemetry.getBuffer(), tlen);
|
|
|
|
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, c->id, c->shared_secret, reply_data, 8 + tlen);
|
|
if (reply) {
|
|
if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
|
|
sendDirect(reply, c->out_path, c->out_path_len, 0);
|
|
} else {
|
|
sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onSensorDataRead();
|
|
|
|
last_read_time = curr;
|
|
}
|
|
|
|
// check the alert send queue
|
|
if (num_alert_tasks > 0) {
|
|
auto t = alert_tasks[0]; // process head of queue
|
|
|
|
if (millisHasNowPassed(t->send_expiry)) { // next send needed?
|
|
if (t->attempt >= 4) { // max attempts reached, try next contact
|
|
t->curr_contact_idx++;
|
|
if (t->curr_contact_idx >= acl.getNumClients()) { // no more contacts to try?
|
|
num_alert_tasks--; // remove t from queue
|
|
for (int i = 0; i < num_alert_tasks; i++) {
|
|
alert_tasks[i] = alert_tasks[i + 1];
|
|
}
|
|
} else {
|
|
auto c = acl.getClientByIdx(t->curr_contact_idx);
|
|
uint16_t pri_mask = (t->pri == HIGH_PRI_ALERT) ? PERM_RECV_ALERTS_HI : PERM_RECV_ALERTS_LO;
|
|
|
|
if (c->permissions & pri_mask) { // contact wants alert
|
|
// reset attempts
|
|
t->attempt = (t->pri == LOW_PRI_ALERT) ? 3 : 0; // Low pri alerts, start at attempt #3 (ie. only make ONE attempt)
|
|
t->timestamp = getRTCClock()->getCurrentTimeUnique(); // need unique timestamp per contact
|
|
|
|
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
|
|
} else {
|
|
// next contact tested in next ::loop()
|
|
}
|
|
}
|
|
} else if (t->curr_contact_idx < acl.getNumClients()) {
|
|
auto c = acl.getClientByIdx(t->curr_contact_idx); // send next attempt
|
|
sendAlert(c, t); // NOTE: modifies attempt, expected_acks[] and send_expiry
|
|
} else {
|
|
// contact list has likely been modified while waiting for alert ACK, cancel this task
|
|
t->attempt = 4; // next ::loop() will remove t from queue
|
|
}
|
|
}
|
|
}
|
|
|
|
// pending dirty contacts write needed?
|
|
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
|
acl.save(_fs);
|
|
dirty_contacts_expiry = 0;
|
|
}
|
|
}
|