feat: add Bluetooth-controlled LoRa OTA

Add the framed BLE mOTA transport and companion control path, complete the full-profile build and runtime fixes, and document and test the OTA workflow.
This commit is contained in:
mikecarper committed 2026-08-27 01:22:18 -07:00
1 parent 91870e4bb0
commit e5fd77e53f
79 files changed
+3178 -304

No files matched your search

@@ -71,5 +71,10 @@ public:
int channel_idx = -1,
const char* channel_name = nullptr) = 0;
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual bool supportsDisplayRotation() const { return false; }
virtual bool setDisplayRotationDegrees(uint16_t degrees) {
(void)degrees;
return false;
}
virtual void loop() = 0;
};
@@ -46,6 +46,9 @@
#ifndef COMPANION_FEATURE_USB_MOTA_SOURCE
#define COMPANION_FEATURE_USB_MOTA_SOURCE 0
#endif
#ifndef COMPANION_FEATURE_BLE_MOTA_SOURCE
#define COMPANION_FEATURE_BLE_MOTA_SOURCE 0
#endif
#ifndef COMPANION_FEATURE_DEDICATED_USB_LOGGING
#define COMPANION_FEATURE_DEDICATED_USB_LOGGING 0
#endif
@@ -70,6 +73,12 @@
#error "COMPANION_FEATURE_USB_MOTA_SOURCE requires nRF52 USB folder seeding"
#endif
#if COMPANION_FEATURE_BLE_MOTA_SOURCE \
&& !(defined(COMPANION_RADIO_FULL) && defined(NRF52_PLATFORM) \
&& defined(BLE_PIN_CODE) && defined(ENABLE_OTA))
#error "COMPANION_FEATURE_BLE_MOTA_SOURCE requires an nRF52 Full Companion with BLE and OTA"
#endif
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS && !defined(ESP32_PLATFORM)
#error "COMPANION_FEATURE_MEMORY_DIAGNOSTICS requires ESP32"
#endif
+9
View File
@@ -355,6 +355,11 @@ void DataStore::loadPrefsInt(const char *filename, CompanionNodePrefs& _prefs, d
if (file.available() >= (int)sizeof(_prefs.bluetooth_name)) {
file.read((uint8_t *)_prefs.bluetooth_name,
sizeof(_prefs.bluetooth_name)); // 141
if (file.available()
>= (int)sizeof(_prefs.display_rotation_degrees)) {
file.read((uint8_t *)&_prefs.display_rotation_degrees,
sizeof(_prefs.display_rotation_degrees));
}
}
}
}
@@ -434,6 +439,10 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
== sizeof(_prefs.usb_logging_enabled); // 140
success = success && file.write((uint8_t *)_prefs.bluetooth_name,
sizeof(_prefs.bluetooth_name)) == sizeof(_prefs.bluetooth_name); // 141
success = success && file.write(
(uint8_t *)&_prefs.display_rotation_degrees,
sizeof(_prefs.display_rotation_degrees))
== sizeof(_prefs.display_rotation_degrees);
#if defined(NRF52_PLATFORM)
success = file.commit(success);
+167 -27
View File
@@ -1386,7 +1386,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_clock_sync(radio, _clock_sync_millis, rtc, _clock_sync_acl, sensors,
_prefs.airtime_factor),
#endif
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_serial(NULL), _mota_source_control(NULL),
telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) {
_iter_started = false;
_cli_rescue = false;
#ifdef ENABLE_USB_INTERFACE
@@ -1412,9 +1413,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
command_radio_cr = 0;
command_radio_repeat = 0;
command_radio_apply_deadline = 0;
#if MESH_USB_LOGGING_AVAILABLE
_usb_logging_reboot_at = 0;
#endif
_scheduled_reboot_at = 0;
#if COMPANION_FEATURE_TEMP_RADIO
_temp_radio_set_at = 0;
_temp_radio_revert_at = 0;
@@ -1477,10 +1476,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.powersaving_policy_version = 0;
_prefs.wifi_enabled = 1;
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
#if defined(COMPANION_RADIO_FULL)
// Keep Full Companion's primary stream exclusively framed on a fresh
// install. Dual-CDC builds can add a diagnostics port; single-TTY builds
// switch the primary stream into the text terminal before emitting logs.
_prefs.display_rotation_degrees = 0;
#if defined(ENABLE_USB_INTERFACE)
// Keep a USB Companion's primary stream exclusively framed on a fresh
// install. Full dual-CDC builds can add a diagnostics port; single-TTY
// builds switch the primary stream into the text terminal before logs.
_prefs.usb_logging_enabled = 0;
#else
_prefs.usb_logging_enabled = 1;
@@ -1567,6 +1567,14 @@ void MyMesh::begin(bool has_display, bool radio_available) {
if (bluetooth_name_repaired) {
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
}
const bool display_rotation_repaired =
_prefs.display_rotation_degrees != 0
&& _prefs.display_rotation_degrees != 90
&& _prefs.display_rotation_degrees != 180
&& _prefs.display_rotation_degrees != 270;
if (display_rotation_repaired) {
_prefs.display_rotation_degrees = 0;
}
// sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
@@ -1601,7 +1609,8 @@ void MyMesh::begin(bool has_display, bool radio_available) {
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 || bluetooth_name_repaired) {
if (power_saving_default_migrated || bluetooth_name_repaired
|| display_rotation_repaired) {
_store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
}
#if MESH_USB_LOGGING_AVAILABLE
@@ -1864,6 +1873,57 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (!command || !reply || reply_size == 0) return false;
while (*command == ' ') command++;
if (strcmp(command, "get display.rotation") == 0) {
if (_ui == NULL || !_ui->supportsDisplayRotation()) {
snprintf(reply, reply_size, "Error: display rotation is unsupported");
} else if (_prefs.display_rotation_degrees == 0) {
snprintf(reply, reply_size, "display.rotation default");
} else {
snprintf(reply, reply_size, "display.rotation %u",
(unsigned)_prefs.display_rotation_degrees);
}
return true;
}
if (strncmp(command, "set display.rotation", 20) == 0
&& (command[20] == 0 || command[20] == ' '
|| command[20] == '\t')) {
const char* value = command + 20;
while (*value == ' ' || *value == '\t') value++;
char* end = NULL;
const unsigned long degrees = strtoul(value, &end, 10);
while (end != NULL && (*end == ' ' || *end == '\t')) end++;
const bool valid = value[0] != 0 && end != NULL && *end == 0
&& (degrees == 0 || degrees == 90 || degrees == 180
|| degrees == 270);
if (!valid) {
snprintf(reply, reply_size,
"Error: use set display.rotation <0|90|180|270>");
} else if (_ui == NULL || !_ui->supportsDisplayRotation()) {
snprintf(reply, reply_size, "Error: display rotation is unsupported");
} else {
const uint16_t previous = _prefs.display_rotation_degrees;
if (!_ui->setDisplayRotationDegrees((uint16_t)degrees)) {
snprintf(reply, reply_size, "Error: display rotation failed");
} else {
_prefs.display_rotation_degrees = (uint16_t)degrees;
if (!savePrefs()) {
_prefs.display_rotation_degrees = previous;
_ui->setDisplayRotationDegrees(previous);
snprintf(reply, reply_size,
"Error: display rotation changed but save failed");
} else if (degrees == 0) {
snprintf(reply, reply_size,
"OK - display rotation reset to board default");
} else {
snprintf(reply, reply_size, "OK - display rotation %lu",
degrees);
}
}
}
return true;
}
if (strcmp(command, "get bluetooth.name") == 0
|| strcmp(command, "get ble.name") == 0) {
formatBluetoothNameStatus(reply, reply_size);
@@ -2074,11 +2134,10 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
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
uint8_t sf = 0, cr = 0;
uint32_t timeout_mins = 0;
if (!mesh::cli::parseTemporaryRadioTupleStrict(
command + 10, freq, bw, sf, cr, timeout_mins)
|| !isfinite(freq) || !isfinite(bw)
|| freq < 150.0f || freq > 2500.0f
|| !isFullCompanionBandwidth(bw)
@@ -2088,8 +2147,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
"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);
scheduleTempRadio(freq, bw, sf, cr, timeout_mins, reply, reply_size);
return true;
}
@@ -2448,17 +2506,16 @@ void MyMesh::execCommand(char* cmd, char* reply) {
}
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
uint8_t sf, cr;
if (!mesh::cli::parseRadioTupleStrict(value, freq, bw, sf, cr)
|| !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);
_prefs.sf = sf;
_prefs.cr = cr;
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
@@ -3825,6 +3882,85 @@ void MyMesh::handleCmdFrame(size_t len) {
writeOKFrame();
}
}
} else if (cmd_frame[0]
== mesh::companion::CMD_EXEC_LOCAL_OTA_CONTROL) {
#if defined(COMPANION_RADIO_FULL)
const size_t command_len = len - 1;
if (!mesh::companion::isBleOtaControlCommandAllowed(
&cmd_frame[1], command_len)) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
char command[MAX_FRAME_SIZE] = {0};
memcpy(command, &cmd_frame[1], command_len);
char reply[MAX_FRAME_SIZE] = {0};
if (!handleLocalControlCommand(command, reply, sizeof(reply))) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
out_frame[0] = RESP_CODE_OK;
const size_t reply_len = strnlen(reply, MAX_FRAME_SIZE - 2);
out_frame[1] = static_cast<uint8_t>(reply_len);
memcpy(&out_frame[2], reply, reply_len);
_serial->writeFrame(out_frame, 2 + reply_len);
}
}
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} else if (cmd_frame[0] == mesh::companion::CMD_BLE_MOTA_SOURCE) {
#if defined(COMPANION_RADIO_FULL)
if (len != 2) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (_mota_source_control == NULL) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
const mesh::companion::MotaSourceAction action =
static_cast<mesh::companion::MotaSourceAction>(cmd_frame[1]);
char control_reply[96] = {0};
bool action_ok = true;
if (action == mesh::companion::MotaSourceAction::Start) {
action_ok = _mota_source_control->start(control_reply,
sizeof(control_reply));
} else if (action == mesh::companion::MotaSourceAction::Stop) {
action_ok = _mota_source_control->stop(control_reply,
sizeof(control_reply));
} else if (action != mesh::companion::MotaSourceAction::Status) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
return;
}
if (!action_ok) {
writeErrFrame(ERR_CODE_BAD_STATE);
} else {
const mesh::companion::MotaSourceStatus status =
_mota_source_control->status();
uint8_t flags = 0;
if (status.channel_ready) {
flags |= mesh::companion::MOTA_SOURCE_FLAG_CHANNEL_READY;
}
if (status.attached) {
flags |= mesh::companion::MOTA_SOURCE_FLAG_ATTACHED;
}
if (status.another_link_active) {
flags |=
mesh::companion::MOTA_SOURCE_FLAG_ANOTHER_LINK_ACTIVE;
}
out_frame[0] = RESP_CODE_OK;
out_frame[1] = cmd_frame[1];
out_frame[2] = flags;
out_frame[3] = static_cast<uint8_t>(status.offered & 0xFF);
out_frame[4] = static_cast<uint8_t>(status.offered >> 8);
out_frame[5] = static_cast<uint8_t>(status.advertised & 0xFF);
out_frame[6] = static_cast<uint8_t>(status.advertised >> 8);
out_frame[7] = static_cast<uint8_t>(status.packets_sent & 0xFF);
out_frame[8] = static_cast<uint8_t>((status.packets_sent >> 8) & 0xFF);
out_frame[9] = static_cast<uint8_t>((status.packets_sent >> 16) & 0xFF);
out_frame[10] = static_cast<uint8_t>((status.packets_sent >> 24) & 0xFF);
_serial->writeFrame(out_frame, 11);
}
}
#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];
@@ -5313,7 +5449,7 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().printf(
" OK - USB logging %s (saved); rebooting to change USB interfaces\r\n",
enabled ? "on" : "off");
_usb_logging_reboot_at = futureMillis(1000);
_scheduled_reboot_at = futureMillis(1000);
} else {
terminalOutput().printf(
" OK - USB logging %s (saved); reboot required to change USB interfaces\r\n",
@@ -5413,11 +5549,16 @@ void MyMesh::handleTerminalCommand(char* command) {
} else {
terminalOutput().printf(" ERROR: unknown setting: %s\r\n", config);
}
} else if (strcmp(command, "reboot") == 0) {
terminalOutput().print(" OK - rebooting in 1 second\r\n");
_scheduled_reboot_at = futureMillis(1000);
} 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(" get display.rotation\r\n");
terminalOutput().print(" set display.rotation <0|90|180|270>\r\n");
terminalOutput().print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
terminalOutput().print(" get bluetooth.name\r\n");
terminalOutput().print(" set bluetooth.name <name|default>\r\n");
@@ -5476,6 +5617,7 @@ void MyMesh::handleTerminalCommand(char* command) {
terminalOutput().print(" ota {status|ls|announce|folder|config|...}\r\n");
#endif
#endif
terminalOutput().print(" reboot\r\n");
terminalOutput().print(" ver\r\n");
if (_terminal_mode) {
terminalOutput().print(" +++MESHCORE-TERM-STOP\r\n");
@@ -5700,14 +5842,12 @@ void MyMesh::checkSerialInterface() {
}
void MyMesh::loop() {
#if MESH_USB_LOGGING_AVAILABLE
if (_usb_logging_reboot_at != 0
&& millisHasNowPassed(_usb_logging_reboot_at)) {
_usb_logging_reboot_at = 0;
if (_scheduled_reboot_at != 0
&& millisHasNowPassed(_scheduled_reboot_at)) {
_scheduled_reboot_at = 0;
board.reboot();
return;
}
#endif
#if COMPANION_FEATURE_TEMP_RADIO
serviceTempRadio();
#endif
+9 -4
View File
@@ -6,7 +6,7 @@
#include "CompanionFeatures.h"
/*------------ Frame Protocol --------------*/
#define FIRMWARE_VER_CODE 13
#define FIRMWARE_VER_CODE 14
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "14 Aug 2026"
@@ -40,6 +40,7 @@
#include <RTClib.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/BaseSerialInterface.h>
#include <helpers/CompanionMotaControl.h>
#include <helpers/IdentityStore.h>
#include <helpers/LogicalMessageCache.h>
#include <helpers/SimpleMeshTables.h>
@@ -137,6 +138,9 @@ public:
CompanionNodePrefs *getNodePrefs();
uint32_t getBLEPin();
int getOfflineQueueCapacity() const;
void setMotaSourceControl(mesh::companion::MotaSourceControl* control) {
_mota_source_control = control;
}
#if defined(WITH_MQTT_BRIDGE) && defined(ESP32_PLATFORM) && defined(WIFI_SSID)
void serviceMQTT(const char* wifi_ssid, const char* wifi_password);
@@ -397,6 +401,7 @@ private:
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
mesh::companion::MotaSourceControl* _mota_source_control;
AbstractUITask* _ui;
ContactsIterator _iter;
@@ -437,9 +442,9 @@ private:
uint8_t command_radio_cr;
uint8_t command_radio_repeat;
unsigned long command_radio_apply_deadline;
#if MESH_USB_LOGGING_AVAILABLE
unsigned long _usb_logging_reboot_at;
#endif
// Deferred so USB/TCP terminals can transmit the acknowledgement before
// the transport disappears. Also used by USB interface changes.
unsigned long _scheduled_reboot_at;
#if COMPANION_FEATURE_TEMP_RADIO
unsigned long _temp_radio_set_at;
unsigned long _temp_radio_revert_at;
+1
View File
@@ -57,6 +57,7 @@ struct CompanionNodePrefs { // persisted to file
uint8_t usb_logging_enabled; // live USB packet/debug output
char bluetooth_name[mesh::companion::BLUETOOTH_NAME_SIZE];
// exact BLE name; empty uses BLE_NAME_PREFIX + node_name
uint16_t display_rotation_degrees; // 0=board default; otherwise 90/180/270
// Keep the upstream repeat API while retaining the existing binary prefs
// layout used by this branch.
+120 -3
View File
@@ -147,6 +147,116 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store
#endif
);
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
#include <helpers/ota/MotaSourceSerial.h>
#include <helpers/ota/OtaContext.h>
class Nrf52BleMotaSourceControl : public mesh::companion::MotaSourceControl {
public:
Nrf52BleMotaSourceControl()
: _source(bluetooth_interface.motaStream(),
mesh::ota::MotaStreamWritePolicy::NoFlush, 3000),
_packets_sent_at_start(0), _last_packets_sent(0) {}
bool start(char* reply, size_t reply_size) override {
if (!reply || reply_size == 0) return false;
if (!bluetooth_interface.isMotaChannelReady()) {
snprintf(reply, reply_size,
"ERR subscribe to the Bluetooth mOTA request characteristic first");
return false;
}
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
if (context.folder_active
&& context.folderLink() != mesh::ota::OtaContext::FOLDER_LINK_BLE) {
snprintf(reply, reply_size, "ERR mOTA source already uses %s",
mesh::ota::OtaContext::folderLinkName(context.folderLink()));
return false;
}
_packets_sent_at_start = context.manager.packetsSent();
_last_packets_sent = 0;
bluetooth_interface.setMotaStreamActive(true);
if (!context.attach_folder_source(
&_source, mesh::ota::OtaContext::FOLDER_LINK_BLE, "ble",
reply, reply_size)) {
bluetooth_interface.setMotaStreamActive(false);
return false;
}
context.manager.announce();
mesh::usbLoggingPort().println("Bluetooth mOTA source attached");
return true;
}
bool stop(char* reply, size_t reply_size) override {
if (!reply || reply_size == 0) return false;
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
bluetooth_interface.setMotaStreamActive(false);
if (context.folder_active
&& context.folderLink() == mesh::ota::OtaContext::FOLDER_LINK_BLE) {
context.detach_folder();
context.manager.announce();
mesh::usbLoggingPort().println("Bluetooth mOTA source detached");
}
_last_packets_sent = context.manager.packetsSent()
- _packets_sent_at_start;
snprintf(reply, reply_size, "OK Bluetooth mOTA source stopped");
return true;
}
mesh::companion::MotaSourceStatus status() const override {
const mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
mesh::companion::MotaSourceStatus result;
result.channel_ready = bluetooth_interface.isMotaChannelReady();
result.attached = context.folder_active
&& context.folderLink() == mesh::ota::OtaContext::FOLDER_LINK_BLE
&& bluetooth_interface.isMotaStreamActive();
result.another_link_active = context.folder_active
&& context.folderLink() != mesh::ota::OtaContext::FOLDER_LINK_BLE;
if (result.attached) {
context.folderSourceStats(result.offered, result.advertised);
result.packets_sent = context.manager.packetsSent()
- _packets_sent_at_start;
} else {
result.packets_sent = _last_packets_sent;
}
return result;
}
void loop() {
mesh::ota::OtaContext& context = mesh::ota::ota_ctx();
const bool owns_folder = context.folder_active
&& context.folderLink() == mesh::ota::OtaContext::FOLDER_LINK_BLE;
if (!owns_folder) {
if (bluetooth_interface.isMotaStreamActive()) {
bluetooth_interface.setMotaStreamActive(false);
}
return;
}
if (bluetooth_interface.isMotaChannelReady()
&& bluetooth_interface.isMotaStreamActive()) {
return;
}
bluetooth_interface.setMotaStreamActive(false);
context.detach_folder();
context.manager.announce();
_last_packets_sent = context.manager.packetsSent()
- _packets_sent_at_start;
mesh::usbLoggingPort().println(
"Bluetooth mOTA source disconnected and was detached");
}
private:
mesh::ota::SerialMotaSource _source;
uint32_t _packets_sent_at_start;
uint32_t _last_packets_sent;
};
static Nrf52BleMotaSourceControl ble_mota_source_control;
#endif
/* END GLOBAL OBJECTS */
#ifdef RECOVERABLE_EXTERNAL_RADIO
@@ -463,7 +573,7 @@ static void serviceUsbTerminal() {
// logs. `set usb.logging off` remains available here and returns this TTY to
// Binary Companion after its command reply, even on USB-UART bridges that
// cannot detect a host disconnect.
#if defined(COMPANION_RADIO_FULL)
#if MESH_USB_LOGGING_AVAILABLE
if (!mesh::hasDedicatedUsbLoggingPort()) {
if (mesh::isUsbLoggingEnabled()) {
if (!the_mesh.isTerminalMode()) {
@@ -522,7 +632,7 @@ static void serviceUsbTerminal() {
Serial.print("\r\n");
the_mesh.handleTerminalCommand(usb_terminal_line);
clearUsbTerminalLine();
#if defined(COMPANION_RADIO_FULL)
#if MESH_USB_LOGGING_AVAILABLE
if (usb_logging_terminal_mode
&& !mesh::isUsbLoggingEnabled()) {
leaveUsbTerminalMode(true);
@@ -1188,6 +1298,10 @@ void setup() {
#error "need to define filesystem"
#endif
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
the_mesh.setMotaSourceControl(&ble_mota_source_control);
#endif
// nRF52 cannot decide whether to add its optional logging CDC interface
// until the saved Companion preferences above are available. ESP32 already
// fixed its descriptor from the early NVS mirror, so this is harmless there.
@@ -1279,7 +1393,7 @@ void setup() {
usb_serial_interface.setConnectedCheck([]() { return (bool)Serial; });
#endif
interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface);
#if defined(COMPANION_RADIO_FULL)
#if MESH_USB_LOGGING_AVAILABLE
if (!mesh::hasDedicatedUsbLoggingPort()
&& mesh::isUsbLoggingEnabled()) {
// Apply a saved single-TTY logging preference before the dispatcher can
@@ -1344,6 +1458,9 @@ void loop() {
serviceUsbTerminal();
#endif
interface_manager.loop();
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
ble_mota_source_control.loop();
#endif
sensors.loop();
#ifdef DISPLAY_CLASS
#if defined(ESP32) && defined(WIFI_SSID) && defined(WITH_WEBCONFIG)
@@ -854,6 +854,9 @@ public:
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
if (_display != NULL) {
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
}
_auto_off = millis() + AUTO_OFF_MILLIS;
#if defined(PIN_USER_BTN)
+6
View File
@@ -107,6 +107,12 @@ public:
int getMsgCount() const { return _msgcount; }
int getPreviewCount() const;
bool hasDisplay() const { return _display != NULL; }
bool supportsDisplayRotation() const override {
return _display != NULL && _display->supportsRotation();
}
bool setDisplayRotationDegrees(uint16_t degrees) override {
return _display != NULL && _display->setRotationDegrees(degrees);
}
bool isButtonPressed() const;
bool isBuzzerQuiet() {
@@ -46,6 +46,9 @@ static const uint8_t meshcore_logo [] PROGMEM = {
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
if (_display != NULL) {
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
}
_auto_off = millis() + AUTO_OFF_MILLIS;
clearMsgPreview();
_node_prefs = node_prefs;
@@ -74,6 +74,12 @@ public:
void begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs);
bool hasDisplay() const { return _display != NULL; }
bool supportsDisplayRotation() const override {
return _display != NULL && _display->supportsRotation();
}
bool setDisplayRotationDegrees(uint16_t degrees) override {
return _display != NULL && _display->setRotationDegrees(degrees);
}
void clearMsgPreview();
// from AbstractUITask
@@ -437,6 +437,9 @@ public:
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
_display = display;
_sensors = sensors;
if (_display != NULL) {
_display->setRotationDegrees(node_prefs->display_rotation_degrees);
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_cached_batt_mv = getBattMilliVolts();
@@ -89,6 +89,12 @@ public:
int getMsgCount() const { return _msgcount; }
uint16_t getCachedBattMV() const { return _cached_batt_mv; }
bool hasDisplay() const { return _display != NULL; }
bool supportsDisplayRotation() const override {
return _display != NULL && _display->supportsRotation();
}
bool setDisplayRotationDegrees(uint16_t degrees) override {
return _display != NULL && _display->setRotationDegrees(degrees);
}
bool isButtonPressed() const;
bool isBuzzerQuiet() {
+17 -2
View File
@@ -664,7 +664,7 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
reply_data[8] = 0; // features
#ifdef WITH_RS232_BRIDGE
reply_data[8] |= 0x01; // is bridge, type UART
if (_prefs.bridge_enabled) reply_data[8] |= 0x01; // is bridge, type UART
#elif WITH_ESPNOW_BRIDGE
reply_data[8] |= 0x03; // is bridge, type ESP-NOW
#endif
@@ -3161,7 +3161,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#if defined(WITH_MQTT_BRIDGE)
, mqtt_bridge(nullptr)
#elif defined(WITH_RS232_BRIDGE)
, bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc)
, bridge(nullptr)
#elif defined(WITH_ESPNOW_BRIDGE)
, bridge(&_prefs, _mgr, &rtc)
#endif
@@ -3335,11 +3335,21 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
&_prefs.rx_ps_sleep_us);
// bridge defaults
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
_prefs.bridge_enabled = 0; // normal repeater until explicitly enabled
#else
_prefs.bridge_enabled = 1; // enabled
#endif
_prefs.bridge_delay = 500; // milliseconds
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
_prefs.bridge_baud = 115200; // baud rate
_prefs.bridge_channel = 1; // channel 1
#ifdef WITH_RS232_BRIDGE
_prefs.bridge_uart = WITH_RS232_BRIDGE_UART;
#else
_prefs.bridge_uart = 0;
#endif
StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret));
@@ -3470,6 +3480,11 @@ void MyMesh::begin(FILESYSTEM *fs) {
node_info.repeat_when_nonzero = false;
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
#endif
#ifdef WITH_RS232_BRIDGE
if (!bridge) {
bridge = createRS232Bridge();
}
#endif
AbstractBridge* active_bridge = activeBridge();
if (active_bridge) {
+46 -2
View File
@@ -493,7 +493,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
#if defined(WITH_MQTT_BRIDGE)
MQTTBridge* mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
RS232Bridge bridge;
RS232Bridge* bridge;
#elif defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge bridge;
#endif
@@ -501,6 +501,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
AbstractBridge* activeBridge() {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
return bridge;
#else
return &bridge;
#endif
@@ -508,6 +510,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
const AbstractBridge* activeBridge() const {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
return bridge;
#else
return &bridge;
#endif
@@ -975,6 +979,21 @@ public:
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
#if defined(WITH_BRIDGE)
#ifdef WITH_RS232_BRIDGE
RS232Bridge* createRS232Bridge() {
#ifdef WITH_RS232_BRIDGE_ALT
if (_prefs.bridge_uart == WITH_RS232_BRIDGE_ALT_UART) {
return new RS232Bridge(&_prefs, WITH_RS232_BRIDGE_ALT,
WITH_RS232_BRIDGE_ALT_RX,
WITH_RS232_BRIDGE_ALT_TX, _mgr, getRTCClock());
}
#endif
return new RS232Bridge(&_prefs, WITH_RS232_BRIDGE,
WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX,
_mgr, getRTCClock());
}
#endif
void setBridgeState(bool enable) override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge) {
@@ -990,9 +1009,24 @@ public:
getRTCClock(), &self_id);
if (!mqtt_bridge) return;
}
#endif
#ifdef WITH_RS232_BRIDGE
if (enable && !bridge) {
bridge = createRS232Bridge();
if (!bridge) return;
}
#endif
AbstractBridge* active_bridge = activeBridge();
if (!active_bridge || enable == active_bridge->isRunning()) return;
if (!active_bridge) return;
if (enable == active_bridge->isRunning()) {
#ifdef WITH_RS232_BRIDGE
if (!enable) {
delete bridge;
bridge = nullptr;
}
#endif
return;
}
if (enable)
{
#ifdef WITH_MQTT_BRIDGE
@@ -1014,6 +1048,10 @@ public:
else
{
active_bridge->end();
#ifdef WITH_RS232_BRIDGE
delete bridge;
bridge = nullptr;
#endif
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(nullptr);
#endif
@@ -1030,6 +1068,12 @@ public:
}
#endif
active_bridge->end();
#ifdef WITH_RS232_BRIDGE
delete bridge;
bridge = createRS232Bridge();
if (bridge) bridge->begin();
return;
#endif
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before restarting bridge (same as in begin())
char device_id[65];
+35 -1
View File
@@ -1,5 +1,8 @@
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#if MESH_PACKET_LOGGING
#include <helpers/SerialPacketLog.h>
#endif
#if defined(NRF52_PLATFORM)
#include <InternalFileSystem.h>
@@ -72,6 +75,7 @@ struct NodePrefs { // persisted to file
float freq;
int8_t tx_power_dbm;
uint8_t unused[3];
uint8_t usb_logging_enabled;
};
class MyMesh : public BaseChatMesh, ContactVisitor {
@@ -293,6 +297,7 @@ public:
strcpy(_prefs.node_name, "NONAME");
_prefs.freq = LORA_FREQ;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.usb_logging_enabled = 1;
command[0] = 0;
curr_recipient = NULL;
@@ -357,6 +362,11 @@ public:
}
}
#if MESH_USB_LOGGING_AVAILABLE
_prefs.usb_logging_enabled = constrain(_prefs.usb_logging_enabled, 0, 1);
mesh::setUsbLoggingEnabled(_prefs.usb_logging_enabled != 0);
#endif
loadContacts();
_public = addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
}
@@ -493,9 +503,28 @@ public:
}
} else if (memcmp(command, "import ", 7) == 0) {
importCard(&command[7]);
} else if (strcmp(command, "get usb.logging") == 0) {
#if MESH_USB_LOGGING_AVAILABLE
Serial.printf(" usb.logging %s\n",
mesh::isUsbLoggingEnabled() ? "on" : "off");
#else
Serial.println(" ERROR: USB logging is unavailable");
#endif
} else if (memcmp(command, "set ", 4) == 0) {
const char* config = &command[4];
if (memcmp(config, "af ", 3) == 0) {
if (strcmp(config, "usb.logging on") == 0
|| strcmp(config, "usb.logging off") == 0) {
#if MESH_USB_LOGGING_AVAILABLE
const bool enabled = strcmp(config, "usb.logging on") == 0;
_prefs.usb_logging_enabled = enabled ? 1 : 0;
mesh::setUsbLoggingEnabled(enabled);
savePrefs();
Serial.printf(" OK - USB logging %s (saved)\n",
enabled ? "on" : "off");
#else
Serial.println(" ERROR: USB logging is unavailable");
#endif
} else if (memcmp(config, "af ", 3) == 0) {
_prefs.airtime_factor = atof(&config[3]);
savePrefs();
Serial.println(" OK");
@@ -527,6 +556,8 @@ public:
} else if (memcmp(command, "help", 4) == 0) {
Serial.println("Commands:");
Serial.println(" set {name|lat|lon|freq|tx|af} {value}");
Serial.println(" get usb.logging");
Serial.println(" set usb.logging {on|off}");
Serial.println(" card");
Serial.println(" import {biz card}");
Serial.println(" clock");
@@ -578,6 +609,9 @@ void halt() {
void setup() {
Serial.begin(115200);
#if MESH_PACKET_LOGGING
mesh::serialLogBegin();
#endif
board.begin();