diff --git a/examples/simple_repeater/ApnDatabase.h b/examples/simple_repeater/ApnDatabase.h new file mode 100644 index 00000000..e3c3d3a5 --- /dev/null +++ b/examples/simple_repeater/ApnDatabase.h @@ -0,0 +1,372 @@ +#pragma once + +// ============================================================================= +// ApnDatabase.h - Embedded APN Lookup Table +// +// Maps MCC/MNC (Mobile Country Code / Mobile Network Code) to default APN +// settings for common carriers worldwide. Compiled directly into flash (~3KB) +// so users never need to manually install a lookup file. +// +// The modem queries IMSI via AT+CIMI to extract MCC (3 digits) + MNC (2-3 +// digits), then looks up the APN here. If not found, falls back to the +// modem's existing PDP context (AT+CGDCONT?) or user-configured APN. +// +// To add a carrier: append to APN_DATABASE[] with the MCC+MNC as a single +// integer. MNC can be 2 or 3 digits: +// MCC=310, MNC=260 → mccmnc = 310260 +// MCC=505, MNC=01 → mccmnc = 50501 +// +// Guard: HAS_4G_MODEM +// ============================================================================= + +#ifdef HAS_4G_MODEM + +#ifndef APN_DATABASE_H +#define APN_DATABASE_H + +struct ApnEntry { + uint32_t mccmnc; // MCC+MNC as integer (e.g. 310260 for T-Mobile US) + const char* apn; // APN string + const char* carrier; // Human-readable carrier name (for debug/display) +}; + +// --------------------------------------------------------------------------- +// APN Database — sorted by MCC for binary search potential (not required) +// +// Sources: carrier documentation, GSMA databases, community wikis. +// This covers ~120 major carriers across key regions. Users with less +// common carriers can set APN manually in Settings. +// --------------------------------------------------------------------------- + +static const ApnEntry APN_DATABASE[] = { + // ========================================================================= + // Australia (MCC 505) + // ========================================================================= + { 50501, "telstra.internet", "Telstra" }, + { 50502, "yesinternet", "Optus" }, + { 50503, "vfinternet.au", "Vodafone AU" }, + { 50506, "3netaccess", "Three AU" }, + { 50507, "telstra.internet", "Vodafone AU (MVNO)" }, // Many MVNOs on Telstra + { 50510, "telstra.internet", "Norfolk Tel" }, + { 50512, "3netaccess", "Amaysim" }, // Optus MVNO + { 50514, "yesinternet", "Aussie Broadband" }, // Optus MVNO + { 50590, "yesinternet", "Optus MVNO" }, + + // ========================================================================= + // New Zealand (MCC 530) + // ========================================================================= + { 53001, "internet", "Vodafone NZ" }, + { 53005, "internet", "Spark NZ" }, + { 53024, "internet", "2degrees" }, + + // ========================================================================= + // United States (MCC 310, 311, 312, 313, 316) + // ========================================================================= + { 310012, "fast.t-mobile.com", "Verizon (old)" }, + { 310026, "fast.t-mobile.com", "T-Mobile US" }, + { 310030, "fast.t-mobile.com", "T-Mobile US" }, + { 310032, "fast.t-mobile.com", "T-Mobile US" }, + { 310060, "fast.t-mobile.com", "T-Mobile US" }, + { 310160, "fast.t-mobile.com", "T-Mobile US" }, + { 310200, "fast.t-mobile.com", "T-Mobile US" }, + { 310210, "fast.t-mobile.com", "T-Mobile US" }, + { 310220, "fast.t-mobile.com", "T-Mobile US" }, + { 310230, "fast.t-mobile.com", "T-Mobile US" }, + { 310240, "fast.t-mobile.com", "T-Mobile US" }, + { 310250, "fast.t-mobile.com", "T-Mobile US" }, + { 310260, "fast.t-mobile.com", "T-Mobile US" }, + { 310270, "fast.t-mobile.com", "T-Mobile US" }, + { 310310, "fast.t-mobile.com", "T-Mobile US" }, + { 310490, "fast.t-mobile.com", "T-Mobile US" }, + { 310530, "fast.t-mobile.com", "T-Mobile US" }, + { 310580, "fast.t-mobile.com", "T-Mobile US" }, + { 310660, "fast.t-mobile.com", "T-Mobile US" }, + { 310800, "fast.t-mobile.com", "T-Mobile US" }, + { 311480, "vzwinternet", "Verizon" }, + { 311481, "vzwinternet", "Verizon" }, + { 311482, "vzwinternet", "Verizon" }, + { 311483, "vzwinternet", "Verizon" }, + { 311484, "vzwinternet", "Verizon" }, + { 311489, "vzwinternet", "Verizon" }, + { 310410, "fast.t-mobile.com", "AT&T (migrated)" }, + { 310120, "att.mvno", "AT&T (Sprint)" }, + { 312530, "iot.1nce.net", "1NCE IoT" }, + { 310120, "tfdata", "Tracfone" }, + + // ========================================================================= + // Canada (MCC 302) + // ========================================================================= + { 30220, "internet.com", "Rogers" }, + { 30221, "internet.com", "Rogers" }, + { 30237, "internet.com", "Rogers" }, + { 30272, "internet.com", "Rogers" }, + { 30234, "sp.telus.com", "Telus" }, + { 30286, "sp.telus.com", "Telus" }, + { 30236, "sp.telus.com", "Telus" }, + { 30261, "sp.bell.ca", "Bell" }, + { 30263, "sp.bell.ca", "Bell" }, + { 30267, "sp.bell.ca", "Bell" }, + { 30268, "fido-core-appl1.apn", "Fido" }, + { 30278, "internet.com", "SaskTel" }, + { 30266, "sp.mb.com", "MTS" }, + + // ========================================================================= + // United Kingdom (MCC 234, 235) + // ========================================================================= + { 23410, "o2-internet", "O2 UK" }, + { 23415, "three.co.uk", "Vodafone UK" }, + { 23420, "three.co.uk", "Three UK" }, + { 23430, "everywhere", "EE" }, + { 23431, "everywhere", "EE" }, + { 23432, "everywhere", "EE" }, + { 23433, "everywhere", "EE" }, + { 23450, "data.lycamobile.co.uk","Lycamobile UK" }, + { 23486, "three.co.uk", "Three UK" }, + + // ========================================================================= + // Germany (MCC 262) + // ========================================================================= + { 26201, "internet.t-mobile", "Telekom DE" }, + { 26202, "web.vodafone.de", "Vodafone DE" }, + { 26203, "internet", "O2 DE" }, + { 26207, "internet", "O2 DE" }, + + // ========================================================================= + // France (MCC 208) + // ========================================================================= + { 20801, "orange", "Orange FR" }, + { 20810, "sl2sfr", "SFR" }, + { 20815, "free", "Free Mobile" }, + { 20820, "ofnew.fr", "Bouygues" }, + + // ========================================================================= + // Italy (MCC 222) + // ========================================================================= + { 22201, "mobile.vodafone.it", "TIM" }, + { 22210, "mobile.vodafone.it", "Vodafone IT" }, + { 22250, "internet.it", "Iliad IT" }, + { 22288, "internet.wind", "WindTre" }, + { 22299, "internet.wind", "WindTre" }, + + // ========================================================================= + // Spain (MCC 214) + // ========================================================================= + { 21401, "internet", "Vodafone ES" }, + { 21403, "internet", "Orange ES" }, + { 21404, "internet", "Yoigo" }, + { 21407, "internet", "Movistar" }, + + // ========================================================================= + // Netherlands (MCC 204) + // ========================================================================= + { 20404, "internet", "Vodafone NL" }, + { 20408, "internet", "KPN" }, + { 20412, "internet", "Telfort" }, + { 20416, "internet", "T-Mobile NL" }, + { 20420, "internet", "T-Mobile NL" }, + + // ========================================================================= + // Sweden (MCC 240) + // ========================================================================= + { 24001, "internet.telia.se", "Telia SE" }, + { 24002, "tre.se", "Three SE" }, + { 24007, "internet.telenor.se", "Telenor SE" }, + + // ========================================================================= + // Norway (MCC 242) + // ========================================================================= + { 24201, "internet.telenor.no", "Telenor NO" }, + { 24202, "internet.netcom.no", "Telia NO" }, + + // ========================================================================= + // Denmark (MCC 238) + // ========================================================================= + { 23801, "internet", "TDC" }, + { 23802, "internet", "Telenor DK" }, + { 23806, "internet", "Three DK" }, + { 23820, "internet", "Telia DK" }, + + // ========================================================================= + // Switzerland (MCC 228) + // ========================================================================= + { 22801, "gprs.swisscom.ch", "Swisscom" }, + { 22802, "internet", "Sunrise" }, + { 22803, "internet", "Salt" }, + + // ========================================================================= + // Austria (MCC 232) + // ========================================================================= + { 23201, "a1.net", "A1" }, + { 23203, "web.one.at", "Three AT" }, + { 23205, "web", "T-Mobile AT" }, + + // ========================================================================= + // Japan (MCC 440, 441) + // ========================================================================= + { 44010, "spmode.ne.jp", "NTT Docomo" }, + { 44020, "plus.4g", "SoftBank" }, + { 44051, "au.au-net.ne.jp", "KDDI au" }, + + // ========================================================================= + // South Korea (MCC 450) + // ========================================================================= + { 45005, "lte.sktelecom.com", "SK Telecom" }, + { 45006, "lte.ktfwing.com", "KT" }, + { 45008, "lte.lguplus.co.kr", "LG U+" }, + + // ========================================================================= + // India (MCC 404, 405) + // ========================================================================= + { 40445, "airtelgprs.com", "Airtel" }, + { 40410, "airtelgprs.com", "Airtel" }, + { 40411, "www", "Vodafone IN (Vi)" }, + { 40413, "www", "Vodafone IN (Vi)" }, + { 40486, "www", "Vodafone IN (Vi)" }, + { 40553, "jionet", "Jio" }, + { 40554, "jionet", "Jio" }, + { 40512, "bsnlnet", "BSNL" }, + + // ========================================================================= + // Singapore (MCC 525) + // ========================================================================= + { 52501, "internet", "Singtel" }, + { 52503, "internet", "M1" }, + { 52505, "internet", "StarHub" }, + + // ========================================================================= + // Hong Kong (MCC 454) + // ========================================================================= + { 45400, "internet", "CSL" }, + { 45406, "internet", "SmarTone" }, + { 45412, "internet", "CMHK" }, + + // ========================================================================= + // Brazil (MCC 724) + // ========================================================================= + { 72405, "claro.com.br", "Claro BR" }, + { 72406, "wap.oi.com.br", "Vivo" }, + { 72410, "wap.oi.com.br", "Vivo" }, + { 72411, "wap.oi.com.br", "Vivo" }, + { 72415, "internet.tim.br", "TIM BR" }, + { 72431, "gprs.oi.com.br", "Oi" }, + + // ========================================================================= + // Mexico (MCC 334) + // ========================================================================= + { 33402, "internet.itelcel.com","Telcel" }, + { 33403, "internet.movistar.mx","Movistar MX" }, + { 33404, "internet.att.net.mx", "AT&T MX" }, + + // ========================================================================= + // South Africa (MCC 655) + // ========================================================================= + { 65501, "internet", "Vodacom" }, + { 65502, "internet", "Telkom ZA" }, + { 65507, "internet", "Cell C" }, + { 65510, "internet", "MTN ZA" }, + + // ========================================================================= + // Philippines (MCC 515) + // ========================================================================= + { 51502, "internet.globe.com.ph","Globe" }, + { 51503, "internet", "Smart" }, + { 51505, "internet", "Sun Cellular" }, + + // ========================================================================= + // Thailand (MCC 520) + // ========================================================================= + { 52001, "internet", "AIS" }, + { 52004, "internet", "TrueMove" }, + { 52005, "internet", "dtac" }, + + // ========================================================================= + // Indonesia (MCC 510) + // ========================================================================= + { 51001, "internet", "Telkomsel" }, + { 51010, "internet", "Telkomsel" }, + { 51011, "3gprs", "XL Axiata" }, + { 51028, "3gprs", "XL Axiata (Axis)" }, + + // ========================================================================= + // Malaysia (MCC 502) + // ========================================================================= + { 50212, "celcom3g", "Celcom" }, + { 50213, "celcom3g", "Celcom" }, + { 50216, "internet", "Digi" }, + { 50219, "celcom3g", "Celcom" }, + + // ========================================================================= + // Czech Republic (MCC 230) + // ========================================================================= + { 23001, "internet.t-mobile.cz","T-Mobile CZ" }, + { 23002, "internet", "O2 CZ" }, + { 23003, "internet.vodafone.cz","Vodafone CZ" }, + + // ========================================================================= + // Poland (MCC 260) + // ========================================================================= + { 26001, "internet", "Plus PL" }, + { 26002, "internet", "T-Mobile PL" }, + { 26003, "internet", "Orange PL" }, + { 26006, "internet", "Play" }, + + // ========================================================================= + // Portugal (MCC 268) + // ========================================================================= + { 26801, "internet", "Vodafone PT" }, + { 26803, "internet", "NOS" }, + { 26806, "internet", "MEO" }, + + // ========================================================================= + // Ireland (MCC 272) + // ========================================================================= + { 27201, "internet", "Vodafone IE" }, + { 27202, "open.internet", "Three IE" }, + { 27205, "three.ie", "Three IE" }, + + // ========================================================================= + // IoT / Global SIMs + // ========================================================================= + { 901028, "iot.1nce.net", "1NCE (IoT)" }, + { 90143, "hologram", "Hologram" }, +}; + +#define APN_DATABASE_SIZE (sizeof(APN_DATABASE) / sizeof(APN_DATABASE[0])) + +// --------------------------------------------------------------------------- +// Lookup function — returns nullptr if not found +// --------------------------------------------------------------------------- + +inline const ApnEntry* apnLookup(uint32_t mccmnc) { + for (int i = 0; i < (int)APN_DATABASE_SIZE; i++) { + if (APN_DATABASE[i].mccmnc == mccmnc) { + return &APN_DATABASE[i]; + } + } + return nullptr; +} + +// Parse IMSI string into MCC+MNC. Tries 3-digit MNC first (6-digit mccmnc), +// falls back to 2-digit MNC (5-digit mccmnc) if not found. +inline const ApnEntry* apnLookupFromIMSI(const char* imsi) { + if (!imsi || strlen(imsi) < 5) return nullptr; + + // Extract MCC (always 3 digits) + uint32_t mcc = (imsi[0] - '0') * 100 + (imsi[1] - '0') * 10 + (imsi[2] - '0'); + + // Try 3-digit MNC first (more specific) + if (strlen(imsi) >= 6) { + uint32_t mnc3 = (imsi[3] - '0') * 100 + (imsi[4] - '0') * 10 + (imsi[5] - '0'); + uint32_t mccmnc6 = mcc * 1000 + mnc3; + const ApnEntry* entry = apnLookup(mccmnc6); + if (entry) return entry; + } + + // Fall back to 2-digit MNC + uint32_t mnc2 = (imsi[3] - '0') * 10 + (imsi[4] - '0'); + uint32_t mccmnc5 = mcc * 100 + mnc2; + return apnLookup(mccmnc5); +} + +#endif // APN_DATABASE_H +#endif // HAS_4G_MODEM \ No newline at end of file diff --git a/examples/simple_repeater/Cellularmqtt.h b/examples/simple_repeater/Cellularmqtt.h new file mode 100644 index 00000000..1a24a012 --- /dev/null +++ b/examples/simple_repeater/Cellularmqtt.h @@ -0,0 +1,235 @@ +#pragma once + +// ============================================================================= +// CellularMQTT — A7682E Modem + MQTT via native AT commands +// +// Stripped-down modem driver for the remote repeater use case. No SMS, no +// voice calls. Just: power on → register → activate data → MQTT connect. +// +// Uses the A7682E's built-in MQTT client with TLS (AT+CMQTT* commands). +// The UART stays in AT command mode permanently — no PPP needed. +// +// Runs on a FreeRTOS task (Core 0) to avoid blocking the mesh radio loop. +// Commands from the MQTT dashboard arrive as queued strings for the main +// loop to process via CommonCLI; responses are queued back for publishing. +// +// Guard: HAS_4G_MODEM (variant.h provides pin definitions) +// ============================================================================= + +#ifdef HAS_4G_MODEM + +#ifndef CELLULAR_MQTT_H +#define CELLULAR_MQTT_H + +#include +#include +#include +#include +#include +#include "variant.h" +#include "ApnDatabase.h" + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- +#define MQTT_TOPIC_MAX 80 +#define MQTT_PAYLOAD_MAX 512 +#define MQTT_CLIENT_ID_MAX 32 + +// Queue sizes +#define CMD_QUEUE_SIZE 4 // MQTT → main loop (CLI commands) +#define RSP_QUEUE_SIZE 4 // main loop → MQTT (CLI responses) + +// Telemetry interval (ms) +#define TELEMETRY_INTERVAL 60000 // 60 seconds + +// Task configuration +#define CELL_TASK_PRIORITY 1 +#define CELL_TASK_STACK_SIZE 8192 // MQTT + TLS AT commands need headroom +#define CELL_TASK_CORE 0 + +// Reconnect timing +#define MQTT_RECONNECT_MIN 5000 // 5 seconds +#define MQTT_RECONNECT_MAX 300000 // 5 minutes (exponential backoff cap) + +// Consecutive publish failures before forced reconnect +#define MQTT_PUB_FAIL_MAX 5 + +// --------------------------------------------------------------------------- +// State machine +// --------------------------------------------------------------------------- +enum class CellState : uint8_t { + OFF, + POWERING_ON, + INITIALIZING, + REGISTERING, + DATA_ACTIVATING, // PDP context + MQTT_STARTING, // AT+CMQTTSTART + MQTT_CONNECTING, // AT+CMQTTCONNECT + CONNECTED, // MQTT up, subscribed, publishing telemetry + RECONNECTING, // Link lost, attempting reconnect + ERROR +}; + +// --------------------------------------------------------------------------- +// Queue message types +// --------------------------------------------------------------------------- + +// Incoming CLI command (from MQTT subscription → main loop) +struct MQTTCommand { + char cmd[MQTT_PAYLOAD_MAX]; +}; + +// Outgoing CLI response (from main loop → MQTT publish) +struct MQTTResponse { + char topic[MQTT_TOPIC_MAX]; + char payload[MQTT_PAYLOAD_MAX]; +}; + +// --------------------------------------------------------------------------- +// MQTT config (loaded from SD: /remote/mqtt.cfg) +// --------------------------------------------------------------------------- +struct MQTTConfig { + char broker[80]; // e.g. "broker.hivemq.cloud" + uint16_t port; // e.g. 8883 + char username[40]; + char password[40]; + char deviceId[MQTT_CLIENT_ID_MAX]; // Auto-generated from MAC if empty +}; + +// --------------------------------------------------------------------------- +// Telemetry snapshot (filled by main loop, published by modem task) +// --------------------------------------------------------------------------- +struct TelemetryData { + uint32_t uptime_secs; + uint16_t battery_mv; + uint8_t battery_pct; + int16_t temperature; // 0.1°C from BQ27220 + int csq; + uint8_t neighbor_count; + float freq; + float bw; + uint8_t sf; + uint8_t cr; + uint8_t tx_power; + char node_name[32]; + char apn[40]; + char oper[24]; + bool mqtt_connected; +}; + +// --------------------------------------------------------------------------- +// CellularMQTT class +// --------------------------------------------------------------------------- +class CellularMQTT { +public: + void begin(); + void stop(); + + // --- Queue API (called from main loop) --- + bool recvCommand(MQTTCommand& out); + bool sendResponse(const char* topic, const char* payload); + + // --- Telemetry (set by main loop, published by modem task) --- + void updateTelemetry(const TelemetryData& data); + + // --- State queries (lock-free reads from main loop) --- + CellState getState() const { return _state; } + bool isConnected() const { return _state == CellState::CONNECTED; } + int getCSQ() const { return _csq; } + int getSignalBars() const; + const char* getOperator() const { return _operator; } + const char* getIPAddress() const { return _ipAddr; } + const char* getBroker() const { return _config.broker; } + const char* getAPN() const { return _apn; } + const char* getRspTopic() const { return _topicRsp; } + const char* stateString() const; + uint32_t getLastCmdTime() const { return _lastCmdTime; } + + // Load config from SD card + static bool loadConfig(MQTTConfig& cfg); + +private: + volatile CellState _state = CellState::OFF; + volatile int _csq = 99; + volatile uint32_t _lastCmdTime = 0; + + char _operator[24] = {0}; + char _ipAddr[20] = {0}; + char _imei[20] = {0}; + char _imsi[20] = {0}; + char _apn[64] = {0}; + + MQTTConfig _config = {}; + TelemetryData _telemetry = {}; + SemaphoreHandle_t _telemetryMutex = nullptr; + + // Topic strings (built from device ID) + char _topicCmd[MQTT_TOPIC_MAX] = {0}; + char _topicRsp[MQTT_TOPIC_MAX] = {0}; + char _topicTelem[MQTT_TOPIC_MAX] = {0}; + char _topicOta[MQTT_TOPIC_MAX] = {0}; + + TaskHandle_t _taskHandle = nullptr; + QueueHandle_t _cmdQueue = nullptr; + QueueHandle_t _rspQueue = nullptr; + SemaphoreHandle_t _uartMutex = nullptr; + + // Publish failure counter for health monitoring + uint8_t _pubFailCount = 0; + + // AT response buffer + static const int AT_BUF_SIZE = 512; + char _atBuf[AT_BUF_SIZE]; + + // URC accumulation + static const int URC_BUF_SIZE = 600; + char _urcBuf[URC_BUF_SIZE]; + int _urcPos = 0; + + // MQTT receive state machine (multi-line URC parsing) + enum MqttRxState { RX_IDLE, RX_WAIT_TOPIC, RX_WAIT_PAYLOAD }; + MqttRxState _rxState = RX_IDLE; + int _rxTopicLen = 0; + int _rxPayloadLen = 0; + char _rxTopic[MQTT_TOPIC_MAX]; + char _rxPayload[MQTT_PAYLOAD_MAX]; + + // Reconnect backoff + uint32_t _reconnectDelay = MQTT_RECONNECT_MIN; + + // --- Modem UART helpers (modem task only) --- + bool modemPowerOn(); + bool sendAT(const char* cmd, const char* expect, uint32_t timeout_ms = 2000); + bool waitResponse(const char* expect, uint32_t timeout_ms, char* buf = nullptr, size_t bufLen = 0); + bool waitPrompt(uint32_t timeout_ms = 5000); + void drainURCs(); + void processURCLine(const char* line); + + // --- Data connection --- + void resolveAPN(); + bool activateData(); + + // --- MQTT operations (modem task only) --- + bool mqttStart(); + bool mqttConnect(); + bool mqttSubscribe(const char* topic); + bool mqttPublish(const char* topic, const char* payload); + void mqttDisconnect(); + + // --- URC handlers --- + void handleMqttRxStart(const char* line); + void handleMqttRxTopic(const char* data, int len); + void handleMqttRxPayload(const char* data, int len); + void handleMqttRxEnd(); + void handleMqttConnLost(const char* line); + + // --- Task --- + static void taskEntry(void* param); + void taskLoop(); +}; + +extern CellularMQTT cellularMQTT; + +#endif // CELLULAR_MQTT_H +#endif // HAS_4G_MODEM \ No newline at end of file diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ec3db4ad..28963f7a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,253 +1,477 @@ #include "MyMesh.h" +#include -#include // needed for PlatformIO -#include +/* ------------------------------ Config -------------------------------- */ -#define CMD_APP_START 1 -#define CMD_SEND_TXT_MSG 2 -#define CMD_SEND_CHANNEL_TXT_MSG 3 -#define CMD_GET_CONTACTS 4 // with optional 'since' (for efficient sync) -#define CMD_GET_DEVICE_TIME 5 -#define CMD_SET_DEVICE_TIME 6 -#define CMD_SEND_SELF_ADVERT 7 -#define CMD_SET_ADVERT_NAME 8 -#define CMD_ADD_UPDATE_CONTACT 9 -#define CMD_SYNC_NEXT_MESSAGE 10 -#define CMD_SET_RADIO_PARAMS 11 -#define CMD_SET_RADIO_TX_POWER 12 -#define CMD_RESET_PATH 13 -#define CMD_SET_ADVERT_LATLON 14 -#define CMD_REMOVE_CONTACT 15 -#define CMD_SHARE_CONTACT 16 -#define CMD_EXPORT_CONTACT 17 -#define CMD_IMPORT_CONTACT 18 -#define CMD_REBOOT 19 -#define CMD_GET_BATT_AND_STORAGE 20 // was CMD_GET_BATTERY_VOLTAGE -#define CMD_SET_TUNING_PARAMS 21 -#define CMD_DEVICE_QEURY 22 -#define CMD_EXPORT_PRIVATE_KEY 23 -#define CMD_IMPORT_PRIVATE_KEY 24 -#define CMD_SEND_RAW_DATA 25 -#define CMD_SEND_LOGIN 26 -#define CMD_SEND_STATUS_REQ 27 -#define CMD_HAS_CONNECTION 28 -#define CMD_LOGOUT 29 // 'Disconnect' -#define CMD_GET_CONTACT_BY_KEY 30 -#define CMD_GET_CHANNEL 31 -#define CMD_SET_CHANNEL 32 -#define CMD_SIGN_START 33 -#define CMD_SIGN_DATA 34 -#define CMD_SIGN_FINISH 35 -#define CMD_SEND_TRACE_PATH 36 -#define CMD_SET_DEVICE_PIN 37 -#define CMD_SET_OTHER_PARAMS 38 -#define CMD_SEND_TELEMETRY_REQ 39 // can deprecate this -#define CMD_GET_CUSTOM_VARS 40 -#define CMD_SET_CUSTOM_VAR 41 -#define CMD_GET_ADVERT_PATH 42 -#define CMD_GET_TUNING_PARAMS 43 -// NOTE: CMD range 44..49 parked, potentially for WiFi operations -#define CMD_SEND_BINARY_REQ 50 -#define CMD_FACTORY_RESET 51 -#define CMD_SEND_PATH_DISCOVERY_REQ 52 -#define CMD_SET_FLOOD_SCOPE 54 // v8+ -#define CMD_SEND_CONTROL_DATA 55 // v8+ -#define CMD_GET_STATS 56 // v8+, second byte is stats type -#define CMD_SEND_ANON_REQ 57 -#define CMD_SET_AUTOADD_CONFIG 58 -#define CMD_GET_AUTOADD_CONFIG 59 +#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 -// Stats sub-types for CMD_GET_STATS -#define STATS_TYPE_CORE 0 -#define STATS_TYPE_RADIO 1 -#define STATS_TYPE_PACKETS 2 +#ifndef ADVERT_NAME + #define ADVERT_NAME "repeater" +#endif +#ifndef ADVERT_LAT + #define ADVERT_LAT 0.0 +#endif +#ifndef ADVERT_LON + #define ADVERT_LON 0.0 +#endif -#define RESP_CODE_OK 0 -#define RESP_CODE_ERR 1 -#define RESP_CODE_CONTACTS_START 2 // first reply to CMD_GET_CONTACTS -#define RESP_CODE_CONTACT 3 // multiple of these (after CMD_GET_CONTACTS) -#define RESP_CODE_END_OF_CONTACTS 4 // last reply to CMD_GET_CONTACTS -#define RESP_CODE_SELF_INFO 5 // reply to CMD_APP_START -#define RESP_CODE_SENT 6 // reply to CMD_SEND_TXT_MSG -#define RESP_CODE_CONTACT_MSG_RECV 7 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) -#define RESP_CODE_CHANNEL_MSG_RECV 8 // a reply to CMD_SYNC_NEXT_MESSAGE (ver < 3) -#define RESP_CODE_CURR_TIME 9 // a reply to CMD_GET_DEVICE_TIME -#define RESP_CODE_NO_MORE_MESSAGES 10 // a reply to CMD_SYNC_NEXT_MESSAGE -#define RESP_CODE_EXPORT_CONTACT 11 -#define RESP_CODE_BATT_AND_STORAGE 12 // a reply to a CMD_GET_BATT_AND_STORAGE -#define RESP_CODE_DEVICE_INFO 13 // a reply to CMD_DEVICE_QEURY -#define RESP_CODE_PRIVATE_KEY 14 // a reply to CMD_EXPORT_PRIVATE_KEY -#define RESP_CODE_DISABLED 15 -#define RESP_CODE_CONTACT_MSG_RECV_V3 16 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) -#define RESP_CODE_CHANNEL_MSG_RECV_V3 17 // a reply to CMD_SYNC_NEXT_MESSAGE (ver >= 3) -#define RESP_CODE_CHANNEL_INFO 18 // a reply to CMD_GET_CHANNEL -#define RESP_CODE_SIGN_START 19 -#define RESP_CODE_SIGNATURE 20 -#define RESP_CODE_CUSTOM_VARS 21 -#define RESP_CODE_ADVERT_PATH 22 -#define RESP_CODE_TUNING_PARAMS 23 -#define RESP_CODE_STATS 24 // v8+, second byte is stats type -#define RESP_CODE_AUTOADD_CONFIG 25 +#ifndef ADMIN_PASSWORD + #define ADMIN_PASSWORD "password" +#endif -#define SEND_TIMEOUT_BASE_MILLIS 500 -#define FLOOD_SEND_TIMEOUT_FACTOR 16.0f -#define DIRECT_SEND_PERHOP_FACTOR 6.0f -#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 -#define LAZY_CONTACTS_WRITE_DELAY 5000 +#ifndef SERVER_RESPONSE_DELAY + #define SERVER_RESPONSE_DELAY 300 +#endif -#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" +#ifndef TXT_ACK_DELAY + #define TXT_ACK_DELAY 200 +#endif -// these are _pushed_ to client app at any time -#define PUSH_CODE_ADVERT 0x80 -#define PUSH_CODE_PATH_UPDATED 0x81 -#define PUSH_CODE_SEND_CONFIRMED 0x82 -#define PUSH_CODE_MSG_WAITING 0x83 -#define PUSH_CODE_RAW_DATA 0x84 -#define PUSH_CODE_LOGIN_SUCCESS 0x85 -#define PUSH_CODE_LOGIN_FAIL 0x86 -#define PUSH_CODE_STATUS_RESPONSE 0x87 -#define PUSH_CODE_LOG_RX_DATA 0x88 -#define PUSH_CODE_TRACE_DATA 0x89 -#define PUSH_CODE_NEW_ADVERT 0x8A -#define PUSH_CODE_TELEMETRY_RESPONSE 0x8B -#define PUSH_CODE_BINARY_RESPONSE 0x8C -#define PUSH_CODE_PATH_DISCOVERY_RESPONSE 0x8D -#define PUSH_CODE_CONTROL_DATA 0x8E // v8+ -#define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest -#define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full +#define FIRMWARE_VER_LEVEL 2 -#define ERR_CODE_UNSUPPORTED_CMD 1 -#define ERR_CODE_NOT_FOUND 2 -#define ERR_CODE_TABLE_FULL 3 -#define ERR_CODE_BAD_STATE 4 -#define ERR_CODE_FILE_IO_ERROR 5 -#define ERR_CODE_ILLEGAL_ARG 6 +#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS +#define REQ_TYPE_KEEP_ALIVE 0x02 +#define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_ACCESS_LIST 0x05 +#define REQ_TYPE_GET_NEIGHBOURS 0x06 +#define REQ_TYPE_GET_OWNER_INFO 0x07 // FIRMWARE_VER_LEVEL >= 2 -#define MAX_SIGN_DATA_LEN (8 * 1024) // 8K +#define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ -// Auto-add config bitmask -// Bit 0: If set, overwrite oldest non-favourite contact when contacts file is full -// Bits 1-4: these indicate which contact types to auto-add when manual_contact_mode = 0x01 -#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full -#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT) -#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER) -#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM) -#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR) +#define ANON_REQ_TYPE_REGIONS 0x01 +#define ANON_REQ_TYPE_OWNER 0x02 +#define ANON_REQ_TYPE_BASIC 0x03 // just remote clock -void MyMesh::writeOKFrame() { - uint8_t buf[1]; - buf[0] = RESP_CODE_OK; - _serial->writeFrame(buf, 1); -} -void MyMesh::writeErrFrame(uint8_t err_code) { - uint8_t buf[2]; - buf[0] = RESP_CODE_ERR; - buf[1] = err_code; - _serial->writeFrame(buf, 2); -} +#define CLI_REPLY_DELAY_MILLIS 600 -void MyMesh::writeDisabledFrame() { - uint8_t buf[1]; - buf[0] = RESP_CODE_DISABLED; - _serial->writeFrame(buf, 1); -} +#define LAZY_CONTACTS_WRITE_DELAY 5000 -void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) { - int i = 0; - out_frame[i++] = code; - memcpy(&out_frame[i], contact.id.pub_key, PUB_KEY_SIZE); - i += PUB_KEY_SIZE; - out_frame[i++] = contact.type; - out_frame[i++] = contact.flags; - out_frame[i++] = contact.out_path_len; - memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); - i += MAX_PATH_SIZE; - StrHelper::strzcpy((char *)&out_frame[i], contact.name, 32); - i += 32; - memcpy(&out_frame[i], &contact.last_advert_timestamp, 4); - i += 4; - memcpy(&out_frame[i], &contact.gps_lat, 4); - i += 4; - memcpy(&out_frame[i], &contact.gps_lon, 4); - i += 4; - memcpy(&out_frame[i], &contact.lastmod, 4); - i += 4; - _serial->writeFrame(out_frame, i); -} +void MyMesh::putNeighbour(const mesh::Identity &id, uint32_t timestamp, float snr) { +#if MAX_NEIGHBOURS // check if neighbours enabled + // find existing neighbour, else use least recently updated + uint32_t oldest_timestamp = 0xFFFFFFFF; + NeighbourInfo *neighbour = &neighbours[0]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + // if neighbour already known, we should update it + if (id.matches(neighbours[i].id)) { + neighbour = &neighbours[i]; + break; + } -void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, const uint8_t *frame, int len) { - int i = 0; - uint8_t code = frame[i++]; // eg. CMD_ADD_UPDATE_CONTACT - memcpy(contact.id.pub_key, &frame[i], PUB_KEY_SIZE); - i += PUB_KEY_SIZE; - contact.type = frame[i++]; - contact.flags = frame[i++]; - contact.out_path_len = frame[i++]; - memcpy(contact.out_path, &frame[i], MAX_PATH_SIZE); - i += MAX_PATH_SIZE; - memcpy(contact.name, &frame[i], 32); - i += 32; - memcpy(&contact.last_advert_timestamp, &frame[i], 4); - i += 4; - if (len >= i + 8) { // optional fields - memcpy(&contact.gps_lat, &frame[i], 4); - i += 4; - memcpy(&contact.gps_lon, &frame[i], 4); - i += 4; - if (len >= i + 4) { - memcpy(&last_mod, &frame[i], 4); + // otherwise we should update the least recently updated neighbour + if (neighbours[i].heard_timestamp < oldest_timestamp) { + neighbour = &neighbours[i]; + oldest_timestamp = neighbour->heard_timestamp; } } + + // update neighbour info + neighbour->id = id; + neighbour->advert_timestamp = timestamp; + neighbour->heard_timestamp = getRTCClock()->getCurrentTime(); + neighbour->snr = (int8_t)(snr * 4); +#endif } -bool MyMesh::Frame::isChannelMsg() const { - return buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3; +uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood) { + ClientInfo* client = NULL; + 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 + } + } + if (client == NULL) { + uint8_t perms; + if (strcmp((char *)data, _prefs.password) == 0) { // check for valid admin password + perms = PERM_ACL_ADMIN; + } else if (strcmp((char *)data, _prefs.guest_password) == 0) { // check guest password + perms = PERM_ACL_GUEST; + } else { +#if MESH_DEBUG + MESH_DEBUG_PRINTLN("Invalid password: %s", data); +#endif + return 0; + } + + client = acl.putClient(sender, 0); // 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 &= ~0x03; + client->permissions |= perms; + memcpy(client->shared_secret, secret, PUB_KEY_SIZE); + + if (perms != PERM_ACL_GUEST) { // keep number of FS writes to a minimum + dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); + } + } + + if (is_flood) { + client->out_path_len = -1; // 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; // Legacy: was recommended keep-alive interval (secs / 16) + 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; // New field + + return 13; // reply length } -void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) { - if (offline_queue_len >= OFFLINE_QUEUE_SIZE) { - MESH_DEBUG_PRINTLN("WARN: offline_queue is full!"); - int pos = 0; - while (pos < offline_queue_len) { - if (offline_queue[pos].isChannelMsg()) { - for (int i = pos; i < offline_queue_len - 1; i++) { // delete oldest channel msg from queue - offline_queue[i] = offline_queue[i + 1]; - } - MESH_DEBUG_PRINTLN("INFO: removed oldest channel message from queue."); - offline_queue[offline_queue_len - 1].len = len; - memcpy(offline_queue[offline_queue_len - 1].buf, frame, len); - return; +uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { + if (anon_limiter.allow(rtc_clock.getCurrentTime())) { + // request data has: {reply-path-len}{reply-path} + reply_path_len = *data++ & 0x3F; + memcpy(reply_path, data, reply_path_len); + // data += reply_path_len; + + memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness) + + return 8 + region_map.exportNamesTo((char *) &reply_data[8], sizeof(reply_data) - 12, REGION_DENY_FLOOD); // reply length + } + return 0; +} + +uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { + if (anon_limiter.allow(rtc_clock.getCurrentTime())) { + // request data has: {reply-path-len}{reply-path} + reply_path_len = *data++ & 0x3F; + memcpy(reply_path, data, reply_path_len); + // data += reply_path_len; + + memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness) + sprintf((char *) &reply_data[8], "%s\n%s", _prefs.node_name, _prefs.owner_info); + + return 8 + strlen((char *) &reply_data[8]); // reply length + } + return 0; +} + +uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { + if (anon_limiter.allow(rtc_clock.getCurrentTime())) { + // request data has: {reply-path-len}{reply-path} + reply_path_len = *data++ & 0x3F; + memcpy(reply_path, data, reply_path_len); + // data += reply_path_len; + + memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag + uint32_t now = getRTCClock()->getCurrentTime(); + memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness) + reply_data[8] = 0; // features +#ifdef WITH_RS232_BRIDGE + reply_data[8] |= 0x01; // is bridge, type UART +#elif WITH_ESPNOW_BRIDGE + reply_data[8] |= 0x03; // is bridge, type ESP-NOW +#endif + if (_prefs.disable_fwd) { // is this repeater currently disabled + reply_data[8] |= 0x80; // is disabled + } + // TODO: add some kind of moving-window utilisation metric, so can query 'how busy' is this repeater + return 9; // reply length + } + return 0; +} + +int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t *payload, size_t payload_len) { + // uint32_t now = getRTCClock()->getCurrentTimeUnique(); + // memcpy(reply_data, &now, 4); // response packets always prefixed with timestamp + memcpy(reply_data, &sender_timestamp, 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') + + if (payload[0] == REQ_TYPE_GET_STATUS) { // guests can also access this now + RepeaterStats stats; + stats.batt_milli_volts = board.getBattMilliVolts(); + stats.curr_tx_queue_len = _mgr->getOutboundCount(0xFFFFFFFF); + stats.noise_floor = (int16_t)_radio->getNoiseFloor(); + stats.last_rssi = (int16_t)radio_driver.getLastRSSI(); + stats.n_packets_recv = radio_driver.getPacketsRecv(); + stats.n_packets_sent = radio_driver.getPacketsSent(); + stats.total_air_time_secs = getTotalAirTime() / 1000; + stats.total_up_time_secs = uptime_millis / 1000; + stats.n_sent_flood = getNumSentFlood(); + stats.n_sent_direct = getNumSentDirect(); + stats.n_recv_flood = getNumRecvFlood(); + stats.n_recv_direct = getNumRecvDirect(); + stats.err_events = _err_flags; + stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); + stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); + stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; + + memcpy(&reply_data[4], &stats, sizeof(stats)); + + return 4 + sizeof(stats); // reply_len + } + if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA) { + uint8_t perm_mask = ~(payload[1]); // 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 + if ((sender->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { + perm_mask = 0x00; // just base telemetry allowed + } + sensors.querySensors(perm_mask, telemetry); + + // This default 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 + } + + uint8_t tlen = telemetry.getSize(); + memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + return 4 + tlen; // reply_len + } + if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) { + uint8_t res1 = payload[1]; // reserved for future (extra query params) + uint8_t res2 = payload[2]; + 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; } - pos++; + return ofs; + } + } + if (payload[0] == REQ_TYPE_GET_NEIGHBOURS) { + uint8_t request_version = payload[1]; + if (request_version == 0) { + + // reply data offset (after response sender_timestamp/tag) + int reply_offset = 4; + + // get request params + uint8_t count = payload[2]; // how many neighbours to fetch (0-255) + uint16_t offset; + memcpy(&offset, &payload[3], 2); // offset from start of neighbours list (0-65535) + uint8_t order_by = payload[5]; // how to order neighbours. 0=newest_to_oldest, 1=oldest_to_newest, 2=strongest_to_weakest, 3=weakest_to_strongest + uint8_t pubkey_prefix_length = payload[6]; // how many bytes of neighbour pub key we want + // we also send a 4 byte random blob in payload[7...10] to help packet uniqueness + + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS count=%d, offset=%d, order_by=%d, pubkey_prefix_length=%d", count, offset, order_by, pubkey_prefix_length); + + // clamp pub key prefix length to max pub key length + if(pubkey_prefix_length > PUB_KEY_SIZE){ + pubkey_prefix_length = PUB_KEY_SIZE; + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS invalid pubkey_prefix_length=%d clamping to %d", pubkey_prefix_length, PUB_KEY_SIZE); + } + + // create copy of neighbours list, skipping empty entries so we can sort it separately from main list + int16_t neighbours_count = 0; + NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + auto neighbour = &neighbours[i]; + if (neighbour->heard_timestamp > 0) { + sorted_neighbours[neighbours_count] = neighbour; + neighbours_count++; + } + } + + // sort neighbours based on order + if (order_by == 0) { + // sort by newest to oldest + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting newest to oldest"); + std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->heard_timestamp > b->heard_timestamp; // desc + }); + } else if (order_by == 1) { + // sort by oldest to newest + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting oldest to newest"); + std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->heard_timestamp < b->heard_timestamp; // asc + }); + } else if (order_by == 2) { + // sort by strongest to weakest + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting strongest to weakest"); + std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->snr > b->snr; // desc + }); + } else if (order_by == 3) { + // sort by weakest to strongest + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS sorting weakest to strongest"); + std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->snr < b->snr; // asc + }); + } + + // build results buffer + int results_count = 0; + int results_offset = 0; + uint8_t results_buffer[130]; + for(int index = 0; index < count && index + offset < neighbours_count; index++){ + + // stop if we can't fit another entry in results + int entry_size = pubkey_prefix_length + 4 + 1; + if(results_offset + entry_size > sizeof(results_buffer)){ + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS no more entries can fit in results buffer"); + break; + } + + // add next neighbour to results + auto neighbour = sorted_neighbours[index + offset]; + uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + memcpy(&results_buffer[results_offset], neighbour->id.pub_key, pubkey_prefix_length); results_offset += pubkey_prefix_length; + memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4; + memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1; + results_count++; + + } + + // build reply + MESH_DEBUG_PRINTLN("REQ_TYPE_GET_NEIGHBOURS neighbours_count=%d results_count=%d", neighbours_count, results_count); + memcpy(&reply_data[reply_offset], &neighbours_count, 2); reply_offset += 2; + memcpy(&reply_data[reply_offset], &results_count, 2); reply_offset += 2; + memcpy(&reply_data[reply_offset], &results_buffer, results_offset); reply_offset += results_offset; + + return reply_offset; + } + } else if (payload[0] == REQ_TYPE_GET_OWNER_INFO) { + sprintf((char *) &reply_data[4], "%s\n%s\n%s", FIRMWARE_VERSION, _prefs.node_name, _prefs.owner_info); + return 4 + strlen((char *) &reply_data[4]); + } + return 0; // unknown command +} + +mesh::Packet *MyMesh::createSelfAdvert() { + uint8_t app_data[MAX_ADVERT_DATA_SIZE]; + uint8_t app_data_len = _cli.buildAdvertData(ADV_TYPE_REPEATER, app_data); + + return createAdvert(self_id, app_data, app_data_len); +} + +File MyMesh::openAppend(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 +} + +bool MyMesh::allowPacketForward(const mesh::Packet *packet) { + if (_prefs.disable_fwd) return false; + if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + if (packet->isRouteFlood() && recv_pkt_region == NULL) { + MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); + return false; + } + return true; +} + +const char *MyMesh::getLogDateTime() { + static char tmp[32]; + uint32_t now = getRTCClock()->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; +} + +void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { +#if MESH_PACKET_LOGGING + Serial.print(getLogDateTime()); + Serial.print(" RAW: "); + mesh::Utils::printHex(Serial, raw, len); + Serial.println(); +#endif +} + +void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { +#ifdef WITH_BRIDGE + if (_prefs.bridge_pkt_src == 1) { + bridge.sendPacket(pkt); + } +#endif + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": RX, len=%d (type=%d, route=%s, payload_len=%d) SNR=%d RSSI=%d score=%d", len, + pkt->getPayloadType(), pkt->isRouteDirect() ? "D" : "F", pkt->payload_len, + (int)_radio->getLastSNR(), (int)_radio->getLastRSSI(), (int)(score * 1000)); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + f.printf("\n"); + } + f.close(); } - MESH_DEBUG_PRINTLN("INFO: no channel messages to remove from queue."); - } else { - offline_queue[offline_queue_len].len = len; - memcpy(offline_queue[offline_queue_len].buf, frame, len); - offline_queue_len++; } } -int MyMesh::getFromOfflineQueue(uint8_t frame[]) { - if (offline_queue_len > 0) { // check offline queue - size_t len = offline_queue[0].len; // take from top of queue - memcpy(frame, offline_queue[0].buf, len); - - offline_queue_len--; - for (int i = 0; i < offline_queue_len; i++) { // delete top item from queue - offline_queue[i] = offline_queue[i + 1]; - } - return len; +void MyMesh::logTx(mesh::Packet *pkt, int len) { +#ifdef WITH_BRIDGE + if (_prefs.bridge_pkt_src == 0) { + bridge.sendPacket(pkt); + } +#endif + + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": TX, len=%d (type=%d, route=%s, payload_len=%d)", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + + if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH || pkt->getPayloadType() == PAYLOAD_TYPE_REQ || + pkt->getPayloadType() == PAYLOAD_TYPE_RESPONSE || pkt->getPayloadType() == PAYLOAD_TYPE_TXT_MSG) { + f.printf(" [%02X -> %02X]\n", (uint32_t)pkt->payload[1], (uint32_t)pkt->payload[0]); + } else { + f.printf("\n"); + } + f.close(); + } } - return 0; // queue is empty } -float MyMesh::getAirtimeBudgetFactor() const { - return _prefs.airtime_factor; -} - -int MyMesh::getInterferenceThreshold() const { - return 0; // disabled for now, until currentRSSI() problem is resolved +void MyMesh::logTxFail(mesh::Packet *pkt, int len) { + if (_logging) { + File f = openAppend(PACKET_LOG_FILE); + if (f) { + f.print(getLogDateTime()); + f.printf(": TX FAIL!, len=%d (type=%d, route=%s, payload_len=%d)\n", len, pkt->getPayloadType(), + pkt->isRouteDirect() ? "D" : "F", pkt->payload_len); + f.close(); + } + } } int MyMesh::calcRxDelay(float score, uint32_t air_time) const { @@ -255,1809 +479,722 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); } -uint8_t MyMesh::getExtraAckTransmitCount() const { - return _prefs.multi_acks; +uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + return getRNG()->nextInt(0, 5*t + 1); +} +uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + return getRNG()->nextInt(0, 5*t + 1); } -void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { - if (_serial->isConnected() && len + 3 <= MAX_FRAME_SIZE) { - int i = 0; - out_frame[i++] = PUSH_CODE_LOG_RX_DATA; - out_frame[i++] = (int8_t)(snr * 4); - out_frame[i++] = (int8_t)(rssi); - memcpy(&out_frame[i], raw, len); - i += len; - - _serial->writeFrame(out_frame, i); - } -} - -bool MyMesh::isAutoAddEnabled() const { - return (_prefs.manual_add_contacts & 1) == 0; -} - -bool MyMesh::shouldAutoAddContactType(uint8_t contact_type) const { - if ((_prefs.manual_add_contacts & 1) == 0) { - return true; - } - - uint8_t type_bit = 0; - switch (contact_type) { - case ADV_TYPE_CHAT: - type_bit = AUTO_ADD_CHAT; - break; - case ADV_TYPE_REPEATER: - type_bit = AUTO_ADD_REPEATER; - break; - case ADV_TYPE_ROOM: - type_bit = AUTO_ADD_ROOM_SERVER; - break; - case ADV_TYPE_SENSOR: - type_bit = AUTO_ADD_SENSOR; - break; - default: - return false; // Unknown type, don't auto-add - } - - return (_prefs.autoadd_config & type_bit) != 0; -} - -bool MyMesh::shouldOverwriteWhenFull() const { - return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0; -} - -void MyMesh::onContactOverwrite(const uint8_t* pub_key) { - if (_serial->isConnected()) { - out_frame[0] = PUSH_CODE_CONTACT_DELETED; - memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE); - _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); - } -} - -void MyMesh::onContactsFull() { - if (_serial->isConnected()) { - out_frame[0] = PUSH_CODE_CONTACTS_FULL; - _serial->writeFrame(out_frame, 1); - } -} - -void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { - if (_serial->isConnected()) { - if (is_new) { - writeContactRespFrame(PUSH_CODE_NEW_ADVERT, contact); +bool MyMesh::filterRecvFloodPacket(mesh::Packet* pkt) { + // just try to determine region for packet (apply later in allowPacketForward()) + 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 { - out_frame[0] = PUSH_CODE_ADVERT; - memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); - _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); + recv_pkt_region = ®ion_map.getWildcard(); } - } -#ifdef DISPLAY_CLASS - if (_ui && !_prefs.buzzer_quiet) _ui->notify(UIEventType::newContactMessage); //buzz if enabled -#endif - - // add inbound-path to mem cache - if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid - AdvertPath* p = advert_paths; - uint32_t oldest = 0xFFFFFFFF; - for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest - if (memcmp(advert_paths[i].pubkey_prefix, contact.id.pub_key, sizeof(AdvertPath::pubkey_prefix)) == 0) { - p = &advert_paths[i]; // found - break; - } - if (advert_paths[i].recv_timestamp < oldest) { - oldest = advert_paths[i].recv_timestamp; - p = &advert_paths[i]; - } - } - - memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix)); - strcpy(p->name, contact.name); - p->recv_timestamp = getRTCClock()->getCurrentTime(); - p->path_len = path_len; - memcpy(p->path, path, p->path_len); - } - - if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] -} - -static int sort_by_recent(const void *a, const void *b) { - return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp; -} - -int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { - if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE; - qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent); - - for (int i = 0; i < max_num; i++) { - dest[i] = advert_paths[i]; - } - return max_num; -} - -void MyMesh::onContactPathUpdated(const ContactInfo &contact) { - out_frame[0] = PUSH_CODE_PATH_UPDATED; - memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); - _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); // NOTE: app may not be connected - - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); -} - -ContactInfo* MyMesh::processAck(const uint8_t *data) { - // see if matches any in a table - for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) { - if (memcmp(data, &expected_ack_table[i].ack, 4) == 0) { // got an ACK from recipient - out_frame[0] = PUSH_CODE_SEND_CONFIRMED; - memcpy(&out_frame[1], data, 4); - uint32_t trip_time = _ms->getMillis() - expected_ack_table[i].msg_sent; - memcpy(&out_frame[5], &trip_time, 4); - _serial->writeFrame(out_frame, 9); - - // NOTE: the same ACK can be received multiple times! - expected_ack_table[i].ack = 0; // clear expected hash, now that we have received ACK - return expected_ack_table[i].contact; - } - } - return checkConnectionsAck(data); -} - -void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packet *pkt, - uint32_t sender_timestamp, const uint8_t *extra, int extra_len, const char *text) { - int i = 0; - if (app_target_ver >= 3) { - out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV_V3; - out_frame[i++] = (int8_t)(pkt->getSNR() * 4); - out_frame[i++] = 0; // reserved1 - out_frame[i++] = 0; // reserved2 } else { - out_frame[i++] = RESP_CODE_CONTACT_MSG_RECV; + recv_pkt_region = NULL; } - memcpy(&out_frame[i], from.id.pub_key, 6); - i += 6; // just 6-byte prefix - uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; - out_frame[i++] = txt_type; - memcpy(&out_frame[i], &sender_timestamp, 4); - i += 4; - if (extra_len > 0) { - memcpy(&out_frame[i], extra, extra_len); - i += extra_len; - } - int tlen = strlen(text); // TODO: UTF-8 ?? - if (i + tlen > MAX_FRAME_SIZE) { - tlen = MAX_FRAME_SIZE - i; - } - memcpy(&out_frame[i], text, tlen); - i += tlen; - addToOfflineQueue(out_frame, i); - - if (_serial->isConnected()) { - uint8_t frame[1]; - frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' - _serial->writeFrame(frame, 1); - } - -#ifdef DISPLAY_CLASS - // we only want to show text messages on display, not cli data - bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _ui) { - _ui->newMsg(path_len, from.name, text, offline_queue_len); - if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled - } -#endif + // do normal processing + return false; } -bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { - // Check if this incoming flood packet is a repeat of a message we recently sent - if (packet->payload_len >= SENT_FINGERPRINT_SIZE) { - unsigned long now = millis(); - for (int i = 0; i < SENT_TRACK_SIZE; i++) { - SentMsgTrack* t = &_sent_track[i]; - if (!t->active) continue; +void MyMesh::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); - // Expire old entries - if ((now - t->sent_millis) > SENT_TRACK_EXPIRY_MS) { - t->active = false; - continue; - } + data[len] = 0; // ensure null terminator + uint8_t reply_len; - // Compare payload fingerprint - if (memcmp(packet->payload, t->fingerprint, SENT_FINGERPRINT_SIZE) == 0) { - t->repeat_count++; - MESH_DEBUG_PRINTLN("SentTrack: heard repeat #%d (SNR=%.1f)", t->repeat_count, packet->getSNR()); - -#ifdef DISPLAY_CLASS - if (_ui) { - char buf[40]; - snprintf(buf, sizeof(buf), "Sent! (%d)", t->repeat_count); - _ui->showAlert(buf, 2000); // show/extend alert with updated count - } -#endif - break; // found match, no need to check other entries - } - } - } - - return false; // never filter — let normal processing continue -} - -void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { - // TODO: dynamic send_scope, depending on recipient and current 'home' Region - if (send_scope.isNull()) { - sendFlood(pkt, delay_millis); - } else { - uint16_t codes[2]; - codes[0] = send_scope.calcTransportCode(pkt); - codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? - sendFlood(pkt, codes, delay_millis); - } -} -void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { - // Capture payload fingerprint for repeat tracking before sending - if (pkt->payload_len >= SENT_FINGERPRINT_SIZE) { - SentMsgTrack* t = &_sent_track[_sent_track_idx]; - memcpy(t->fingerprint, pkt->payload, SENT_FINGERPRINT_SIZE); - t->repeat_count = 0; - t->sent_millis = millis(); - t->active = true; - _sent_track_idx = (_sent_track_idx + 1) % SENT_TRACK_SIZE; - MESH_DEBUG_PRINTLN("SentTrack: captured fingerprint for channel msg"); - } - - // TODO: have per-channel send_scope - if (send_scope.isNull()) { - sendFlood(pkt, delay_millis); - } else { - uint16_t codes[2]; - codes[0] = send_scope.calcTransportCode(pkt); - codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? - sendFlood(pkt, codes, delay_millis); - } -} - -void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) { - markConnectionActive(from); // in case this is from a server, and we have a connection - queueMessage(from, TXT_TYPE_PLAIN, pkt, sender_timestamp, NULL, 0, text); -} - -void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const char *text) { - markConnectionActive(from); // in case this is from a server, and we have a connection - queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text); -} - -void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp, - const uint8_t *sender_prefix, const char *text) { - markConnectionActive(from); - // from.sync_since change needs to be persisted - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - queueMessage(from, TXT_TYPE_SIGNED_PLAIN, pkt, sender_timestamp, sender_prefix, 4, text); -} - -void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint32_t timestamp, - const char *text) { - int i = 0; - if (app_target_ver >= 3) { - out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3; - out_frame[i++] = (int8_t)(pkt->getSNR() * 4); - out_frame[i++] = 0; // reserved1 - out_frame[i++] = 0; // reserved2 - } else { - out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV; - } - - uint8_t channel_idx = findChannelIdx(channel); - out_frame[i++] = channel_idx; - uint8_t path_len = out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF; - - out_frame[i++] = TXT_TYPE_PLAIN; - memcpy(&out_frame[i], ×tamp, 4); - i += 4; - int tlen = strlen(text); // TODO: UTF-8 ?? - if (i + tlen > MAX_FRAME_SIZE) { - tlen = MAX_FRAME_SIZE - i; - } - memcpy(&out_frame[i], text, tlen); - i += tlen; - addToOfflineQueue(out_frame, i); - - if (_serial->isConnected()) { - uint8_t frame[1]; - frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' - _serial->writeFrame(frame, 1); - } - -#ifdef DISPLAY_CLASS - // Get the channel name from the channel index - const char *channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; - } - if (_ui) { - _ui->newMsg(path_len, channel_name, text, offline_queue_len); - if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled - } -#endif -} - -void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text) { - // Format message the same way as onChannelMessageRecv for BLE app sync - // This allows sent messages from device keyboard to appear in the app - int i = 0; - if (app_target_ver >= 3) { - out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3; - out_frame[i++] = 0; // SNR not applicable for sent messages - out_frame[i++] = 0; // reserved1 - out_frame[i++] = 0; // reserved2 - } else { - out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV; - } - - out_frame[i++] = channel_idx; - out_frame[i++] = 0; // path_len = 0 indicates local/sent message - - out_frame[i++] = TXT_TYPE_PLAIN; - memcpy(&out_frame[i], ×tamp, 4); - i += 4; - - // Format as "sender: text" like the app expects - char formatted[MAX_FRAME_SIZE]; - snprintf(formatted, sizeof(formatted), "%s: %s", sender, text); - int tlen = strlen(formatted); - if (i + tlen > MAX_FRAME_SIZE) { - tlen = MAX_FRAME_SIZE - i; - } - memcpy(&out_frame[i], formatted, tlen); - i += tlen; - - addToOfflineQueue(out_frame, i); - - // If app is connected, send push notification - if (_serial->isConnected()) { - uint8_t frame[1]; - frame[0] = PUSH_CODE_MSG_WAITING; - _serial->writeFrame(frame, 1); - } -} - -uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, - uint8_t len, uint8_t *reply) { - if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) { - uint8_t permissions = 0; - uint8_t cp = contact.flags >> 1; // LSB used as 'favourite' bit (so only use upper bits) - - if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_ALL) { - permissions = TELEM_PERM_BASE; - } else if (_prefs.telemetry_mode_base == TELEM_MODE_ALLOW_FLAGS) { - permissions = cp & TELEM_PERM_BASE; - } - - if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) { - permissions |= TELEM_PERM_LOCATION; - } else if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS) { - permissions |= cp & TELEM_PERM_LOCATION; - } - - if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_ALL) { - permissions |= TELEM_PERM_ENVIRONMENT; - } else if (_prefs.telemetry_mode_env == TELEM_MODE_ALLOW_FLAGS) { - permissions |= cp & TELEM_PERM_ENVIRONMENT; - } - - uint8_t perm_mask = ~(data[1]); // NEW: first reserved byte (of 4), is now inverse mask to apply to permissions - permissions &= perm_mask; - - if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set - telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); - // query other sensors -- target specific - sensors.querySensors(permissions, telemetry); - - memcpy(reply, &sender_timestamp, - 4); // reflect sender_timestamp back in response packet (kind of like a 'tag') - - uint8_t tlen = telemetry.getSize(); - memcpy(&reply[4], telemetry.getBuffer(), tlen); - return 4 + tlen; - } - } - return 0; // unknown -} - -void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, uint8_t len) { - uint32_t tag; - memcpy(&tag, data, 4); - - if (pending_login && memcmp(&pending_login, contact.id.pub_key, 4) == 0) { // check for login response - // yes, is response to pending sendLogin() - pending_login = 0; - - int i = 0; - if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response - out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; - out_frame[i++] = 0; // legacy: is_admin = false - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix - } else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response - uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16; - if (keep_alive_secs > 0) { - startConnection(contact, keep_alive_secs); - } - out_frame[i++] = PUSH_CODE_LOGIN_SUCCESS; - out_frame[i++] = data[6]; // permissions (eg. is_admin) - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix - memcpy(&out_frame[i], &tag, 4); - i += 4; // NEW: include server timestamp - out_frame[i++] = data[7]; // NEW (v7): ACL permissions - out_frame[i++] = data[12]; // FIRMWARE_VER_LEVEL + reply_path_len = -1; + 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_REGIONS && packet->isRouteDirect()) { + reply_len = handleAnonRegionsReq(sender, timestamp, &data[5]); + } else if (data[4] == ANON_REQ_TYPE_OWNER && packet->isRouteDirect()) { + reply_len = handleAnonOwnerReq(sender, timestamp, &data[5]); + } else if (data[4] == ANON_REQ_TYPE_BASIC && packet->isRouteDirect()) { + reply_len = handleAnonClockReq(sender, timestamp, &data[5]); } else { - out_frame[i++] = PUSH_CODE_LOGIN_FAIL; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix + reply_len = 0; // unknown/invalid request type } - _serial->writeFrame(out_frame, i); - } else if (len > 4 && // check for status response - pending_status && - memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme - // FUTURE: tag == pending_status - ) { - pending_status = 0; - int i = 0; - out_frame[i++] = PUSH_CODE_STATUS_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix - memcpy(&out_frame[i], &data[4], len - 4); - i += (len - 4); - _serial->writeFrame(out_frame, i); - } else if (len > 4 && tag == pending_telemetry) { // check for matching response tag - pending_telemetry = 0; + if (reply_len == 0) return; // invalid request - int i = 0; - out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix - memcpy(&out_frame[i], &data[4], len - 4); - i += (len - 4); - _serial->writeFrame(out_frame, i); - } else if (len > 4 && tag == pending_req) { // check for matching response tag - pending_req = 0; - - int i = 0; - out_frame[i++] = PUSH_CODE_BINARY_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], &tag, 4); // app needs to match this to RESP_CODE_SENT.tag - i += 4; - memcpy(&out_frame[i], &data[4], len - 4); - i += (len - 4); - _serial->writeFrame(out_frame, i); + 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) sendFlood(path, SERVER_RESPONSE_DELAY); + } else if (reply_path_len < 0) { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); + } else { + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY); + } } } -bool MyMesh::onContactPathRecv(ContactInfo& contact, 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) { - if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) { - uint32_t tag; - memcpy(&tag, extra, 4); +int MyMesh::searchPeersByHash(const uint8_t *hash) { + int n = 0; + for (int i = 0; i < acl.getNumClients(); 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; +} - if (tag == pending_discovery) { // check for matching response tag) - pending_discovery = 0; +void MyMesh::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); + } +} - if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) { - MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len); +static bool isShare(const mesh::Packet *packet) { + if (packet->hasTransportCodes()) { + return packet->transport_codes[0] == 0 && packet->transport_codes[1] == 0; // codes { 0, 0 } means 'send to nowhere' + } + return false; +} + +void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp, + const uint8_t *app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl + + // if this a zero hop advert (and not via 'Share'), add it to neighbours + if (packet->path_len == 0 && !isShare(packet)) { + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters + putNeighbour(id, timestamp, packet->getSNR()); + } + } +} + +void MyMesh::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()) { // get from our known_clients table (sender SHOULD already be known in this context) + MESH_DEBUG_PRINTLN("onPeerDataRecv: invalid peer idx: %d", i); + return; + } + ClientInfo* client = acl.getClientByIdx(i); + + if (type == PAYLOAD_TYPE_REQ) { // request (from a Known admin client!) + uint32_t timestamp; + memcpy(×tamp, data, 4); + + if (timestamp > client->last_timestamp) { // prevent replay attacks + int reply_len = handleRequest(client, timestamp, &data[4], len - 4); + if (reply_len == 0) return; // invalid command + + client->last_timestamp = timestamp; + client->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(client->id, secret, packet->path, packet->path_len, + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - int i = 0; - out_frame[i++] = PUSH_CODE_PATH_DISCOVERY_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], contact.id.pub_key, 6); - i += 6; // pub_key_prefix - out_frame[i++] = out_path_len; - memcpy(&out_frame[i], out_path, out_path_len); - i += out_path_len; - out_frame[i++] = in_path_len; - memcpy(&out_frame[i], in_path, in_path_len); - i += in_path_len; - // NOTE: telemetry data in 'extra' is discarded at present - - _serial->writeFrame(out_frame, i); + mesh::Packet *reply = + createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + if (reply) { + if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); + } else { + sendFlood(reply, SERVER_RESPONSE_DELAY); + } + } } - return false; // DON'T send reciprocal path! + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); + } + } else if (type == PAYLOAD_TYPE_TXT_MSG && len > 5 && client->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 (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); + } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks + bool is_retry = (sender_timestamp == client->last_timestamp); + client->last_timestamp = sender_timestamp; + client->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 + + if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks + 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]), client->id.pub_key, + PUB_KEY_SIZE); + + mesh::Packet *ack = createAck(ack_hash); + if (ack) { + if (client->out_path_len < 0) { + sendFlood(ack, TXT_ACK_DELAY); + } else { + sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); + } + } + } + + uint8_t temp[166]; + char *command = (char *)&data[5]; + char *reply = (char *)&temp[5]; + if (is_retry) { + *reply = 0; + } else { + handleCommand(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); // NOTE: legacy was: TXT_TYPE_PLAIN + + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); + if (reply) { + if (client->out_path_len < 0) { + sendFlood(reply, CLI_REPLY_DELAY_MILLIS); + } else { + sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); + } + } + } + } else { + MESH_DEBUG_PRINTLN("onPeerDataRecv: possible replay attack detected"); } } - // let base class handle received path and data - return BaseChatMesh::onContactPathRecv(contact, in_path, in_path_len, out_path, out_path_len, extra_type, extra, extra_len); } -void MyMesh::onControlDataRecv(mesh::Packet *packet) { - if (packet->payload_len + 4 > sizeof(out_frame)) { - MESH_DEBUG_PRINTLN("onControlDataRecv(), payload_len too long: %d", packet->payload_len); - return; - } - int i = 0; - out_frame[i++] = PUSH_CODE_CONTROL_DATA; - out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); - out_frame[i++] = (int8_t)(_radio->getLastRSSI()); - out_frame[i++] = packet->path_len; - memcpy(&out_frame[i], packet->payload, packet->payload_len); - i += packet->payload_len; +bool MyMesh::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) { + // TODO: prevent replay attacks + int i = matching_peer_indexes[sender_idx]; - if (_serial->isConnected()) { - _serial->writeFrame(out_frame, i); + if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) + MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); + auto client = acl.getClientByIdx(i); + + memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + client->last_activity = getRTCClock()->getCurrentTime(); } else { - MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); + MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); + } + + // NOTE: no reciprocal path send!! + return false; +} + +#define CTL_TYPE_NODE_DISCOVER_REQ 0x80 +#define CTL_TYPE_NODE_DISCOVER_RESP 0x90 + +void MyMesh::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 + && !_prefs.disable_fwd && discover_limiter.allow(rtc_clock.getCurrentTime()) + ) { + 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_REPEATER)) != 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_REPEATER; // 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 + } + } } } -void MyMesh::onRawDataRecv(mesh::Packet *packet) { - if (packet->payload_len + 4 > sizeof(out_frame)) { - MESH_DEBUG_PRINTLN("onRawDataRecv(), payload_len too long: %d", packet->payload_len); - return; - } - int i = 0; - out_frame[i++] = PUSH_CODE_RAW_DATA; - out_frame[i++] = (int8_t)(_radio->getLastSNR() * 4); - out_frame[i++] = (int8_t)(_radio->getLastRSSI()); - out_frame[i++] = 0xFF; // reserved (possibly path_len in future) - memcpy(&out_frame[i], packet->payload, packet->payload_len); - i += packet->payload_len; - - if (_serial->isConnected()) { - _serial->writeFrame(out_frame, i); - } else { - MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); - } -} - -void MyMesh::onTraceRecv(mesh::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) { - uint8_t path_sz = flags & 0x03; // NEW v1.11+ - if (12 + path_len + (path_len >> path_sz) + 1 > sizeof(out_frame)) { - MESH_DEBUG_PRINTLN("onTraceRecv(), path_len is too long: %d", (uint32_t)path_len); - return; - } - int i = 0; - out_frame[i++] = PUSH_CODE_TRACE_DATA; - out_frame[i++] = 0; // reserved - out_frame[i++] = path_len; - out_frame[i++] = flags; - memcpy(&out_frame[i], &tag, 4); - i += 4; - memcpy(&out_frame[i], &auth_code, 4); - i += 4; - memcpy(&out_frame[i], path_hashes, path_len); - i += path_len; - - memcpy(&out_frame[i], path_snrs, path_len >> path_sz); - i += path_len >> path_sz; - out_frame[i++] = (int8_t)(packet->getSNR() * 4); // extra/final SNR (to this node) - - if (_serial->isConnected()) { - _serial->writeFrame(out_frame, i); - } else { - MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); - } -} - -uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { - return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); -} -uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const { - return SEND_TIMEOUT_BASE_MILLIS + - ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * - (path_len + 1)); -} - -void MyMesh::onSendTimeout() {} - -MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) - : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { - _iter_started = false; - _cli_rescue = false; - offline_queue_len = 0; - app_target_ver = 0; - clearPendingReqs(); - next_ack_idx = 0; - sign_data = NULL; +MyMesh::MyMesh(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), + _cli(board, rtc, sensors, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), region_map(key_store), temp_map(key_store), + discover_limiter(4, 120), // max 4 every 2 minutes + anon_limiter(4, 180) // max 4 every 3 minutes +#if defined(WITH_RS232_BRIDGE) + , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) +#endif +#if defined(WITH_ESPNOW_BRIDGE) + , bridge(&_prefs, _mgr, &rtc) +#endif +{ + last_millis = 0; + uptime_millis = 0; + next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; - memset(advert_paths, 0, sizeof(advert_paths)); - memset(send_scope.key, 0, sizeof(send_scope.key)); - memset(_sent_track, 0, sizeof(_sent_track)); - _sent_track_idx = 0; + set_radio_at = revert_radio_at = 0; + _logging = false; + region_load_active = false; + +#if MAX_NEIGHBOURS + memset(neighbours, 0, sizeof(neighbours)); +#endif // defaults memset(&_prefs, 0, sizeof(_prefs)); - _prefs.airtime_factor = 1.0; // one half - strcpy(_prefs.node_name, "NONAME"); + _prefs.airtime_factor = 1.0; // one half + _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.buzzer_quiet = 0; - _prefs.gps_enabled = 0; // GPS disabled by default - _prefs.gps_interval = 0; // No automatic GPS updates by default - //_prefs.rx_delay_base = 10.0f; enable once new algo fixed + _prefs.advert_interval = 1; // default to 2 minutes for NEW installs + _prefs.flood_advert_interval = 12; // 12 hours + _prefs.flood_max = 64; + _prefs.interference_threshold = 0; // disabled + + // bridge defaults + _prefs.bridge_enabled = 1; // enabled + _prefs.bridge_delay = 500; // milliseconds + _prefs.bridge_pkt_src = 0; // logTx + _prefs.bridge_baud = 115200; // baud rate + _prefs.bridge_channel = 1; // channel 1 + + StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret)); + + // GPS defaults + _prefs.gps_enabled = 0; + _prefs.gps_interval = 0; + _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + + _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier } -void MyMesh::begin(bool has_display) { - BaseChatMesh::begin(); - - if (!_store->loadMainIdentity(self_id)) { - self_id = radio_new_identity(); // create new random identity - int count = 0; - while (count < 10 && (self_id.pub_key[0] == 0x00 || self_id.pub_key[0] == 0xFF)) { // reserved id hashes - self_id = radio_new_identity(); - count++; - } - _store->saveMainIdentity(self_id); - } - -// if name is provided as a build flag, use that as default node name instead -#ifdef ADVERT_NAME - strcpy(_prefs.node_name, ADVERT_NAME); -#else - // use hex of first 4 bytes of identity public key as default node name - char pub_key_hex[10]; - mesh::Utils::toHex(pub_key_hex, self_id.pub_key, 4); - strcpy(_prefs.node_name, pub_key_hex); -#endif - +void MyMesh::begin(FILESYSTEM *fs) { + mesh::Mesh::begin(); + _fs = fs; // load persisted prefs - _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); + _cli.loadPrefs(_fs); + acl.load(_fs, self_id); + // TODO: key_store.begin(); + region_map.load(_fs); - // sanitise bad pref values - _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); - _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); - _prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f); - _prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f); - _prefs.sf = constrain(_prefs.sf, 5, 12); - _prefs.cr = constrain(_prefs.cr, 5, 8); - _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER); - _prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1 - _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 - _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours - -#ifdef BLE_PIN_CODE // 123456 by default - if (_prefs.ble_pin == 0) { -#ifdef DISPLAY_CLASS - if (has_display && BLE_PIN_CODE == 123456) { - StdRNG rng; - _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session - } else { - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - } -#else - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin -#endif - } else { - _active_ble_pin = _prefs.ble_pin; +#if defined(WITH_BRIDGE) + if (_prefs.bridge_enabled) { + bridge.begin(); } -#else - _active_ble_pin = 0; #endif - resetContacts(); - _store->loadContacts(this); - bootstrapRTCfromContacts(); - addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel - _store->loadChannels(this); - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_set_tx_power(_prefs.tx_power_dbm); -} -const char *MyMesh::getNodeName() { - return _prefs.node_name; -} -NodePrefs *MyMesh::getNodePrefs() { - return &_prefs; -} -uint32_t MyMesh::getBLEPin() { - return _active_ble_pin; -} + updateAdvertTimer(); + updateFloodAdvertTimer(); -void MyMesh::startInterface(BaseSerialInterface &serial) { - _serial = &serial; - serial.enable(); -} + board.setAdcMultiplier(_prefs.adc_multiplier); -void MyMesh::handleCmdFrame(size_t len) { - if (cmd_frame[0] == CMD_DEVICE_QEURY && len >= 2) { // sent when app establishes connection - app_target_ver = cmd_frame[1]; // which version of protocol does app understand - - int i = 0; - out_frame[i++] = RESP_CODE_DEVICE_INFO; - out_frame[i++] = FIRMWARE_VER_CODE; - out_frame[i++] = MAX_CONTACTS / 2; // v3+ - out_frame[i++] = MAX_GROUP_CHANNELS; // v3+ - memcpy(&out_frame[i], &_prefs.ble_pin, 4); - i += 4; - memset(&out_frame[i], 0, 12); - strcpy((char *)&out_frame[i], FIRMWARE_BUILD_DATE); - i += 12; - StrHelper::strzcpy((char *)&out_frame[i], board.getManufacturerName(), 40); - i += 40; - StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); - i += 20; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_APP_START && - len >= 8) { // sent when app establishes connection, respond with node ID - // cmd_frame[1..7] reserved future - char *app_name = (char *)&cmd_frame[8]; - cmd_frame[len] = 0; // make app_name null terminated - MESH_DEBUG_PRINTLN("App %s connected", app_name); - - _iter_started = false; // stop any left-over ContactsIterator - int i = 0; - out_frame[i++] = RESP_CODE_SELF_INFO; - out_frame[i++] = ADV_TYPE_CHAT; // what this node Advert identifies as (maybe node's pronouns too?? :-) - out_frame[i++] = _prefs.tx_power_dbm; - out_frame[i++] = MAX_LORA_TX_POWER; - memcpy(&out_frame[i], self_id.pub_key, PUB_KEY_SIZE); - i += PUB_KEY_SIZE; - - int32_t lat, lon; - lat = (sensors.node_lat * 1000000.0); - lon = (sensors.node_lon * 1000000.0); - memcpy(&out_frame[i], &lat, 4); - i += 4; - memcpy(&out_frame[i], &lon, 4); - i += 4; - out_frame[i++] = _prefs.multi_acks; // new v7+ - out_frame[i++] = _prefs.advert_loc_policy; - out_frame[i++] = (_prefs.telemetry_mode_env << 4) | (_prefs.telemetry_mode_loc << 2) | - (_prefs.telemetry_mode_base); // v5+ - out_frame[i++] = _prefs.manual_add_contacts; - - uint32_t freq = _prefs.freq * 1000; - memcpy(&out_frame[i], &freq, 4); - i += 4; - uint32_t bw = _prefs.bw * 1000; - memcpy(&out_frame[i], &bw, 4); - i += 4; - out_frame[i++] = _prefs.sf; - out_frame[i++] = _prefs.cr; - - int tlen = strlen(_prefs.node_name); // revisit: UTF_8 ?? - memcpy(&out_frame[i], _prefs.node_name, tlen); - i += tlen; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_SEND_TXT_MSG && len >= 14) { - int i = 1; - uint8_t txt_type = cmd_frame[i++]; - uint8_t attempt = cmd_frame[i++]; - uint32_t msg_timestamp; - memcpy(&msg_timestamp, &cmd_frame[i], 4); - i += 4; - uint8_t *pub_key_prefix = &cmd_frame[i]; - i += 6; - ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6); - if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { - char *text = (char *)&cmd_frame[i]; - int tlen = len - i; - uint32_t est_timeout; - text[tlen] = 0; // ensure null - int result; - uint32_t expected_ack; - if (txt_type == TXT_TYPE_CLI_DATA) { - msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection - result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); - expected_ack = 0; // no Ack expected - } else { - result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); - } - // TODO: add expected ACK to table - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - if (expected_ack) { - expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis(); // add to circular table - expected_ack_table[next_ack_idx].ack = expected_ack; - expected_ack_table[next_ack_idx].contact = recipient; - next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE; - } - - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &expected_ack, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(recipient == NULL - ? ERR_CODE_NOT_FOUND - : ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsuported TXT_TYPE_* - } - } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel msg - int i = 1; - uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN - uint8_t channel_idx = cmd_frame[i++]; - uint32_t msg_timestamp; - memcpy(&msg_timestamp, &cmd_frame[i], 4); - i += 4; - const char *text = (char *)&cmd_frame[i]; - - if (txt_type != TXT_TYPE_PLAIN) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - } else { - ChannelDetails channel; - bool success = getChannel(channel_idx, channel); - if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx - } - } - } else if (cmd_frame[0] == CMD_GET_CONTACTS) { // get Contact list - if (_iter_started) { - writeErrFrame(ERR_CODE_BAD_STATE); // iterator is currently busy - } else { - if (len >= 5) { // has optional 'since' param - memcpy(&_iter_filter_since, &cmd_frame[1], 4); - } else { - _iter_filter_since = 0; - } - - uint8_t reply[5]; - reply[0] = RESP_CODE_CONTACTS_START; - uint32_t count = getNumContacts(); // total, NOT filtered count - memcpy(&reply[1], &count, 4); - _serial->writeFrame(reply, 5); - - // start iterator - _iter = startContactsIterator(); - _iter_started = true; - _most_recent_lastmod = 0; - } - } else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) { - int nlen = len - 1; - if (nlen > sizeof(_prefs.node_name) - 1) nlen = sizeof(_prefs.node_name) - 1; // max len - memcpy(_prefs.node_name, &cmd_frame[1], nlen); - _prefs.node_name[nlen] = 0; // null terminator - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_SET_ADVERT_LATLON && len >= 9) { - int32_t lat, lon, alt = 0; - memcpy(&lat, &cmd_frame[1], 4); - memcpy(&lon, &cmd_frame[5], 4); - if (len >= 13) { - memcpy(&alt, &cmd_frame[9], 4); // for FUTURE support - } - if (lat <= 90 * 1E6 && lat >= -90 * 1E6 && lon <= 180 * 1E6 && lon >= -180 * 1E6) { - sensors.node_lat = ((double)lat) / 1000000.0; - sensors.node_lon = ((double)lon) / 1000000.0; - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid geo coordinate - } - } else if (cmd_frame[0] == CMD_GET_DEVICE_TIME) { - uint8_t reply[5]; - reply[0] = RESP_CODE_CURR_TIME; - uint32_t now = getRTCClock()->getCurrentTime(); - memcpy(&reply[1], &now, 4); - _serial->writeFrame(reply, 5); - } else if (cmd_frame[0] == CMD_SET_DEVICE_TIME && len >= 5) { - uint32_t secs; - memcpy(&secs, &cmd_frame[1], 4); - uint32_t curr = getRTCClock()->getCurrentTime(); - if (secs >= curr) { - getRTCClock()->setCurrentTime(secs); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_SEND_SELF_ADVERT) { - mesh::Packet* pkt; - if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { - pkt = createSelfAdvert(_prefs.node_name); - } else { - pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); - } - if (pkt) { - if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) - sendFlood(pkt); - } else { - sendZeroHop(pkt); - } - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else if (cmd_frame[0] == CMD_RESET_PATH && len >= 1 + 32) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - recipient->out_path_len = -1; - // recipient->lastmod = ?? shouldn't be needed, app already has this version of contact - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // unknown contact - } - } else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT && len >= 1 + 32 + 2 + 1) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - uint32_t last_mod = getRTCClock()->getCurrentTime(); // fallback value if not present in cmd_frame - if (recipient) { - updateContactFromFrame(*recipient, last_mod, cmd_frame, len); - recipient->lastmod = last_mod; - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - ContactInfo contact; - updateContactFromFrame(contact, last_mod, cmd_frame, len); - contact.lastmod = last_mod; - contact.sync_since = 0; - if (addContact(contact)) { - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } - } else if (cmd_frame[0] == CMD_REMOVE_CONTACT) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient && removeContact(*recipient)) { - dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove - } - } else if (cmd_frame[0] == CMD_SHARE_CONTACT) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - if (shareContactZeroHop(*recipient)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); // unable to send - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_GET_CONTACT_BY_KEY) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (contact) { - writeContactRespFrame(RESP_CODE_CONTACT, *contact); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // not found - } - } else if (cmd_frame[0] == CMD_EXPORT_CONTACT) { - if (len < 1 + PUB_KEY_SIZE) { - // export SELF - mesh::Packet* pkt; - if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { - pkt = createSelfAdvert(_prefs.node_name); - } else { - pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); - } - if (pkt) { - pkt->header |= ROUTE_TYPE_FLOOD; // would normally be sent in this mode - - out_frame[0] = RESP_CODE_EXPORT_CONTACT; - uint8_t out_len = pkt->writeTo(&out_frame[1]); - releasePacket(pkt); // undo the obtainNewPacket() - _serial->writeFrame(out_frame, out_len + 1); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); // Error - } - } else { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - uint8_t out_len; - if (recipient && (out_len = exportContact(*recipient, &out_frame[1])) > 0) { - out_frame[0] = RESP_CODE_EXPORT_CONTACT; - _serial->writeFrame(out_frame, out_len + 1); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // not found - } - } - } else if (cmd_frame[0] == CMD_IMPORT_CONTACT && len > 2 + 32 + 64) { - if (importContact(&cmd_frame[1], len - 1)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) { - int out_len; - if ((out_len = getFromOfflineQueue(out_frame)) > 0) { - _serial->writeFrame(out_frame, out_len); -#ifdef DISPLAY_CLASS - if (_ui) _ui->msgRead(offline_queue_len); +#if ENV_INCLUDE_GPS == 1 + applyGpsPrefs(); #endif - } else { - out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; - _serial->writeFrame(out_frame, 1); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_PARAMS) { - int i = 1; - uint32_t freq; - memcpy(&freq, &cmd_frame[i], 4); - i += 4; - uint32_t bw; - memcpy(&bw, &cmd_frame[i], 4); - i += 4; - uint8_t sf = cmd_frame[i++]; - uint8_t cr = cmd_frame[i++]; +} - if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && - bw <= 500000) { - _prefs.sf = sf; - _prefs.cr = cr; - _prefs.freq = (float)freq / 1000.0; - _prefs.bw = (float)bw / 1000.0; - savePrefs(); +void MyMesh::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; - radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, - (uint32_t)cr); + revert_radio_at = futureMillis(2000 + timeout_mins * 60 * 1000); // schedule when to revert radio params +} - writeOKFrame(); - } else { - MESH_DEBUG_PRINTLN("Error: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, - (uint32_t)cr); - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_SET_RADIO_TX_POWER) { - if (cmd_frame[1] > MAX_LORA_TX_POWER) { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else { - _prefs.tx_power_dbm = cmd_frame[1]; - savePrefs(); - radio_set_tx_power(_prefs.tx_power_dbm); - writeOKFrame(); - } - } else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) { - int i = 1; - uint32_t rx, af; - memcpy(&rx, &cmd_frame[i], 4); - i += 4; - memcpy(&af, &cmd_frame[i], 4); - i += 4; - _prefs.rx_delay_base = ((float)rx) / 1000.0f; - _prefs.airtime_factor = ((float)af) / 1000.0f; - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_GET_TUNING_PARAMS) { - uint32_t rx = _prefs.rx_delay_base * 1000, af = _prefs.airtime_factor * 1000; - int i = 0; - out_frame[i++] = RESP_CODE_TUNING_PARAMS; - memcpy(&out_frame[i], &rx, 4); i += 4; - memcpy(&out_frame[i], &af, 4); i += 4; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_SET_OTHER_PARAMS) { - _prefs.manual_add_contacts = cmd_frame[1]; - if (len >= 3) { - _prefs.telemetry_mode_base = cmd_frame[2] & 0x03; // v5+ - _prefs.telemetry_mode_loc = (cmd_frame[2] >> 2) & 0x03; - _prefs.telemetry_mode_env = (cmd_frame[2] >> 4) & 0x03; - - if (len >= 4) { - _prefs.advert_loc_policy = cmd_frame[3]; - if (len >= 5) { - _prefs.multi_acks = cmd_frame[4]; - } - } - } - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { - if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? - saveContacts(); - } - board.reboot(); - } else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) { - uint8_t reply[11]; - int i = 0; - reply[i++] = RESP_CODE_BATT_AND_STORAGE; - uint16_t battery_millivolts = board.getBattMilliVolts(); - uint32_t used = _store->getStorageUsedKb(); - uint32_t total = _store->getStorageTotalKb(); - memcpy(&reply[i], &battery_millivolts, 2); i += 2; - memcpy(&reply[i], &used, 4); i += 4; - memcpy(&reply[i], &total, 4); i += 4; - _serial->writeFrame(reply, i); - } else if (cmd_frame[0] == CMD_EXPORT_PRIVATE_KEY) { -#if ENABLE_PRIVATE_KEY_EXPORT - uint8_t reply[65]; - reply[0] = RESP_CODE_PRIVATE_KEY; - self_id.writeTo(&reply[1], 64); - _serial->writeFrame(reply, 65); +bool MyMesh::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 - writeDisabledFrame(); +#error "need to implement file system erase" + return false; #endif - } else if (cmd_frame[0] == CMD_IMPORT_PRIVATE_KEY && len >= 65) { -#if ENABLE_PRIVATE_KEY_IMPORT - if (!mesh::LocalIdentity::validatePrivateKey(&cmd_frame[1])) { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid key - } else { - mesh::LocalIdentity identity; - identity.readFrom(&cmd_frame[1], 64); - if (_store->saveMainIdentity(identity)) { - self_id = identity; - writeOKFrame(); - // re-load contacts, to invalidate ecdh shared_secrets - resetContacts(); - _store->loadContacts(this); - } else { - writeErrFrame(ERR_CODE_FILE_IO_ERROR); - } - } -#else - writeDisabledFrame(); -#endif - } else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) { - int i = 1; - int8_t path_len = cmd_frame[i++]; - if (path_len >= 0 && i + path_len + 4 <= len) { // minimum 4 byte payload - uint8_t *path = &cmd_frame[i]; - i += path_len; - auto pkt = createRawData(&cmd_frame[i], len - i); - if (pkt) { - sendDirect(pkt, path, path_len); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // flood, not supported (yet) - } - } else if (cmd_frame[0] == CMD_SEND_LOGIN && len >= 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - char *password = (char *)&cmd_frame[1 + PUB_KEY_SIZE]; - cmd_frame[len] = 0; // ensure null terminator in password - if (recipient) { - uint32_t est_timeout; - int result = sendLogin(*recipient, password, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - memcpy(&pending_login, recipient->id.pub_key, 4); // match this to onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &pending_login, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_ANON_REQ && len > 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - uint8_t *data = &cmd_frame[1 + PUB_KEY_SIZE]; - if (recipient) { - uint32_t tag, est_timeout; - int result = sendAnonReq(*recipient, data, len - (1 + PUB_KEY_SIZE), tag, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - pending_req = tag; // match this to onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_STATUS_REQ && len >= 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint32_t tag, est_timeout; - int result = sendRequest(*recipient, REQ_TYPE_GET_STATUS, tag, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - // FUTURE: pending_status = tag; // match this in onContactResponse() - memcpy(&pending_status, recipient->id.pub_key, 4); // legacy matching scheme - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_PATH_DISCOVERY_REQ && cmd_frame[1] == 0 && len >= 2 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[2]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint32_t tag, est_timeout; - // 'Path Discovery' is just a special case of flood + Telemetry req - uint8_t req_data[9]; - req_data[0] = REQ_TYPE_GET_TELEMETRY_DATA; - req_data[1] = ~(TELEM_PERM_BASE); // NEW: inverse permissions mask (ie. we only want BASE telemetry) - memset(&req_data[2], 0, 3); // reserved - getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique - auto save = recipient->out_path_len; // temporarily force sendRequest() to flood - recipient->out_path_len = -1; - int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout); - recipient->out_path_len = save; - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - pending_discovery = tag; // match this in onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len >= 4 + PUB_KEY_SIZE) { // can deprecate, in favour of CMD_SEND_BINARY_REQ - uint8_t *pub_key = &cmd_frame[4]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint32_t tag, est_timeout; - int result = sendRequest(*recipient, REQ_TYPE_GET_TELEMETRY_DATA, tag, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - pending_telemetry = tag; // match this in onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request - telemetry.reset(); - telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); - // query other sensors -- target specific - sensors.querySensors(0xFF, telemetry); +} - int i = 0; - out_frame[i++] = PUSH_CODE_TELEMETRY_RESPONSE; - out_frame[i++] = 0; // reserved - memcpy(&out_frame[i], self_id.pub_key, 6); - i += 6; // pub_key_prefix - uint8_t tlen = telemetry.getSize(); - memcpy(&out_frame[i], telemetry.getBuffer(), tlen); - i += tlen; - _serial->writeFrame(out_frame, i); - } else if (cmd_frame[0] == CMD_SEND_BINARY_REQ && len >= 2 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); - if (recipient) { - uint8_t *req_data = &cmd_frame[1 + PUB_KEY_SIZE]; - uint32_t tag, est_timeout; - int result = sendRequest(*recipient, req_data, len - (1 + PUB_KEY_SIZE), tag, est_timeout); - if (result == MSG_SEND_FAILED) { - writeErrFrame(ERR_CODE_TABLE_FULL); - } else { - clearPendingReqs(); - pending_req = tag; // match this in onContactResponse() - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = (result == MSG_SEND_SENT_FLOOD) ? 1 : 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // contact not found - } - } else if (cmd_frame[0] == CMD_HAS_CONNECTION && len >= 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - if (hasConnectionTo(pub_key)) { - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_LOGOUT && len >= 1 + PUB_KEY_SIZE) { - uint8_t *pub_key = &cmd_frame[1]; - stopConnection(pub_key); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2) { - uint8_t channel_idx = cmd_frame[1]; - ChannelDetails channel; - if (getChannel(channel_idx, channel)) { - int i = 0; - out_frame[i++] = RESP_CODE_CHANNEL_INFO; - out_frame[i++] = channel_idx; - strcpy((char *)&out_frame[i], channel.name); - i += 32; - memcpy(&out_frame[i], channel.channel.secret, 16); - i += 16; // NOTE: only 128-bit supported - _serial->writeFrame(out_frame, i); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 32) { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); // not supported (yet) - } else if (cmd_frame[0] == CMD_SET_CHANNEL && len >= 2 + 32 + 16) { - uint8_t channel_idx = cmd_frame[1]; - ChannelDetails channel; - StrHelper::strncpy(channel.name, (char *)&cmd_frame[2], 32); - memset(channel.channel.secret, 0, sizeof(channel.channel.secret)); - memcpy(channel.channel.secret, &cmd_frame[2 + 32], 16); // NOTE: only 128-bit supported - if (setChannel(channel_idx, channel)) { - saveChannels(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx - } - } else if (cmd_frame[0] == CMD_SIGN_START) { - out_frame[0] = RESP_CODE_SIGN_START; - out_frame[1] = 0; // reserved - uint32_t len = MAX_SIGN_DATA_LEN; - memcpy(&out_frame[2], &len, 4); - _serial->writeFrame(out_frame, 6); - - if (sign_data) { - free(sign_data); - } - sign_data = (uint8_t *)malloc(MAX_SIGN_DATA_LEN); - sign_data_len = 0; - } else if (cmd_frame[0] == CMD_SIGN_DATA && len > 1) { - if (sign_data == NULL || sign_data_len + (len - 1) > MAX_SIGN_DATA_LEN) { - writeErrFrame(sign_data == NULL ? ERR_CODE_BAD_STATE : ERR_CODE_TABLE_FULL); // error: too long - } else { - memcpy(&sign_data[sign_data_len], &cmd_frame[1], len - 1); - sign_data_len += (len - 1); - writeOKFrame(); - } - } else if (cmd_frame[0] == CMD_SIGN_FINISH) { - if (sign_data) { - self_id.sign(&out_frame[1], sign_data, sign_data_len); - - free(sign_data); // don't need sign_data now - sign_data = NULL; - - out_frame[0] = RESP_CODE_SIGNATURE; - _serial->writeFrame(out_frame, 1 + SIGNATURE_SIZE); - } else { - writeErrFrame(ERR_CODE_BAD_STATE); - } - } else if (cmd_frame[0] == CMD_SEND_TRACE_PATH && len > 10 && len - 10 < MAX_PACKET_PAYLOAD-5) { - uint8_t path_len = len - 10; - uint8_t flags = cmd_frame[9]; - uint8_t path_sz = flags & 0x03; // NEW v1.11+ - if ((path_len >> path_sz) > MAX_PATH_SIZE || (path_len % (1 << path_sz)) != 0) { // make sure is multiple of path_sz - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } else { - uint32_t tag, auth; - memcpy(&tag, &cmd_frame[1], 4); - memcpy(&auth, &cmd_frame[5], 4); - auto pkt = createTrace(tag, auth, flags); - if (pkt) { - sendDirect(pkt, &cmd_frame[10], path_len); - - uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); - uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len); - - out_frame[0] = RESP_CODE_SENT; - out_frame[1] = 0; - memcpy(&out_frame[2], &tag, 4); - memcpy(&out_frame[6], &est_timeout, 4); - _serial->writeFrame(out_frame, 10); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } - } else if (cmd_frame[0] == CMD_SET_DEVICE_PIN && len >= 5) { - - // get pin from command frame - uint32_t pin; - memcpy(&pin, &cmd_frame[1], 4); - - // ensure pin is zero, or a valid 6 digit pin - if (pin == 0 || (pin >= 100000 && pin <= 999999)) { - _prefs.ble_pin = pin; - savePrefs(); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { - out_frame[0] = RESP_CODE_CUSTOM_VARS; - char *dp = (char *)&out_frame[1]; - for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { - if (i > 0) { - *dp++ = ','; - } - strcpy(dp, sensors.getSettingName(i)); - dp = strchr(dp, 0); - *dp++ = ':'; - strcpy(dp, sensors.getSettingValue(i)); - dp = strchr(dp, 0); - } - _serial->writeFrame(out_frame, dp - (char *)out_frame); - } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { - cmd_frame[len] = 0; - char *sp = (char *)&cmd_frame[1]; - char *np = strchr(sp, ':'); // look for separator char - if (np) { - *np++ = 0; // modify 'cmd_frame', replace ':' with null - bool success = sensors.setSettingValue(sp, np); - if (success) { - #if ENV_INCLUDE_GPS == 1 - // Update node preferences for GPS settings - if (strcmp(sp, "gps") == 0) { - _prefs.gps_enabled = (np[0] == '1') ? 1 : 0; - savePrefs(); - } else if (strcmp(sp, "gps_interval") == 0) { - uint32_t interval_seconds = atoi(np); - _prefs.gps_interval = constrain(interval_seconds, 0, 86400); - savePrefs(); - } - #endif - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); - } - } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { - // FUTURE use: uint8_t reserved = cmd_frame[1]; - uint8_t *pub_key = &cmd_frame[2]; - AdvertPath* found = NULL; - for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { - auto p = &advert_paths[i]; - if (memcmp(p->pubkey_prefix, pub_key, sizeof(p->pubkey_prefix)) == 0) { - found = p; - break; - } - } - if (found) { - out_frame[0] = RESP_CODE_ADVERT_PATH; - memcpy(&out_frame[1], &found->recv_timestamp, 4); - out_frame[5] = found->path_len; - memcpy(&out_frame[6], found->path, found->path_len); - _serial->writeFrame(out_frame, 6 + found->path_len); - } else { - writeErrFrame(ERR_CODE_NOT_FOUND); - } - } else if (cmd_frame[0] == CMD_GET_STATS && len >= 2) { - uint8_t stats_type = cmd_frame[1]; - if (stats_type == STATS_TYPE_CORE) { - int i = 0; - out_frame[i++] = RESP_CODE_STATS; - out_frame[i++] = STATS_TYPE_CORE; - uint16_t battery_mv = board.getBattMilliVolts(); - uint32_t uptime_secs = _ms->getMillis() / 1000; - uint8_t queue_len = (uint8_t)_mgr->getOutboundCount(0xFFFFFFFF); - memcpy(&out_frame[i], &battery_mv, 2); i += 2; - memcpy(&out_frame[i], &uptime_secs, 4); i += 4; - memcpy(&out_frame[i], &_err_flags, 2); i += 2; - out_frame[i++] = queue_len; - _serial->writeFrame(out_frame, i); - } else if (stats_type == STATS_TYPE_RADIO) { - int i = 0; - out_frame[i++] = RESP_CODE_STATS; - out_frame[i++] = STATS_TYPE_RADIO; - int16_t noise_floor = (int16_t)_radio->getNoiseFloor(); - int8_t last_rssi = (int8_t)radio_driver.getLastRSSI(); - int8_t last_snr = (int8_t)(radio_driver.getLastSNR() * 4); // scaled by 4 for 0.25 dB precision - uint32_t tx_air_secs = getTotalAirTime() / 1000; - uint32_t rx_air_secs = getReceiveAirTime() / 1000; - memcpy(&out_frame[i], &noise_floor, 2); i += 2; - out_frame[i++] = last_rssi; - out_frame[i++] = last_snr; - memcpy(&out_frame[i], &tx_air_secs, 4); i += 4; - memcpy(&out_frame[i], &rx_air_secs, 4); i += 4; - _serial->writeFrame(out_frame, i); - } else if (stats_type == STATS_TYPE_PACKETS) { - int i = 0; - out_frame[i++] = RESP_CODE_STATS; - out_frame[i++] = STATS_TYPE_PACKETS; - uint32_t recv = radio_driver.getPacketsRecv(); - uint32_t sent = radio_driver.getPacketsSent(); - uint32_t n_sent_flood = getNumSentFlood(); - uint32_t n_sent_direct = getNumSentDirect(); - uint32_t n_recv_flood = getNumRecvFlood(); - uint32_t n_recv_direct = getNumRecvDirect(); - memcpy(&out_frame[i], &recv, 4); i += 4; - memcpy(&out_frame[i], &sent, 4); i += 4; - memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; - memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; - memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; - memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; - _serial->writeFrame(out_frame, i); - } else { - writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type - } - } else if (cmd_frame[0] == CMD_FACTORY_RESET && memcmp(&cmd_frame[1], "reset", 5) == 0) { - if (_serial) { - MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)"); - _serial->disable(); // Phone app disconnects before we can send OK frame so it's safe here - } - bool success = _store->formatFileSystem(); - if (success) { - writeOKFrame(); - delay(1000); - board.reboot(); // doesn't return - } else { - writeErrFrame(ERR_CODE_FILE_IO_ERROR); - } - } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE && len >= 2 && cmd_frame[1] == 0) { - if (len >= 2 + 16) { - memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set curr scope TransportKey - } else { - memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null - } - writeOKFrame(); - } else if (cmd_frame[0] == CMD_SEND_CONTROL_DATA && len >= 2 && (cmd_frame[1] & 0x80) != 0) { - auto resp = createControlData(&cmd_frame[1], len - 1); - if (resp) { - sendZeroHop(resp); - writeOKFrame(); - } else { - writeErrFrame(ERR_CODE_TABLE_FULL); - } - } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { - _prefs.autoadd_config = cmd_frame[1]; - savePrefs(); - writeOKFrame(); - } else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) { - int i = 0; - out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; - out_frame[i++] = _prefs.autoadd_config; - _serial->writeFrame(out_frame, i); +void MyMesh::sendSelfAdvertisement(int delay_millis) { + mesh::Packet *pkt = createSelfAdvert(); + if (pkt) { + sendFlood(pkt, delay_millis); } else { - writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); - MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); + MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!"); } } -void MyMesh::enterCLIRescue() { - _cli_rescue = true; - cli_command[0] = 0; - Serial.println("========= CLI Rescue ========="); +void MyMesh::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 MyMesh::checkCLIRescueCmd() { - int len = strlen(cli_command); - while (Serial.available() && len < sizeof(cli_command)-1) { - char c = Serial.read(); - if (c != '\n') { - cli_command[len++] = c; - cli_command[len] = 0; +void MyMesh::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 MyMesh::dumpLogFile() { +#if defined(RP2040_PLATFORM) + File f = _fs->open(PACKET_LOG_FILE, "r"); +#else + File f = _fs->open(PACKET_LOG_FILE); +#endif + if (f) { + while (f.available()) { + int c = f.read(); + if (c < 0) break; + Serial.print((char)c); } - Serial.print(c); // echo + f.close(); } - if (len == sizeof(cli_command)-1) { // command buffer full - cli_command[sizeof(cli_command)-1] = '\r'; +} + +void MyMesh::setTxPower(uint8_t power_dbm) { + radio_set_tx_power(power_dbm); +} + +void MyMesh::formatNeighborsReply(char *reply) { + char *dp = reply; + +#if MAX_NEIGHBOURS + // create copy of neighbours list, skipping empty entries so we can sort it separately from main list + int16_t neighbours_count = 0; + NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS]; + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + auto neighbour = &neighbours[i]; + if (neighbour->heard_timestamp > 0) { + sorted_neighbours[neighbours_count] = neighbour; + neighbours_count++; + } } - if (len > 0 && cli_command[len - 1] == '\r') { // received complete line - cli_command[len - 1] = 0; // replace newline with C string null terminator + // sort neighbours newest to oldest + std::sort(sorted_neighbours, sorted_neighbours + neighbours_count, [](const NeighbourInfo* a, const NeighbourInfo* b) { + return a->heard_timestamp > b->heard_timestamp; // desc + }); - if (memcmp(cli_command, "set ", 4) == 0) { - const char* config = &cli_command[4]; - if (memcmp(config, "pin ", 4) == 0) { - _prefs.ble_pin = atoi(&config[4]); - savePrefs(); - Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); - } else { - Serial.printf(" Error: unknown config: %s\n", config); - } - } else if (strcmp(cli_command, "rebuild") == 0) { - bool success = _store->formatFileSystem(); - if (success) { - _store->saveMainIdentity(self_id); - savePrefs(); - saveContacts(); - saveChannels(); - Serial.println(" > erase and rebuild done"); - } else { - Serial.println(" Error: erase failed"); - } - } else if (strcmp(cli_command, "erase") == 0) { - bool success = _store->formatFileSystem(); - if (success) { - Serial.println(" > erase done"); - } else { - Serial.println(" Error: erase failed"); - } - } else if (memcmp(cli_command, "ls", 2) == 0) { + for (int i = 0; i < neighbours_count && dp - reply < 134; i++) { + NeighbourInfo *neighbour = sorted_neighbours[i]; - // get path from command e.g: "ls /adafruit" - const char *path = &cli_command[3]; + // add new line if not first item + if (i > 0) *dp++ = '\n'; - bool is_fs2 = false; - if (memcmp(path, "UserData/", 9) == 0) { - path += 8; // skip "UserData" - } else if (memcmp(path, "ExtraFS/", 8) == 0) { - path += 7; // skip "ExtraFS" - is_fs2 = true; - } - Serial.printf("Listing files in %s\n", path); + char hex[10]; + // get 4 bytes of neighbour id as hex + mesh::Utils::toHex(hex, neighbour->id.pub_key, 4); - // log each file and directory - File root = _store->openRead(path); - if (is_fs2 == false) { - if (root) { - File file = root.openNextFile(); - while (file) { - if (file.isDirectory()) { - Serial.printf("[dir] UserData%s/%s\n", path, file.name()); - } else { - Serial.printf("[file] UserData%s/%s (%d bytes)\n", path, file.name(), file.size()); - } - // move to next file - file = root.openNextFile(); - } - root.close(); - } - } + // add next neighbour + uint32_t secs_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; + sprintf(dp, "%s:%d:%d", hex, secs_ago, neighbour->snr); + while (*dp) + dp++; // find end of string + } +#endif + if (dp == reply) { // no neighbours, need empty response + strcpy(dp, "-none-"); + dp += 6; + } + *dp = 0; // null terminator +} - if (is_fs2 == true || strlen(path) == 0 || strcmp(path, "/") == 0) { - if (_store->getSecondaryFS() != nullptr) { - File root2 = _store->openRead(_store->getSecondaryFS(), path); - File file = root2.openNextFile(); - while (file) { - if (file.isDirectory()) { - Serial.printf("[dir] ExtraFS%s/%s\n", path, file.name()); - } else { - Serial.printf("[file] ExtraFS%s/%s (%d bytes)\n", path, file.name(), file.size()); - } - // move to next file - file = root2.openNextFile(); - } - root2.close(); - } - } - } else if (memcmp(cli_command, "cat", 3) == 0) { +void MyMesh::removeNeighbor(const uint8_t *pubkey, int key_len) { +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + NeighbourInfo *neighbour = &neighbours[i]; + if (memcmp(neighbour->id.pub_key, pubkey, key_len) == 0) { + neighbours[i] = NeighbourInfo(); // clear neighbour entry + } + } +#endif +} - // get path from command e.g: "cat /contacts3" - const char *path = &cli_command[4]; - - bool is_fs2 = false; - if (memcmp(path, "UserData/", 9) == 0) { - path += 8; // skip "UserData" - } else if (memcmp(path, "ExtraFS/", 8) == 0) { - path += 7; // skip "ExtraFS" - is_fs2 = true; - } else { - Serial.println("Invalid path provided, must start with UserData/ or ExtraFS/"); - cli_command[0] = 0; - return; - } +void MyMesh::formatStatsReply(char *reply) { + StatsFormatHelper::formatCoreStats(reply, board, *_ms, _err_flags, _mgr); +} - // log file content as hex - File file = _store->openRead(path); - if (is_fs2 == true) { - file = _store->openRead(_store->getSecondaryFS(), path); - } - if(file){ +void MyMesh::formatRadioStatsReply(char *reply) { + StatsFormatHelper::formatRadioStats(reply, _radio, radio_driver, getTotalAirTime(), getReceiveAirTime()); +} - // get file content - int file_size = file.available(); - uint8_t buffer[file_size]; - file.read(buffer, file_size); +void MyMesh::formatPacketStatsReply(char *reply) { + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + getNumRecvFlood(), getNumRecvDirect()); +} - // print hex - mesh::Utils::printHex(Serial, buffer, file_size); - Serial.print("\n"); +void MyMesh::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); +} - file.close(); +void MyMesh::clearStats() { + radio_driver.resetStats(); + resetStats(); + ((SimpleMeshTables *)getTables())->resetStats(); +} - } +void MyMesh::handleCommand(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; - } else if (memcmp(cli_command, "rm ", 3) == 0) { - // get path from command e.g: "rm /adv_blobs" - const char *path = &cli_command[3]; - MESH_DEBUG_PRINTLN("Removing file: %s", path); - // ensure path is not empty, or root dir - if(!path || strlen(path) == 0 || strcmp(path, "/") == 0){ - Serial.println("Invalid path provided"); - } else { - bool is_fs2 = false; - if (memcmp(path, "UserData/", 9) == 0) { - path += 8; // skip "UserData" - } else if (memcmp(path, "ExtraFS/", 8) == 0) { - path += 7; // skip "ExtraFS" - is_fs2 = true; - } - - // remove file - bool removed; - if (is_fs2) { - MESH_DEBUG_PRINTLN("Removing file from ExtraFS: %s", path); - removed = _store->removeFile(_store->getSecondaryFS(), path); - } else { - MESH_DEBUG_PRINTLN("Removing file from UserData: %s", path); - removed = _store->removeFile(path); - } - if(removed){ - Serial.println("File removed"); - } else { - Serial.println("Failed to remove file"); - } - - } - - } else if (strcmp(cli_command, "reboot") == 0) { - board.reboot(); // doesn't return + sprintf(reply, "OK - loaded %d regions", region_map.getCount()); } else { - Serial.println(" Error: unknown command"); - } + char *np = command; + while (*np == ' ') np++; // skip indent + int indent = np - command; - cli_command[0] = 0; // reset command buffer - } -} + char *ep = np; + while (RegionMap::is_name_char(*ep)) ep++; + if (*ep) { *ep++ = 0; } // set null terminator for end of name -void MyMesh::checkSerialInterface() { - size_t len = _serial->checkRecvFrame(cmd_frame); - if (len > 0) { - handleCmdFrame(len); - } else if (_iter_started // check if our ContactsIterator is 'running' - && !_serial->isWriteBusy() // don't spam the Serial Interface too quickly! - ) { - ContactInfo contact; - if (_iter.hasNext(this, contact)) { - if (contact.lastmod > _iter_filter_since) { // apply the 'since' filter - writeContactRespFrame(RESP_CODE_CONTACT, contact); - if (contact.lastmod > _most_recent_lastmod) { - _most_recent_lastmod = contact.lastmod; // save for the RESP_CODE_END_OF_CONTACTS frame + 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 + } } } - } else { // EOF - out_frame[0] = RESP_CODE_END_OF_CONTACTS; - memcpy(&out_frame[1], &_most_recent_lastmod, - 4); // include the most recent lastmod, so app can update their 'since' - _serial->writeFrame(out_frame, 5); - _iter_started = false; + reply[0] = 0; } - //} else if (!_serial->isWriteBusy()) { - // checkConnections(); // TODO - deprecate the 'Connections' stuff + 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; + } + + // handle ACL related 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 { + *sp++ = 0; // replace space with null terminator + + uint8_t pubkey[PUB_KEY_SIZE]; + int hex_len = min(sp - hex, PUB_KEY_SIZE*2); + 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 (or guest) 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, "region", 6) == 0) { + reply[0] = 0; + + const char* parts[4]; + int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); + if (n == 1 && sender_timestamp == 0) { + region_map.exportTo(Serial); + } else if (n >= 2 && strcmp(parts[1], "load") == 0) { + 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; + } else if (n >= 2 && strcmp(parts[1], "save") == 0) { + _prefs.discovery_mod_timestamp = rtc_clock.getCurrentTime(); // this node is now 'modified' (for discovery info) + savePrefs(); + bool success = region_map.save(_fs); + strcpy(reply, success ? "OK" : "Err - save failed"); + } else if (n >= 3 && strcmp(parts[1], "allowf") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + region->flags &= ~REGION_DENY_FLOOD; + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "denyf") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + region->flags |= REGION_DENY_FLOOD; + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "get") == 0) { + auto region = region_map.findByNamePrefix(parts[2]); + if (region) { + auto parent = region_map.findById(region->parent); + if (parent && parent->id != 0) { + sprintf(reply, " %s (%s) %s", region->name, parent->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + } else { + sprintf(reply, " %s %s", region->name, (region->flags & REGION_DENY_FLOOD) ? "" : "F"); + } + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n >= 3 && strcmp(parts[1], "home") == 0) { + auto home = region_map.findByNamePrefix(parts[2]); + if (home) { + region_map.setHomeRegion(home); + sprintf(reply, " home is now %s", home->name); + } else { + strcpy(reply, "Err - unknown region"); + } + } else if (n == 2 && strcmp(parts[1], "home") == 0) { + auto home = region_map.getHomeRegion(); + sprintf(reply, " home is %s", home ? home->name : "*"); + } else if (n >= 3 && strcmp(parts[1], "put") == 0) { + auto parent = n >= 4 ? region_map.findByNamePrefix(parts[3]) : ®ion_map.getWildcard(); + if (parent == NULL) { + strcpy(reply, "Err - unknown parent"); + } else { + auto region = region_map.putRegion(parts[2], parent->id); + if (region == NULL) { + strcpy(reply, "Err - unable to put"); + } else { + strcpy(reply, "OK"); + } + } + } else if (n >= 3 && strcmp(parts[1], "remove") == 0) { + auto region = region_map.findByName(parts[2]); + if (region) { + if (region_map.removeRegion(*region)) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - not empty"); + } + } else { + strcpy(reply, "Err - not found"); + } + } else { + strcpy(reply, "Err - ??"); + } + } else{ + _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } } void MyMesh::loop() { - BaseChatMesh::loop(); +#ifdef WITH_BRIDGE + bridge.loop(); +#endif - if (_cli_rescue) { - checkCLIRescueCmd(); - } else { - checkSerialInterface(); + mesh::Mesh::loop(); + + if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { + mesh::Packet *pkt = createSelfAdvert(); + if (pkt) sendFlood(pkt); + + 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 } - // is there are pending dirty contacts write needed? + if (set_radio_at && millisHasNowPassed(set_radio_at)) { // apply pending (temporary) radio params + set_radio_at = 0; // clear timer + radio_set_params(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_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + MESH_DEBUG_PRINTLN("Radio params restored"); + } + + // is pending dirty contacts write needed? if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) { - saveContacts(); + acl.save(_fs); dirty_contacts_expiry = 0; } -#ifdef DISPLAY_CLASS - if (_ui) _ui->setHasConnection(_serial->isConnected()); -#endif + // update uptime + uint32_t now = millis(); + uptime_millis += now - last_millis; + last_millis = now; } -bool MyMesh::advert() { - mesh::Packet* pkt; - if (_prefs.advert_loc_policy == ADVERT_LOC_NONE) { - pkt = createSelfAdvert(_prefs.node_name); - } else { - pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); - } - if (pkt) { - sendZeroHop(pkt); - return true; - } else { - return false; - } +// To check if there is pending work +bool MyMesh::hasPendingWork() const { + return _mgr->getOutboundCount(0xFFFFFFFF) > 0; } \ No newline at end of file diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index d096d14b..57b2d4ab 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -2,7 +2,14 @@ #include #include -#define AUTO_OFF_MILLIS 20000 // 20 seconds +#ifdef HAS_4G_MODEM +#include "CellularMQTT.h" +#define AUTO_OFF_DISABLED true +#else +#define AUTO_OFF_DISABLED false +#endif + +#define AUTO_OFF_MILLIS 20000 // 20 seconds (ignored when AUTO_OFF_DISABLED) #define BOOT_SCREEN_MILLIS 4000 // 4 seconds // 'meshcore', 128x13px @@ -28,55 +35,97 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi _node_prefs = node_prefs; _display->turnOn(); - // strip off dash and commit hash by changing dash to null terminator - // e.g: v1.2.3-abcdef -> v1.2.3 char *version = strdup(firmware_version); char *dash = strchr(version, '-'); - if(dash){ - *dash = 0; - } + if (dash) *dash = 0; - // v1.2.3 (1 Jan 2025) - sprintf(_version_info, "%s (%s)", version, build_date); + snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date); + free(version); } void UITask::renderCurrScreen() { char tmp[80]; - if (millis() < BOOT_SCREEN_MILLIS) { // boot screen - // meshcore logo + if (millis() < BOOT_SCREEN_MILLIS) { + // Boot screen — logo + version _display->setColor(DisplayDriver::BLUE); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); - // version info _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); uint16_t versionWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - versionWidth) / 2, 22); _display->print(_version_info); - // node type +#ifdef HAS_4G_MODEM + const char* node_type = "< Remote Repeater >"; +#else const char* node_type = "< Repeater >"; +#endif uint16_t typeWidth = _display->getTextWidth(node_type); _display->setCursor((_display->width() - typeWidth) / 2, 35); _display->print(node_type); - } else { // home screen - // node name + } else { + // Home screen — node info + cellular status _display->setCursor(0, 0); _display->setTextSize(1); _display->setColor(DisplayDriver::GREEN); _display->print(_node_prefs->node_name); - // freq / sf _display->setCursor(0, 20); _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); - // bw / cr _display->setCursor(0, 30); sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); _display->print(tmp); + +#ifdef HAS_4G_MODEM + int y = 44; + + _display->setCursor(0, y); + _display->setColor(DisplayDriver::LIGHT); + sprintf(tmp, "4G: %s", cellularMQTT.stateString()); + _display->print(tmp); + y += 10; + + _display->setCursor(0, y); + sprintf(tmp, "CSQ: %d (%d bars)", cellularMQTT.getCSQ(), cellularMQTT.getSignalBars()); + _display->print(tmp); + y += 10; + + const char* oper = cellularMQTT.getOperator(); + if (oper[0]) { + _display->setCursor(0, y); + sprintf(tmp, "Op: %.16s", oper); + _display->print(tmp); + y += 10; + } + + _display->setCursor(0, y); + _display->setColor(cellularMQTT.isConnected() ? DisplayDriver::GREEN : DisplayDriver::YELLOW); + sprintf(tmp, "MQTT: %s", cellularMQTT.isConnected() ? "Connected" : "---"); + _display->print(tmp); + y += 10; + + const char* ip = cellularMQTT.getIPAddress(); + if (ip[0]) { + _display->setColor(DisplayDriver::LIGHT); + _display->setCursor(0, y); + sprintf(tmp, "IP: %s", ip); + _display->print(tmp); + y += 10; + } + + uint32_t upSec = millis() / 1000; + uint32_t upH = upSec / 3600; + uint32_t upM = (upSec % 3600) / 60; + _display->setColor(DisplayDriver::LIGHT); + _display->setCursor(0, y); + sprintf(tmp, "Up: %luh %lum Heap:%dk", upH, upM, ESP.getFreeHeap() / 1024); + _display->print(tmp); +#endif } } @@ -85,17 +134,15 @@ void UITask::loop() { if (millis() >= _next_read) { int btnState = digitalRead(PIN_USER_BTN); if (btnState != _prevBtnState) { - if (btnState == LOW) { // pressed? - if (_display->isOn()) { - // TODO: any action ? - } else { + if (btnState == LOW) { + if (!_display->isOn()) { _display->turnOn(); } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + _auto_off = millis() + AUTO_OFF_MILLIS; } _prevBtnState = btnState; } - _next_read = millis() + 200; // 5 reads per second + _next_read = millis() + 200; } #endif @@ -105,10 +152,10 @@ void UITask::loop() { renderCurrScreen(); _display->endFrame(); - _next_refresh = millis() + 1000; // refresh every second + _next_refresh = millis() + 10000; } - if (millis() > _auto_off) { + if (!AUTO_OFF_DISABLED && millis() > _auto_off) { _display->turnOff(); } } -} +} \ No newline at end of file diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index a27259f1..05c4a845 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -8,7 +8,7 @@ class UITask { unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; NodePrefs* _node_prefs; - char _version_info[32]; + char _version_info[48]; void renderCurrScreen(); public: diff --git a/examples/simple_repeater/cellularmqtt.cpp b/examples/simple_repeater/cellularmqtt.cpp new file mode 100644 index 00000000..01f91499 --- /dev/null +++ b/examples/simple_repeater/cellularmqtt.cpp @@ -0,0 +1,860 @@ +#ifdef HAS_4G_MODEM + +#include "CellularMQTT.h" +#include +#include +#include + +CellularMQTT cellularMQTT; + +#define MODEM_SERIAL Serial1 +#define MODEM_BAUD 115200 + +#define CONFIG_FILE "/remote/mqtt.cfg" + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void CellularMQTT::begin() { + Serial.println("[Cell] begin()"); + + _state = CellState::OFF; + _csq = 99; + _reconnectDelay = MQTT_RECONNECT_MIN; + _pubFailCount = 0; + + if (!loadConfig(_config)) { + Serial.println("[Cell] ERROR: No /remote/mqtt.cfg — cannot start MQTT"); + _state = CellState::ERROR; + return; + } + Serial.printf("[Cell] Config: broker=%s:%d user=%s id=%s\n", + _config.broker, _config.port, _config.username, _config.deviceId); + + snprintf(_topicCmd, sizeof(_topicCmd), "meck/%s/cmd", _config.deviceId); + snprintf(_topicRsp, sizeof(_topicRsp), "meck/%s/rsp", _config.deviceId); + snprintf(_topicTelem, sizeof(_topicTelem), "meck/%s/telemetry", _config.deviceId); + snprintf(_topicOta, sizeof(_topicOta), "meck/%s/ota", _config.deviceId); + + _cmdQueue = xQueueCreate(CMD_QUEUE_SIZE, sizeof(MQTTCommand)); + _rspQueue = xQueueCreate(RSP_QUEUE_SIZE, sizeof(MQTTResponse)); + _uartMutex = xSemaphoreCreateMutex(); + _telemetryMutex = xSemaphoreCreateMutex(); + + xTaskCreatePinnedToCore(taskEntry, "cell", CELL_TASK_STACK_SIZE, + this, CELL_TASK_PRIORITY, &_taskHandle, CELL_TASK_CORE); +} + +void CellularMQTT::stop() { + if (!_taskHandle) return; + mqttDisconnect(); + vTaskDelete(_taskHandle); + _taskHandle = nullptr; + _state = CellState::OFF; +} + +bool CellularMQTT::recvCommand(MQTTCommand& out) { + if (!_cmdQueue) return false; + return xQueueReceive(_cmdQueue, &out, 0) == pdTRUE; +} + +bool CellularMQTT::sendResponse(const char* topic, const char* payload) { + if (!_rspQueue) return false; + MQTTResponse rsp; + memset(&rsp, 0, sizeof(rsp)); + strncpy(rsp.topic, topic, MQTT_TOPIC_MAX - 1); + strncpy(rsp.payload, payload, MQTT_PAYLOAD_MAX - 1); + return xQueueSend(_rspQueue, &rsp, 0) == pdTRUE; +} + +void CellularMQTT::updateTelemetry(const TelemetryData& data) { + if (xSemaphoreTake(_telemetryMutex, pdMS_TO_TICKS(50))) { + memcpy(&_telemetry, &data, sizeof(data)); + xSemaphoreGive(_telemetryMutex); + } +} + +int CellularMQTT::getSignalBars() const { + if (_csq == 99 || _csq == 0) return 0; + if (_csq <= 5) return 1; + if (_csq <= 10) return 2; + if (_csq <= 15) return 3; + if (_csq <= 20) return 4; + return 5; +} + +const char* CellularMQTT::stateString() const { + switch (_state) { + case CellState::OFF: return "OFF"; + case CellState::POWERING_ON: return "PWR ON"; + case CellState::INITIALIZING: return "INIT"; + case CellState::REGISTERING: return "REG"; + case CellState::DATA_ACTIVATING: return "DATA"; + case CellState::MQTT_STARTING: return "MQTT INIT"; + case CellState::MQTT_CONNECTING: return "MQTT CONN"; + case CellState::CONNECTED: return "CONNECTED"; + case CellState::RECONNECTING: return "RECONN"; + case CellState::ERROR: return "ERROR"; + default: return "???"; + } +} + +// --------------------------------------------------------------------------- +// Config file: /remote/mqtt.cfg +// Format (one value per line): +// broker.hivemq.cloud +// 8883 +// myusername +// mypassword +// mydeviceid (optional — auto-generated from MAC if omitted) +// --------------------------------------------------------------------------- + +bool CellularMQTT::loadConfig(MQTTConfig& cfg) { + memset(&cfg, 0, sizeof(cfg)); + + File f = SD.open(CONFIG_FILE, FILE_READ); + if (!f) return false; + + String line; + + line = f.readStringUntil('\n'); line.trim(); + if (line.length() == 0) { f.close(); return false; } + strncpy(cfg.broker, line.c_str(), sizeof(cfg.broker) - 1); + + line = f.readStringUntil('\n'); line.trim(); + cfg.port = line.length() > 0 ? line.toInt() : 8883; + + line = f.readStringUntil('\n'); line.trim(); + strncpy(cfg.username, line.c_str(), sizeof(cfg.username) - 1); + + line = f.readStringUntil('\n'); line.trim(); + strncpy(cfg.password, line.c_str(), sizeof(cfg.password) - 1); + + // Optional device ID + if (f.available()) { + line = f.readStringUntil('\n'); line.trim(); + if (line.length() > 0) { + strncpy(cfg.deviceId, line.c_str(), sizeof(cfg.deviceId) - 1); + } + } + + f.close(); + + // Auto-generate device ID from ESP32 MAC if not provided + if (cfg.deviceId[0] == '\0') { + uint8_t mac[6]; + esp_efuse_mac_get_default(mac); + snprintf(cfg.deviceId, sizeof(cfg.deviceId), "meck-%02x%02x%02x%02x", + mac[2], mac[3], mac[4], mac[5]); + } + + return cfg.broker[0] != '\0'; +} + +// --------------------------------------------------------------------------- +// Modem power-on (same sequence as ModemManager) +// --------------------------------------------------------------------------- + +bool CellularMQTT::modemPowerOn() { + Serial.println("[Cell] Powering on modem..."); + + pinMode(MODEM_POWER_EN, OUTPUT); + digitalWrite(MODEM_POWER_EN, HIGH); + vTaskDelay(pdMS_TO_TICKS(500)); + + pinMode(MODEM_RST, OUTPUT); + digitalWrite(MODEM_RST, LOW); + vTaskDelay(pdMS_TO_TICKS(200)); + digitalWrite(MODEM_RST, HIGH); + vTaskDelay(pdMS_TO_TICKS(500)); + + pinMode(MODEM_PWRKEY, OUTPUT); + digitalWrite(MODEM_PWRKEY, HIGH); + vTaskDelay(pdMS_TO_TICKS(100)); + digitalWrite(MODEM_PWRKEY, LOW); + vTaskDelay(pdMS_TO_TICKS(1500)); + digitalWrite(MODEM_PWRKEY, HIGH); + + vTaskDelay(pdMS_TO_TICKS(5000)); + + pinMode(MODEM_DTR, OUTPUT); + digitalWrite(MODEM_DTR, LOW); + + MODEM_SERIAL.begin(MODEM_BAUD, SERIAL_8N1, MODEM_TX, MODEM_RX); + vTaskDelay(pdMS_TO_TICKS(500)); + + while (MODEM_SERIAL.available()) MODEM_SERIAL.read(); + + for (int i = 0; i < 10; i++) { + if (sendAT("AT", "OK", 1500)) { + Serial.println("[Cell] AT responded OK"); + return true; + } + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + Serial.println("[Cell] No AT response after power-on"); + return false; +} + +// --------------------------------------------------------------------------- +// AT command helpers +// --------------------------------------------------------------------------- + +bool CellularMQTT::sendAT(const char* cmd, const char* expect, uint32_t timeout_ms) { + drainURCs(); + Serial.printf("[Cell] TX: %s\n", cmd); + MODEM_SERIAL.println(cmd); + bool ok = waitResponse(expect, timeout_ms, _atBuf, AT_BUF_SIZE); + int len = strlen(_atBuf); + while (len > 0 && (_atBuf[len-1] == '\r' || _atBuf[len-1] == '\n')) _atBuf[--len] = '\0'; + if (_atBuf[0]) { + Serial.printf("[Cell] RX: %.120s [%s]\n", _atBuf, ok ? "OK" : "FAIL"); + } + return ok; +} + +bool CellularMQTT::waitResponse(const char* expect, uint32_t timeout_ms, + char* buf, size_t bufLen) { + unsigned long start = millis(); + int pos = 0; + if (buf && bufLen > 0) buf[0] = '\0'; + + while (millis() - start < timeout_ms) { + while (MODEM_SERIAL.available()) { + char c = MODEM_SERIAL.read(); + if (buf && pos < (int)bufLen - 1) { + buf[pos++] = c; + buf[pos] = '\0'; + } + if (buf && expect && strstr(buf, expect)) return true; + if (buf && strstr(buf, "ERROR")) return false; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + if (buf && expect && strstr(buf, expect)) return true; + return false; +} + +bool CellularMQTT::waitPrompt(uint32_t timeout_ms) { + unsigned long start = millis(); + while (millis() - start < timeout_ms) { + while (MODEM_SERIAL.available()) { + char c = MODEM_SERIAL.read(); + if (c == '>') return true; + } + vTaskDelay(pdMS_TO_TICKS(10)); + } + return false; +} + +// --------------------------------------------------------------------------- +// URC handling +// --------------------------------------------------------------------------- + +void CellularMQTT::drainURCs() { + while (MODEM_SERIAL.available()) { + char c = MODEM_SERIAL.read(); + + if (_rxState == RX_WAIT_TOPIC) { + if (_urcPos < _rxTopicLen && _urcPos < MQTT_TOPIC_MAX - 1) { + _rxTopic[_urcPos] = c; + } + _urcPos++; + if (_urcPos >= _rxTopicLen) { + _rxTopic[min(_rxTopicLen, MQTT_TOPIC_MAX - 1)] = '\0'; + handleMqttRxTopic(_rxTopic, _rxTopicLen); + _urcPos = 0; + _rxState = RX_IDLE; + } + continue; + } + + if (_rxState == RX_WAIT_PAYLOAD) { + if (_urcPos < _rxPayloadLen && _urcPos < MQTT_PAYLOAD_MAX - 1) { + _rxPayload[_urcPos] = c; + } + _urcPos++; + if (_urcPos >= _rxPayloadLen) { + _rxPayload[min(_rxPayloadLen, MQTT_PAYLOAD_MAX - 1)] = '\0'; + handleMqttRxPayload(_rxPayload, _rxPayloadLen); + _urcPos = 0; + _rxState = RX_IDLE; + } + continue; + } + + if (c == '\n') { + if (_urcPos > 0) { + while (_urcPos > 0 && _urcBuf[_urcPos - 1] == '\r') _urcPos--; + _urcBuf[_urcPos] = '\0'; + if (_urcPos > 0) processURCLine(_urcBuf); + } + _urcPos = 0; + } else if (c != '\r' || _urcPos > 0) { + if (_urcPos < URC_BUF_SIZE - 1) _urcBuf[_urcPos++] = c; + } + } +} + +void CellularMQTT::processURCLine(const char* line) { + if (strncmp(line, "+CMQTTRXSTART:", 14) == 0) { + handleMqttRxStart(line); + return; + } + if (strncmp(line, "+CMQTTRXTOPIC:", 14) == 0) { + int client, tlen; + if (sscanf(line, "+CMQTTRXTOPIC: %d,%d", &client, &tlen) == 2) { + _rxTopicLen = tlen; + _urcPos = 0; + _rxState = RX_WAIT_TOPIC; + } + return; + } + if (strncmp(line, "+CMQTTRXPAYLOAD:", 16) == 0) { + int client, plen; + if (sscanf(line, "+CMQTTRXPAYLOAD: %d,%d", &client, &plen) == 2) { + _rxPayloadLen = plen; + _urcPos = 0; + _rxState = RX_WAIT_PAYLOAD; + } + return; + } + if (strncmp(line, "+CMQTTRXEND:", 12) == 0) { + handleMqttRxEnd(); + return; + } + if (strncmp(line, "+CMQTTCONNLOST:", 15) == 0) { + handleMqttConnLost(line); + return; + } +} + +// --------------------------------------------------------------------------- +// MQTT receive handlers +// --------------------------------------------------------------------------- + +void CellularMQTT::handleMqttRxStart(const char* line) { + int client, tlen, plen; + if (sscanf(line, "+CMQTTRXSTART: %d,%d,%d", &client, &tlen, &plen) == 3) { + _rxTopicLen = tlen; + _rxPayloadLen = plen; + _rxTopic[0] = '\0'; + _rxPayload[0] = '\0'; + Serial.printf("[Cell] MQTT RX start: topic_len=%d payload_len=%d\n", tlen, plen); + } +} + +void CellularMQTT::handleMqttRxTopic(const char* data, int len) { + Serial.printf("[Cell] MQTT RX topic: %s\n", data); +} + +void CellularMQTT::handleMqttRxPayload(const char* data, int len) { + Serial.printf("[Cell] MQTT RX payload: %.80s\n", data); + + if (strstr(_rxTopic, "/cmd")) { + MQTTCommand cmd; + memset(&cmd, 0, sizeof(cmd)); + strncpy(cmd.cmd, data, MQTT_PAYLOAD_MAX - 1); + if (xQueueSend(_cmdQueue, &cmd, 0) == pdTRUE) { + _lastCmdTime = millis(); + Serial.printf("[Cell] Queued CLI command: %s\n", cmd.cmd); + } else { + Serial.println("[Cell] Command queue full, dropping"); + } + } else if (strstr(_rxTopic, "/ota")) { + MQTTCommand cmd; + memset(&cmd, 0, sizeof(cmd)); + snprintf(cmd.cmd, sizeof(cmd.cmd), "ota:%s", data); + xQueueSend(_cmdQueue, &cmd, 0); + Serial.printf("[Cell] Queued OTA URL: %s\n", data); + } +} + +void CellularMQTT::handleMqttRxEnd() { + Serial.println("[Cell] MQTT RX end"); +} + +void CellularMQTT::handleMqttConnLost(const char* line) { + int client, cause; + sscanf(line, "+CMQTTCONNLOST: %d,%d", &client, &cause); + Serial.printf("[Cell] MQTT connection lost (cause=%d)\n", cause); + _state = CellState::RECONNECTING; +} + +// --------------------------------------------------------------------------- +// APN resolution (reuses Meck's ApnDatabase) +// --------------------------------------------------------------------------- + +void CellularMQTT::resolveAPN() { + // 1. Check SD config + File f = SD.open("/remote/apn.cfg", FILE_READ); + if (f) { + String line = f.readStringUntil('\n'); + f.close(); + line.trim(); + if (line.length() > 0) { + strncpy(_apn, line.c_str(), sizeof(_apn) - 1); + Serial.printf("[Cell] APN from config: %s\n", _apn); + char cmd[80]; + snprintf(cmd, sizeof(cmd), "AT+CGDCONT=1,\"IP\",\"%s\"", _apn); + sendAT(cmd, "OK", 3000); + return; + } + } + + // 2. Check modem's current APN + if (sendAT("AT+CGDCONT?", "OK", 3000)) { + char* p = strstr(_atBuf, "+CGDCONT:"); + if (p) { + char* q1 = strchr(p, '"'); + if (q1) q1 = strchr(q1 + 1, '"'); + if (q1) q1 = strchr(q1 + 1, '"'); + if (q1) { + q1++; + char* q2 = strchr(q1, '"'); + if (q2 && q2 > q1) { + int len = q2 - q1; + if (len > 0 && len < (int)sizeof(_apn)) { + memcpy(_apn, q1, len); + _apn[len] = '\0'; + Serial.printf("[Cell] APN from network: %s\n", _apn); + return; + } + } + } + } + } + + // 3. Auto-detect from IMSI + if (_imsi[0]) { + const ApnEntry* entry = apnLookupFromIMSI(_imsi); + if (entry) { + strncpy(_apn, entry->apn, sizeof(_apn) - 1); + Serial.printf("[Cell] APN auto-detected: %s (%s)\n", _apn, entry->carrier); + char cmd[80]; + snprintf(cmd, sizeof(cmd), "AT+CGDCONT=1,\"IP\",\"%s\"", _apn); + sendAT(cmd, "OK", 3000); + return; + } + } + + _apn[0] = '\0'; + Serial.println("[Cell] APN: none detected"); +} + +// --------------------------------------------------------------------------- +// Data connection — activate PDP context +// --------------------------------------------------------------------------- + +bool CellularMQTT::activateData() { + Serial.println("[Cell] Activating data connection..."); + + if (!sendAT("AT+CGACT=1,1", "OK", 15000)) { + Serial.println("[Cell] PDP activation failed, trying CGATT first..."); + sendAT("AT+CGATT=1", "OK", 30000); + if (!sendAT("AT+CGACT=1,1", "OK", 15000)) { + Serial.println("[Cell] PDP activation failed"); + return false; + } + } + + // Query IP address + if (sendAT("AT+CGPADDR=1", "OK", 5000)) { + char* p = strstr(_atBuf, "+CGPADDR:"); + if (p) { + char* q = strchr(p, '"'); + if (q) { + q++; + char* e = strchr(q, '"'); + if (e && e > q) { + int len = e - q; + if (len < (int)sizeof(_ipAddr)) { + memcpy(_ipAddr, q, len); + _ipAddr[len] = '\0'; + } + } + } + } + } + + Serial.printf("[Cell] Data active, IP: %s\n", _ipAddr[0] ? _ipAddr : "unknown"); + return true; +} + +// --------------------------------------------------------------------------- +// MQTT operations via AT commands +// --------------------------------------------------------------------------- + +bool CellularMQTT::mqttStart() { + if (!sendAT("AT+CMQTTSTART", "OK", 5000)) { + if (!strstr(_atBuf, "+CMQTTSTART: 0")) { + Serial.println("[Cell] MQTT start failed"); + return false; + } + } + + // Acquire client with SSL enabled (third param = 1 for SSL) + char cmd[120]; + snprintf(cmd, sizeof(cmd), "AT+CMQTTACCQ=0,\"%s\",1", _config.deviceId); + if (!sendAT(cmd, "OK", 5000)) { + Serial.println("[Cell] MQTT client acquire failed"); + return false; + } + + // Configure TLS 1.2 (sslversion 4 = TLS 1.2) + sendAT("AT+CSSLCFG=\"sslversion\",0,4", "OK", 3000); + + // Skip certificate verification (no CA cert loaded on device) + sendAT("AT+CSSLCFG=\"authmode\",0,0", "OK", 3000); + + // Enable SNI — required for HiveMQ Cloud (shared IP, multiple clusters) + sendAT("AT+CSSLCFG=\"enableSNI\",0,1", "OK", 3000); + + // Bind SSL config to MQTT session + sendAT("AT+CMQTTSSLCFG=0,0", "OK", 3000); + + return true; +} + +bool CellularMQTT::mqttConnect() { + char cmd[256]; + snprintf(cmd, sizeof(cmd), + "AT+CMQTTCONNECT=0,\"tcp://%s:%d\",120,1,\"%s\",\"%s\"", + _config.broker, _config.port, + _config.username, _config.password); + + Serial.printf("[Cell] TX: AT+CMQTTCONNECT=0,\"ssl://%s:%d\",...\n", + _config.broker, _config.port); + Serial.printf("[Cell] Full cmd (%d chars): %s\n", strlen(cmd), cmd); + MODEM_SERIAL.println(cmd); + + // Wait for +CMQTTCONNECT URC (any result code, not just success) + // Don't use waitResponse — it bails on "ERROR" before we see the code + unsigned long start = millis(); + int pos = 0; + _atBuf[0] = '\0'; + + while (millis() - start < 30000) { + while (MODEM_SERIAL.available()) { + char c = MODEM_SERIAL.read(); + if (pos < AT_BUF_SIZE - 1) { + _atBuf[pos++] = c; + _atBuf[pos] = '\0'; + } + // Check for the URC regardless of what else is in the buffer + char* p = strstr(_atBuf, "+CMQTTCONNECT:"); + if (p) { + // Give it a moment to complete the line + vTaskDelay(pdMS_TO_TICKS(100)); + while (MODEM_SERIAL.available() && pos < AT_BUF_SIZE - 1) { + _atBuf[pos++] = MODEM_SERIAL.read(); + _atBuf[pos] = '\0'; + } + + int client, result; + if (sscanf(p, "+CMQTTCONNECT: %d,%d", &client, &result) == 2) { + Serial.printf("[Cell] MQTT connect result: %d\n", result); + if (result == 0) { + Serial.println("[Cell] MQTT connected!"); + return true; + } + } + Serial.printf("[Cell] MQTT connect failed (code from URC): %.80s\n", p); + return false; + } + } + vTaskDelay(pdMS_TO_TICKS(50)); + } + + // Timeout — dump what we got + int len = strlen(_atBuf); + while (len > 0 && (_atBuf[len-1] == '\r' || _atBuf[len-1] == '\n')) _atBuf[--len] = '\0'; + Serial.printf("[Cell] MQTT connect timeout. Buffer: %.200s\n", _atBuf); + return false; +} + +bool CellularMQTT::mqttSubscribe(const char* topic) { + int tlen = strlen(topic); + char cmd[80]; + snprintf(cmd, sizeof(cmd), "AT+CMQTTSUB=0,%d,1", tlen); + MODEM_SERIAL.println(cmd); + + if (!waitPrompt(5000)) { + Serial.println("[Cell] No prompt for CMQTTSUB"); + return false; + } + + MODEM_SERIAL.write((const uint8_t*)topic, tlen); + return waitResponse("OK", 10000, _atBuf, AT_BUF_SIZE); +} + +bool CellularMQTT::mqttPublish(const char* topic, const char* payload) { + int tlen = strlen(topic); + int plen = strlen(payload); + + // Step 1: Set topic + char cmd[80]; + snprintf(cmd, sizeof(cmd), "AT+CMQTTTOPIC=0,%d", tlen); + MODEM_SERIAL.println(cmd); + if (!waitPrompt(5000)) { + _pubFailCount++; + Serial.println("[Cell] No prompt for CMQTTTOPIC"); + return false; + } + MODEM_SERIAL.write((const uint8_t*)topic, tlen); + if (!waitResponse("OK", 5000, _atBuf, AT_BUF_SIZE)) { + _pubFailCount++; + return false; + } + + // Step 2: Set payload + snprintf(cmd, sizeof(cmd), "AT+CMQTTPAYLOAD=0,%d", plen); + MODEM_SERIAL.println(cmd); + if (!waitPrompt(5000)) { + _pubFailCount++; + return false; + } + MODEM_SERIAL.write((const uint8_t*)payload, plen); + if (!waitResponse("OK", 5000, _atBuf, AT_BUF_SIZE)) { + _pubFailCount++; + return false; + } + + // Step 3: Publish QoS 1, 60s timeout + if (!sendAT("AT+CMQTTPUB=0,1,60", "OK", 15000)) { + _pubFailCount++; + Serial.printf("[Cell] Publish failed (%d consecutive)\n", _pubFailCount); + return false; + } + + // Success — reset failure counter + _pubFailCount = 0; + return true; +} + +void CellularMQTT::mqttDisconnect() { + sendAT("AT+CMQTTDISC=0,60", "OK", 5000); + sendAT("AT+CMQTTREL=0", "OK", 3000); + sendAT("AT+CMQTTSTOP", "OK", 5000); +} + +// --------------------------------------------------------------------------- +// FreeRTOS Task +// --------------------------------------------------------------------------- + +void CellularMQTT::taskEntry(void* param) { + static_cast(param)->taskLoop(); +} + +void CellularMQTT::taskLoop() { + Serial.printf("[Cell] Task started on core %d\n", xPortGetCoreID()); + +restart: + _pubFailCount = 0; + + // ---- Phase 1: Power on ---- + _state = CellState::POWERING_ON; + if (!modemPowerOn()) { + Serial.println("[Cell] Power-on failed, retry in 30s"); + _state = CellState::ERROR; + vTaskDelay(pdMS_TO_TICKS(30000)); + goto restart; + } + + // ---- Phase 2: Initialize ---- + _state = CellState::INITIALIZING; + sendAT("ATE0", "OK"); + + if (sendAT("AT+GSN", "OK", 3000)) { + char* p = _atBuf; + while (*p && !isdigit(*p)) p++; + int i = 0; + while (isdigit(p[i]) && i < 19) { _imei[i] = p[i]; i++; } + _imei[i] = '\0'; + Serial.printf("[Cell] IMEI: %s\n", _imei); + } + + if (sendAT("AT+CIMI", "OK", 3000)) { + char* p = _atBuf; + while (*p && !isdigit(*p)) p++; + int i = 0; + while (isdigit(p[i]) && i < 19) { _imsi[i] = p[i]; i++; } + _imsi[i] = '\0'; + Serial.printf("[Cell] IMSI: %s\n", _imsi); + } + + sendAT("AT+CTZU=1", "OK"); + + // ---- Phase 3: Network registration ---- + _state = CellState::REGISTERING; + Serial.println("[Cell] Waiting for network..."); + + { + bool registered = false; + for (int i = 0; i < 60; i++) { + if (sendAT("AT+CREG?", "OK", 2000)) { + char* p = strstr(_atBuf, "+CREG:"); + if (p) { + int n, stat; + if (sscanf(p, "+CREG: %d,%d", &n, &stat) == 2) { + if (stat == 1 || stat == 5) { registered = true; break; } + } + } + } + vTaskDelay(pdMS_TO_TICKS(1000)); + } + if (!registered) Serial.println("[Cell] Registration timeout — continuing"); + } + + // Operator name + sendAT("AT+COPS=3,0", "OK", 2000); + if (sendAT("AT+COPS?", "OK", 5000)) { + char* p = strchr(_atBuf, '"'); + if (p) { + p++; + char* e = strchr(p, '"'); + if (e) { + int len = e - p; + if (len >= (int)sizeof(_operator)) len = sizeof(_operator) - 1; + memcpy(_operator, p, len); + _operator[len] = '\0'; + } + } + } + + resolveAPN(); + + if (sendAT("AT+CSQ", "OK", 2000)) { + char* p = strstr(_atBuf, "+CSQ:"); + if (p) { int csq, ber; if (sscanf(p, "+CSQ: %d,%d", &csq, &ber) >= 1) _csq = csq; } + } + + Serial.printf("[Cell] Registered: oper=%s CSQ=%d APN=%s IMEI=%s\n", + _operator, _csq, _apn[0] ? _apn : "(none)", _imei); + + // ---- Phase 4: Activate data ---- + _state = CellState::DATA_ACTIVATING; + if (!activateData()) { + Serial.println("[Cell] Data activation failed, retry in 30s"); + _state = CellState::ERROR; + vTaskDelay(pdMS_TO_TICKS(30000)); + goto restart; + } + + // ---- Phase 5: MQTT connect ---- + _state = CellState::MQTT_STARTING; + if (!mqttStart()) { + _state = CellState::ERROR; + vTaskDelay(pdMS_TO_TICKS(30000)); + goto restart; + } + + _state = CellState::MQTT_CONNECTING; + if (!mqttConnect()) { + mqttDisconnect(); + _state = CellState::ERROR; + vTaskDelay(pdMS_TO_TICKS(30000)); + goto restart; + } + + mqttSubscribe(_topicCmd); + mqttSubscribe(_topicOta); + + _state = CellState::CONNECTED; + _reconnectDelay = MQTT_RECONNECT_MIN; + Serial.println("[Cell] MQTT connected and subscribed — ready"); + + mqttPublish(_topicTelem, "{\"event\":\"boot\",\"state\":\"connected\"}"); + + // ---- Phase 6: Main loop ---- + unsigned long lastCSQ = 0; + unsigned long lastTelem = 0; + + while (true) { + drainURCs(); + + // Health check: too many consecutive publish failures = silent disconnect + if (_pubFailCount >= MQTT_PUB_FAIL_MAX && _state == CellState::CONNECTED) { + Serial.printf("[Cell] %d consecutive publish failures — forcing reconnect\n", _pubFailCount); + _state = CellState::RECONNECTING; + } + + // Reconnect logic + if (_state == CellState::RECONNECTING) { + Serial.printf("[Cell] Reconnecting in %lums...\n", _reconnectDelay); + vTaskDelay(pdMS_TO_TICKS(_reconnectDelay)); + _reconnectDelay = min(_reconnectDelay * 2, (uint32_t)MQTT_RECONNECT_MAX); + + mqttDisconnect(); + vTaskDelay(pdMS_TO_TICKS(2000)); + + // Check data is still active + if (!sendAT("AT+CGACT?", "OK", 5000) || !strstr(_atBuf, ",1")) { + if (!activateData()) { + vTaskDelay(pdMS_TO_TICKS(10000)); + goto restart; + } + } + + if (!mqttStart() || !mqttConnect()) { + continue; // Retry with backoff + } + + mqttSubscribe(_topicCmd); + mqttSubscribe(_topicOta); + _state = CellState::CONNECTED; + _reconnectDelay = MQTT_RECONNECT_MIN; + _pubFailCount = 0; + Serial.println("[Cell] Reconnected"); + } + + // Publish queued responses + if (_state == CellState::CONNECTED) { + MQTTResponse rsp; + while (xQueueReceive(_rspQueue, &rsp, 0) == pdTRUE) { + mqttPublish(rsp.topic, rsp.payload); + } + } + + // Periodic CSQ poll + if (millis() - lastCSQ > 60000) { + if (sendAT("AT+CSQ", "OK", 2000)) { + char* p = strstr(_atBuf, "+CSQ:"); + if (p) { int csq, ber; if (sscanf(p, "+CSQ: %d,%d", &csq, &ber) >= 1) _csq = csq; } + } + lastCSQ = millis(); + } + + // Periodic telemetry publish + if (_state == CellState::CONNECTED && millis() - lastTelem > TELEMETRY_INTERVAL) { + TelemetryData td; + if (xSemaphoreTake(_telemetryMutex, pdMS_TO_TICKS(50))) { + memcpy(&td, &_telemetry, sizeof(td)); + xSemaphoreGive(_telemetryMutex); + } + + char json[400]; + snprintf(json, sizeof(json), + "{\"uptime\":%lu,\"batt_mv\":%d,\"batt_pct\":%d,\"temp\":%.1f," + "\"csq\":%d,\"bars\":%d,\"neighbors\":%d," + "\"freq\":%.3f,\"bw\":%.1f,\"sf\":%d,\"cr\":%d,\"tx\":%d," + "\"name\":\"%s\",\"ip\":\"%s\",\"oper\":\"%s\",\"apn\":\"%s\"," + "\"heap\":%d}", + td.uptime_secs, td.battery_mv, td.battery_pct, + td.temperature / 10.0f, + _csq, getSignalBars(), td.neighbor_count, + td.freq, td.bw, td.sf, td.cr, td.tx_power, + td.node_name, _ipAddr, _operator, _apn, + ESP.getFreeHeap()); + + mqttPublish(_topicTelem, json); + lastTelem = millis(); + } + + vTaskDelay(pdMS_TO_TICKS(200)); + } +} + +#endif // HAS_4G_MODEM \ No newline at end of file diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 8c745613..1845140f 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -3,6 +3,11 @@ #include "MyMesh.h" +#ifdef HAS_4G_MODEM +#include +#include "CellularMQTT.h" +#endif + #ifdef DISPLAY_CLASS #include "UITask.h" static UITask ui_task(display); @@ -23,6 +28,10 @@ static char command[160]; unsigned long lastActive = 0; // mark last active time unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot +#ifdef HAS_4G_MODEM +static bool sdCardReady = false; +#endif + void setup() { Serial.begin(115200); delay(1000); @@ -34,10 +43,12 @@ void setup() { #ifdef DISPLAY_CLASS if (display.begin()) { +#ifndef HAS_4G_MODEM display.startFrame(); display.setCursor(0, 0); display.print("Please wait..."); display.endFrame(); +#endif } #endif @@ -83,6 +94,52 @@ void setup() { the_mesh.begin(fs); + // --------------------------------------------------------------------------- + // SD card init — needed for CellularMQTT config (/remote/mqtt.cfg) + // SD, LoRa, and e-ink share the same SPI bus on T-Deck Pro. + // --------------------------------------------------------------------------- +#ifdef HAS_4G_MODEM + { + // Deselect all SPI devices before SD init to prevent bus contention + #ifdef SDCARD_CS + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + #endif + #ifdef PIN_DISPLAY_CS + pinMode(PIN_DISPLAY_CS, OUTPUT); + digitalWrite(PIN_DISPLAY_CS, HIGH); + #endif + #ifdef P_LORA_NSS + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + #endif + delay(100); + + for (int i = 0; i < 3; i++) { + #ifdef SDCARD_CS + if (SD.begin(SDCARD_CS)) { sdCardReady = true; break; } + #else + if (SD.begin(SPI_CS)) { sdCardReady = true; break; } + #endif + delay(200); + } +Serial.printf("SD card: %s\n", sdCardReady ? "ready" : "FAILED"); + + // Re-claim SPI bus for display — SD.begin() steals the shared + // GPIO pins (36/47/33) from the display's HSPI peripheral + extern SPIClass displaySpi; + displaySpi.begin(PIN_DISPLAY_SCLK, 47, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS); + } + + // Start cellular MQTT + if (sdCardReady) { + cellularMQTT.begin(); + Serial.println("Cellular MQTT starting..."); + } else { + Serial.println("Cellular MQTT skipped — no SD card for config"); + } +#endif + #ifdef DISPLAY_CLASS ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION); #endif @@ -118,6 +175,64 @@ void loop() { command[0] = 0; // reset command buffer } + // --------------------------------------------------------------------------- + // MQTT → CLI bridge: process incoming commands from MQTT + // --------------------------------------------------------------------------- +#ifdef HAS_4G_MODEM + { + MQTTCommand mqttCmd; + while (cellularMQTT.recvCommand(mqttCmd)) { + // Check for OTA command + if (strncmp(mqttCmd.cmd, "ota:", 4) == 0) { + const char* url = &mqttCmd.cmd[4]; + Serial.printf("[MQTT] OTA request: %s\n", url); + // TODO: RemoteOTA — download firmware from URL and flash + cellularMQTT.sendResponse(cellularMQTT.getRspTopic(), "{\"ota\":\"not yet implemented\"}"); + continue; + } + + // CLI command — process through the same handler as serial/LoRa admin + Serial.printf("[MQTT] CLI: %s\n", mqttCmd.cmd); + char reply[512]; + reply[0] = '\0'; + the_mesh.handleCommand(0, mqttCmd.cmd, reply); + + if (reply[0] == '\0') strcpy(reply, "OK"); + + cellularMQTT.sendResponse(cellularMQTT.getRspTopic(), reply); + Serial.printf("[MQTT] Reply: %.80s\n", reply); + } + } + + // Periodic telemetry snapshot for MQTT publishing + { + static unsigned long lastTelemUpdate = 0; + if (millis() - lastTelemUpdate > 10000) { + NodePrefs* p = the_mesh.getNodePrefs(); + TelemetryData td; + memset(&td, 0, sizeof(td)); + td.uptime_secs = millis() / 1000; + td.battery_mv = board.getBattMilliVolts(); + td.battery_pct = board.getBatteryPercent(); + td.temperature = board.getBattTemperature(); + td.csq = cellularMQTT.getCSQ(); + td.freq = p->freq; + td.bw = p->bw; + td.sf = p->sf; + td.cr = p->cr; + td.tx_power = p->tx_power_dbm; + strncpy(td.node_name, p->node_name, sizeof(td.node_name) - 1); + strncpy(td.apn, cellularMQTT.getAPN(), sizeof(td.apn) - 1); + strncpy(td.oper, cellularMQTT.getOperator(), sizeof(td.oper) - 1); + td.mqtt_connected = cellularMQTT.isConnected(); + td.neighbor_count = 0; // TODO: expose from MyMesh + + cellularMQTT.updateTelemetry(td); + lastTelemUpdate = millis(); + } + } +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS @@ -135,4 +250,4 @@ void loop() { nextSleepinSecs += 5; // When there is pending work, to work another 5s } } -} +} \ No newline at end of file diff --git a/variants/lilygo_tdeck_pro/TDeckBoard.h b/variants/lilygo_tdeck_pro/TDeckBoard.h index c9a09b29..f7bee9f7 100644 --- a/variants/lilygo_tdeck_pro/TDeckBoard.h +++ b/variants/lilygo_tdeck_pro/TDeckBoard.h @@ -22,7 +22,7 @@ // T-Deck Pro battery capacity (all variants use 1400 mAh cell) #ifndef BQ27220_DESIGN_CAPACITY_MAH -#define BQ27220_DESIGN_CAPACITY_MAH 1400 +#define BQ27220_DESIGN_CAPACITY_MAH 2000 #endif class TDeckBoard : public ESP32Board { diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index 5fdff6ee..a805afd9 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -274,4 +274,37 @@ build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + lib_deps = ${LilyGo_TDeck_Pro.lib_deps} - densaugeo/base64 @ ~1.4.0 \ No newline at end of file + densaugeo/base64 @ ~1.4.0 + + ; --------------------------------------------------------------------------- +; Remote Repeater (T-Deck Pro 4G, cellular MQTT remote management) +; +; MeshCore repeater firmware + A7682E cellular MQTT for remote admin. +; No BLE, no SMS/calls, no companion protocol. All management via MQTT +; or USB serial CLI. +; +; SD card config required: /remote/mqtt.cfg (broker, port, user, pass) +; Optional: /remote/apn.cfg (APN override) +; +; Add this block to the bottom of platformio.ini +; Flash with: pio run -e meck_remote_repeater +; --------------------------------------------------------------------------- +[env:meck_remote_repeater] +extends = LilyGo_TDeck_Pro +build_flags = + ${LilyGo_TDeck_Pro.build_flags} + -I examples/simple_repeater + -D ADMIN_PASSWORD='"admin"' + -D HAS_4G_MODEM=1 + -D DISABLE_WIFI_OTA=1 + -D MECK_REMOTE_REPEATER=1 + -D MAX_NEIGHBOURS=16 + -D FIRMWARE_VERSION='"Meck RemRptr v0.1"' +build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter} + + + + + + + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${LilyGo_TDeck_Pro.lib_deps} + \ No newline at end of file