Files
MeshCore/examples/companion_radio/MyMesh.cpp
T
mikecarper b699e552ec Add secure host CLI bridge and harden OTA workflows
Expose the Full Companion terminal over TCP and add an authenticated, replay-resistant host command service with documented CPU temperature, reboot, and allowlisted program examples.\n\nImprove LoRa OTA diagnostics and persistent CLI handling, reduce clock correction drift to ten minutes, and make mOTA stream flush behavior an explicit transport policy with regression coverage.
2026-08-23 15:29:30 -07:00

5448 lines
200 KiB
C++

#include "MyMesh.h"
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityGeneration.h>
#include "helpers/radiolib/RXPowerSaving.h"
#include "helpers/radiolib/RxBoostedGainDefaults.h"
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
#include <esp_heap_caps.h>
#endif
#if defined(ESP32) && defined(WIFI_SSID)
#include "CompanionWiFi.h"
#include <helpers/WiFiPowerSave.h>
#endif
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
#endif
#ifdef ENABLE_USB_INTERFACE
#include <helpers/CLICommandUtils.h>
#include <helpers/TracePathHelpers.h>
#endif
#if defined(COMPANION_RADIO_FULL)
#include <helpers/ota/OtaCli.h>
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
#include <helpers/MQTTDefaults.h>
#endif
#ifdef WITH_WEBCONFIG
#include <helpers/WiFiSetupPortal.h>
#include <WiFi.h>
#include <esp_wifi.h>
#endif
static uint32_t nextRadioApplyRetryDelay(uint8_t& failure_count) {
uint8_t shift = failure_count < 5 ? failure_count : 5;
if (failure_count < 6) failure_count++;
uint32_t delay_ms = 1000UL << shift;
return delay_ms > 30000UL ? 30000UL : delay_ms;
}
static const uint32_t COMMAND_RADIO_APPLY_TIMEOUT_MS = 5000UL;
#if RXPS_FIXED_ENABLED
#if RXPS_FIXED_LEVEL < 1 || RXPS_FIXED_LEVEL > 10
#error "RXPS_FIXED_LEVEL must be between 1 and 10"
#endif
#if RXPS_FIXED_PREAMBLE != 16 && RXPS_FIXED_PREAMBLE != 32
#error "RXPS_FIXED_PREAMBLE must be 16 or 32"
#endif
#endif
#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_QUERY 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: decimal command IDs 44..49 (0x2C..0x31) remain parked
#define CMD_SEND_BINARY_REQ 50
#define CMD_FACTORY_RESET 51
#define CMD_SEND_PATH_DISCOVERY_REQ 52
#define CMD_SET_FLOOD_SCOPE_KEY 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
#define CMD_GET_ALLOWED_REPEAT_FREQ 60
#define CMD_SET_PATH_HASH_MODE 61
#define CMD_SEND_CHANNEL_DATA 62
#define CMD_SET_DEFAULT_FLOOD_SCOPE 63
#define CMD_GET_DEFAULT_FLOOD_SCOPE 64
#define CMD_SEND_RAW_PACKET 65
#define CMD_GET_RADIO_FEM_RXGAIN 66
#define CMD_SET_RADIO_FEM_RXGAIN 67
#define CMD_GET_RADIO_RXGAIN 68
#define CMD_SET_RADIO_RXGAIN 69
#define CMD_GET_WIFI_POWER_SAVE 70
#define CMD_SET_WIFI_POWER_SAVE 71
#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0)
static constexpr uint8_t DEFAULT_FEM_RX_GAIN = 0;
#else
static constexpr uint8_t DEFAULT_FEM_RX_GAIN = 1;
#endif
// Stats sub-types for CMD_GET_STATS
#define STATS_TYPE_CORE 0
#define STATS_TYPE_RADIO 1
#define STATS_TYPE_PACKETS 2
#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_QUERY
#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
#define RESP_ALLOWED_REPEAT_FREQ 26
#define RESP_CODE_CHANNEL_DATA_RECV 27
#define RESP_CODE_DEFAULT_FLOOD_SCOPE 28
#define MAX_CHANNEL_DATA_LENGTH (MAX_FRAME_SIZE - 9)
#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
#define CONTACT_PAGE_WRITE_GAP 100
#define EXPECTED_ACK_RETRY_RECHECK_MILLIS 1000
static bool save_filter(const ContactInfo& c);
#ifndef DEFAULT_MULTI_ACKS
#define DEFAULT_MULTI_ACKS 0
#endif
#ifndef DEFAULT_PATH_HASH_MODE
#define DEFAULT_PATH_HASH_MODE 0
#endif
#ifndef DEFAULT_MANUAL_ADD_CONTACTS
#define DEFAULT_MANUAL_ADD_CONTACTS 0
#endif
#ifndef DEFAULT_AUTOADD_CONFIG
#define DEFAULT_AUTOADD_CONFIG 0
#endif
#ifndef DEFAULT_BUZZER_QUIET
#define DEFAULT_BUZZER_QUIET 0
#endif
#ifndef EMERGENCY_CLIENT_REPEAT_HOLD_MS
#define EMERGENCY_CLIENT_REPEAT_HOLD_MS 120000UL
#endif
#ifndef EMERGENCY_CLIENT_REPEAT_JITTER_MS
#define EMERGENCY_CLIENT_REPEAT_JITTER_MS 15000UL
#endif
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
// First 16 bytes of SHA-256("#testing"), encoded as base64.
#define TESTING_GROUP_PSK "zeXoLPUVZH3LVHp5pPBl0Q=="
#ifdef ENABLE_USB_INTERFACE
static const char* terminalContactTypeName(uint8_t type) {
if (type == ADV_TYPE_CHAT) return "Chat";
if (type == ADV_TYPE_REPEATER) return "Repeater";
if (type == ADV_TYPE_ROOM) return "Room";
if (type == ADV_TYPE_SENSOR) return "Sensor";
return "Unknown";
}
#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
static const uint8_t EMERGENCY_CHANNEL_SECRET[CIPHER_KEY_SIZE] = {
0xe1, 0xad, 0x57, 0x8d, 0x25, 0x10, 0x8e, 0x34,
0x48, 0x08, 0xf3, 0x0d, 0xfd, 0xaa, 0xf9, 0x26
};
#define EMERGENCY_CLIENT_REPEAT_TABLE_SIZE 63
static uint16_t emergency_client_repeats[EMERGENCY_CLIENT_REPEAT_TABLE_SIZE];
static uint8_t emergency_client_repeat_next;
static unsigned long emergency_client_repeat_send_at;
static uint16_t emergency_client_repeat_key;
static mesh::Packet* emergency_client_repeat_packet;
#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 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 MAX_SIGN_DATA_LEN (8 * 1024) // 8K
// 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)
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);
}
void MyMesh::writeDisabledFrame() {
uint8_t buf[1];
buf[0] = RESP_CODE_DISABLED;
_serial->writeFrame(buf, 1);
}
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::stopContactsIterator() {
if (!_iter_started) return;
_iter_started = false;
if (_serial != NULL) _serial->unlockReplyRoute();
}
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);
}
}
}
bool MyMesh::Frame::isChannelMsg() const {
return len > 0 && (buf[0] == RESP_CODE_CHANNEL_MSG_RECV || buf[0] == RESP_CODE_CHANNEL_MSG_RECV_V3 ||
buf[0] == RESP_CODE_CHANNEL_DATA_RECV);
}
int MyMesh::getOfflineQueueCapacity() const {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
return offline_queue_capacity;
#else
return OFFLINE_QUEUE_SIZE;
#endif
}
MyMesh::Frame& MyMesh::offlineQueueFrameAt(int logical_index) {
return offline_queue[(offline_queue_head + logical_index) % getOfflineQueueCapacity()];
}
void MyMesh::initializeOfflineQueue() {
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
if (offline_queue != offline_queue_fallback || OFFLINE_QUEUE_SIZE <= offline_queue_capacity) return;
int requested_capacity = OFFLINE_QUEUE_SIZE;
while (requested_capacity > offline_queue_capacity) {
void* storage = heap_caps_malloc(sizeof(Frame) * requested_capacity,
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (storage) {
offline_queue = static_cast<Frame*>(storage);
offline_queue_capacity = requested_capacity;
return;
}
if (requested_capacity > 256) {
requested_capacity = 256;
} else if (requested_capacity > 128) {
requested_capacity = 128;
} else {
requested_capacity = offline_queue_capacity;
}
}
#endif
}
void MyMesh::addToOfflineQueue(const uint8_t frame[], int len) {
const int capacity = getOfflineQueueCapacity();
if (!frame || len <= 0 || len > MAX_FRAME_SIZE || capacity <= 0) {
MESH_DEBUG_PRINTLN("WARN: invalid offline queue frame length: %d", len);
return;
}
if (offline_queue_len >= capacity) {
MESH_DEBUG_PRINTLN("WARN: offline_queue is full!");
int pos = 0;
while (pos < offline_queue_len) {
if (offlineQueueFrameAt(pos).isChannelMsg()) {
for (int i = pos; i < offline_queue_len - 1; i++) { // delete oldest channel msg from queue
offlineQueueFrameAt(i) = offlineQueueFrameAt(i + 1);
}
MESH_DEBUG_PRINTLN("INFO: removed oldest channel message from queue.");
Frame& tail = offlineQueueFrameAt(offline_queue_len - 1);
tail.len = len;
memcpy(tail.buf, frame, len);
return;
}
pos++;
}
MESH_DEBUG_PRINTLN("INFO: no channel messages to remove from queue.");
} else {
Frame& tail = offlineQueueFrameAt(offline_queue_len);
tail.len = len;
memcpy(tail.buf, frame, len);
offline_queue_len++;
}
}
int MyMesh::getFromOfflineQueue(uint8_t frame[]) {
if (offline_queue_len > 0) { // check offline queue
Frame& head = offlineQueueFrameAt(0);
size_t len = head.len; // take from top of queue
memcpy(frame, head.buf, len);
offline_queue_len--;
if (offline_queue_len == 0) {
offline_queue_head = 0;
} else {
offline_queue_head = (offline_queue_head + 1) % getOfflineQueueCapacity();
}
return len;
}
return 0; // queue is empty
}
float MyMesh::getAirtimeBudgetFactor() const {
// TempRadio is a short-lived, explicitly coordinated OTA channel. Let its
// primary transfer use the full dispatcher budget; the persisted public
// channel duty factor becomes authoritative again as soon as TempRadio ends.
#if defined(COMPANION_RADIO_FULL)
if (isTempRadioActive()) return 0.0f;
#endif
return _prefs.airtime_factor;
}
bool MyMesh::getCADEnabled() const {
return true; // tuned branch behavior: hardware CAD before every companion TX
}
int MyMesh::getInterferenceThreshold() const {
return 0; // disabled for now, until currentRSSI() problem is resolved
}
int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
if (_prefs.rx_delay_base <= 0.0f) return 0;
return (int)((powf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time);
}
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
return getRNG()->nextInt(0, 5*t + 1);
}
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
return getRNG()->nextInt(0, 5*t + 1);
}
uint8_t MyMesh::getExtraAckTransmitCount() const {
return _prefs.multi_acks;
}
bool MyMesh::hasLocationTelemetryRecipient() {
if (_prefs.telemetry_mode_loc == TELEM_MODE_DENY) return false;
ContactsIterator iter = startContactsIterator();
ContactInfo contact;
while (iter.hasNext(this, contact)) {
if (contact.type == ADV_TYPE_NONE) continue;
if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_ALL) return true;
if (_prefs.telemetry_mode_loc == TELEM_MODE_ALLOW_FLAGS &&
((contact.flags >> 1) & TELEM_PERM_LOCATION)) {
return true;
}
}
return false;
}
void MyMesh::updateGpsTelemetryPolicy() {
sensors.setTelemetryLocationAccessAvailable(hasLocationTelemetryRecipient());
}
void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) {
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
if (_mqtt_bridge && _mqtt_bridge->isRunning()) {
_mqtt_bridge->storeRawRadioData(raw, len, snr, rssi);
}
#endif
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);
}
}
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
void MyMesh::logRx(mesh::Packet* packet, int, float) {
if (_mqtt_bridge && _mqtt_bridge->isRunning()) {
_mqtt_bridge->onPacketReceived(packet);
}
}
void MyMesh::logTx(mesh::Packet* packet, int) {
if (_mqtt_bridge && _mqtt_bridge->isRunning()) {
_mqtt_bridge->sendPacket(packet);
}
}
#endif
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;
}
uint8_t MyMesh::getAutoAddMaxHops() const {
return _prefs.autoadd_max_hops;
}
void MyMesh::onContactOverwrite(const uint8_t* pub_key) {
ContactInfo* contact = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
if (contact) scheduleContactWriteAfterRelease(*contact);
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage
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);
} 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);
}
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::newContactMessage);
#endif
}
#ifdef ENABLE_USB_INTERFACE
if (hasTerminalOutput() && _terminal_display.shouldShowAdvert()) {
Stream& output = terminalOutput();
output.printf("\r\nADVERT from -> %s\r\n", contact.name);
output.printf(" type: %s\r\n", terminalContactTypeName(contact.type));
output.print(" public key: ");
mesh::Utils::printHex(output, contact.id.pub_key, PUB_KEY_SIZE);
output.print("\r\n> ");
}
#endif
// add inbound-path to mem cache
if (path && mesh::Packet::isValidPathLen(path_len)) { // 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 = mesh::Packet::copyPath(p->path, path, path_len);
}
ContactInfo* stored = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
if (stored == &contact) scheduleContactWrite(contact);
updateGpsTelemetryPolicy();
}
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;
}
#ifdef ENABLE_USB_INTERFACE
void MyMesh::onContactVisit(const ContactInfo& contact) {
if (contact.type == ADV_TYPE_NONE) return;
Stream& output = terminalOutput();
output.printf(" %s (%s) - ", contact.name, terminalContactTypeName(contact.type));
char relative_time[40];
int32_t seconds_from_now = contact.last_advert_timestamp - getRTCClock()->getCurrentTime();
AdvertTimeHelper::formatRelativeTimeDiff(relative_time, seconds_from_now, false);
output.println(relative_time);
}
#endif
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
scheduleContactWrite(contact);
#ifdef ENABLE_USB_INTERFACE
if (hasTerminalOutput()) {
terminalOutput().printf("\r\nPATH updated -> %s\r\n> ", contact.name);
}
#endif
}
void MyMesh::clearExpectedAck(AckTableEntry& entry, bool cancel_retries) {
if (cancel_retries && entry.ack != 0) {
cancelActiveRetries(entry.retry_key);
}
memset(&entry, 0, sizeof(entry));
}
void MyMesh::expireExpectedAcks() {
unsigned long now = _ms->getMillis();
unsigned long nearest_delay = 0;
has_next_ack_expiry = false;
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
AckTableEntry& entry = expected_ack_table[i];
if (entry.ack == 0) {
continue;
}
if (entry.expires_at == now || millisHasNowPassed(entry.expires_at)) {
if (!hasActiveRetries(entry.retry_key)) {
#ifdef ENABLE_USB_INTERFACE
if (entry.terminal_origin && hasTerminalOutput()) {
terminalOutput().print("\r\n ERROR: timed out, no ACK.\r\n> ");
}
#endif
clearExpectedAck(entry, false);
continue;
}
// Keep the semantic match alive while its lower-level retry sequence is
// active, so a newer app submission can replace that sequence cleanly.
entry.expires_at = futureMillis(EXPECTED_ACK_RETRY_RECHECK_MILLIS);
}
unsigned long delay = entry.expires_at - now;
if (!has_next_ack_expiry || delay < nearest_delay) {
nearest_delay = delay;
next_ack_expiry = entry.expires_at;
has_next_ack_expiry = true;
}
}
if (!has_next_ack_expiry) {
next_ack_expiry = 0;
}
}
MyMesh::AckTableEntry* MyMesh::findPendingTextMessage(
const uint8_t text_fingerprint[MAX_HASH_SIZE], uint32_t message_timestamp) {
expireExpectedAcks();
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
AckTableEntry& entry = expected_ack_table[i];
if (entry.ack != 0
&& entry.message_timestamp != message_timestamp
&& memcmp(entry.text_fingerprint, text_fingerprint, MAX_HASH_SIZE) == 0
&& hasActiveRetries(entry.retry_key)) {
return &entry;
}
}
return NULL;
}
ContactInfo* MyMesh::processAck(const uint8_t *data) {
expireExpectedAcks();
// see if matches any in a table
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
if (expected_ack_table[i].ack != 0
&& 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);
#ifdef ENABLE_USB_INTERFACE
if (expected_ack_table[i].terminal_origin && hasTerminalOutput()) {
terminalOutput().printf("\r\n Got ACK! (round trip: %lu ms)\r\n> ",
(unsigned long)trip_time);
}
#endif
// NOTE: the same ACK can be received multiple times!
ContactInfo* contact = expected_ack_table[i].contact;
clearExpectedAck(expected_ack_table[i]);
expireExpectedAcks();
return 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,
bool terminal_command_reply,
uint32_t terminal_command_elapsed_millis) {
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;
}
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 ENABLE_USB_INTERFACE
if (hasTerminalOutput()) {
Stream& output = terminalOutput();
const char* kind = txt_type == TXT_TYPE_CLI_DATA ? "CLI" : "MSG";
output.printf("\r\n(%s) %s -> from %s",
mesh::cli::terminalInboundRouteLabel(pkt->isRouteDirect()),
kind, from.name);
if (terminal_command_reply) {
output.printf(" (round trip %lu ms)",
(unsigned long)terminal_command_elapsed_millis);
}
output.printf("\r\n %s\r\n> ", text);
}
#endif
#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 (!_serial->isConnected()) {
_ui->notify(UIEventType::contactMessage);
}
}
#endif
}
static uint16_t emergencyClientRepeatKey(const mesh::Packet* packet) {
const uint8_t* p = packet->payload;
return ((uint16_t)p[1]) | ((uint16_t)p[2] << 8);
}
static bool __attribute__((noinline)) hasEmergencyClientRepeat(uint16_t key) {
for (uint8_t i = 0; i < EMERGENCY_CLIENT_REPEAT_TABLE_SIZE; i++) {
if (emergency_client_repeats[i] == key) {
return true;
}
}
return false;
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
if (emergency_client_repeat_packet != NULL && packet->getPathHashCount() > 0) {
if (emergencyClientRepeatKey(packet) == emergency_client_repeat_key) {
releasePacket(emergency_client_repeat_packet);
emergency_client_repeat_packet = NULL;
}
}
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
return false;
}
bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
return _prefs.isRepeatEn();
}
bool MyMesh::allowFloodRetry(const mesh::Packet* packet) const {
if (packet == NULL) return false;
// A companion may retry its own advert once, using the core's deliberately
// slow origin-advert delay. Do not add retries while relaying a neighbour's
// advert; the ordinary forwarding and recent-echo guard still apply.
return packet->getPayloadType() != PAYLOAD_TYPE_ADVERT
|| isSelfOriginAdvert(packet);
}
bool MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) {
if (scope.isNull()) {
return sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
} else {
uint16_t codes[2];
codes[0] = scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
return sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
}
}
bool 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_unscoped) {
return sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app explicitly requested un-scoped
} else {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
return sendFloodScoped(*scope, pkt, delay_millis);
}
}
bool MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// TODO: have per-channel send_scope
if (send_unscoped) {
return sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app explicitly requested un-scoped
} else {
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
auto scope = send_scope.isNull() ? &default_scope : &send_scope;
return sendFloodScoped(*scope, pkt, 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
// BaseChatMesh updates lastmod immediately before this callback.
scheduleContactWrite(from);
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
bool terminal_command_reply = false;
uint32_t terminal_command_elapsed_millis = 0;
#ifdef ENABLE_USB_INTERFACE
terminal_command_reply = _terminal_command.takeReply(
from.id.pub_key, _ms->getMillis(), terminal_command_elapsed_millis);
if (terminal_command_reply) _terminal_command_target[0] = 0;
#endif
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text,
terminal_command_reply, terminal_command_elapsed_millis);
}
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
scheduleContactWrite(from);
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) {
const bool is_emergency_channel =
memcmp(channel.secret, EMERGENCY_CHANNEL_SECRET,
sizeof(EMERGENCY_CHANNEL_SECRET)) == 0;
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], &timestamp, 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);
} else {
#ifdef DISPLAY_CLASS
if (_ui) _ui->notify(UIEventType::channelMessage);
#endif
}
#ifdef ENABLE_USB_INTERFACE
if (hasTerminalOutput()
&& _terminal_display.shouldShowChannel(is_emergency_channel)) {
ChannelDetails details;
const char* channel_name = getChannel(channel_idx, details) ? details.name : "Unknown";
terminalOutput().printf(
"\r\nCHANNEL MSG -> %s (%s)\r\n %s\r\n> ", channel_name,
pkt->isRouteDirect() ? "DIRECT" : "FLOOD", text);
}
#endif
#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);
#endif
if (pkt->isRouteFlood() && is_emergency_channel) {
bool zero_path = pkt->getPathHashCount() == 0;
uint16_t key = emergencyClientRepeatKey(pkt);
if (hasEmergencyClientRepeat(key)) {
pkt->markDoNotRetransmit();
} else {
emergency_client_repeats[emergency_client_repeat_next] = key;
emergency_client_repeat_next++;
if (emergency_client_repeat_next >= EMERGENCY_CLIENT_REPEAT_TABLE_SIZE) emergency_client_repeat_next = 0;
if (zero_path && emergency_client_repeat_packet == NULL) {
emergency_client_repeat_packet = obtainNewPacket();
if (emergency_client_repeat_packet != NULL) {
*emergency_client_repeat_packet = *pkt;
emergency_client_repeat_key = key;
emergency_client_repeat_send_at = futureMillis(
(int)(EMERGENCY_CLIENT_REPEAT_HOLD_MS + getRNG()->nextInt(0, EMERGENCY_CLIENT_REPEAT_JITTER_MS + 1)));
pkt->markDoNotRetransmit();
}
}
}
}
}
void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet *pkt, uint16_t data_type,
const uint8_t *data, size_t data_len) {
if (data_len > MAX_CHANNEL_DATA_LENGTH) {
MESH_DEBUG_PRINTLN("onChannelDataRecv: dropping payload_len=%d exceeds frame limit=%d",
(uint32_t)data_len, (uint32_t)MAX_CHANNEL_DATA_LENGTH);
return;
}
int i = 0;
out_frame[i++] = RESP_CODE_CHANNEL_DATA_RECV;
out_frame[i++] = (int8_t)(pkt->getSNR() * 4);
out_frame[i++] = 0; // reserved1
out_frame[i++] = 0; // reserved2
uint8_t channel_idx = findChannelIdx(channel);
out_frame[i++] = channel_idx;
out_frame[i++] = pkt->isRouteFlood() ? pkt->path_len : 0xFF;
out_frame[i++] = (uint8_t)(data_type & 0xFF);
out_frame[i++] = (uint8_t)(data_type >> 8);
out_frame[i++] = (uint8_t)data_len;
int copy_len = (int)data_len;
if (copy_len > 0) {
memcpy(&out_frame[i], data, copy_len);
i += copy_len;
}
addToOfflineQueue(out_frame, i);
if (_serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle'
_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);
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
}
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) {
if (data == NULL || len < 4) return;
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()
#ifdef ENABLE_USB_INTERFACE
const bool terminal_login_response = _terminal_login_pending
&& memcmp(_terminal_login_key, contact.id.pub_key,
sizeof(_terminal_login_key)) == 0;
#endif
pending_login = 0;
int i = 0;
#ifdef ENABLE_USB_INTERFACE
bool login_success = false;
bool modern_login = false;
#endif
if (len >= 6 && memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
#ifdef ENABLE_USB_INTERFACE
login_success = true;
#endif
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 (len >= 13 && data[4] == RESP_SERVER_LOGIN_OK) { // new login response
#ifdef ENABLE_USB_INTERFACE
login_success = true;
modern_login = true;
#endif
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
} 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
}
_serial->writeFrame(out_frame, i);
#ifdef ENABLE_USB_INTERFACE
if (terminal_login_response) {
if (hasTerminalOutput()) {
Stream& output = terminalOutput();
if (login_success && modern_login) {
output.printf(
"\r\nLOGIN -> %s accepted (ACL permissions 0x%02X, server v%u)\r\n> ",
_terminal_login_target, (unsigned)data[7], (unsigned)data[12]);
} else if (login_success) {
output.printf("\r\nLOGIN -> %s accepted (legacy server)\r\n> ",
_terminal_login_target);
} else {
output.printf("\r\nLOGIN -> %s rejected\r\n> ",
_terminal_login_target);
}
}
clearTerminalLogin();
}
#endif
} 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;
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);
}
}
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);
if (tag == pending_discovery) { // check for matching response tag)
pending_discovery = 0;
if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) {
MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len);
} 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;
i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len);
out_frame[i++] = in_path_len;
i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len);
// NOTE: telemetry data in 'extra' is discarded at present
_serial->writeFrame(out_frame, i);
}
return false; // DON'T send reciprocal path!
}
}
// 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;
if (_serial->isConnected()) {
_serial->writeFrame(out_frame, i);
} else {
MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline");
}
}
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)
#ifdef ENABLE_USB_INTERFACE
if (hasTerminalOutput() && _terminal_trace_pending
&& tag == _terminal_trace_tag && auth_code == _terminal_trace_auth) {
Stream& output = terminalOutput();
const uint8_t hash_size = _terminal_trace_hash_size;
const uint8_t hop_count = hash_size == 0 ? 0 : path_len / hash_size;
const uint8_t response_hash_size = 1 << (flags & 0x03);
const unsigned long elapsed = _ms->getMillis() - _terminal_trace_sent_at;
output.printf("\r\nTRACE -> %s (%lu ms)\r\n",
_terminal_trace_target, elapsed);
if (hash_size == 0 || response_hash_size != hash_size
|| path_len % hash_size != 0
|| hop_count >= MAX_PATH_SIZE) {
output.print(" ERROR: malformed trace response\r\n> ");
} else {
output.print(" ");
for (uint8_t hop = 0; hop < hop_count; hop++) {
output.print(((float)(int8_t)path_snrs[hop]) / 4.0f, 2);
output.print(" dB -> [");
mesh::Utils::printHex(output, &path_hashes[hop * hash_size],
hash_size);
output.print("] -> ");
}
output.print(packet->getSNR(), 2);
output.print(" dB\r\n> ");
}
clearTerminalTrace();
}
#endif
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 {
uint8_t path_hash_count = path_len & 63;
return SEND_TIMEOUT_BASE_MILLIS +
((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) *
(path_hash_count + 1));
}
void MyMesh::onSendTimeout() {
expireExpectedAcks();
}
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(0) {
_iter_started = false;
_cli_rescue = false;
#ifdef ENABLE_USB_INTERFACE
_terminal_mode = false;
_terminal_output = NULL;
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
_terminal_login_pending = false;
memset(_terminal_login_key, 0, sizeof(_terminal_login_key));
_terminal_login_expires_at = 0;
_terminal_login_target[0] = 0;
clearTerminalCommand();
clearTerminalTrace();
#endif
saved_radio_apply_pending = false;
_radio_available = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
command_radio_apply_pending = false;
command_radio_freq = 0.0f;
command_radio_bw = 0.0f;
command_radio_sf = 0;
command_radio_cr = 0;
command_radio_repeat = 0;
command_radio_apply_deadline = 0;
#if defined(COMPANION_RADIO_FULL)
_temp_radio_set_at = 0;
_temp_radio_revert_at = 0;
_temp_radio_retry_at = 0;
_temp_radio_freq = 0.0f;
_temp_radio_bw = 0.0f;
_temp_radio_sf = 0;
_temp_radio_cr = 0;
_temp_radio_failures = 0;
_temp_radio_applied = false;
#endif
offline_queue_len = 0;
offline_queue_head = 0;
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
offline_queue = offline_queue_fallback;
offline_queue_capacity = OFFLINE_QUEUE_PSRAM_FALLBACK_SIZE;
#endif
app_target_ver = 0;
clearPendingReqs();
memset(expected_ack_table, 0, sizeof(expected_ack_table));
next_ack_idx = 0;
next_ack_expiry = 0;
has_next_ack_expiry = false;
sign_data = NULL;
dirty_contacts_expiry = 0;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
send_unscoped = false;
// defaults
_prefs.airtime_factor = 1.0; // one half
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.sf = LORA_SF;
_prefs.bw = LORA_BW;
_prefs.cr = LORA_CR;
_prefs.multi_acks = DEFAULT_MULTI_ACKS;
_prefs.manual_add_contacts = DEFAULT_MANUAL_ADD_CONTACTS;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.buzzer_quiet = DEFAULT_BUZZER_QUIET ? 1 : 0;
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // Use the default 1-second fix-processing interval
_prefs.autoadd_config = DEFAULT_AUTOADD_CONFIG;
_prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE;
_prefs.radio_fem_txgain = 0;
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
_prefs.setRepeatEn(false);
#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) \
|| defined(USE_LR2021) || defined(SX126X_RX_BOOSTED_GAIN) \
|| defined(RX_BOOSTED_GAIN)
_prefs.rx_boosted_gain = mesh::radio::configuredRxBoostedGainDefault();
#endif
_prefs.radio_fem_rxgain = DEFAULT_FEM_RX_GAIN;
_prefs.rx_powersaving_enabled = RXPS_FIXED_ENABLED ? 1 : 0;
_prefs.rx_ps_level = RXPS_FIXED_LEVEL;
_prefs.rx_ps_preamble = RXPS_FIXED_PREAMBLE;
_prefs.rx_ps_rx_us = RX_POWERSAVING_DEFAULT_RX_US;
_prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US;
_prefs.powersaving_enabled = 1;
_prefs.powersaving_policy_version = 0;
_prefs.wifi_enabled = 1;
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
memset(&_mqtt_prefs, 0, sizeof(_mqtt_prefs));
_mqtt_bridge = nullptr;
_mqtt_configured = false;
_mqtt_started = false;
#endif
#ifdef WITH_WEBCONFIG
_webconfig = nullptr;
_wc_mqtt_dirty = false;
#endif
}
void MyMesh::begin(bool has_display, bool radio_available) {
_radio_available = radio_available;
setRadioAvailable(radio_available);
initializeOfflineQueue();
BaseChatMesh::begin();
const bool is_new_install = !_store->loadMainIdentity(self_id)
|| mesh::hasReservedIdentityPrefix(self_id);
bool identity_ready = true;
if (is_new_install) {
identity_ready = mesh::generateUsableLocalIdentity(self_id, radio_new_identity);
if (identity_ready) _store->saveMainIdentity(self_id);
}
#if defined(ESP32_PLATFORM)
mesh::discardESP32TrueRandom();
#endif
if (!identity_ready) {
MESH_DEBUG_PRINTLN("Identity generation exhausted all attempts; rebooting");
board.reboot();
return;
}
// 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
// if build provides default-scope, init with that
#ifdef DEFAULT_FLOOD_SCOPE_NAME
strcpy(_prefs.default_scope_name, DEFAULT_FLOOD_SCOPE_NAME);
{
TransportKeyStore temp;
TransportKey key;
temp.getAutoKeyFor(0, "#" DEFAULT_FLOOD_SCOPE_NAME, key);
memcpy(_prefs.default_scope_key, key.key, sizeof(key.key));
}
#endif
// load persisted prefs
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
// v1.17.1.2 repairs the Companion default-off regression for both fresh
// installs and devices that already persisted the regressed value. The
// appended policy marker makes this a one-time migration, so a later
// explicit `powersaving off` choice remains persistent.
const bool power_saving_default_migrated =
migrateCompanionPowerSavingDefault(_prefs);
// 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, 150.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, -9, MAX_LORA_TX_POWER);
_prefs.multi_acks = constrain(_prefs.multi_acks, 0, 1);
_prefs.manual_add_contacts = constrain(_prefs.manual_add_contacts, 0, 1);
_prefs.vibe_quiet = constrain(_prefs.vibe_quiet, 0, 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
_prefs.autoadd_config &= AUTO_ADD_OVERWRITE_OLDEST | AUTO_ADD_CHAT | AUTO_ADD_REPEATER | AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR;
_prefs.path_hash_mode = constrain(_prefs.path_hash_mode, 0, 2);
_prefs.radio_fem_rxgain_override = constrain(_prefs.radio_fem_rxgain_override, 0, 1);
if (!_prefs.radio_fem_rxgain_override) {
_prefs.radio_fem_rxgain = DEFAULT_FEM_RX_GAIN;
}
_prefs.radio_fem_rxgain = constrain(_prefs.radio_fem_rxgain, 0, 1);
_prefs.radio_fem_txgain = constrain(_prefs.radio_fem_txgain, 0, 1);
_prefs.rx_powersaving_enabled = constrain(_prefs.rx_powersaving_enabled, 0, 1);
_prefs.powersaving_enabled = constrain(_prefs.powersaving_enabled, 0, 1);
_prefs.wifi_enabled = constrain(_prefs.wifi_enabled, 0, 1);
_prefs.rx_ps_level = constrain(_prefs.rx_ps_level, 0, 10);
if (_prefs.rx_ps_preamble != 16 && _prefs.rx_ps_preamble != 32) {
_prefs.rx_ps_preamble = 0;
}
ensureRxPowerSavingDefaults(&_prefs.rx_ps_rx_us, &_prefs.rx_ps_sleep_us);
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
if (power_saving_default_migrated) {
_store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
}
#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;
}
#else
_active_ble_pin = 0;
#endif
resetContacts();
_store->loadContacts(this);
if (_store->hasPendingContactWrites()) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
updateGpsTelemetryPolicy();
bootstrapRTCfromContacts();
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
if (is_new_install) {
addChannel("#testing", TESTING_GROUP_PSK);
}
// A saved table takes precedence over compiled defaults on later boots.
_store->loadChannels(this);
if (is_new_install) {
// Make the first-boot defaults survive before a companion app changes them.
_store->saveChannels(this);
}
configureRadioFromPrefs();
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
applyMQTTDefaults(&_mqtt_prefs);
_mqtt_configured = CompanionMqttSetupPortal::loadStoredConfig(_mqtt_prefs);
if (!_mqtt_configured) applyMQTTDefaults(&_mqtt_prefs);
// Companion WiFi owns the connection and its mesh-wifi NVS setting is
// canonical. Keep MQTT reconnects from restoring a stale MQTT-pref value.
_mqtt_prefs.wifi_power_save = getCompanionWiFiPowerSave();
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
node_info.freq = &_prefs.freq;
node_info.bw = &_prefs.bw;
node_info.sf = &_prefs.sf;
node_info.cr = &_prefs.cr;
node_info.repeat_flag = &_prefs.client_repeat;
node_info.repeat_when_nonzero = true;
_mqtt_bridge = new MQTTBridge(node_info, &_mqtt_prefs,
getRTCClock(), &self_id, false);
if (_mqtt_bridge) {
char device_id[65];
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
_mqtt_bridge->setDeviceID(device_id);
_mqtt_bridge->setFirmwareVersion(FIRMWARE_VERSION);
_mqtt_bridge->setBoardModel(board.getManufacturerName());
_mqtt_bridge->setBuildDate(FIRMWARE_BUILD_DATE);
_mqtt_bridge->setStatsSources(this, _radio, &board, _ms);
}
#endif
#ifdef WITH_WEBCONFIG
void* web_mqtt_prefs = nullptr;
#ifdef WITH_MQTT_BRIDGE
web_mqtt_prefs = &_mqtt_prefs;
#endif
_webconfig = new WebConfigServer(this, web_mqtt_prefs, false,
self_id.pub_key, FIRMWARE_VERSION,
FIRMWARE_BUILD_DATE,
"companion", board.getManufacturerName());
#endif
}
void MyMesh::configureRadioFromPrefs() {
if (!_radio_available) {
saved_radio_apply_pending = false;
board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain);
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("Radio unavailable: companion services are running in recovery mode");
return;
}
saved_radio_apply_pending = !applySavedRadioParams();
if (!saved_radio_apply_pending) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
}
const bool fem_gain_changed = board.canControlLoRaFemLna()
&& board.isLoRaFemLnaEnabled() != (_prefs.radio_fem_rxgain != 0);
if (board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain) && fem_gain_changed) {
_radio->recalibrateNoiseFloor();
}
board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain);
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
}
void MyMesh::activateRadio() {
if (_radio_available) return;
_radio_available = true;
setRadioAvailable(true);
configureRadioFromPrefs();
MESH_DEBUG_PRINTLN("Radio recovery completed; mesh transport is active");
}
mesh::RadioParamApplyResult MyMesh::tryApplyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) {
if (!_radio_available) return mesh::RadioParamApplyResult::FAILED;
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
if (_prefs.rx_powersaving_enabled && _prefs.rx_ps_level != 0
&& !recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, sf, bw,
_prefs.rx_ps_preamble, &rx_us, &sleep_us)) {
return mesh::RadioParamApplyResult::FAILED;
}
uint32_t timings[2] = {rx_us, sleep_us};
const uint32_t* applied_timings = _prefs.rx_powersaving_enabled
&& radio_driver.supportsRxPowerSaving() ? timings : NULL;
return radio_driver.trySetParams(freq, bw, sf, cr, applied_timings);
}
bool MyMesh::applySavedRadioParams() {
return tryApplyRadioParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr)
== mesh::RadioParamApplyResult::APPLIED;
}
void MyMesh::finishRadioParamApply(float freq, float bw, uint8_t sf, uint8_t cr, uint8_t repeat) {
_prefs.sf = sf;
_prefs.cr = cr;
_prefs.freq = freq;
_prefs.bw = bw;
_prefs.client_repeat = repeat;
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
savePrefs();
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d",
(uint32_t)(freq * 1000.0f), (uint32_t)(bw * 1000.0f),
(uint32_t)sf, (uint32_t)cr);
writeOKFrame();
}
void MyMesh::cancelPendingRadioParamApply() {
if (!command_radio_apply_pending) return;
command_radio_apply_pending = false;
command_radio_apply_deadline = 0;
// The requested tuple was not committed. Reassert the persisted tuple in
// case a failed hardware apply only restored part of the old configuration.
saved_radio_apply_pending = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
}
void MyMesh::servicePendingRadioParamApply() {
if (!command_radio_apply_pending) return;
if (!_serial || !_serial->isConnected()) {
cancelPendingRadioParamApply();
return;
}
// Leave enough queue headroom for the eventual command response.
if (_serial->isWriteBusy()) return;
mesh::RadioParamApplyResult result = tryApplyRadioParams(
command_radio_freq, command_radio_bw, command_radio_sf, command_radio_cr);
if (result == mesh::RadioParamApplyResult::APPLIED) {
float freq = command_radio_freq;
float bw = command_radio_bw;
uint8_t sf = command_radio_sf;
uint8_t cr = command_radio_cr;
uint8_t repeat = command_radio_repeat;
command_radio_apply_pending = false;
command_radio_apply_deadline = 0;
finishRadioParamApply(freq, bw, sf, cr, repeat);
} else if (result == mesh::RadioParamApplyResult::FAILED) {
cancelPendingRadioParamApply();
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (command_radio_apply_deadline == _ms->getMillis()
|| millisHasNowPassed(command_radio_apply_deadline)) {
cancelPendingRadioParamApply();
writeErrFrame(ERR_CODE_BAD_STATE);
}
}
#if defined(COMPANION_RADIO_FULL)
static bool isFullCompanionBandwidth(float bw) {
static const float supported[] = {
7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f,
62.5f, 125.0f, 250.0f, 500.0f
};
for (float candidate : supported) {
if (fabsf(candidate - bw) <= 0.01f) return true;
}
return false;
}
bool MyMesh::scheduleTempRadio(float freq, float bw, uint8_t sf, uint8_t cr,
uint32_t timeout_mins, char* reply,
size_t reply_size) {
if (command_radio_apply_pending) {
snprintf(reply, reply_size, "ERR companion radio change is already pending");
return false;
}
_temp_radio_freq = freq;
_temp_radio_bw = bw;
_temp_radio_sf = sf;
_temp_radio_cr = cr;
_temp_radio_set_at = futureMillis(1500); // let the local reply drain first
_temp_radio_revert_at = futureMillis(1500 + (int)(timeout_mins * 60000UL));
_temp_radio_retry_at = 0;
_temp_radio_failures = 0;
snprintf(reply, reply_size, "OK - temp params for %lu mins",
(unsigned long)timeout_mins);
return true;
}
void MyMesh::scheduleNormalRadio(char* reply, size_t reply_size) {
_temp_radio_set_at = 0;
_temp_radio_revert_at = futureMillis(1500); // keep the reply on the active tuple
_temp_radio_retry_at = 0;
_temp_radio_failures = 0;
snprintf(reply, reply_size, "OK - normal radio restore scheduled");
}
bool MyMesh::handleFullOtaCommand(const char* command, char* reply,
size_t reply_size) {
if (!command || !reply || reply_size == 0) return false;
while (*command == ' ') command++;
#if defined(ESP32) && defined(WIFI_SSID)
#ifdef WITH_WEBCONFIG
if (strncmp(command, "get ", 4) == 0) {
const mesh::cli::StandaloneWiFiKey wifi_key =
mesh::cli::classifyStandaloneWiFiGet(command + 4);
switch (wifi_key) {
case mesh::cli::StandaloneWiFiKey::SSID:
WebConfigServer::formatWiFiSSID(reply, reply_size);
return true;
case mesh::cli::StandaloneWiFiKey::Status:
WebConfigServer::formatWiFiStatus(reply, reply_size);
return true;
case mesh::cli::StandaloneWiFiKey::CLI:
WebConfigServer::formatWiFiCliStatus(reply, reply_size);
return true;
default:
break;
}
}
if (strncmp(command, "set ", 4) == 0) {
const char* value = NULL;
const mesh::cli::StandaloneWiFiKey wifi_key =
mesh::cli::classifyStandaloneWiFiSet(command + 4, &value);
bool saved = false;
switch (wifi_key) {
case mesh::cli::StandaloneWiFiKey::SSID:
saved = WebConfigServer::setStandaloneWiFiSSID(
value, reply, reply_size);
break;
case mesh::cli::StandaloneWiFiKey::Password:
saved = WebConfigServer::setStandaloneWiFiPassword(
value, reply, reply_size);
break;
case mesh::cli::StandaloneWiFiKey::CLI:
WebConfigServer::setWiFiCliEnabled(value, reply, reply_size);
return true;
default:
break;
}
if (wifi_key == mesh::cli::StandaloneWiFiKey::SSID
|| wifi_key == mesh::cli::StandaloneWiFiKey::Password) {
if (saved) {
char saved_ssid[32] = {0};
char saved_password[64] = {0};
uint8_t saved_power_save = mesh::wifi::kDefaultPowerSave;
const bool has_ssid = WebConfigServer::loadStandaloneWiFi(
saved_ssid, sizeof(saved_ssid), saved_password,
sizeof(saved_password), &saved_power_save);
memset(saved_password, 0, sizeof(saved_password));
if (_webconfig) {
_webconfig->reloadStandaloneWiFi();
if (has_ssid && _webconfig->isRunning()) {
_webconfig->requestStop();
}
}
if (has_ssid) {
scheduleCompanionWiFiCredentialReload();
snprintf(reply, reply_size,
"OK - WiFi %s saved; reconnect scheduled",
wifi_key == mesh::cli::StandaloneWiFiKey::SSID
? "SSID" : "password");
} else {
snprintf(reply, reply_size,
"OK - WiFi password saved; set wifi.ssid to connect");
}
}
return true;
}
}
if (strcmp(command, "get webui") == 0) {
const bool enabled = WebConfigServer::loadEnabled(true);
if (!_webconfig
|| (!_webconfig->isRunning() && !_webconfig->isStopping())) {
snprintf(reply, reply_size, "> %s, inactive", enabled ? "on" : "off");
} else if (_webconfig->mode() == WebConfigServer::MODE_SETUP) {
char ssid[33] = {0};
char ip[16] = {0};
WebConfigServer::getSetupInfo(ssid, sizeof(ssid), ip, sizeof(ip));
snprintf(reply, reply_size, "> %s, setup AP %s http://%s/",
enabled ? "on" : "off", ssid, ip);
} else if (_webconfig->mode() == WebConfigServer::MODE_CONNECTING) {
snprintf(reply, reply_size, "> %s, connecting to WiFi",
enabled ? "on" : "off");
} else {
snprintf(reply, reply_size, "> %s, http://%s/",
enabled ? "on" : "off",
WiFi.localIP().toString().c_str());
}
return true;
}
if (strcmp(command, "set webui on") == 0
|| strcmp(command, "set webui off") == 0) {
const bool enabled = command[10] == 'o' && command[11] == 'n';
if (!WebConfigServer::saveEnabled(enabled)) {
snprintf(reply, reply_size, "Error: failed to save webui setting");
} else if (!enabled) {
if (_webconfig && _webconfig->isRunning()) _webconfig->requestStop();
snprintf(reply, reply_size, "OK - webui off");
} else if (_webconfig
&& (_webconfig->isRunning() || _webconfig->isStopping())) {
snprintf(reply, reply_size, "OK - webui on (already active)");
} else {
char start_reply[160] = {0};
startWebConfig(false, start_reply);
if (strncmp(start_reply, "WebConfig", 9) == 0) {
snprintf(reply, reply_size, "OK - webui on; %s", start_reply);
} else {
snprintf(reply, reply_size, "%s", start_reply);
}
}
return true;
}
if (strncmp(command, "set webui", 9) == 0
&& (command[9] == 0 || command[9] == ' ' || command[9] == '\t')) {
snprintf(reply, reply_size, "Error: usage set webui on|off");
return true;
}
if (strcmp(command, "start webconfig") == 0
|| strcmp(command, "start webconfig ap") == 0) {
startWebConfig(strcmp(command, "start webconfig ap") == 0, reply);
return true;
}
if (strncmp(command, "start webconfig", 15) == 0
&& (command[15] == 0 || command[15] == ' ' || command[15] == '\t')) {
snprintf(reply, reply_size, "ERR: usage start webconfig [ap]");
return true;
}
if (strcmp(command, "stop webconfig") == 0) {
if (!_webconfig || !_webconfig->isRunning()) {
snprintf(reply, reply_size, "Err: webconfig not running");
} else {
_webconfig->requestStop();
snprintf(reply, reply_size, "OK - webconfig stopping");
}
return true;
}
#endif
if (strcmp(command, "get wifi.powersave") == 0) {
formatWiFiPowerSaving(reply, reply_size);
return true;
}
if (strcmp(command, "set wifi.powersave") == 0
|| strncmp(command, "set wifi.powersave ", 19) == 0) {
const char* value = command + 18;
while (*value == ' ') value++;
applyAndSaveWiFiPowerSaving(value, reply, reply_size);
return true;
}
#endif
if (strcmp(command, "tempradio") == 0) {
if (_temp_radio_set_at) {
snprintf(reply, reply_size, "TempRadio pending: %.3f,%.2f,%u,%u",
_temp_radio_freq, _temp_radio_bw,
(unsigned)_temp_radio_sf, (unsigned)_temp_radio_cr);
} else if (isTempRadioActive()) {
uint32_t seconds = (_temp_radio_revert_at - _ms->getMillis()) / 1000UL;
snprintf(reply, reply_size, "TempRadio active: %.3f,%.2f,%u,%u %lus left",
_temp_radio_freq, _temp_radio_bw,
(unsigned)_temp_radio_sf, (unsigned)_temp_radio_cr,
(unsigned long)seconds);
} else {
snprintf(reply, reply_size, "TempRadio inactive");
}
return true;
}
if (strncmp(command, "tempradio ", 10) == 0) {
float freq = 0.0f, bw = 0.0f;
int sf = 0, cr = 0;
unsigned long timeout_mins = 0;
char extra = 0;
if (sscanf(command + 10, "%f,%f,%d,%d,%lu%c",
&freq, &bw, &sf, &cr, &timeout_mins, &extra) != 5
|| !isfinite(freq) || !isfinite(bw)
|| freq < 150.0f || freq > 2500.0f
|| !isFullCompanionBandwidth(bw)
|| sf < 5 || sf > 12 || cr < 5 || cr > 8
|| timeout_mins == 0 || timeout_mins > 10080UL) {
snprintf(reply, reply_size,
"ERR usage: tempradio freq,bw,sf,cr,minutes (minutes 1-10080)");
return true;
}
scheduleTempRadio(freq, bw, (uint8_t)sf, (uint8_t)cr,
(uint32_t)timeout_mins, reply, reply_size);
return true;
}
if (strcmp(command, "normalradio") == 0) {
scheduleNormalRadio(reply, reply_size);
return true;
}
if (strncmp(command, "ota", 3) == 0
&& (command[3] == 0 || command[3] == ' ')) {
char ota_reply[160] = {0};
if (!mesh::ota::handle_ota_command(command, ota_reply, board)) return false;
snprintf(reply, reply_size, "%s", ota_reply);
return true;
}
return false;
}
void MyMesh::serviceTempRadio() {
const unsigned long now = _ms->getMillis();
const bool retry_ready = !_temp_radio_retry_at
|| _temp_radio_retry_at == now || millisHasNowPassed(_temp_radio_retry_at);
const bool revert_due = _temp_radio_revert_at
&& (_temp_radio_revert_at == now || millisHasNowPassed(_temp_radio_revert_at));
if (revert_due) {
if (hasOutbound() || !retry_ready) return;
mesh::RadioParamApplyResult result = tryApplyRadioParams(
_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
if (result == mesh::RadioParamApplyResult::APPLIED) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
_temp_radio_set_at = 0;
_temp_radio_revert_at = 0;
_temp_radio_retry_at = 0;
_temp_radio_failures = 0;
_temp_radio_applied = false;
saved_radio_apply_pending = false;
MESH_DEBUG_PRINTLN("Full companion restored normal radio");
} else {
_temp_radio_retry_at = futureMillis(
nextRadioApplyRetryDelay(_temp_radio_failures));
}
return;
}
if (!_temp_radio_set_at
|| (_temp_radio_set_at != now && !millisHasNowPassed(_temp_radio_set_at))
|| hasOutbound() || !retry_ready) return;
mesh::RadioParamApplyResult result = tryApplyRadioParams(
_temp_radio_freq, _temp_radio_bw, _temp_radio_sf, _temp_radio_cr);
if (result == mesh::RadioParamApplyResult::APPLIED) {
_temp_radio_set_at = 0;
_temp_radio_retry_at = 0;
_temp_radio_failures = 0;
_temp_radio_applied = true;
MESH_DEBUG_PRINTLN("Full companion entered TempRadio");
} else if (result == mesh::RadioParamApplyResult::BUSY) {
_temp_radio_retry_at = futureMillis(250);
} else {
// A partially applied tuple must never become persistent. Restore the
// saved Companion settings through the same bounded retry path.
_temp_radio_set_at = 0;
_temp_radio_revert_at = futureMillis(1);
_temp_radio_retry_at = 0;
saved_radio_apply_pending = true;
}
}
#endif
const char *MyMesh::getNodeName() {
return _prefs.node_name;
}
CompanionNodePrefs *MyMesh::getNodePrefs() {
return &_prefs;
}
uint32_t MyMesh::getBLEPin() {
return _active_ble_pin;
}
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
static void copyMqttString(char* dest, size_t dest_size, const char* src) {
strncpy(dest, src ? src : "", dest_size - 1);
dest[dest_size - 1] = 0;
}
void MyMesh::serviceMQTT(const char* wifi_ssid, const char* wifi_password) {
if (!_mqtt_started) {
if (strcmp(_mqtt_prefs.wifi_ssid, wifi_ssid ? wifi_ssid : "") != 0) {
copyMqttString(_mqtt_prefs.wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid), wifi_ssid);
}
if (strcmp(_mqtt_prefs.wifi_password, wifi_password ? wifi_password : "") != 0) {
copyMqttString(_mqtt_prefs.wifi_password, sizeof(_mqtt_prefs.wifi_password), wifi_password);
}
}
if (WiFi.status() == WL_CONNECTED && _mqtt_configured && !_mqtt_started && _mqtt_bridge
#ifdef WITH_WEBCONFIG
&& !isWebConfigSetupActive()
#endif
) {
_mqtt_started = true; // begin is one-shot; avoid retrying partial allocations
_mqtt_bridge->begin();
if (_mqtt_bridge->isRunning()) {
Serial.println("MQTT companion: bridge started");
} else {
Serial.println("MQTT companion: bridge could not start");
}
}
}
void MyMesh::stopMQTT() {
if (_mqtt_started && _mqtt_bridge) _mqtt_bridge->end();
_mqtt_started = false;
}
#endif
#ifdef WITH_WEBCONFIG
static bool wcParseBool(const char* value, bool& out) {
if (strcmp(value, "on") == 0) { out = true; return true; }
if (strcmp(value, "off") == 0) { out = false; return true; }
return false;
}
static bool wcParseLong(const char* value, long min_value, long max_value, long& out) {
if (!value || !value[0]) return false;
char* end = nullptr;
long parsed = strtol(value, &end, 10);
if (!end || *end != 0 || parsed < min_value || parsed > max_value) return false;
out = parsed;
return true;
}
static bool wcParseDouble(const char* value, double min_value, double max_value, double& out) {
if (!value || !value[0]) return false;
char* end = nullptr;
double parsed = strtod(value, &end);
if (!end || *end != 0 || !isfinite(parsed) || parsed < min_value || parsed > max_value) return false;
out = parsed;
return true;
}
static bool wcValidNtpHost(const char* value) {
if (!value || !value[0] || strlen(value) > 63 || value[0] == '.') return false;
size_t len = strlen(value);
if (value[len - 1] == '.') return false;
for (size_t i = 0; i < len; i++) {
char c = value[i];
if (!isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '-') return false;
}
return true;
}
static bool wcValidHexKey(const char* value) {
if (!value || strlen(value) != 64) return false;
for (int i = 0; i < 64; i++) {
if (!isxdigit(static_cast<unsigned char>(value[i]))) return false;
}
return true;
}
static bool wcCopyValue(char* dest, size_t dest_size, const char* value) {
if (!dest || dest_size == 0 || !value || strlen(value) >= dest_size) return false;
strncpy(dest, value, dest_size - 1);
dest[dest_size - 1] = 0;
return true;
}
void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
memset(&s, 0, sizeof(s));
wcCopyValue(s.name, sizeof(s.name), _prefs.node_name);
s.lat = sensors.node_lat;
s.lon = sensors.node_lon;
s.freq = _prefs.freq;
s.bw = _prefs.bw;
s.sf = _prefs.sf;
s.cr = _prefs.cr;
s.tx_power = _prefs.tx_power_dbm;
s.airtime_factor = _prefs.airtime_factor;
s.rx_delay = _prefs.rx_delay_base;
s.rx_gain = _prefs.rx_boosted_gain;
s.fem_rx_gain = board.isLoRaFemLnaEnabled();
s.rx_ps_enabled = _prefs.rx_powersaving_enabled;
s.rx_ps_level = _prefs.rx_ps_level;
s.rx_ps_preamble = _prefs.rx_ps_preamble;
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.power_saving = _prefs.powersaving_enabled;
s.repeat = _prefs.client_repeat != 0;
s.capabilities = WebConfigServer::CAP_LOCATION | WebConfigServer::CAP_AIRTIME
| WebConfigServer::CAP_RX_DELAY | WebConfigServer::CAP_POWER_SAVING;
#if defined(ESP32) && defined(WIFI_SSID)
s.capabilities |= WebConfigServer::CAP_WIFI_POWER_SAVE;
#endif
if (radio_driver.supportsRxBoostedGainMode()) {
s.capabilities |= WebConfigServer::CAP_RX_GAIN;
}
if (board.canControlLoRaFemLna()) {
s.capabilities |= WebConfigServer::CAP_FEM_RX_GAIN;
}
if (radio_driver.supportsRxPowerSaving()) {
s.capabilities |= WebConfigServer::CAP_RX_POWER_SAVING;
}
}
bool MyMesh::startWebConfig(bool force_ap, char* reply) {
if (!_webconfig) {
strcpy(reply, "Err: WebUI unavailable (not enough memory)");
return false;
}
if (_webconfig->isRunning() || _webconfig->isStopping()) {
if (force_ap && !_webconfig->isStopping()
&& _webconfig->mode() == WebConfigServer::MODE_LAN) {
#ifdef WITH_MQTT_BRIDGE
if (_mqtt_started && _mqtt_bridge) {
_mqtt_bridge->end();
_mqtt_started = false;
}
#endif
return _webconfig->startSetupMode(reply);
}
strcpy(reply, _webconfig->isStopping() ? "Err: WebUI still stopping"
: "Err: WebUI already running");
return false;
}
if (force_ap) {
#ifdef WITH_MQTT_BRIDGE
if (_mqtt_started && _mqtt_bridge) {
_mqtt_bridge->end();
_mqtt_started = false;
}
#endif
return _webconfig->startSetupMode(reply);
}
return _webconfig->startAutoMode(reply);
}
void MyMesh::stopWebConfig() {
if (_webconfig && _webconfig->isRunning()) _webconfig->requestStop();
}
void MyMesh::serviceWebConfig() {
if (_webconfig) _webconfig->tick(millis());
}
bool MyMesh::isWebConfigActiveOrStopping() const {
return _webconfig && (_webconfig->isRunning() || _webconfig->isStopping());
}
bool MyMesh::isWebConfigSetupActive() const {
return _webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP
&& !_webconfig->isStopping();
}
bool MyMesh::isWebConfigWiFiRecoveryActive() const {
return _webconfig && _webconfig->isSavedWiFiRecoveryActive();
}
void MyMesh::rebootNow() {
board.reboot();
}
void MyMesh::onConfigBatchStart() {
_wc_mqtt_dirty = false;
}
void MyMesh::onConfigBatchEnd() {
#if defined(ESP32) && defined(WIFI_SSID)
// The WebConfig WiFi form writes the canonical mesh-wifi namespace itself.
// Reload it here so the Companion runtime and MQTT reconnect policy cannot
// retain a stale modem-sleep mode.
reloadCompanionWiFiPowerSave();
syncWiFiPowerSaving();
#endif
#ifdef WITH_MQTT_BRIDGE
if (_wc_mqtt_dirty) {
CompanionMqttSetupPortal::saveStoredConfig(_mqtt_prefs);
if (_mqtt_started && _mqtt_bridge) _mqtt_bridge->end();
_mqtt_started = false;
MQTTPrefs verified;
_mqtt_configured = CompanionMqttSetupPortal::loadStoredConfig(verified);
if (_mqtt_configured) _mqtt_prefs = verified;
// The standalone Companion setting remains canonical even if MQTT config
// verification reloaded an older copy of this field.
_mqtt_prefs.wifi_power_save = getCompanionWiFiPowerSave();
}
#endif
_wc_mqtt_dirty = false;
}
void MyMesh::execCommand(char* cmd, char* reply) {
reply[0] = 0;
#if defined(ESP32) && defined(WIFI_SSID)
if (cmd && strcmp(cmd, "get wifi.powersave") == 0) {
formatWiFiPowerSaving(reply, 160);
return;
}
#endif
if (!cmd || strncmp(cmd, "set ", 4) != 0) {
strcpy(reply, "Error: unsupported command");
return;
}
char* key = cmd + 4;
char* split = strchr(key, ' ');
if (!split) {
strcpy(reply, "Error: missing value");
return;
}
*split = 0;
const char* value = split + 1;
if (strcmp(key, "name") == 0) {
if (!value[0] || !wcCopyValue(_prefs.node_name, sizeof(_prefs.node_name), value)) {
strcpy(reply, "Error: name must be 1-31 characters");
} else {
savePrefs();
strcpy(reply, "OK");
}
return;
}
if (strcmp(key, "lat") == 0 || strcmp(key, "lon") == 0) {
double parsed;
const bool latitude = key[1] == 'a';
if (!wcParseDouble(value, latitude ? -90.0 : -180.0,
latitude ? 90.0 : 180.0, parsed)) {
strcpy(reply, latitude ? "Error: latitude must be -90 to 90"
: "Error: longitude must be -180 to 180");
} else {
if (latitude) sensors.node_lat = parsed; else sensors.node_lon = parsed;
savePrefs();
strcpy(reply, "OK");
}
return;
}
if (strcmp(key, "radio") == 0) {
float freq, bw;
int sf, cr;
char extra;
if (sscanf(value, "%f,%f,%d,%d%c", &freq, &bw, &sf, &cr, &extra) != 4
|| !isfinite(freq) || !isfinite(bw) || freq < 150.0f || freq > 2500.0f
|| bw < 7.0f || bw > 500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8) {
strcpy(reply, "Error: radio must be freq,bw,sf,cr");
} else {
_prefs.freq = freq;
_prefs.bw = bw;
_prefs.sf = static_cast<uint8_t>(sf);
_prefs.cr = static_cast<uint8_t>(cr);
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
savePrefs();
strcpy(reply, "OK - reboot required");
}
return;
}
if (strcmp(key, "tx") == 0) {
long parsed;
if (!wcParseLong(value, -9, MAX_LORA_TX_POWER, parsed)) {
snprintf(reply, 160, "Error: TX power must be -9 to %d", MAX_LORA_TX_POWER);
} else {
_prefs.tx_power_dbm = static_cast<int8_t>(parsed);
if (_radio_available) radio_driver.setTxPower(_prefs.tx_power_dbm);
savePrefs();
strcpy(reply, "OK");
}
return;
}
if (strcmp(key, "af") == 0 || strcmp(key, "rxdelay") == 0) {
double parsed;
const bool is_af = strcmp(key, "af") == 0;
if (!wcParseDouble(value, 0.0, is_af ? 9.0 : 20.0, parsed)) {
strcpy(reply, is_af ? "Error: airtime factor must be 0-9"
: "Error: RX delay must be 0-20");
} else {
if (is_af) _prefs.airtime_factor = parsed; else _prefs.rx_delay_base = parsed;
savePrefs();
strcpy(reply, "OK");
}
return;
}
if (strcmp(key, "radio.rxgain") == 0 || strcmp(key, "repeat") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
strcpy(reply, "Error: must be on or off");
} else {
if (strcmp(key, "radio.rxgain") == 0) {
if (!radio_driver.supportsRxBoostedGainMode()) {
strcpy(reply, "Error: RX boosted gain unsupported");
return;
}
if (!applyAndSaveRxBoostedGain(enabled)) {
strcpy(reply, "Error: radio busy; retry");
return;
}
} else {
_prefs.client_repeat = enabled;
savePrefs();
}
strcpy(reply, "OK");
}
return;
}
if (strcmp(key, "radio.rxps") == 0) {
applyAndSaveRxPowerSaving(value, reply);
return;
}
if (strcmp(key, "powersaving") == 0) {
applyAndSavePowerSaving(value, reply);
return;
}
#if defined(ESP32) && defined(WIFI_SSID)
if (strcmp(key, "wifi.powersave") == 0) {
applyAndSaveWiFiPowerSaving(value, reply, 160);
return;
}
#endif
if (strcmp(key, "radio.fem.rxgain") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
strcpy(reply, "Error: must be on or off");
} else if (!board.canControlLoRaFemLna()) {
strcpy(reply, "Error: unsupported");
} else if (!applyAndSaveFemRxGain(enabled)) {
strcpy(reply, "Error: failed to apply FEM RX gain");
} else {
strcpy(reply, "OK");
}
return;
}
if (strcmp(key, "radio.fem.txgain") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
strcpy(reply, "Error: must be on or off");
} else if (!board.canControlLoRaFemPaGain()) {
strcpy(reply, "Error: unsupported");
} else if (!applyAndSaveFemTxGain(enabled)) {
strcpy(reply, "Error: failed to apply FEM TX gain");
} else {
strcpy(reply, "OK");
}
return;
}
#ifdef WITH_MQTT_BRIDGE
bool changed = false;
if (strcmp(key, "mqtt.origin") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin), value);
} else if (strcmp(key, "mqtt.iata") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata), value);
if (changed) {
for (char* p = _mqtt_prefs.mqtt_iata; *p; p++) *p = toupper(static_cast<unsigned char>(*p));
}
} else if (strcmp(key, "mqtt.status") == 0 || strcmp(key, "mqtt.packets") == 0
|| strcmp(key, "mqtt.raw") == 0 || strcmp(key, "mqtt.rx") == 0
|| strcmp(key, "snmp") == 0) {
bool enabled;
if (!wcParseBool(value, enabled)) {
strcpy(reply, "Error: must be on or off");
return;
}
if (strcmp(key, "mqtt.status") == 0) _mqtt_prefs.mqtt_status_enabled = enabled;
else if (strcmp(key, "mqtt.packets") == 0) _mqtt_prefs.mqtt_packets_enabled = enabled;
else if (strcmp(key, "mqtt.raw") == 0) _mqtt_prefs.mqtt_raw_enabled = enabled;
else if (strcmp(key, "mqtt.rx") == 0) _mqtt_prefs.mqtt_rx_enabled = enabled;
else _mqtt_prefs.snmp_enabled = enabled;
changed = true;
} else if (strcmp(key, "mqtt.tx") == 0) {
if (strcmp(value, "off") == 0) _mqtt_prefs.mqtt_tx_enabled = 0;
else if (strcmp(value, "on") == 0) _mqtt_prefs.mqtt_tx_enabled = 1;
else if (strcmp(value, "advert") == 0) _mqtt_prefs.mqtt_tx_enabled = 2;
else {
strcpy(reply, "Error: MQTT TX must be off, on, or advert");
return;
}
changed = true;
} else if (strcmp(key, "mqtt.interval") == 0) {
long minutes;
if (!wcParseLong(value, 1, 60, minutes)) {
strcpy(reply, "Error: interval must be 1-60 minutes");
return;
}
_mqtt_prefs.mqtt_status_interval = static_cast<uint32_t>(minutes) * 60000UL;
changed = true;
} else if (strcmp(key, "mqtt.ntp") == 0) {
if (strcmp(value, "none") == 0) {
_mqtt_prefs.mqtt_ntp_server[0] = 0;
changed = true;
} else if (wcValidNtpHost(value)) {
changed = wcCopyValue(_mqtt_prefs.mqtt_ntp_server,
sizeof(_mqtt_prefs.mqtt_ntp_server), value);
}
} else if (strcmp(key, "mqtt.owner") == 0) {
if (!value[0]) {
_mqtt_prefs.mqtt_owner_public_key[0] = 0;
changed = true;
} else if (wcValidHexKey(value)) {
changed = wcCopyValue(_mqtt_prefs.mqtt_owner_public_key,
sizeof(_mqtt_prefs.mqtt_owner_public_key), value);
}
} else if (strcmp(key, "mqtt.email") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_email, sizeof(_mqtt_prefs.mqtt_email), value);
} else if (strcmp(key, "timezone") == 0) {
changed = wcCopyValue(_mqtt_prefs.timezone_string, sizeof(_mqtt_prefs.timezone_string), value);
} else if (strcmp(key, "timezone.offset") == 0) {
long offset;
if (!wcParseLong(value, -12, 14, offset)) {
strcpy(reply, "Error: timezone offset must be -12 to 14");
return;
}
_mqtt_prefs.timezone_offset = static_cast<int8_t>(offset);
changed = true;
} else if (strcmp(key, "snmp.community") == 0) {
changed = wcCopyValue(_mqtt_prefs.snmp_community,
sizeof(_mqtt_prefs.snmp_community), value);
} else if (strncmp(key, "mqtt", 4) == 0 && key[4] >= '1'
&& key[4] <= ('0' + MAX_MQTT_SLOTS) && key[5] == '.') {
int slot = key[4] - '1';
const char* field = key + 6;
if (strcmp(field, "preset") == 0) {
const bool valid = findMQTTPreset(value) != nullptr
|| strcmp(value, MQTT_PRESET_CUSTOM) == 0
|| strcmp(value, MQTT_PRESET_NONE) == 0;
if (!valid) {
strcpy(reply, "Error: unknown MQTT preset");
return;
}
if (findMQTTPreset(value)) {
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
if (i != slot && strcmp(_mqtt_prefs.mqtt_slot_preset[i], value) == 0) {
snprintf(reply, 160, "Error: preset already assigned to slot %d", i + 1);
return;
}
}
}
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_preset[slot],
sizeof(_mqtt_prefs.mqtt_slot_preset[slot]), value);
} else if (strcmp(field, "server") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_host[slot],
sizeof(_mqtt_prefs.mqtt_slot_host[slot]), value);
} else if (strcmp(field, "port") == 0) {
long port;
if (!wcParseLong(value, 1, 65535, port)) {
strcpy(reply, "Error: port must be 1-65535");
return;
}
_mqtt_prefs.mqtt_slot_port[slot] = static_cast<uint16_t>(port);
changed = true;
} else if (strcmp(field, "username") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_username[slot],
sizeof(_mqtt_prefs.mqtt_slot_username[slot]), value);
} else if (strcmp(field, "password") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_password[slot],
sizeof(_mqtt_prefs.mqtt_slot_password[slot]), value);
} else if (strcmp(field, "token") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_token[slot],
sizeof(_mqtt_prefs.mqtt_slot_token[slot]), value);
} else if (strcmp(field, "topic") == 0) {
if (strcmp(_mqtt_prefs.mqtt_slot_preset[slot], MQTT_PRESET_CUSTOM) != 0) {
strcpy(reply, "Error: topic only applies to custom slots");
return;
}
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_topic[slot],
sizeof(_mqtt_prefs.mqtt_slot_topic[slot]), value);
} else if (strcmp(field, "audience") == 0) {
changed = wcCopyValue(_mqtt_prefs.mqtt_slot_audience[slot],
sizeof(_mqtt_prefs.mqtt_slot_audience[slot]), value);
}
}
if (changed) {
_wc_mqtt_dirty = true;
strcpy(reply, "OK");
} else if (reply[0] == 0) {
strcpy(reply, "Error: invalid or unsupported value");
}
#else
strcpy(reply, "Error: unsupported setting for this companion");
#endif
}
void MyMesh::buildStatsJson(char* buf, size_t buf_size) {
char ip[20] = "";
int wifi_rssi = 0;
if (WiFi.status() == WL_CONNECTED) {
strncpy(ip, WiFi.localIP().toString().c_str(), sizeof(ip) - 1);
wifi_rssi = WiFi.RSSI();
} else if (_webconfig && _webconfig->mode() == WebConfigServer::MODE_SETUP) {
strncpy(ip, WiFi.softAPIP().toString().c_str(), sizeof(ip) - 1);
}
int pos = snprintf(buf, buf_size,
"{\"uptime_s\":%lu,\"batt_mv\":%u,"
"\"heap_free\":%lu,\"heap_min\":%lu,\"heap_max_alloc\":%lu,"
"\"noise\":%d,\"rssi\":%d,\"snr\":%.1f,"
"\"airtime_s\":%lu,\"rx_airtime_s\":%lu,"
"\"recv\":%lu,\"sent\":%lu,\"rx_err\":%lu,"
"\"sent_flood\":%lu,\"sent_direct\":%lu,\"recv_flood\":%lu,\"recv_direct\":%lu,"
"\"tx_queue\":%d,\"wifi_rssi\":%d,\"ip\":\"%s\",\"mqtt_queue\":%d,\"slots\":[",
(unsigned long)(millis() / 1000), (unsigned)board.getBattMilliVolts(),
(unsigned long)ESP.getFreeHeap(), (unsigned long)ESP.getMinFreeHeap(),
(unsigned long)ESP.getMaxAllocHeap(),
(int)_radio->getNoiseFloor(), (int)radio_driver.getLastRSSI(),
radio_driver.getLastSNR(),
(unsigned long)(getTotalAirTime() / 1000), (unsigned long)(getReceiveAirTime() / 1000),
(unsigned long)radio_driver.getPacketsRecv(), (unsigned long)radio_driver.getPacketsSent(),
(unsigned long)radio_driver.getPacketsRecvErrors(),
(unsigned long)getNumSentFlood(), (unsigned long)getNumSentDirect(),
(unsigned long)getNumRecvFlood(), (unsigned long)getNumRecvDirect(),
(int)_mgr->getOutboundCount(0xFFFFFFFF), wifi_rssi, ip,
#ifdef WITH_MQTT_BRIDGE
_mqtt_bridge ? _mqtt_bridge->getQueueSize() : 0);
#else
0);
#endif
if (pos < 0 || pos >= static_cast<int>(buf_size) - 3) return;
#ifdef WITH_MQTT_BRIDGE
bool first = true;
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
MQTTBridge::SlotStatusSnapshot status;
if (!MQTTBridge::getSlotStatusSnapshot(i, &status)) continue;
int written = snprintf(buf + pos, buf_size - pos,
"%s{\"n\":%d,\"name\":\"%s\",\"state\":\"%s\"}",
first ? "" : ",", i + 1, status.name, status.state);
if (written < 0 || written >= static_cast<int>(buf_size - pos)) break;
pos += written;
first = false;
}
#endif
snprintf(buf + pos, buf_size - pos, "]}");
}
#endif
struct FreqRange {
uint32_t lower_freq, upper_freq;
};
static FreqRange repeat_freq_ranges[] = {
#ifdef ALLOWED_REPEAT_FREQ_RANGE
ALLOWED_REPEAT_FREQ_RANGE
#else
{ 433000, 433000 },
{ 869495, 869495 },
{ 918000, 918000 }
#endif
};
bool MyMesh::isValidClientRepeatFreq(uint32_t f) const {
for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) {
auto r = &repeat_freq_ranges[i];
if (f >= r->lower_freq && f <= r->upper_freq) return true;
}
return false;
}
void MyMesh::startInterface(BaseSerialInterface &serial) {
_serial = &serial;
serial.enable();
}
void MyMesh::cancelSerialResponseStream() {
stopContactsIterator();
}
void MyMesh::handleCmdFrame(size_t len) {
if (len == 0) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
if (cmd_frame[0] == CMD_DEVICE_QUERY && 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], &_active_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;
out_frame[i++] = _prefs.isRepeatEn() ? 1 : 0; // v9+
out_frame[i++] = _prefs.path_hash_mode; // v10+
_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);
stopContactsIterator(); // stop any left-over ContactsIterator
cancelPendingRadioParamApply();
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
uint8_t text_fingerprint[MAX_HASH_SIZE] = { 0 };
uint8_t packet_retry_key[MAX_HASH_SIZE] = { 0 };
AckTableEntry* replacement_entry = NULL;
if (txt_type == TXT_TYPE_PLAIN) {
mesh::Utils::sha256(text_fingerprint, sizeof(text_fingerprint),
recipient->id.pub_key, PUB_KEY_SIZE,
(const uint8_t*)text, strlen(text));
}
int result;
uint32_t expected_ack;
if (txt_type == TXT_TYPE_CLI_DATA) {
const uint32_t logical_request_id = msg_timestamp;
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, logical_request_id);
expected_ack = 0; // no Ack expected
} else {
const uint32_t app_timestamp = msg_timestamp;
const bool is_room_post = recipient->type == ADV_TYPE_ROOM;
if (is_room_post
&& !room_message_timestamps.find(text_fingerprint, app_timestamp,
&msg_timestamp)) {
// Room login/control packets already use the node's monotonic clock.
// Give a new post that same clock source, then preserve the mapping
// so application retries keep one logical server-side timestamp.
msg_timestamp = getRTCClock()->getCurrentTimeUnique();
}
replacement_entry = findPendingTextMessage(text_fingerprint, msg_timestamp);
result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout,
packet_retry_key, NULL, text_fingerprint);
if (result != MSG_SEND_FAILED && is_room_post) {
room_message_timestamps.remember(text_fingerprint, app_timestamp,
msg_timestamp);
}
}
if (result == MSG_SEND_FAILED) {
writeErrFrame(ERR_CODE_TABLE_FULL);
} else {
if (replacement_entry != NULL) {
// The newest successfully-queued submission wins. Keep the older
// entry intact if composition, validation, or queueing failed.
clearExpectedAck(*replacement_entry, false);
}
if (expected_ack) {
AckTableEntry& entry = replacement_entry != NULL
? *replacement_entry
: expected_ack_table[next_ack_idx];
// Reusing this circular slot changes only ACK bookkeeping. Retry
// ownership lives in Mesh and ends only on an ACK, retry completion,
// or a successfully queued semantic replacement.
clearExpectedAck(entry, false);
entry.msg_sent = _ms->getMillis(); // add to circular table
entry.expires_at = futureMillis(est_timeout);
entry.ack = expected_ack;
entry.message_timestamp = msg_timestamp;
entry.contact = recipient;
memcpy(entry.text_fingerprint, text_fingerprint, sizeof(entry.text_fingerprint));
memcpy(entry.retry_key, packet_retry_key, sizeof(entry.retry_key));
if (replacement_entry == NULL) {
next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE;
}
}
if (replacement_entry != NULL || expected_ack != 0) {
expireExpectedAcks();
}
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 unsupported TXT_TYPE_*
}
} else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel text msg
if (len < 7) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
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_SEND_CHANNEL_DATA) { // send GroupChannel datagram
if (len < 3) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
int i = 1;
uint8_t channel_idx = cmd_frame[i++];
uint8_t path_len = cmd_frame[i++];
// validate path len, allowing 0xFF for flood
if (!mesh::Packet::isValidPathLen(path_len) && path_len != OUT_PATH_UNKNOWN) {
MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA invalid path size: %d", path_len);
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
// parse provided path if not flood
uint8_t path[MAX_PATH_SIZE];
size_t path_bytes = 0;
if (path_len != OUT_PATH_UNKNOWN) {
path_bytes = (size_t)(path_len & 63) * (size_t)((path_len >> 6) + 1);
}
if ((size_t)i + path_bytes + 2 > len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
if (path_len != OUT_PATH_UNKNOWN) {
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
}
uint16_t data_type = ((uint16_t)cmd_frame[i]) | (((uint16_t)cmd_frame[i + 1]) << 8);
i += 2;
const uint8_t *payload = &cmd_frame[i];
int payload_len = (len > (size_t)i) ? (int)(len - i) : 0;
ChannelDetails channel;
if (!getChannel(channel_idx, channel)) {
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
} else if (data_type == DATA_TYPE_RESERVED) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (payload_len > MAX_CHANNEL_DATA_LENGTH) {
MESH_DEBUG_PRINTLN("CMD_SEND_CHANNEL_DATA payload too long: %d > %d", payload_len, MAX_CHANNEL_DATA_LENGTH);
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (sendGroupData(channel.channel, path, path_len, data_type, payload, payload_len)) {
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} 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;
}
// CONTACTS_START, every CONTACT, and END_OF_CONTACTS are one response
// transaction. Keep them on the transport which requested the list.
_serial->lockReplyRoute();
_iter = startContactsIterator();
_iter_started = true;
_most_recent_lastmod = 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);
}
} 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)
unsigned long delay_millis = 0;
TransportKey default_scope;
memcpy(&default_scope.key, _prefs.default_scope_key, sizeof(default_scope.key));
sendFloodScoped(default_scope, pkt, delay_millis);
} 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 = OUT_PATH_UNKNOWN;
// recipient->lastmod = ?? shouldn't be needed, app already has this version of contact
scheduleContactWrite(*recipient);
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;
scheduleContactWrite(*recipient);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
ContactInfo contact;
updateContactFromFrame(contact, last_mod, cmd_frame, len);
contact.lastmod = last_mod;
contact.sync_since = 0;
if (addContact(contact)) {
ContactInfo* added = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
if (added) scheduleContactWrite(*added);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
}
} else if (cmd_frame[0] == CMD_REMOVE_CONTACT && len >= 1 + PUB_KEY_SIZE) {
uint8_t *pub_key = &cmd_frame[1];
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
ContactInfo removed;
if (recipient) removed = *recipient;
if (recipient && removeContact(*recipient)) {
scheduleContactWriteAfterRelease(removed);
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
updateGpsTelemetryPolicy();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // not found, or unable to remove
}
} else if (cmd_frame[0] == CMD_SHARE_CONTACT && len >= 1 + PUB_KEY_SIZE) {
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 && len >= 1 + PUB_KEY_SIZE) {
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)) {
updateGpsTelemetryPolicy();
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);
#endif
} else {
out_frame[0] = RESP_CODE_NO_MORE_MESSAGES;
_serial->writeFrame(out_frame, 1);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_PARAMS) {
if (len < 11) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
#if defined(COMPANION_RADIO_FULL)
if (isTempRadioActive() || _temp_radio_set_at != 0
|| _temp_radio_revert_at != 0) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
#endif
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++];
uint8_t repeat = 0; // default - false
if (len > i) {
repeat = cmd_frame[i++]; // FIRMWARE_VER_CODE 9+
}
if (repeat && !isValidClientRepeatFreq(freq)) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (freq >= 150000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 &&
bw <= 500000) {
float new_freq = (float)freq / 1000.0;
float new_bw = (float)bw / 1000.0;
if (command_radio_apply_pending) {
writeErrFrame(ERR_CODE_BAD_STATE);
return;
}
mesh::RadioParamApplyResult result = tryApplyRadioParams(new_freq, new_bw, sf, cr);
if (result == mesh::RadioParamApplyResult::BUSY) {
command_radio_apply_pending = true;
command_radio_freq = new_freq;
command_radio_bw = new_bw;
command_radio_sf = sf;
command_radio_cr = cr;
command_radio_repeat = repeat;
command_radio_apply_deadline = futureMillis(COMMAND_RADIO_APPLY_TIMEOUT_MS);
MESH_DEBUG_PRINTLN("Deferred CMD_SET_RADIO_PARAMS while radio is busy");
return;
}
if (result == mesh::RadioParamApplyResult::FAILED) {
// Persisted settings remain authoritative after a rejected change or
// a hardware apply that had to be rolled back.
saved_radio_apply_pending = true;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
finishRadioParamApply(new_freq, new_bw, sf, cr, repeat);
} 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 (len < 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
int8_t power = (int8_t)cmd_frame[1];
if (power < -9 || power > MAX_LORA_TX_POWER) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
_prefs.tx_power_dbm = power;
savePrefs();
if (_radio_available) radio_driver.setTxPower(_prefs.tx_power_dbm);
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_SET_TUNING_PARAMS) {
if (len < 9) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
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) {
if (len < 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
_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];
}
}
}
updateGpsTelemetryPolicy();
savePrefs();
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && len >= 3 && cmd_frame[1] == 0) {
if (cmd_frame[2] >= 3) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
_prefs.path_hash_mode = cmd_frame[2];
savePrefs();
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_REBOOT && len >= 7 && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
// Non-nRF stores use the legacy monolithic file and therefore do not
// report dirty pages. The lazy-write timer is still proof that RAM holds
// newer contact data which must be persisted before rebooting.
if (dirty_contacts_expiry || _store->hasPendingContactWrites()) {
if (!_store->flushContactWrites(this, save_filter)) {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
return;
}
}
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);
#else
writeDisabledFrame();
#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
stopContactsIterator();
resetContacts();
_store->loadContacts(this);
updateGpsTelemetryPolicy();
} else {
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
}
}
#else
writeDisabledFrame();
#endif
} else if (cmd_frame[0] == CMD_SEND_RAW_DATA && len >= 6) {
int i = 1;
uint8_t path_len = cmd_frame[i++];
if (path_len >= 0 && mesh::Packet::isValidPathLen(path_len)) {
uint8_t path[MAX_PATH_SIZE];
i += mesh::Packet::writePath(path, &cmd_frame[i], path_len);
if (i + 4 > len) { // min payload 4 bytes
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
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);
ContactInfo anon;
if (recipient == NULL) { // FIRMWARE_VER_CODE 13+, allow non-contact requests
memset(&anon, 0, sizeof(anon));
#if defined(NRF52_PLATFORM)
anon.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
#endif
memcpy(anon.id.pub_key, pub_key, PUB_KEY_SIZE);
anon.out_path_len = 0; // default to zero-hop direct
anon.type = ADV_TYPE_NONE; // unknown
anon.lastmod = getRTCClock()->getCurrentTime();
if (addContact(anon)) recipient = &anon;
}
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_TABLE_FULL); // contacts full
}
} 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 && len >= 2 + PUB_KEY_SIZE && cmd_frame[1] == 0) {
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 = OUT_PATH_UNKNOWN;
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);
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
}
// 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 - 9) {
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) {
// Compute before handing ownership to sendDirect(), which releases the
// packet itself if validation or queueing fails.
uint32_t t = _radio->getEstAirtimeFor(9 + path_len + 2);
uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz);
if (sendDirect(pkt, &cmd_frame[10], 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 {
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_RADIO_FEM_RXGAIN) {
if (!board.canControlLoRaFemLna()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
uint8_t value = board.isLoRaFemLnaEnabled() ? 1 : 0;
memcpy(&out_frame[1], &value, 1);
_serial->writeFrame(out_frame, 2);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_FEM_RXGAIN && len >= 2) {
uint8_t value = cmd_frame[1];
if (!board.canControlLoRaFemLna()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else if (value <= 1) {
if (applyAndSaveFemRxGain(value != 0)) {
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
}
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_GET_RADIO_RXGAIN) {
if (!radio_driver.supportsRxBoostedGainMode()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
out_frame[1] = _prefs.rx_boosted_gain ? 1 : 0;
_serial->writeFrame(out_frame, 2);
}
} else if (cmd_frame[0] == CMD_SET_RADIO_RXGAIN && len >= 2) {
uint8_t value = cmd_frame[1];
if (value > 1) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (!radio_driver.supportsRxBoostedGainMode()) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else if (!applyAndSaveRxBoostedGain(value != 0)) {
writeErrFrame(ERR_CODE_BAD_STATE);
} else {
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_GET_WIFI_POWER_SAVE) {
#if defined(ESP32) && defined(WIFI_SSID)
out_frame[0] = RESP_CODE_OK;
out_frame[1] = getCompanionWiFiPowerSave();
_serial->writeFrame(out_frame, 2);
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} else if (cmd_frame[0] == CMD_SET_WIFI_POWER_SAVE && len >= 2) {
#if defined(ESP32) && defined(WIFI_SSID)
const uint8_t value = cmd_frame[1];
if (value > mesh::wifi::kPowerSaveMax) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
char reply[160];
if (applyAndSaveWiFiPowerSaving(
companionWiFiPowerSaveName(value), reply, sizeof(reply))) {
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_BAD_STATE);
}
}
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} 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) {
int i = 0;
out_frame[i++] = RESP_CODE_ADVERT_PATH;
memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4;
out_frame[i++] = found->path_len;
i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len);
_serial->writeFrame(out_frame, i);
} 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();
uint32_t n_recv_errors = radio_driver.getPacketsRecvErrors();
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;
memcpy(&out_frame[i], &n_recv_errors, 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 && len >= 6 && 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_KEY && len >= 2 && cmd_frame[1] == 0) {
if (len >= 2 + 16) {
memcpy(send_scope.key, &cmd_frame[2], sizeof(send_scope.key)); // set scope override TransportKey
} else {
memset(send_scope.key, 0, sizeof(send_scope.key)); // reset scope override
}
send_unscoped = false;
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 1) { // ver 12+
send_unscoped = true;
writeOKFrame();
} else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) {
if (len >= 1+31+16) {
const void* terminator = memchr(&cmd_frame[1], 0, 31);
size_t n = terminator == NULL ? 31 : (const uint8_t*)terminator - &cmd_frame[1];
if (n > 0 && n < 31) {
memcpy(_prefs.default_scope_name, &cmd_frame[1], n);
_prefs.default_scope_name[n] = 0;
memcpy(_prefs.default_scope_key, &cmd_frame[1+31], 16);
savePrefs();
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else {
memset(_prefs.default_scope_name, 0, sizeof(_prefs.default_scope_name)); // set default scope to null
memset(_prefs.default_scope_key, 0, sizeof(_prefs.default_scope_key));
savePrefs();
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_GET_DEFAULT_FLOOD_SCOPE) {
out_frame[0] = RESP_CODE_DEFAULT_FLOOD_SCOPE;
if (strlen(_prefs.default_scope_name) > 0) {
memcpy(&out_frame[1], _prefs.default_scope_name, 31);
memcpy(&out_frame[1+31], _prefs.default_scope_key, 16);
_serial->writeFrame(out_frame, 1+31+16);
} else {
_serial->writeFrame(out_frame, 1); // no name or key means null
}
} 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) {
if (len < 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
_prefs.autoadd_config = cmd_frame[1];
if (len >= 3) {
_prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)64);
}
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;
out_frame[i++] = _prefs.autoadd_max_hops;
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) {
int i = 0;
out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ;
for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) {
auto r = &repeat_freq_ranges[k];
memcpy(&out_frame[i], &r->lower_freq, 4); i += 4;
memcpy(&out_frame[i], &r->upper_freq, 4); i += 4;
}
_serial->writeFrame(out_frame, i);
} else if (cmd_frame[0] == CMD_SEND_RAW_PACKET && len >= 4) {
auto pkt = obtainNewPacket();
if (pkt) {
uint8_t priority = cmd_frame[1];
if (tryParsePacket(pkt, &cmd_frame[2], len - 2)) {
sendPacket(pkt, priority, 0);
writeOKFrame();
} else {
releasePacket(pkt);
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else {
writeErrFrame(ERR_CODE_TABLE_FULL);
}
} else {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]);
}
}
static bool save_filter(const ContactInfo& c) {
return c.type != ADV_TYPE_NONE; // don't save the transient/anon entries
}
void MyMesh::saveContacts() {
const bool success = _store->saveContacts(this, save_filter);
dirty_contacts_expiry = (!success || _store->hasPendingContactWrites())
? futureMillis(1000) : 0;
}
void MyMesh::scheduleContactWrite(const ContactInfo& contact) {
if (_store->markContactDirty(contact)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
}
void MyMesh::scheduleContactWriteAfterRelease(const ContactInfo& contact) {
if (_store->releaseContact(contact)) {
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
}
}
bool MyMesh::applyAndSaveFemRxGain(bool enabled) {
if (!board.canControlLoRaFemLna()) return false;
const bool changed = board.isLoRaFemLnaEnabled() != enabled;
if (!board.setLoRaFemLnaEnabled(enabled)) return false;
if (changed && _radio_available) _radio->recalibrateNoiseFloor();
_prefs.radio_fem_rxgain = enabled ? 1 : 0;
_prefs.radio_fem_rxgain_override = 1;
savePrefs();
return true;
}
bool MyMesh::applyAndSaveFemTxGain(bool enabled) {
if (!board.canControlLoRaFemPaGain()) return false;
if (!board.setLoRaFemPaGainEnabled(enabled)) return false;
_prefs.radio_fem_txgain = enabled ? 1 : 0;
savePrefs();
return true;
}
bool MyMesh::applyAndSaveRxBoostedGain(bool enabled) {
if (!radio_driver.supportsRxBoostedGainMode()
|| (_radio_available && !radio_driver.setRxBoostedGainMode(enabled))) {
return false;
}
_prefs.rx_boosted_gain = enabled ? 1 : 0;
savePrefs();
return true;
}
bool MyMesh::applyAndSavePowerSaving(const char* value, char* reply) {
bool enabled;
if (strcmp(value, "on") == 0) {
enabled = true;
} else if (strcmp(value, "off") == 0) {
enabled = false;
} else {
strcpy(reply, "Error: use powersaving on or powersaving off");
return false;
}
_prefs.powersaving_enabled = enabled ? 1 : 0;
sensors.setPowerSavingEnabled(enabled);
savePrefs();
snprintf(reply, 160, "OK - powersaving %s", enabled ? "on" : "off");
return true;
}
#if defined(ESP32) && defined(WIFI_SSID)
void MyMesh::syncWiFiPowerSaving() {
const uint8_t mode = getCompanionWiFiPowerSave();
#ifdef WITH_MQTT_BRIDGE
_mqtt_prefs.wifi_power_save = mode;
#endif
#ifdef WITH_WEBCONFIG
if (_webconfig) _webconfig->reloadStandaloneWiFi();
#endif
}
void MyMesh::formatWiFiPowerSaving(char* reply, size_t reply_size) const {
if (!reply || reply_size == 0) return;
snprintf(reply, reply_size, "> %s", getCompanionWiFiPowerSaveName());
}
bool MyMesh::applyAndSaveWiFiPowerSaving(const char* value, char* reply,
size_t reply_size) {
if (!reply || reply_size == 0) return false;
uint8_t mode = mesh::wifi::kDefaultPowerSave;
if (value && strcmp(value, "min") == 0) {
mode = mesh::wifi::kPowerSaveMin;
} else if (value && strcmp(value, "none") == 0) {
mode = mesh::wifi::kPowerSaveNone;
} else if (value && strcmp(value, "max") == 0) {
mode = mesh::wifi::kPowerSaveMax;
} else {
snprintf(reply, reply_size,
"Error: power save must be none, min, or max");
return false;
}
const CompanionWiFiPowerSaveResult result =
setCompanionWiFiPowerSave(mode);
if (result == CompanionWiFiPowerSaveResult::BluetoothConflict) {
snprintf(reply, reply_size,
"Error: power save none is unavailable while Bluetooth is active");
return false;
}
if (result == CompanionWiFiPowerSaveResult::InvalidMode) {
snprintf(reply, reply_size,
"Error: power save must be none, min, or max");
return false;
}
if (result == CompanionWiFiPowerSaveResult::StorageError) {
snprintf(reply, reply_size, "Error: failed to save WiFi power save");
return false;
}
syncWiFiPowerSaving();
if (result == CompanionWiFiPowerSaveResult::Applied) {
snprintf(reply, reply_size, "OK - WiFi power save set to %s", value);
} else {
snprintf(reply, reply_size,
"OK - saved; WiFi power save applies on next connection");
}
return true;
}
#endif
bool MyMesh::applyAndSaveRxPowerSaving(const char* value, char* reply) {
if (!radio_driver.supportsRxPowerSaving()) {
strcpy(reply, "Error: RX power saving unsupported");
return false;
}
uint8_t enabled = _prefs.rx_powersaving_enabled;
uint8_t level = _prefs.rx_ps_level;
uint8_t preamble = _prefs.rx_ps_preamble;
uint32_t rx_us = _prefs.rx_ps_rx_us;
uint32_t sleep_us = _prefs.rx_ps_sleep_us;
bool level_requested = false;
bool manual_requested = false;
unsigned long parsed_level = 0;
unsigned long parsed_preamble = 0;
unsigned long parsed_rx = 0;
unsigned long parsed_sleep = 0;
char extra = 0;
if (strcmp(value, "off") == 0) {
enabled = 0;
} else if (strcmp(value, "on") == 0 || strcmp(value, "conservative") == 0) {
enabled = 1;
level = RX_POWERSAVING_CONSERVATIVE_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
} else if (strcmp(value, "balanced") == 0) {
enabled = 1;
level = RX_POWERSAVING_BALANCED_LEVEL;
preamble = RX_POWERSAVING_PROFILE_PREAMBLE;
level_requested = true;
} else if (sscanf(value, "level %lu preamble %lu %c",
&parsed_level, &parsed_preamble, &extra) == 2) {
if (parsed_level < 1 || parsed_level > 10
|| (parsed_preamble != 16 && parsed_preamble != 32)) {
strcpy(reply, "Error: level must be 1-10; preamble must be 16 or 32");
return false;
}
enabled = 1;
level = static_cast<uint8_t>(parsed_level);
preamble = static_cast<uint8_t>(parsed_preamble);
level_requested = true;
} else if (sscanf(value, "level %lu %c", &parsed_level, &extra) == 1
|| sscanf(value, "%lu %c", &parsed_level, &extra) == 1) {
if (parsed_level < 1 || parsed_level > 10) {
strcpy(reply, "Error: level must be 1-10");
return false;
}
enabled = 1;
level = static_cast<uint8_t>(parsed_level);
preamble = 0;
level_requested = true;
} else if (sscanf(value, "%lu %lu %c", &parsed_rx, &parsed_sleep, &extra) == 2) {
if (parsed_rx < RX_POWERSAVING_MIN_PERIOD_US
|| parsed_rx > RX_POWERSAVING_MAX_PERIOD_US
|| parsed_sleep < RX_POWERSAVING_MIN_PERIOD_US
|| parsed_sleep > RX_POWERSAVING_MAX_PERIOD_US) {
snprintf(reply, 160, "Error: RX/SLEEP must be %lu-%lu us",
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
return false;
}
enabled = 1;
rx_us = static_cast<uint32_t>(parsed_rx);
sleep_us = static_cast<uint32_t>(parsed_sleep);
level = 0;
preamble = 0;
manual_requested = true;
} else {
strcpy(reply, "Error: use off, level 1-10, or RX/SLEEP microseconds");
return false;
}
if (level_requested) {
if (level < 1 || level > 10
|| (preamble != 0 && preamble != 16 && preamble != 32)
|| !recalcRxPowerSavingFromLevel(level, _prefs.sf, _prefs.bw, preamble,
&rx_us, &sleep_us)) {
strcpy(reply, "Error: level must be 1-10; preamble must be auto, 16, or 32");
return false;
}
}
if ((manual_requested || enabled)
&& (!isValidRxPowerSavingPeriod(rx_us)
|| !isValidRxPowerSavingPeriod(sleep_us))) {
snprintf(reply, 160, "Error: RX/SLEEP must be %lu-%lu us",
(unsigned long)RX_POWERSAVING_MIN_PERIOD_US,
(unsigned long)RX_POWERSAVING_MAX_PERIOD_US);
return false;
}
if (_radio_available
&& !radio_driver.setRxPowerSaving(enabled != 0, rx_us, sleep_us)) {
strcpy(reply, "Error: radio busy; retry");
return false;
}
_prefs.rx_powersaving_enabled = enabled;
_prefs.rx_ps_rx_us = rx_us;
_prefs.rx_ps_sleep_us = sleep_us;
_prefs.rx_ps_level = level;
_prefs.rx_ps_preamble = preamble;
savePrefs();
snprintf(reply, 160, "OK - %s,%lu,%lu",
enabled ? "on" : "off", (unsigned long)rx_us,
(unsigned long)sleep_us);
return true;
}
#ifdef ENABLE_USB_INTERFACE
Stream& MyMesh::terminalOutput() {
return _terminal_output != NULL ? *_terminal_output : Serial;
}
void MyMesh::resetTerminalSession() {
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
clearTerminalLogin();
clearTerminalCommand();
clearTerminalTrace();
// A reply from an abandoned TCP or USB terminal session must not be shown
// to whichever terminal connects next. Keep the radio retry alive for the
// Binary Companion interface, but remove its old text-terminal ownership.
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++) {
expected_ack_table[i].terminal_origin = false;
}
}
void MyMesh::printTerminalBanner(bool show_binary_stop) {
Stream& output = terminalOutput();
#if defined(COMPANION_RADIO_FULL)
output.print("\r\n===== MeshCore Full Companion Terminal =====\r\n\r\n");
#else
output.print("\r\n===== MeshCore Companion Terminal =====\r\n\r\n");
#endif
output.printf("WELCOME %s\r\n", _prefs.node_name);
mesh::Utils::printHex(output, self_id.pub_key, PUB_KEY_SIZE);
output.printf("\r\nCompanion %s\r\n", FIRMWARE_VERSION);
output.print(" (enter 'help' for commands)\r\n");
if (show_binary_stop) {
output.print(" (+++MESHCORE-TERM-STOP returns to Binary mode)\r\n");
} else {
output.print(" (disconnect to close this TCP terminal)\r\n");
}
output.print("\r\n> ");
}
void MyMesh::enterTerminalMode() {
_terminal_mode = true;
_terminal_output = &Serial;
resetTerminalSession();
printTerminalBanner(true);
}
void MyMesh::exitTerminalMode() {
_terminal_mode = false;
resetTerminalSession();
if (_terminal_output == &Serial) _terminal_output = NULL;
}
#if defined(COMPANION_RADIO_FULL)
bool MyMesh::enterNetworkTerminalMode(Stream& output) {
if (_terminal_mode) return false;
_terminal_output = &output;
resetTerminalSession();
printTerminalBanner(false);
return true;
}
void MyMesh::exitNetworkTerminalMode(Stream& output) {
if (_terminal_mode || _terminal_output != &output) return;
resetTerminalSession();
_terminal_output = NULL;
}
bool MyMesh::isNetworkTerminalMode(const Stream& output) const {
return !_terminal_mode && _terminal_output == &output;
}
#endif
ContactInfo* MyMesh::getTerminalRecipient() {
if (!_terminal_recipient_set) return NULL;
ContactInfo* recipient = lookupContactByPubKey(_terminal_recipient_key, PUB_KEY_SIZE);
if (recipient == NULL) {
_terminal_recipient_set = false;
memset(_terminal_recipient_key, 0, sizeof(_terminal_recipient_key));
}
return recipient;
}
void MyMesh::printTerminalPath(const ContactInfo& recipient) {
terminalOutput().printf(" Path to %s: ", recipient.name);
if (recipient.out_path_len == OUT_PATH_UNKNOWN) {
terminalOutput().print("unknown (next send uses FLOOD)\r\n");
return;
}
if (!mesh::Packet::isValidPathLen(recipient.out_path_len)) {
terminalOutput().print("invalid\r\n");
return;
}
const uint8_t hash_size = (recipient.out_path_len >> 6) + 1;
const uint8_t hop_count = recipient.out_path_len & 63;
if (hop_count == 0) {
terminalOutput().print("direct (zero hop)\r\n");
return;
}
for (uint8_t hop = 0; hop < hop_count; hop++) {
if (hop != 0) terminalOutput().print(',');
mesh::Utils::printHex(terminalOutput(),
&recipient.out_path[(size_t)hop * hash_size],
hash_size);
}
terminalOutput().printf(" (%u %s, %u-byte hashes; used by DIRECT sends)\r\n",
(unsigned)hop_count, hop_count == 1 ? "hop" : "hops",
(unsigned)hash_size);
}
void MyMesh::handleTerminalPath(ContactInfo& recipient,
const char* path_spec) {
if (path_spec == NULL) {
printTerminalPath(recipient);
return;
}
mesh::cli::TerminalPath path;
const mesh::cli::TerminalPathParseResult parsed =
mesh::cli::parseTerminalPath(path_spec, _terminal_tmp_buf,
sizeof(recipient.out_path), 63, path);
switch (parsed) {
case mesh::cli::TerminalPathParseResult::Valid:
break;
case mesh::cli::TerminalPathParseResult::Missing:
terminalOutput().print(" ERROR: use path <direct|clear|hops separated by spaces or commas>\r\n");
return;
case mesh::cli::TerminalPathParseResult::InvalidPrefix:
terminalOutput().print(" ERROR: each path hop must be 2, 4, or 6 hex digits\r\n");
return;
case mesh::cli::TerminalPathParseResult::MixedPrefixSize:
terminalOutput().print(" ERROR: all path hops must use the same width\r\n");
return;
case mesh::cli::TerminalPathParseResult::InvalidSeparator:
terminalOutput().print(" ERROR: separate path hops with spaces or commas\r\n");
return;
case mesh::cli::TerminalPathParseResult::TooManyHops:
terminalOutput().print(" ERROR: path must contain at most 63 hops\r\n");
return;
case mesh::cli::TerminalPathParseResult::RouteTooLong:
terminalOutput().print(" ERROR: path is too long\r\n");
return;
}
memset(recipient.out_path, 0, sizeof(recipient.out_path));
if (path.mode == mesh::cli::TerminalPathMode::Clear) {
recipient.out_path_len = OUT_PATH_UNKNOWN;
} else {
recipient.out_path_len = mesh::Packet::copyPath(
recipient.out_path, _terminal_tmp_buf, path.encoded_len);
}
scheduleContactWrite(recipient);
printTerminalPath(recipient);
}
void MyMesh::rememberTerminalAck(ContactInfo& recipient, const char* text,
uint32_t message_timestamp, uint32_t expected_ack,
uint32_t est_timeout,
const uint8_t packet_retry_key[MAX_HASH_SIZE],
AckTableEntry* replacement_entry) {
if (expected_ack == 0) {
if (replacement_entry != NULL) {
clearExpectedAck(*replacement_entry, false);
}
return;
}
AckTableEntry& entry = replacement_entry != NULL
? *replacement_entry
: expected_ack_table[next_ack_idx];
clearExpectedAck(entry, false);
entry.msg_sent = _ms->getMillis();
entry.expires_at = futureMillis(est_timeout);
entry.ack = expected_ack;
entry.message_timestamp = message_timestamp;
entry.contact = &recipient;
mesh::Utils::sha256(entry.text_fingerprint, sizeof(entry.text_fingerprint),
recipient.id.pub_key, PUB_KEY_SIZE,
(const uint8_t*)text, strlen(text));
memcpy(entry.retry_key, packet_retry_key, sizeof(entry.retry_key));
entry.terminal_origin = true;
if (replacement_entry == NULL) {
next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE;
}
expireExpectedAcks();
}
void MyMesh::importTerminalCard(char* command) {
while (*command == ' ') command++;
if (strncmp(command, "meshcore://", 11) != 0) {
terminalOutput().print(" ERROR: invalid card format\r\n");
return;
}
char* encoded = command + 11;
char* end = encoded + strlen(encoded);
while (end > encoded && !mesh::Utils::isHexChar(end[-1])) {
*--end = 0;
}
size_t encoded_len = strlen(encoded);
if (encoded_len == 0 || (encoded_len & 1) != 0
|| encoded_len / 2 > sizeof(_terminal_tmp_buf)) {
terminalOutput().print(" ERROR: invalid card format\r\n");
return;
}
size_t raw_len = encoded_len / 2;
if (!mesh::Utils::fromHex(_terminal_tmp_buf, raw_len, encoded)
|| !importContact(_terminal_tmp_buf, raw_len)) {
terminalOutput().print(" ERROR: invalid card\r\n");
return;
}
terminalOutput().print(" OK - contact import queued\r\n");
}
void MyMesh::listTerminalChannels() {
bool found = false;
terminalOutput().print("Channels:\r\n");
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails channel;
if (getChannel(i, channel) && channel.name[0] != 0) {
terminalOutput().printf(" %d: %s\r\n", i, channel.name);
found = true;
}
}
if (!found) terminalOutput().print(" (none configured)\r\n");
}
void MyMesh::handleTerminalDisplayCommand(const char* arguments) {
mesh::TerminalDisplayCommand display_command;
const mesh::TerminalDisplayParseResult result =
mesh::parseTerminalDisplayCommand(arguments, display_command);
if (result == mesh::TerminalDisplayParseResult::InvalidCategory
|| result == mesh::TerminalDisplayParseResult::InvalidValue) {
terminalOutput().print(
" ERROR: use show [adverts|channels|emergency] [on|off]\r\n");
return;
}
if (result == mesh::TerminalDisplayParseResult::StatusAll) {
terminalOutput().printf(" Terminal display: adverts %s, channels %s, emergency %s\r\n",
_terminal_display.isEnabled(
mesh::TerminalDisplayCategory::Adverts) ? "on" : "off",
_terminal_display.isEnabled(
mesh::TerminalDisplayCategory::Channels) ? "on" : "off",
_terminal_display.isEnabled(
mesh::TerminalDisplayCategory::Emergency) ? "on" : "off");
return;
}
if (result == mesh::TerminalDisplayParseResult::Updated) {
_terminal_display.setEnabled(display_command.category,
display_command.enabled);
}
terminalOutput().printf(" Terminal display: %s %s\r\n",
mesh::terminalDisplayCategoryName(display_command.category),
_terminal_display.isEnabled(display_command.category)
? "on" : "off");
}
void MyMesh::sendTerminalChannelMessage(ChannelDetails& channel,
const char* text) {
const size_t prefix_len = strlen(_prefs.node_name) + 2; // "name: "
const size_t max_text_len = prefix_len < MAX_TEXT_LEN
? MAX_TEXT_LEN - prefix_len : 0;
const size_t text_len = text == NULL ? 0 : strlen(text);
if (text_len == 0) {
terminalOutput().print(" ERROR: message is empty\r\n");
} else if (text_len > max_text_len) {
terminalOutput().printf(" ERROR: message must be 1-%u UTF-8 bytes for this node name\r\n",
(unsigned)max_text_len);
} else if (sendGroupMessage(getRTCClock()->getCurrentTimeUnique(),
channel.channel, _prefs.node_name, text,
text_len)) {
terminalOutput().printf(" Sent to %s.\r\n", channel.name);
} else {
terminalOutput().print(" ERROR: unable to send\r\n");
}
}
void MyMesh::printTerminalSendStatus(const char* operation,
const ContactInfo& recipient, int result,
uint32_t timeout_millis) {
terminalOutput().printf(" %s sent to %s (", operation, recipient.name);
if (result == MSG_SEND_SENT_FLOOD) {
terminalOutput().print("FLOOD");
} else {
const uint8_t hop_count = recipient.out_path_len & 63;
if (hop_count == 0) {
terminalOutput().print("zero-hop DIRECT");
} else {
const uint8_t hash_size = (recipient.out_path_len >> 6) + 1;
terminalOutput().print("DIRECT via path ");
for (uint8_t hop = 0; hop < hop_count; hop++) {
if (hop != 0) terminalOutput().print(',');
mesh::Utils::printHex(
terminalOutput(), &recipient.out_path[(size_t)hop * hash_size], hash_size);
}
terminalOutput().printf("; %u %s, %u-byte hashes", (unsigned)hop_count,
hop_count == 1 ? "hop" : "hops", (unsigned)hash_size);
}
}
terminalOutput().printf(", timeout %lu ms)\r\n", (unsigned long)timeout_millis);
}
void MyMesh::clearTerminalLogin() {
if (_terminal_login_pending && pending_login != 0
&& memcmp(&pending_login, _terminal_login_key,
sizeof(_terminal_login_key)) == 0) {
pending_login = 0;
}
_terminal_login_pending = false;
memset(_terminal_login_key, 0, sizeof(_terminal_login_key));
_terminal_login_expires_at = 0;
_terminal_login_target[0] = 0;
}
void MyMesh::serviceTerminalLogin() {
if (!_terminal_login_pending) return;
const unsigned long now = _ms->getMillis();
if (_terminal_login_expires_at != now
&& !millisHasNowPassed(_terminal_login_expires_at)) {
return;
}
if (hasTerminalOutput()) {
terminalOutput().printf("\r\n ERROR: login to %s timed out (wrong password or no response).\r\n> ",
_terminal_login_target);
}
clearTerminalLogin();
}
void MyMesh::sendTerminalLogin(ContactInfo& recipient,
const char* password) {
serviceTerminalLogin();
if (_terminal_login_pending) {
terminalOutput().printf(" ERROR: login to %s is still pending\r\n",
_terminal_login_target);
return;
}
const size_t password_len = password == NULL ? 0 : strlen(password);
if (password_len == 0 || password_len > 15) {
terminalOutput().print(" ERROR: password must be 1-15 UTF-8 bytes\r\n");
return;
}
uint32_t est_timeout = 0;
const int result = sendLogin(recipient, password, est_timeout);
if (result == MSG_SEND_FAILED) {
terminalOutput().print(" ERROR: unable to send login\r\n");
return;
}
clearPendingReqs();
memcpy(&pending_login, recipient.id.pub_key, sizeof(pending_login));
_terminal_login_pending = true;
memcpy(_terminal_login_key, recipient.id.pub_key,
sizeof(_terminal_login_key));
const uint32_t timeout = est_timeout + est_timeout / 5;
_terminal_login_expires_at = futureMillis(timeout);
StrHelper::strzcpy(_terminal_login_target, recipient.name,
sizeof(_terminal_login_target));
printTerminalSendStatus("Login", recipient, result, timeout);
}
void MyMesh::clearTerminalCommand() {
_terminal_command.clear();
_terminal_command_target[0] = 0;
}
void MyMesh::serviceTerminalCommand() {
uint32_t elapsed_millis = 0;
if (!_terminal_command.expire(_ms->getMillis(), elapsed_millis)) return;
if (hasTerminalOutput()) {
terminalOutput().printf("\r\n ERROR: command to %s timed out after %lu ms.\r\n> ",
_terminal_command_target, (unsigned long)elapsed_millis);
}
_terminal_command_target[0] = 0;
}
void MyMesh::sendTerminalCommand(ContactInfo& recipient,
const char* command) {
serviceTerminalLogin();
if (_terminal_login_pending) {
terminalOutput().printf(" ERROR: login to %s is still pending\r\n",
_terminal_login_target);
return;
}
serviceTerminalCommand();
if (_terminal_command.isPending()) {
terminalOutput().printf(" ERROR: command to %s is still pending\r\n",
_terminal_command_target);
return;
}
// Be defensive if a caller passes the complete terminal line instead of
// the already-parsed argument. Never transmit the local "cmd" wrapper.
const char* on_air_command = command;
const char* nested_command = NULL;
if (mesh::cli::parseTerminalArgumentCommand(
command, "cmd", nested_command)
== mesh::cli::TerminalArgumentCommandMatch::Valid) {
on_air_command = nested_command;
}
const size_t command_len =
on_air_command == NULL ? 0 : strlen(on_air_command);
if (command_len == 0 || command_len > MAX_TEXT_LEN) {
terminalOutput().printf(" ERROR: remote command must be 1-%u UTF-8 bytes\r\n",
(unsigned)MAX_TEXT_LEN);
return;
}
const uint32_t logical_request_id =
getRTCClock()->getCurrentTimeUnique();
const uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
const uint32_t command_started_at = _ms->getMillis();
uint32_t est_timeout = 0;
const int result = sendCommandData(recipient, timestamp, 0, on_air_command,
est_timeout, logical_request_id);
if (result == MSG_SEND_FAILED) {
terminalOutput().print(" ERROR: unable to send remote command\r\n");
return;
}
_terminal_command.begin(recipient.id.pub_key, command_started_at,
est_timeout);
StrHelper::strzcpy(_terminal_command_target, recipient.name,
sizeof(_terminal_command_target));
printTerminalSendStatus("Remote command", recipient, result, est_timeout);
}
void MyMesh::clearTerminalTrace() {
_terminal_trace_pending = false;
_terminal_trace_hash_size = 0;
_terminal_trace_tag = 0;
_terminal_trace_auth = 0;
_terminal_trace_sent_at = 0;
_terminal_trace_expires_at = 0;
_terminal_trace_target[0] = 0;
}
void MyMesh::serviceTerminalTrace() {
if (!_terminal_trace_pending) return;
const unsigned long now = _ms->getMillis();
if (_terminal_trace_expires_at != now
&& !millisHasNowPassed(_terminal_trace_expires_at)) {
return;
}
if (hasTerminalOutput()) {
terminalOutput().printf("\r\n ERROR: trace to %s timed out.\r\n> ",
_terminal_trace_target);
}
clearTerminalTrace();
}
void MyMesh::sendTerminalTraceRoute(const uint8_t* route, uint8_t hash_size,
uint8_t hop_count, const char* target) {
serviceTerminalTrace();
if (_terminal_trace_pending) {
terminalOutput().printf(" ERROR: trace to %s is still pending\r\n",
_terminal_trace_target);
return;
}
if (route == NULL || hop_count == 0 || hop_count >= MAX_PATH_SIZE) {
terminalOutput().print(" ERROR: trace path must contain 1-63 prefixes\r\n");
return;
}
const size_t route_byte_len = static_cast<size_t>(hash_size) * hop_count;
if (route_byte_len > MAX_PACKET_PAYLOAD - 9) {
terminalOutput().print(" ERROR: trace path is too long\r\n");
return;
}
const uint8_t flags = mesh::traceFlagsForHashSize(hash_size);
if (flags == 0xFF) {
terminalOutput().print(" ERROR: trace hash size must be 1, 2, or 4 bytes\r\n");
return;
}
const uint32_t airtime = _radio->getEstAirtimeFor(9 + route_byte_len + 2);
if (airtime == 0) {
terminalOutput().print(" ERROR: unable to estimate trace timeout\r\n");
return;
}
const uint32_t base_timeout =
calcDirectTimeoutMillisFor(airtime, hop_count);
uint32_t trace_timeout = 0;
if (!mesh::calculateTerminalTraceTimeoutMillis(base_timeout,
trace_timeout)) {
terminalOutput().print(" ERROR: trace timeout is out of range\r\n");
return;
}
uint32_t tag = 0;
uint32_t auth = 0;
getRNG()->random((uint8_t*)&tag, sizeof(tag));
getRNG()->random((uint8_t*)&auth, sizeof(auth));
mesh::Packet* packet = createTrace(tag, auth, flags);
if (packet == NULL) {
terminalOutput().print(" ERROR: unable to allocate trace packet\r\n");
return;
}
if (!sendDirect(packet, route, static_cast<uint8_t>(route_byte_len))) {
terminalOutput().print(" ERROR: unable to send trace\r\n");
return;
}
_terminal_trace_pending = true;
_terminal_trace_hash_size = hash_size;
_terminal_trace_tag = tag;
_terminal_trace_auth = auth;
_terminal_trace_sent_at = _ms->getMillis();
_terminal_trace_expires_at = futureMillis(trace_timeout);
StrHelper::strzcpy(_terminal_trace_target, target,
sizeof(_terminal_trace_target));
terminalOutput().printf(" Trace sent to %s (%u route hops, timeout %lu ms)\r\n",
target, (unsigned)hop_count,
(unsigned long)trace_timeout);
}
void MyMesh::sendTerminalTrace(ContactInfo& recipient) {
if (recipient.out_path_len == OUT_PATH_UNKNOWN
|| !mesh::Packet::isValidPathLen(recipient.out_path_len)) {
terminalOutput().print(" ERROR: recipient has no valid direct path\r\n");
return;
}
const bool include_endpoint = recipient.type == ADV_TYPE_REPEATER
|| recipient.type == ADV_TYPE_ROOM;
mesh::RoundTripTracePath route;
if (!mesh::buildRoundTripTracePath(
recipient.out_path, recipient.out_path_len, recipient.id.pub_key,
include_endpoint, _terminal_tmp_buf, MAX_PACKET_PAYLOAD - 9,
route)) {
terminalOutput().print(" ERROR: recipient has no traceable round-trip path\r\n");
return;
}
if (route.hop_count >= MAX_PATH_SIZE) {
terminalOutput().print(" ERROR: round-trip trace path is too long\r\n");
return;
}
sendTerminalTraceRoute(_terminal_tmp_buf, route.hash_size, route.hop_count,
recipient.name);
}
void MyMesh::sendTerminalRawTrace(const char* arguments) {
mesh::RawTracePath route;
const mesh::RawTracePathParseResult parsed = mesh::parseRawTracePath(
arguments, _terminal_tmp_buf, MAX_PACKET_PAYLOAD - 9,
MAX_PATH_SIZE - 1, route);
switch (parsed) {
case mesh::RawTracePathParseResult::Valid:
break;
case mesh::RawTracePathParseResult::MissingHashSize:
case mesh::RawTracePathParseResult::MissingPrefixes:
terminalOutput().print(" ERROR: use trace path <1|2|4> <prefixes...>\r\n");
return;
case mesh::RawTracePathParseResult::InvalidHashSize:
terminalOutput().print(" ERROR: trace hash size must be 1, 2, or 4 bytes\r\n");
return;
case mesh::RawTracePathParseResult::InvalidPrefix:
terminalOutput().printf(" ERROR: every prefix must be exactly %u hex digits\r\n",
(unsigned)route.hash_size * 2);
return;
case mesh::RawTracePathParseResult::TooManyHops:
terminalOutput().print(" ERROR: trace path must contain at most 63 prefixes\r\n");
return;
case mesh::RawTracePathParseResult::RouteTooLong:
terminalOutput().print(" ERROR: trace path is too long\r\n");
return;
}
char target[32];
snprintf(target, sizeof(target), "raw %u-byte path",
(unsigned)route.hash_size);
sendTerminalTraceRoute(_terminal_tmp_buf, route.hash_size, route.hop_count,
target);
}
void MyMesh::handleTerminalCommand(char* command) {
while (*command == ' ' || *command == '\t') command++;
if (*command == 0) return;
mesh::cli::normalizeCommandVerb(command);
#if defined(COMPANION_RADIO_FULL)
char full_reply[160];
if (handleFullOtaCommand(command, full_reply, sizeof(full_reply))) {
terminalOutput().printf(" %s\r\n", full_reply);
return;
}
#endif
mesh::cli::TerminalChannelMessage channel_message;
const mesh::cli::TerminalChannelCommandMatch channel_match =
mesh::cli::parseTerminalChannelMessage(command, channel_message);
const char* login_password = NULL;
const mesh::cli::TerminalArgumentCommandMatch login_match =
mesh::cli::parseTerminalArgumentCommand(command, "login",
login_password);
const char* remote_command = NULL;
const mesh::cli::TerminalArgumentCommandMatch command_match =
mesh::cli::parseTerminalArgumentCommand(command, "cmd",
remote_command);
const char* path_spec = NULL;
const mesh::cli::TerminalArgumentCommandMatch path_match =
mesh::cli::parseTerminalArgumentCommand(command, "path", path_spec);
const char* recipient_prefix = NULL;
const mesh::cli::TerminalArgumentCommandMatch recipient_match =
mesh::cli::parseTerminalArgumentCommand(command, "to",
recipient_prefix);
const char* display_arguments = NULL;
const mesh::cli::TerminalArgumentCommandMatch display_match =
mesh::cli::parseTerminalArgumentCommand(command, "show",
display_arguments);
if (display_match != mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
handleTerminalDisplayCommand(
display_match == mesh::cli::TerminalArgumentCommandMatch::Valid
? display_arguments : NULL);
} else if (strcmp(command, "channels") == 0) {
listTerminalChannels();
} else if (channel_match != mesh::cli::TerminalChannelCommandMatch::NoMatch) {
if (channel_match != mesh::cli::TerminalChannelCommandMatch::Valid) {
terminalOutput().print(" ERROR: use channel <name-or-slot> <message>\r\n");
return;
}
ChannelDetails channel;
bool found = false;
size_t requested_index = 0;
if (mesh::cli::parseTerminalChannelIndex(
channel_message, MAX_GROUP_CHANNELS, requested_index)) {
found = getChannel((int)requested_index, channel)
&& channel.name[0] != 0;
} else {
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
if (getChannel(i, channel) && channel.name[0] != 0
&& mesh::cli::terminalChannelNameMatches(channel_message,
channel.name)) {
found = true;
break;
}
}
}
if (!found) {
terminalOutput().print(" ERROR: channel not found (use 'channels')\r\n");
} else {
sendTerminalChannelMessage(channel, channel_message.text);
}
} else if (strncmp(command, "send ", 5) == 0) {
ContactInfo* recipient = getTerminalRecipient();
const char* text = command + 5;
if (recipient == NULL) {
terminalOutput().print(" ERROR: no recipient selected (use 'to' first)\r\n");
} else if (*text == 0 || strlen(text) > MAX_TEXT_LEN) {
terminalOutput().printf(" ERROR: message must be 1-%u characters\r\n", (unsigned)MAX_TEXT_LEN);
} else {
uint32_t expected_ack = 0;
uint32_t est_timeout = 0;
uint32_t message_timestamp = getRTCClock()->getCurrentTimeUnique();
uint8_t text_fingerprint[MAX_HASH_SIZE] = { 0 };
uint8_t packet_retry_key[MAX_HASH_SIZE] = { 0 };
mesh::Utils::sha256(text_fingerprint, sizeof(text_fingerprint),
recipient->id.pub_key, PUB_KEY_SIZE,
(const uint8_t*)text, strlen(text));
AckTableEntry* replacement_entry =
findPendingTextMessage(text_fingerprint, message_timestamp);
int result = sendMessage(*recipient, message_timestamp, 0, text,
expected_ack, est_timeout, packet_retry_key,
NULL, text_fingerprint);
if (result == MSG_SEND_FAILED) {
terminalOutput().print(" ERROR: unable to send\r\n");
} else {
rememberTerminalAck(*recipient, text, message_timestamp, expected_ack,
est_timeout, packet_retry_key, replacement_entry);
terminalOutput().printf(" message sent - %s\r\n",
result == MSG_SEND_SENT_FLOOD ? "FLOOD" : "DIRECT");
}
}
} else if (strncmp(command, "public ", 7) == 0) {
ChannelDetails channel;
const char* text = command + 7;
if (!getChannel(0, channel) || channel.name[0] == 0) {
terminalOutput().print(" ERROR: Public channel is unavailable\r\n");
} else {
sendTerminalChannelMessage(channel, text);
}
} else if (strcmp(command, "list") == 0 || strncmp(command, "list ", 5) == 0) {
int count = command[4] == ' ' ? atoi(command + 5) : 0;
scanRecentContacts(count, this);
} else if (strcmp(command, "clock") == 0) {
DateTime dt(getRTCClock()->getCurrentTime());
terminalOutput().printf("%02d:%02d - %d/%d/%d UTC\r\n",
dt.hour(), dt.minute(), dt.day(), dt.month(), dt.year());
} else if (strncmp(command, "time ", 5) == 0) {
uint32_t timestamp = strtoul(command + 5, NULL, 10);
uint32_t current = getRTCClock()->getCurrentTime();
if (timestamp >= current) {
getRTCClock()->setCurrentTime(timestamp);
terminalOutput().print(" OK - clock set\r\n");
} else {
terminalOutput().print(" ERROR: clock cannot go backwards\r\n");
}
} else if (recipient_match
== mesh::cli::TerminalArgumentCommandMatch::Valid) {
ContactInfo* recipient = NULL;
if (strlen(recipient_prefix) < sizeof(ContactInfo::name)) {
recipient = searchContactsByPrefix(recipient_prefix);
}
if (recipient == NULL || recipient->type == ADV_TYPE_NONE) {
terminalOutput().print(" ERROR: name prefix not found\r\n");
} else {
memcpy(_terminal_recipient_key, recipient->id.pub_key, PUB_KEY_SIZE);
_terminal_recipient_set = true;
terminalOutput().printf(" Recipient %s selected\r\n", recipient->name);
}
} else if (recipient_match
== mesh::cli::TerminalArgumentCommandMatch::MissingArgument) {
ContactInfo* recipient = getTerminalRecipient();
if (recipient != NULL) {
terminalOutput().printf(" Current recipient: %s\r\n", recipient->name);
} else {
terminalOutput().print(" No recipient selected\r\n");
}
} else if (path_match
!= mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
ContactInfo* recipient = getTerminalRecipient();
if (recipient == NULL) {
terminalOutput().print(" ERROR: no recipient selected (use 'to' first)\r\n");
} else {
handleTerminalPath(
*recipient,
path_match == mesh::cli::TerminalArgumentCommandMatch::Valid
? path_spec : NULL);
}
} else if (login_match
!= mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
ContactInfo* recipient = getTerminalRecipient();
if (login_match
!= mesh::cli::TerminalArgumentCommandMatch::Valid) {
terminalOutput().print(" ERROR: use login <admin-password>\r\n");
} else if (recipient == NULL) {
terminalOutput().print(" ERROR: no recipient selected (use 'to' first)\r\n");
} else {
sendTerminalLogin(*recipient, login_password);
}
} else if (command_match
!= mesh::cli::TerminalArgumentCommandMatch::NoMatch) {
ContactInfo* recipient = getTerminalRecipient();
if (command_match
!= mesh::cli::TerminalArgumentCommandMatch::Valid) {
terminalOutput().print(" ERROR: use cmd <remote-command>\r\n");
} else if (recipient == NULL) {
terminalOutput().print(" ERROR: no recipient selected (use 'to' first)\r\n");
} else {
sendTerminalCommand(*recipient, remote_command);
}
} else if (strcmp(command, "trace path") == 0
|| strncmp(command, "trace path ", 11) == 0
|| strncmp(command, "trace path\t", 11) == 0) {
sendTerminalRawTrace(command + 10);
} else if (strcmp(command, "trace") == 0
|| strncmp(command, "trace ", 6) == 0) {
ContactInfo* recipient = NULL;
if (command[5] == ' ') {
const char* prefix = command + 6;
while (*prefix == ' ') prefix++;
if (*prefix != 0 && strlen(prefix) < sizeof(ContactInfo::name)) {
recipient = searchContactsByPrefix(prefix);
}
if (recipient == NULL || recipient->type == ADV_TYPE_NONE) {
terminalOutput().print(" ERROR: recipient prefix not found\r\n");
return;
}
} else {
recipient = getTerminalRecipient();
if (recipient == NULL) {
terminalOutput().print(" ERROR: no recipient selected (use 'to' first)\r\n");
return;
}
}
sendTerminalTrace(*recipient);
} else if (strcmp(command, "advert") == 0) {
terminalOutput().print(advert() ? " advert sent (zero hop)\r\n"
: " ERROR: unable to send advert\r\n");
} else if (strcmp(command, "reset path") == 0) {
ContactInfo* recipient = getTerminalRecipient();
if (recipient == NULL) {
terminalOutput().print(" ERROR: no recipient selected\r\n");
} else {
resetPathTo(*recipient);
scheduleContactWrite(*recipient);
terminalOutput().print(" Done.\r\n");
}
} else if (strcmp(command, "card") == 0) {
mesh::Packet* packet = _prefs.advert_loc_policy == ADVERT_LOC_NONE
? createSelfAdvert(_prefs.node_name)
: createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon);
if (packet == NULL) {
terminalOutput().print(" ERROR: unable to create card\r\n");
} else {
packet->header |= ROUTE_TYPE_FLOOD;
uint8_t raw_len = packet->writeTo(_terminal_tmp_buf);
releasePacket(packet);
terminalOutput().print("meshcore://");
mesh::Utils::printHex(terminalOutput(), _terminal_tmp_buf, raw_len);
terminalOutput().print("\r\n");
}
} else if (strncmp(command, "import ", 7) == 0) {
importTerminalCard(command + 7);
} else if (strcmp(command, "powersaving") == 0
|| strcmp(command, "get powersaving") == 0) {
terminalOutput().printf(" powersaving %s\r\n",
_prefs.powersaving_enabled ? "on" : "off");
#if MESH_USB_LOGGING_AVAILABLE
} else if (strcmp(command, "get usb.logging") == 0) {
terminalOutput().printf(" usb.logging %s\r\n",
mesh::isUsbLoggingEnabled() ? "on" : "off");
#endif
} else if (strncmp(command, "powersaving ", 12) == 0) {
char reply[160];
applyAndSavePowerSaving(command + 12, reply);
terminalOutput().printf(" %s\r\n", reply);
} else if (strcmp(command, "get radio.rxps") == 0) {
if (!radio_driver.supportsRxPowerSaving()) {
terminalOutput().print(" ERROR: RX power saving is unsupported on this radio\r\n");
} else {
terminalOutput().printf(" radio.rxps %s,level=%u,preamble=%u,rx=%lu,sleep=%lu\r\n",
_prefs.rx_powersaving_enabled ? "on" : "off",
(unsigned)_prefs.rx_ps_level,
(unsigned)_prefs.rx_ps_preamble,
(unsigned long)_prefs.rx_ps_rx_us,
(unsigned long)_prefs.rx_ps_sleep_us);
}
#if defined(ESP32) && defined(WIFI_SSID)
} else if (strcmp(command, "get wifi.powersave") == 0) {
char reply[160];
formatWiFiPowerSaving(reply, sizeof(reply));
terminalOutput().printf(" %s\r\n", reply);
#endif
} else if (strcmp(command, "get radio.rxgain") == 0) {
if (!radio_driver.supportsRxBoostedGainMode()) {
terminalOutput().print(" ERROR: RX boosted gain is unsupported on this radio\r\n");
} else {
terminalOutput().printf(" radio.rxgain %s\r\n",
_prefs.rx_boosted_gain ? "on" : "off");
}
} else if (strcmp(command, "get radio.fem.rxgain") == 0) {
if (!board.canControlLoRaFemLna()) {
terminalOutput().print(" ERROR: FEM RX gain control is unsupported on this board\r\n");
} else {
terminalOutput().printf(" FEM RX gain: %s\r\n",
board.isLoRaFemLnaEnabled() ? "on" : "off");
}
} else if (strcmp(command, "get radio.fem.txgain") == 0) {
if (!board.canControlLoRaFemPaGain()) {
terminalOutput().print(" ERROR: FEM TX gain control is unsupported on this board\r\n");
} else {
terminalOutput().printf(" FEM TX gain: %s\r\n",
board.isLoRaFemPaGainEnabled() ? "on" : "off");
}
} else if (strncmp(command, "set ", 4) == 0) {
const char* config = command + 4;
#if MESH_USB_LOGGING_AVAILABLE
if (strncmp(config, "usb.logging", 11) == 0
&& (config[11] == 0 || config[11] == ' '
|| config[11] == '\t')) {
const char* value = config + 11;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
terminalOutput().print(" ERROR: use set usb.logging <on|off>\r\n");
} else {
const bool enabled = strcmp(value, "on") == 0;
mesh::setUsbLoggingEnabled(enabled);
terminalOutput().printf(" OK - USB logging %s until reboot\r\n",
enabled ? "on" : "off");
}
} else
#endif
if (strncmp(config, "powersaving ", 12) == 0) {
char reply[160];
applyAndSavePowerSaving(config + 12, reply);
terminalOutput().printf(" %s\r\n", reply);
} else if (strncmp(config, "radio.rxps ", 11) == 0) {
char reply[160];
applyAndSaveRxPowerSaving(config + 11, reply);
terminalOutput().printf(" %s\r\n", reply);
#if defined(ESP32) && defined(WIFI_SSID)
} else if (strncmp(config, "wifi.powersave", 14) == 0
&& (config[14] == 0 || config[14] == ' '
|| config[14] == '\t')) {
const char* value = config + 14;
while (*value == ' ' || *value == '\t') value++;
char reply[160];
applyAndSaveWiFiPowerSaving(value, reply, sizeof(reply));
terminalOutput().printf(" %s\r\n", reply);
#endif
} else if (strncmp(config, "radio.rxgain", 12) == 0
&& (config[12] == 0 || config[12] == ' '
|| config[12] == '\t')) {
const char* value = config + 12;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
terminalOutput().print(" ERROR: use set radio.rxgain <on|off>\r\n");
} else if (!radio_driver.supportsRxBoostedGainMode()) {
terminalOutput().print(" ERROR: RX boosted gain is unsupported on this radio\r\n");
} else if (!applyAndSaveRxBoostedGain(strcmp(value, "on") == 0)) {
terminalOutput().print(" ERROR: radio busy; retry\r\n");
} else {
terminalOutput().printf(" OK - radio.rxgain %s\r\n", value);
}
} else if (strncmp(config, "af ", 3) == 0) {
_prefs.airtime_factor = constrain((float)atof(config + 3), 0.0f, 9.0f);
savePrefs();
terminalOutput().print(" OK\r\n");
} else if (strncmp(config, "name ", 5) == 0 && config[5] != 0) {
StrHelper::strncpy(_prefs.node_name, config + 5, sizeof(_prefs.node_name));
savePrefs();
terminalOutput().print(" OK\r\n");
} else if (strncmp(config, "lat ", 4) == 0) {
sensors.node_lat = constrain(atof(config + 4), -90.0, 90.0);
savePrefs();
terminalOutput().print(" OK\r\n");
} else if (strncmp(config, "lon ", 4) == 0) {
sensors.node_lon = constrain(atof(config + 4), -180.0, 180.0);
savePrefs();
terminalOutput().print(" OK\r\n");
} else if (strncmp(config, "tx ", 3) == 0) {
_prefs.tx_power_dbm = constrain(atoi(config + 3), -9, MAX_LORA_TX_POWER);
savePrefs();
terminalOutput().print(" OK - reboot to apply\r\n");
} else if (strncmp(config, "freq ", 5) == 0) {
_prefs.freq = constrain((float)atof(config + 5), 150.0f, 2500.0f);
savePrefs();
terminalOutput().print(" OK - reboot to apply\r\n");
} else if (strncmp(config, "radio.fem.rxgain", 16) == 0
&& (config[16] == 0 || config[16] == ' '
|| config[16] == '\t')) {
const char* value = config + 16;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
terminalOutput().print(" ERROR: use set radio.fem.rxgain <on|off>\r\n");
} else if (!board.canControlLoRaFemLna()) {
terminalOutput().print(" ERROR: FEM RX gain control is unsupported on this board\r\n");
} else if (!applyAndSaveFemRxGain(strcmp(value, "on") == 0)) {
terminalOutput().print(" ERROR: failed to apply FEM RX gain\r\n");
} else {
terminalOutput().printf(" OK - FEM RX gain %s\r\n", value);
}
} else if (strncmp(config, "radio.fem.txgain", 16) == 0
&& (config[16] == 0 || config[16] == ' '
|| config[16] == '\t')) {
const char* value = config + 16;
while (*value == ' ' || *value == '\t') value++;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
terminalOutput().print(" ERROR: use set radio.fem.txgain <on|off>\r\n");
} else if (!board.canControlLoRaFemPaGain()) {
terminalOutput().print(" ERROR: FEM TX gain control is unsupported on this board\r\n");
} else if (!applyAndSaveFemTxGain(strcmp(value, "on") == 0)) {
terminalOutput().print(" ERROR: failed to apply FEM TX gain\r\n");
} else {
terminalOutput().printf(" OK - FEM TX gain %s\r\n", value);
}
} else {
terminalOutput().printf(" ERROR: unknown setting: %s\r\n", config);
}
} else if (strcmp(command, "ver") == 0) {
terminalOutput().printf("Companion %s (protocol %u, build %s)\r\n",
FIRMWARE_VERSION, (unsigned)FIRMWARE_VER_CODE, FIRMWARE_BUILD_DATE);
} else if (strcmp(command, "help") == 0) {
terminalOutput().print("Commands:\r\n");
terminalOutput().print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
terminalOutput().print(" powersaving [on|off]\r\n");
#if MESH_USB_LOGGING_AVAILABLE
terminalOutput().print(" get usb.logging\r\n");
terminalOutput().print(" set usb.logging <on|off>\r\n");
#endif
#if defined(ESP32) && defined(WIFI_SSID)
terminalOutput().print(" get wifi.powersave\r\n");
terminalOutput().print(" set wifi.powersave <none|min|max>\r\n");
#ifdef WITH_WEBCONFIG
terminalOutput().print(" get wifi.{ssid|status|cli}\r\n");
terminalOutput().print(" set wifi.ssid <network name>\r\n");
terminalOutput().print(" set wifi.pwd <password>\r\n");
terminalOutput().print(" set wifi.cli <on|off>\r\n");
terminalOutput().print(" get webui\r\n");
terminalOutput().print(" set webui <on|off>\r\n");
terminalOutput().print(" start webconfig [ap]\r\n");
terminalOutput().print(" stop webconfig\r\n");
#endif
#endif
terminalOutput().print(" get radio.rxps\r\n");
terminalOutput().print(" set radio.rxps <off|on|level 1-10 [preamble 16|32]|rx_us sleep_us>\r\n");
terminalOutput().print(" get radio.rxgain\r\n");
terminalOutput().print(" set radio.rxgain <on|off>\r\n");
terminalOutput().print(" get radio.fem.rxgain\r\n");
terminalOutput().print(" set radio.fem.rxgain <on|off>\r\n");
terminalOutput().print(" get radio.fem.txgain\r\n");
terminalOutput().print(" set radio.fem.txgain <on|off>\r\n");
terminalOutput().print(" card\r\n");
terminalOutput().print(" import <meshcore://card>\r\n");
terminalOutput().print(" clock\r\n");
terminalOutput().print(" time <epoch-seconds>\r\n");
terminalOutput().print(" list [n]\r\n");
terminalOutput().print(" show [adverts|channels|emergency] [on|off]\r\n");
terminalOutput().print(" to [recipient name or prefix]\r\n");
terminalOutput().print(" path [direct|clear|hops separated by spaces or commas]\r\n");
terminalOutput().print(" send <text>\r\n");
terminalOutput().print(" login <admin-password>\r\n");
terminalOutput().print(" cmd <remote-command>\r\n");
terminalOutput().print(" trace [recipient name or prefix]\r\n");
terminalOutput().print(" trace path <1|2|4> <prefixes...>\r\n");
terminalOutput().print(" advert\r\n");
terminalOutput().print(" reset path\r\n");
terminalOutput().print(" public <text>\r\n");
terminalOutput().print(" channels\r\n");
terminalOutput().print(" channel <name-or-slot> <text>\r\n");
#if defined(COMPANION_RADIO_FULL)
terminalOutput().print(" tempradio [freq,bw,sf,cr,minutes]\r\n");
terminalOutput().print(" normalradio\r\n");
terminalOutput().print(" ota {status|ls|announce|folder|config|...}\r\n");
#endif
terminalOutput().print(" ver\r\n");
if (_terminal_mode) {
terminalOutput().print(" +++MESHCORE-TERM-STOP\r\n");
} else {
terminalOutput().print(" disconnect (closes the TCP terminal)\r\n");
}
} else {
terminalOutput().printf(" ERROR: unknown command: %s\r\n", command);
}
}
#endif
void MyMesh::enterCLIRescue() {
_cli_rescue = true;
cli_command[0] = 0;
Serial.println("========= CLI Rescue =========");
}
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;
}
Serial.print(c); // echo
}
if (len == sizeof(cli_command)-1) { // command buffer full
cli_command[sizeof(cli_command)-1] = '\r';
}
if (len > 0 && cli_command[len - 1] == '\r') { // received complete line
cli_command[len - 1] = 0; // replace newline with C string null terminator
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) {
// get path from command e.g: "ls /adafruit"
const char *path = &cli_command[3];
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);
// 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();
}
}
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) {
// 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;
}
// log file content as hex
File file = _store->openRead(path);
if (is_fs2 == true) {
file = _store->openRead(_store->getSecondaryFS(), path);
}
if(file){
// get file content
int file_size = file.available();
uint8_t buffer[file_size];
file.read(buffer, file_size);
// print hex
mesh::Utils::printHex(Serial, buffer, file_size);
Serial.print("\n");
file.close();
}
} 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
} else {
Serial.println(" Error: unknown command");
}
cli_command[0] = 0; // reset command buffer
}
}
void MyMesh::checkSerialInterface() {
size_t len = _serial->checkRecvFrame(cmd_frame);
if (!_serial->isConnected()) {
stopContactsIterator();
cancelPendingRadioParamApply();
return;
}
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
}
}
} 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);
stopContactsIterator();
}
//} else if (!_serial->isWriteBusy()) {
// checkConnections(); // TODO - deprecate the 'Connections' stuff
}
}
void MyMesh::loop() {
#if defined(COMPANION_RADIO_FULL)
serviceTempRadio();
#endif
BaseChatMesh::loop();
#ifdef ENABLE_USB_INTERFACE
serviceTerminalLogin();
serviceTerminalCommand();
serviceTerminalTrace();
#endif
if (!command_radio_apply_pending && saved_radio_apply_pending && !hasOutbound()
#if defined(COMPANION_RADIO_FULL)
&& !_temp_radio_applied && _temp_radio_set_at == 0
&& _temp_radio_revert_at == 0
#endif
&& (!radio_apply_retry_at || millisHasNowPassed(radio_apply_retry_at))) {
// A power-saving wake can enter begin() with a complete packet already
// waiting. Preserve that packet, then apply the persisted radio settings
// once the receive/response path is idle.
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
if (applySavedRadioParams()) {
radio_driver.setTxPower(_prefs.tx_power_dbm);
saved_radio_apply_pending = false;
radio_apply_retry_at = 0;
radio_apply_failures = 0;
} else {
radio_apply_retry_at = futureMillis(nextRadioApplyRetryDelay(radio_apply_failures));
}
}
if (has_next_ack_expiry
&& (next_ack_expiry == _ms->getMillis() || millisHasNowPassed(next_ack_expiry))) {
expireExpectedAcks();
}
if (emergency_client_repeat_packet != NULL && millisHasNowPassed(emergency_client_repeat_send_at)) {
mesh::Packet* pkt = emergency_client_repeat_packet;
emergency_client_repeat_packet = NULL;
sendPacket(pkt, 1, 0);
}
if (_cli_rescue) {
checkCLIRescueCmd();
} else {
checkSerialInterface();
}
servicePendingRadioParamApply();
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
const bool success = _store->serviceContactWrites(this, save_filter);
if (!success || _store->hasPendingContactWrites()) {
dirty_contacts_expiry = futureMillis(success ? CONTACT_PAGE_WRITE_GAP : 1000);
} else {
dirty_contacts_expiry = 0;
}
}
#ifdef DISPLAY_CLASS
if (_ui) _ui->setHasConnection(_serial->isConnected());
#endif
}
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 {
if (_radio_available
&& (radio_driver.isWatchdogObserving()
|| radio_driver.isCalibratingNoiseFloor())) return true;
return (_serial != NULL && _serial->hasPendingIO())
|| (_iter_started && _serial != NULL && _serial->isConnected())
|| command_radio_apply_pending
|| hasQueuedWorkDue() || hasRetryWorkDue()
|| (saved_radio_apply_pending
&& (!radio_apply_retry_at || millisHasNowPassed(radio_apply_retry_at)))
|| (dirty_contacts_expiry != 0 && millisHasNowPassed(dirty_contacts_expiry))
|| (emergency_client_repeat_packet != NULL
&& millisHasNowPassed(emergency_client_repeat_send_at))
#if defined(COMPANION_RADIO_FULL)
|| (_temp_radio_set_at != 0 && millisHasNowPassed(_temp_radio_set_at))
|| (_temp_radio_revert_at != 0 && millisHasNowPassed(_temp_radio_revert_at))
|| (_temp_radio_retry_at != 0 && millisHasNowPassed(_temp_radio_retry_at))
#endif
;
}