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:
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user