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
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// MeshCore BLE mOTA seeder service. The device notifies one framed seeder
|
||||
// request on REQUEST; the paired host writes the framed response to RESPONSE.
|
||||
// Both characteristics require an encrypted MITM-authenticated BLE link.
|
||||
static constexpr char BLE_MOTA_SERVICE_UUID[] =
|
||||
"14518fc2-7e7a-4d84-8cae-6664b0234cf2";
|
||||
static constexpr char BLE_MOTA_REQUEST_UUID[] =
|
||||
"2bfaa1ee-7030-459a-b65a-e7cfd5b09735";
|
||||
static constexpr char BLE_MOTA_RESPONSE_UUID[] =
|
||||
"acf38a51-dd58-4dce-917f-0b1135e41b1a";
|
||||
|
||||
static constexpr uint16_t BLE_MOTA_REQUEST_MAX = 11;
|
||||
static constexpr uint16_t BLE_MOTA_RESPONSE_MAX = 197;
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,163 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <atomic>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
// A single-producer/single-consumer Stream adapter for the nRF52 BLE mOTA
|
||||
// GATT service. The BLE callback appends host response fragments while the
|
||||
// mesh loop synchronously consumes them through SerialMotaSource. Device
|
||||
// requests travel in the other direction through the bounded send callback.
|
||||
//
|
||||
// One seeder response is at most 197 bytes. Keeping 256 bytes here holds a
|
||||
// complete response while still failing closed on duplicate or injected data.
|
||||
class BleMotaStream : public Stream {
|
||||
public:
|
||||
static constexpr uint16_t RX_CAPACITY = 256;
|
||||
using SendCallback = size_t (*)(void* context, const uint8_t* data,
|
||||
size_t length);
|
||||
|
||||
BleMotaStream() = default;
|
||||
|
||||
void setSender(SendCallback callback, void* context) {
|
||||
_send_context = context;
|
||||
_send = callback;
|
||||
}
|
||||
|
||||
void setActive(bool active) {
|
||||
_active.store(false, std::memory_order_release);
|
||||
if (active) {
|
||||
clear();
|
||||
} else {
|
||||
invalidateWriters();
|
||||
}
|
||||
_overflowed.store(false, std::memory_order_release);
|
||||
_active.store(active, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool isActive() const {
|
||||
return _active.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool overflowed() const {
|
||||
return _overflowed.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// Called only by the BLE response-characteristic callback. A fragment is
|
||||
// accepted atomically or rejected in full; a partial frame is never queued.
|
||||
bool pushRx(const uint8_t* data, size_t length) {
|
||||
if (!isActive() || data == nullptr || length == 0
|
||||
|| length >= RX_CAPACITY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t head_state = _head_state.load(std::memory_order_acquire);
|
||||
const uint16_t head = static_cast<uint16_t>(head_state);
|
||||
const uint16_t tail = _tail.load(std::memory_order_acquire);
|
||||
const uint16_t used = head >= tail ? head - tail
|
||||
: RX_CAPACITY - (tail - head);
|
||||
const uint16_t free_bytes = RX_CAPACITY - used - 1;
|
||||
if (length > free_bytes) {
|
||||
_overflowed.store(true, std::memory_order_release);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cursor = head;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
_rx[cursor] = data[i];
|
||||
cursor = static_cast<uint16_t>((cursor + 1) % RX_CAPACITY);
|
||||
}
|
||||
const uint32_t next_state = (head_state & 0xFFFF0000UL) | cursor;
|
||||
uint32_t expected = head_state;
|
||||
// setActive() changes the generation, and a new session also resets the
|
||||
// head. The CAS ensures a callback which overlapped either transition
|
||||
// cannot publish stale bytes into the next source session, even when both
|
||||
// sessions happened to use the same ring index.
|
||||
return isActive()
|
||||
&& _head_state.compare_exchange_strong(
|
||||
expected, next_state, std::memory_order_release,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int available() override {
|
||||
if (!isActive()) return 0;
|
||||
const uint16_t head = static_cast<uint16_t>(
|
||||
_head_state.load(std::memory_order_acquire));
|
||||
const uint16_t tail = _tail.load(std::memory_order_relaxed);
|
||||
return head >= tail ? head - tail : RX_CAPACITY - (tail - head);
|
||||
}
|
||||
|
||||
int read() override {
|
||||
if (!isActive()) return -1;
|
||||
const uint16_t tail = _tail.load(std::memory_order_relaxed);
|
||||
const uint16_t head = static_cast<uint16_t>(
|
||||
_head_state.load(std::memory_order_acquire));
|
||||
if (tail == head) return -1;
|
||||
const uint8_t value = _rx[tail];
|
||||
_tail.store(static_cast<uint16_t>((tail + 1) % RX_CAPACITY),
|
||||
std::memory_order_release);
|
||||
return value;
|
||||
}
|
||||
|
||||
int peek() override {
|
||||
if (!isActive()) return -1;
|
||||
const uint16_t tail = _tail.load(std::memory_order_relaxed);
|
||||
const uint16_t head = static_cast<uint16_t>(
|
||||
_head_state.load(std::memory_order_acquire));
|
||||
return tail == head ? -1 : _rx[tail];
|
||||
}
|
||||
|
||||
void flush() override {}
|
||||
|
||||
size_t write(uint8_t value) override {
|
||||
return write(&value, 1);
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* data, size_t length) override {
|
||||
if (!isActive() || data == nullptr || length == 0 || _send == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
return _send(_send_context, data, length);
|
||||
}
|
||||
|
||||
using Print::write;
|
||||
|
||||
private:
|
||||
static uint16_t nextGeneration(uint32_t state) {
|
||||
return static_cast<uint16_t>(state >> 16) + 1;
|
||||
}
|
||||
|
||||
void invalidateWriters() {
|
||||
const uint32_t previous = _head_state.load(std::memory_order_relaxed);
|
||||
const uint32_t next =
|
||||
(static_cast<uint32_t>(nextGeneration(previous)) << 16)
|
||||
| static_cast<uint16_t>(previous);
|
||||
_head_state.store(next, std::memory_order_release);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
const uint32_t previous = _head_state.load(std::memory_order_relaxed);
|
||||
_head_state.store(
|
||||
static_cast<uint32_t>(nextGeneration(previous)) << 16,
|
||||
std::memory_order_release);
|
||||
_tail.store(0, std::memory_order_release);
|
||||
}
|
||||
|
||||
uint8_t _rx[RX_CAPACITY] = {};
|
||||
// High 16 bits are a source-session generation; low 16 bits are the ring
|
||||
// head. Packing both into one lock-free nRF52840 atomic prevents an ABA
|
||||
// commit across clear()/restart.
|
||||
std::atomic<uint32_t> _head_state{0};
|
||||
std::atomic<uint16_t> _tail{0};
|
||||
std::atomic<bool> _active{false};
|
||||
std::atomic<bool> _overflowed{false};
|
||||
SendCallback _send = nullptr;
|
||||
void* _send_context = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -588,5 +588,98 @@ inline bool parseIntegerStrict(const char* text, int32_t& result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool splitCommaFieldsStrict(const char* text, char* storage,
|
||||
size_t storage_size,
|
||||
const char** fields,
|
||||
size_t field_count) {
|
||||
if (text == nullptr || storage == nullptr || storage_size == 0
|
||||
|| fields == nullptr || field_count == 0) {
|
||||
return false;
|
||||
}
|
||||
const size_t length = strlen(text);
|
||||
if (length == 0 || length >= storage_size) return false;
|
||||
memcpy(storage, text, length + 1);
|
||||
|
||||
char* cursor = storage;
|
||||
for (size_t i = 0; i < field_count; ++i) {
|
||||
fields[i] = cursor;
|
||||
char* separator = strchr(cursor, ',');
|
||||
if (i + 1 == field_count) {
|
||||
if (separator != nullptr) return false;
|
||||
} else {
|
||||
if (separator == nullptr) return false;
|
||||
*separator = 0;
|
||||
cursor = separator + 1;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse the radio tuple syntax without libc's optional float-scanf support.
|
||||
// In particular, newlib-nano nRF52 builds commonly compile `%f` but return no
|
||||
// conversions unless the much larger _scanf_float implementation is linked.
|
||||
inline bool parseRadioTupleStrict(const char* text, float& frequency,
|
||||
float& bandwidth, uint8_t& spreading_factor,
|
||||
uint8_t& coding_rate) {
|
||||
char storage[96] = {0};
|
||||
const char* fields[4] = {nullptr};
|
||||
if (!splitCommaFieldsStrict(text, storage, sizeof(storage), fields, 4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float parsed_frequency = 0.0f;
|
||||
float parsed_bandwidth = 0.0f;
|
||||
int32_t parsed_sf = 0;
|
||||
int32_t parsed_cr = 0;
|
||||
if (!parseDecimalStrict(fields[0], parsed_frequency)
|
||||
|| !parseDecimalStrict(fields[1], parsed_bandwidth)
|
||||
|| !parseIntegerStrict(fields[2], parsed_sf)
|
||||
|| !parseIntegerStrict(fields[3], parsed_cr)
|
||||
|| parsed_sf < 0 || parsed_sf > 255
|
||||
|| parsed_cr < 0 || parsed_cr > 255) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frequency = parsed_frequency;
|
||||
bandwidth = parsed_bandwidth;
|
||||
spreading_factor = static_cast<uint8_t>(parsed_sf);
|
||||
coding_rate = static_cast<uint8_t>(parsed_cr);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool parseTemporaryRadioTupleStrict(
|
||||
const char* text, float& frequency, float& bandwidth,
|
||||
uint8_t& spreading_factor, uint8_t& coding_rate,
|
||||
uint32_t& timeout_minutes) {
|
||||
char storage[96] = {0};
|
||||
const char* fields[5] = {nullptr};
|
||||
if (!splitCommaFieldsStrict(text, storage, sizeof(storage), fields, 5)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float parsed_frequency = 0.0f;
|
||||
float parsed_bandwidth = 0.0f;
|
||||
int32_t parsed_sf = 0;
|
||||
int32_t parsed_cr = 0;
|
||||
int32_t parsed_timeout = 0;
|
||||
if (!parseDecimalStrict(fields[0], parsed_frequency)
|
||||
|| !parseDecimalStrict(fields[1], parsed_bandwidth)
|
||||
|| !parseIntegerStrict(fields[2], parsed_sf)
|
||||
|| !parseIntegerStrict(fields[3], parsed_cr)
|
||||
|| !parseIntegerStrict(fields[4], parsed_timeout)
|
||||
|| parsed_sf < 0 || parsed_sf > 255
|
||||
|| parsed_cr < 0 || parsed_cr > 255
|
||||
|| parsed_timeout < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frequency = parsed_frequency;
|
||||
bandwidth = parsed_bandwidth;
|
||||
spreading_factor = static_cast<uint8_t>(parsed_sf);
|
||||
coding_rate = static_cast<uint8_t>(parsed_cr);
|
||||
timeout_minutes = static_cast<uint32_t>(parsed_timeout);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace mesh
|
||||
@@ -768,6 +768,11 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
_prefs->system_watchdog_enabled = 1;
|
||||
memset(_prefs->extra_sf, 0, sizeof(_prefs->extra_sf));
|
||||
_prefs->usb_logging_enabled = 1;
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
#else
|
||||
_prefs->bridge_uart = 0;
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool node_prefs_needs_migration = false;
|
||||
@@ -898,6 +903,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
File file = fs->open(filename);
|
||||
#endif
|
||||
if (file) {
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED)
|
||||
bool has_runtime_bridge_uart = false;
|
||||
#endif
|
||||
// Every supported layout contains the fixed 290-byte common core. Reject
|
||||
// a truncated in-place write before it can leave strings unterminated or
|
||||
// feed partial radio values into startup. loadPrefs() rewrites the image.
|
||||
@@ -1257,6 +1265,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
if (file.available() >= (int)sizeof(_prefs->usb_logging_enabled)) {
|
||||
file.read((uint8_t *)&_prefs->usb_logging_enabled,
|
||||
sizeof(_prefs->usb_logging_enabled));
|
||||
if (file.available() >= (int)sizeof(_prefs->bridge_uart)) {
|
||||
file.read((uint8_t *)&_prefs->bridge_uart,
|
||||
sizeof(_prefs->bridge_uart));
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED)
|
||||
has_runtime_bridge_uart = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (file.available() > 0) {
|
||||
@@ -1314,11 +1329,32 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3); // LOOP_DETECT_OFF..LOOP_DETECT_STRICT
|
||||
|
||||
// sanitise bad bridge pref values
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED)
|
||||
if (!has_runtime_bridge_uart) {
|
||||
// Pre-merge normal repeaters persisted bridge_enabled=1 even though no
|
||||
// bridge was compiled. Fail safe on the first merged boot instead of
|
||||
// unexpectedly claiming a UART; the user can explicitly enable it.
|
||||
_prefs->bridge_enabled = 0;
|
||||
_com_prefs_needs_upgrade = true;
|
||||
}
|
||||
#endif
|
||||
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
|
||||
_prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000);
|
||||
_prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
|
||||
_prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD);
|
||||
_prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14);
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
|
||||
#endif
|
||||
) {
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
_com_prefs_needs_upgrade = true;
|
||||
}
|
||||
#else
|
||||
_prefs->bridge_uart = 0;
|
||||
#endif
|
||||
|
||||
_prefs->powersaving_enabled = constrain(_prefs->powersaving_enabled, 0, 1);
|
||||
_prefs->reboot_interval = constrain(_prefs->reboot_interval, 0, 255);
|
||||
@@ -1538,6 +1574,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
WRITE_COMMON_PREFS(&prefs->extra_sf); // 856
|
||||
WRITE_COMMON_PREFS(&prefs->radio_fem_txgain); // 860
|
||||
WRITE_COMMON_PREFS(&prefs->usb_logging_enabled); // 861
|
||||
WRITE_COMMON_PREFS(&prefs->bridge_uart); // 862
|
||||
|
||||
#undef WRITE_COMMON_PREFS_BYTES
|
||||
#undef WRITE_COMMON_PREFS
|
||||
@@ -1690,7 +1727,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
file.write((uint8_t *)_prefs->extra_sf, sizeof(_prefs->extra_sf)); // 856
|
||||
file.write((uint8_t *)&_prefs->radio_fem_txgain, sizeof(_prefs->radio_fem_txgain)); // 860
|
||||
file.write((uint8_t *)&_prefs->usb_logging_enabled, sizeof(_prefs->usb_logging_enabled)); // 861
|
||||
// next: 862
|
||||
file.write((uint8_t *)&_prefs->bridge_uart, sizeof(_prefs->bridge_uart)); // 862
|
||||
// next: 863
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (!file.commit()) {
|
||||
@@ -2538,7 +2576,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
strcpy(reply, "gps_interval must be 0..86400 seconds");
|
||||
} else if (!valid_gps_toggle) {
|
||||
strcpy(reply, "gps must be 0 or 1");
|
||||
} else if (_sensors->setSettingValue(key, value)) {
|
||||
}
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
else if (is_gps_toggle && strcmp(value, "1") == 0
|
||||
&& _prefs->bridge_enabled
|
||||
&& _prefs->bridge_uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) {
|
||||
strcpy(reply, "turn the RS232 bridge off or select another UART first");
|
||||
}
|
||||
#endif
|
||||
else if (_sensors->setSettingValue(key, value)) {
|
||||
if (is_gps_interval) {
|
||||
_prefs->gps_interval = gps_interval;
|
||||
savePrefs();
|
||||
@@ -2579,6 +2625,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
handleRegionCmd(command, reply);
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
} else if (memcmp(command, "gps on", 6) == 0) {
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
if (_prefs->bridge_enabled
|
||||
&& _prefs->bridge_uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) {
|
||||
strcpy(reply, "turn the RS232 bridge off or select another UART first");
|
||||
} else
|
||||
#endif
|
||||
if (_sensors->setSettingValue("gps", "1")) {
|
||||
_prefs->gps_enabled = 1;
|
||||
savePrefs();
|
||||
@@ -4019,10 +4071,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
}
|
||||
#ifdef WITH_BRIDGE
|
||||
} else if (memcmp(config, "bridge.enabled ", 15) == 0) {
|
||||
_prefs->bridge_enabled = memcmp(&config[15], "on", 2) == 0;
|
||||
_callbacks->setBridgeState(_prefs->bridge_enabled);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
const bool enable = memcmp(&config[15], "on", 2) == 0;
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
if (enable && _prefs->gps_enabled
|
||||
&& _prefs->bridge_uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART) {
|
||||
strcpy(reply, "Error: turn GPS off or select another UART first");
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
_prefs->bridge_enabled = enable;
|
||||
_callbacks->setBridgeState(_prefs->bridge_enabled);
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
|
||||
int delay = _atoi(&config[13]);
|
||||
if (delay >= 0 && delay <= 10000) {
|
||||
@@ -4059,6 +4120,33 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
sprintf(reply, "Error: baud rate must be between 9600-%d",BRIDGE_MAX_BAUD);
|
||||
}
|
||||
} else if (memcmp(config, "bridge.uart ", 12) == 0) {
|
||||
const int uart = atoi(&config[12]);
|
||||
const bool supported = uart == WITH_RS232_BRIDGE_UART
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
|| uart == WITH_RS232_BRIDGE_ALT_UART
|
||||
#endif
|
||||
;
|
||||
if (!supported) {
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
sprintf(reply, "Error: UART must be %d or %d",
|
||||
WITH_RS232_BRIDGE_UART, WITH_RS232_BRIDGE_ALT_UART);
|
||||
#else
|
||||
sprintf(reply, "Error: UART is fixed at %d", WITH_RS232_BRIDGE_UART);
|
||||
#endif
|
||||
}
|
||||
#if defined(WITH_RS232_BRIDGE_GPS_CONFLICT_UART)
|
||||
else if (uart == WITH_RS232_BRIDGE_GPS_CONFLICT_UART
|
||||
&& _prefs->gps_enabled && _prefs->bridge_enabled) {
|
||||
strcpy(reply, "Error: turn GPS or the bridge off first");
|
||||
}
|
||||
#endif
|
||||
else {
|
||||
_prefs->bridge_uart = (uint8_t)uart;
|
||||
_callbacks->restartBridge();
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
} else if (memcmp(config, "bridge.channel ", 15) == 0) {
|
||||
@@ -4453,6 +4541,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
} else if (memcmp(config, "bridge.baud", 11) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
|
||||
} else if (memcmp(config, "bridge.uart", 11) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_uart);
|
||||
#endif
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
} else if (memcmp(config, "bridge.channel", 14) == 0) {
|
||||
|
||||
+10
-2
@@ -25,6 +25,10 @@
|
||||
#define WITH_BRIDGE
|
||||
#endif
|
||||
|
||||
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_RS232_BRIDGE_UART)
|
||||
#define WITH_RS232_BRIDGE_UART 1
|
||||
#endif
|
||||
|
||||
#define ADVERT_LOC_NONE 0
|
||||
#define ADVERT_LOC_SHARE 1
|
||||
#define ADVERT_LOC_PREFS 2
|
||||
@@ -169,9 +173,12 @@ public:
|
||||
uint8_t extra_sf[4] = {};
|
||||
uint8_t radio_fem_txgain = 0; // LoRa FEM TX gain; persisted at /com_prefs offset 860
|
||||
// Runtime USB packet output gate. Appended at /com_prefs offset 861 so
|
||||
// older images remain readable and logging builds keep their historical
|
||||
// enabled-at-first-boot behavior.
|
||||
// older images remain readable and merged standard builds keep the former
|
||||
// logging artifact's enabled-at-first-boot behavior.
|
||||
uint8_t usb_logging_enabled = 1;
|
||||
// Runtime UART choice for merged RS-232 repeater artifacts. Appended at
|
||||
// /com_prefs offset 862; single-UART builds keep their compiled port here.
|
||||
uint8_t bridge_uart = 0;
|
||||
uint8_t retry_preset = 0;
|
||||
uint8_t direct_retry_attempts = 0;
|
||||
uint16_t direct_retry_base_ms = 0;
|
||||
@@ -267,6 +274,7 @@ private:
|
||||
def("delay", _parent->bridge_delay);
|
||||
def("src", _parent->bridge_pkt_src);
|
||||
def("baud", _parent->bridge_baud);
|
||||
def("uart", _parent->bridge_uart);
|
||||
def("ch", _parent->bridge_channel);
|
||||
def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret));
|
||||
def("usb_log", _parent->usb_logging_enabled);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace companion {
|
||||
|
||||
static constexpr uint8_t CMD_EXEC_LOCAL_OTA_CONTROL = 0x4A;
|
||||
static constexpr uint8_t CMD_BLE_MOTA_SOURCE = 0x4B;
|
||||
|
||||
enum class MotaSourceAction : uint8_t {
|
||||
Status = 0,
|
||||
Start = 1,
|
||||
Stop = 2,
|
||||
};
|
||||
|
||||
static constexpr uint8_t MOTA_SOURCE_FLAG_CHANNEL_READY = 0x01;
|
||||
static constexpr uint8_t MOTA_SOURCE_FLAG_ATTACHED = 0x02;
|
||||
static constexpr uint8_t MOTA_SOURCE_FLAG_ANOTHER_LINK_ACTIVE = 0x04;
|
||||
|
||||
struct MotaSourceStatus {
|
||||
bool channel_ready = false;
|
||||
bool attached = false;
|
||||
bool another_link_active = false;
|
||||
uint16_t offered = 0;
|
||||
uint16_t advertised = 0;
|
||||
uint32_t packets_sent = 0;
|
||||
};
|
||||
|
||||
class MotaSourceControl {
|
||||
public:
|
||||
virtual ~MotaSourceControl() = default;
|
||||
virtual bool start(char* reply, size_t reply_size) = 0;
|
||||
virtual bool stop(char* reply, size_t reply_size) = 0;
|
||||
virtual MotaSourceStatus status() const = 0;
|
||||
};
|
||||
|
||||
// Bluetooth may expose only the local commands needed to coordinate a LoRa
|
||||
// mOTA session. The command arrives as one length-delimited Companion frame,
|
||||
// so reject embedded NUL/control bytes and USB folder ownership commands.
|
||||
inline bool isBleOtaControlCommandAllowed(const uint8_t* command,
|
||||
size_t length) {
|
||||
if (command == nullptr || length == 0 || length > 174) return false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
if (command[i] < 0x20 || command[i] > 0x7E) return false;
|
||||
switch (command[i]) {
|
||||
case ';':
|
||||
case '&':
|
||||
case '|':
|
||||
case '`':
|
||||
case '$':
|
||||
case '\\':
|
||||
case '\'':
|
||||
case '"':
|
||||
return false;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto exact = [command, length](const char* text, size_t text_length) {
|
||||
if (length != text_length) return false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
if (command[i] != static_cast<uint8_t>(text[i])) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const auto token = [command, length](const char* text, size_t text_length) {
|
||||
if (length < text_length) return false;
|
||||
for (size_t i = 0; i < text_length; ++i) {
|
||||
if (command[i] != static_cast<uint8_t>(text[i])) return false;
|
||||
}
|
||||
return length == text_length || command[text_length] == ' ';
|
||||
};
|
||||
|
||||
if (exact("normalradio", 11)) return true;
|
||||
// Bare `tempradio` is a read-only status query. Keep the parameterized
|
||||
// form strict, but let a BLE controller prove that cleanup completed.
|
||||
if (exact("tempradio", 9)) return true;
|
||||
if (token("tempradio", 9)) return length > 10;
|
||||
if (!token("ota", 3)) return false;
|
||||
if (token("ota folder", 10)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace companion
|
||||
} // namespace mesh
|
||||
@@ -34,8 +34,8 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#if defined(COMPANION_RADIO_FULL)
|
||||
// Full Companion owns its primary USB stream for framed traffic until saved
|
||||
#if defined(ENABLE_USB_INTERFACE)
|
||||
// A USB Companion owns its primary stream for framed traffic until saved
|
||||
// preferences are loaded. Starting disabled prevents early boot diagnostics
|
||||
// from corrupting that stream before single-TTY builds can enter terminal mode.
|
||||
static std::atomic<bool> usb_logging_enabled{false};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
// Logging artifacts compile at least one of these two diagnostics. Keep the
|
||||
// command surface out of ordinary images while providing one runtime gate for
|
||||
// every diagnostic category that writes to the USB Serial stream.
|
||||
// Canonical USB-capable images compile at least one of these two diagnostics.
|
||||
// One runtime gate covers every diagnostic category that writes to the USB
|
||||
// Serial stream, so a separate logging firmware is unnecessary.
|
||||
#if defined(ARDUINO) && \
|
||||
((defined(MESH_DEBUG) && MESH_DEBUG) || \
|
||||
(defined(MESH_PACKET_LOGGING) && MESH_PACKET_LOGGING))
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
namespace mesh {
|
||||
|
||||
// Ordinary logging images start enabled. Every Full Companion starts with
|
||||
// logging disabled and restores its saved choice after preferences load.
|
||||
// Ordinary merged images start enabled. USB/Full Companion starts disabled to
|
||||
// protect framed traffic and restores its saved choice after preferences load.
|
||||
bool isUsbLoggingEnabled();
|
||||
void setUsbLoggingEnabled(bool enabled);
|
||||
|
||||
|
||||
@@ -251,6 +251,12 @@ inline bool isFirmwareReserved(uint8_t pin) {
|
||||
#ifdef WITH_RS232_BRIDGE_TX
|
||||
WITH_RS232_BRIDGE_TX,
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE_ALT_RX
|
||||
WITH_RS232_BRIDGE_ALT_RX,
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE_ALT_TX
|
||||
WITH_RS232_BRIDGE_ALT_TX,
|
||||
#endif
|
||||
#ifdef PIN_SERIAL_RX
|
||||
PIN_SERIAL_RX,
|
||||
#endif
|
||||
|
||||
@@ -5,30 +5,28 @@
|
||||
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
|
||||
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
|
||||
: BridgeBase(prefs, mgr, rtc), _serial(&serial) {}
|
||||
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
|
||||
int16_t tx_pin, mesh::PacketManager *mgr,
|
||||
mesh::RTCClock *rtc)
|
||||
: BridgeBase(prefs, mgr, rtc), _serial(&serial), _rx_pin(rx_pin),
|
||||
_tx_pin(tx_pin) {}
|
||||
|
||||
void RS232Bridge::begin() {
|
||||
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
|
||||
#if !defined(WITH_RS232_BRIDGE_RX) || !defined(WITH_RS232_BRIDGE_TX)
|
||||
#error "WITH_RS232_BRIDGE_RX and WITH_RS232_BRIDGE_TX must be defined"
|
||||
#endif
|
||||
|
||||
#if defined(ESP32)
|
||||
((HardwareSerial *)_serial)->setPins(WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
|
||||
((HardwareSerial *)_serial)->setPins(_rx_pin, _tx_pin);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
// Tested with RAK_4631 and T114
|
||||
// The Adafruit Uart object may already be active on its variant defaults.
|
||||
// Stop it before changing pins or the EasyDMA instance can retain the old
|
||||
// pin selection and silently receive nothing.
|
||||
mesh::bridge::prepareNrfUart(*((Uart *)_serial),
|
||||
WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX);
|
||||
mesh::bridge::prepareNrfUart(*((Uart *)_serial), _rx_pin, _tx_pin);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
((SerialUART *)_serial)->setRX(WITH_RS232_BRIDGE_RX);
|
||||
((SerialUART *)_serial)->setTX(WITH_RS232_BRIDGE_TX);
|
||||
((SerialUART *)_serial)->setRX(_rx_pin);
|
||||
((SerialUART *)_serial)->setTX(_tx_pin);
|
||||
#elif defined(STM32_PLATFORM)
|
||||
((HardwareSerial *)_serial)->setRx(WITH_RS232_BRIDGE_RX);
|
||||
((HardwareSerial *)_serial)->setTx(WITH_RS232_BRIDGE_TX);
|
||||
((HardwareSerial *)_serial)->setRx(_rx_pin);
|
||||
((HardwareSerial *)_serial)->setTx(_tx_pin);
|
||||
#else
|
||||
#error RS232Bridge was not tested on the current platform
|
||||
#endif
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* - Fletcher-16 checksum for data integrity verification
|
||||
* - Magic header for packet synchronization and frame alignment
|
||||
* - Duplicate packet detection using SimpleMeshTables tracking
|
||||
* - Configurable RX/TX pins via build defines
|
||||
* - Configurable UART and RX/TX pins selected by the owning runtime
|
||||
* - Fixed baud rate at 115200 for consistent timing
|
||||
*
|
||||
* Packet Structure:
|
||||
@@ -34,8 +34,7 @@
|
||||
*
|
||||
* Configuration:
|
||||
* - Define WITH_RS232_BRIDGE to enable this bridge
|
||||
* - Define WITH_RS232_BRIDGE_RX with the RX pin number
|
||||
* - Define WITH_RS232_BRIDGE_TX with the TX pin number
|
||||
* - The owning role supplies the selected UART and pins to the constructor
|
||||
*
|
||||
* Platform Support:
|
||||
* Different platforms require different pin configuration methods:
|
||||
@@ -54,12 +53,12 @@ public:
|
||||
* @param mgr PacketManager for allocating and queuing packets
|
||||
* @param rtc RTCClock for timestamping debug messages
|
||||
*/
|
||||
RS232Bridge(NodePrefs *prefs, Stream &serial, mesh::PacketManager *mgr, mesh::RTCClock *rtc);
|
||||
RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin, int16_t tx_pin,
|
||||
mesh::PacketManager *mgr, mesh::RTCClock *rtc);
|
||||
|
||||
/**
|
||||
* Initializes the RS232 bridge
|
||||
*
|
||||
* - Validates that RX/TX pins are defined
|
||||
* - Configures UART pins based on target platform
|
||||
* - Sets baud rate to 115200 for consistent communication
|
||||
* - Platform-specific pin configuration methods are used
|
||||
@@ -137,6 +136,8 @@ private:
|
||||
|
||||
/** Hardware serial port interface */
|
||||
Stream *_serial;
|
||||
int16_t _rx_pin;
|
||||
int16_t _tx_pin;
|
||||
|
||||
/** Buffer for building received packets */
|
||||
uint8_t _rx_buffer[MAX_SERIAL_PACKET_SIZE];
|
||||
|
||||
@@ -67,9 +67,16 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
BLESecurity sec;
|
||||
#if defined(CONFIG_NIMBLE_ENABLED)
|
||||
sec.setPassKey(true, pin_code);
|
||||
// A passkey alone does not provide MITM protection when the controller's
|
||||
// default capability is NoInputNoOutput: the peers can silently fall back
|
||||
// to Just Works and create an unauthenticated bond. The Companion displays
|
||||
// (or otherwise publishes) this PIN for entry on the phone/host, so declare
|
||||
// the peripheral as DisplayOnly and force the passkey-entry association.
|
||||
sec.setCapability(ESP_IO_CAP_OUT);
|
||||
sec.setAuthenticationMode(true, true, true);
|
||||
#else
|
||||
sec.setStaticPIN(pin_code);
|
||||
sec.setCapability(ESP_IO_CAP_OUT);
|
||||
sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ void SerialBLEInterface::onConnect(uint16_t connection_handle) {
|
||||
instance->_isDeviceConnected = false;
|
||||
instance->_security_timer.start(millis());
|
||||
instance->clearBuffers();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
instance->setMotaStreamActive(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,10 +55,46 @@ void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason
|
||||
instance->_isDeviceConnected = false;
|
||||
instance->_security_timer.cancel();
|
||||
instance->clearBuffers();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
instance->setMotaStreamActive(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
void SerialBLEInterface::onMotaResponse(
|
||||
uint16_t conn_handle, BLECharacteristic* characteristic,
|
||||
uint8_t* data, uint16_t length) {
|
||||
if (!instance || characteristic != &instance->_mota_response
|
||||
|| instance->_conn_handle != conn_handle || !instance->isConnected()
|
||||
|| !instance->_mota_stream.isActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (length == 0 || length > mesh::ota::BLE_MOTA_RESPONSE_MAX
|
||||
|| !instance->_mota_stream.pushRx(data, length)) {
|
||||
// A response that cannot fit intact would make the byte stream ambiguous.
|
||||
// Disable the link so the current transaction times out and the main loop
|
||||
// detaches it instead of consuming a partial or injected frame.
|
||||
instance->setMotaStreamActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
size_t SerialBLEInterface::sendMotaRequest(void* context,
|
||||
const uint8_t* data,
|
||||
size_t length) {
|
||||
SerialBLEInterface* self = static_cast<SerialBLEInterface*>(context);
|
||||
if (!self || !data || length == 0
|
||||
|| length > mesh::ota::BLE_MOTA_REQUEST_MAX
|
||||
|| !self->isMotaChannelReady() || !self->_mota_stream.isActive()) {
|
||||
return 0;
|
||||
}
|
||||
return self->_mota_request.notify(self->_conn_handle, data, length)
|
||||
? length : 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void SerialBLEInterface::onSecured(uint16_t connection_handle) {
|
||||
BLE_DEBUG_PRINTLN("SerialBLEInterface: onSecured handle=0x%04X", connection_handle);
|
||||
if (instance) {
|
||||
@@ -272,6 +311,38 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
bleuart.begin();
|
||||
bleuart.setRxCallback(onBleUartRX);
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
_mota_stream.setSender(sendMotaRequest, this);
|
||||
_mota_stream.setActive(false);
|
||||
|
||||
_mota_service.setPermission(SECMODE_ENC_WITH_MITM,
|
||||
SECMODE_ENC_WITH_MITM);
|
||||
if (_mota_service.begin() != ERROR_NONE) {
|
||||
BLE_DEBUG_PRINTLN("Bluetooth mOTA service begin failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_mota_request.setProperties(CHR_PROPS_NOTIFY);
|
||||
_mota_request.setPermission(SECMODE_ENC_WITH_MITM,
|
||||
SECMODE_NO_ACCESS);
|
||||
_mota_request.setMaxLen(mesh::ota::BLE_MOTA_REQUEST_MAX);
|
||||
_mota_request.setUserDescriptor("mOTA device request");
|
||||
if (_mota_request.begin() != ERROR_NONE) {
|
||||
BLE_DEBUG_PRINTLN("Bluetooth mOTA request characteristic begin failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
_mota_response.setProperties(CHR_PROPS_WRITE);
|
||||
_mota_response.setPermission(SECMODE_NO_ACCESS,
|
||||
SECMODE_ENC_WITH_MITM);
|
||||
_mota_response.setMaxLen(mesh::ota::BLE_MOTA_RESPONSE_MAX);
|
||||
_mota_response.setUserDescriptor("mOTA host response");
|
||||
_mota_response.setWriteCallback(onMotaResponse);
|
||||
if (_mota_response.begin() != ERROR_NONE) {
|
||||
BLE_DEBUG_PRINTLN("Bluetooth mOTA response characteristic begin failed");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
// Register DFU on the main BLE stack so paired clients can discover it
|
||||
@@ -390,6 +461,9 @@ void SerialBLEInterface::recoverStalledTx(const char* cause) {
|
||||
_last_retry_attempt = 0;
|
||||
_tx_stall_watchdog.reset();
|
||||
bleuart.flush();
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
setMotaStreamActive(false);
|
||||
#endif
|
||||
_tx_disconnect_recovery.begin();
|
||||
serviceTxRecovery((uint32_t)millis());
|
||||
}
|
||||
@@ -442,6 +516,9 @@ void SerialBLEInterface::disable() {
|
||||
disconnect();
|
||||
_security_timer.cancel();
|
||||
_last_health_check = 0;
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
setMotaStreamActive(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
@@ -610,6 +687,13 @@ bool SerialBLEInterface::isConnected() const {
|
||||
Bluefruit.connected() > 0;
|
||||
}
|
||||
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
bool SerialBLEInterface::isMotaChannelReady() {
|
||||
return isConnected() && _conn_handle != BLE_CONN_HANDLE_INVALID
|
||||
&& _mota_request.notifyEnabled(_conn_handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SerialBLEInterface::isReadBusy() const {
|
||||
return (recv_queue_len > 0);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "SecuritySessionTimer.h"
|
||||
#include <bluefruit.h>
|
||||
#include <atomic>
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
#include "../BleMotaProtocol.h"
|
||||
#include "../BleMotaStream.h"
|
||||
#endif
|
||||
|
||||
#ifndef BLE_TX_POWER
|
||||
#define BLE_TX_POWER 4
|
||||
@@ -14,6 +18,14 @@
|
||||
class SerialBLEInterface : public BaseSerialInterface {
|
||||
BLEDfu bledfu;
|
||||
BLEUart bleuart;
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
BLEService _mota_service = BLEService(mesh::ota::BLE_MOTA_SERVICE_UUID);
|
||||
BLECharacteristic _mota_request =
|
||||
BLECharacteristic(mesh::ota::BLE_MOTA_REQUEST_UUID);
|
||||
BLECharacteristic _mota_response =
|
||||
BLECharacteristic(mesh::ota::BLE_MOTA_RESPONSE_UUID);
|
||||
mesh::ota::BleMotaStream _mota_stream;
|
||||
#endif
|
||||
bool _isEnabled;
|
||||
bool _isDeviceConnected;
|
||||
uint16_t _conn_handle;
|
||||
@@ -56,6 +68,13 @@ class SerialBLEInterface : public BaseSerialInterface {
|
||||
static void onPairingComplete(uint16_t connection_handle, uint8_t auth_status);
|
||||
static void onBLEEvent(ble_evt_t* evt);
|
||||
static void onBleUartRX(uint16_t conn_handle);
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
static void onMotaResponse(uint16_t conn_handle,
|
||||
BLECharacteristic* characteristic,
|
||||
uint8_t* data, uint16_t length);
|
||||
static size_t sendMotaRequest(void* context, const uint8_t* data,
|
||||
size_t length);
|
||||
#endif
|
||||
|
||||
public:
|
||||
SerialBLEInterface() {
|
||||
@@ -91,6 +110,12 @@ public:
|
||||
}
|
||||
size_t writeFrame(const uint8_t src[], size_t len) override;
|
||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||
#if COMPANION_FEATURE_BLE_MOTA_SOURCE
|
||||
Stream& motaStream() { return _mota_stream; }
|
||||
bool isMotaChannelReady();
|
||||
bool isMotaStreamActive() const { return _mota_stream.isActive(); }
|
||||
void setMotaStreamActive(bool active) { _mota_stream.setActive(active); }
|
||||
#endif
|
||||
};
|
||||
|
||||
#if BLE_DEBUG_LOGGING && ARDUINO
|
||||
|
||||
@@ -11,6 +11,7 @@ bool FolderMotaStore::readByteT(uint8_t& b) const {
|
||||
while ((millis() - t0) < _to) {
|
||||
int c = _io.read();
|
||||
if (c >= 0) { b = (uint8_t)c; return true; }
|
||||
delay(1); // let BLE/WiFi callbacks deliver the response
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ bool FolderMotaStore::txn(uint8_t op, const uint8_t* args, uint16_t arglen,
|
||||
uint32_t t0 = millis(); bool got = false; uint8_t prev = 0;
|
||||
while ((millis() - t0) < _to) { // scan for response magic 'm' 's' (tolerate noise)
|
||||
int c = _io.read();
|
||||
if (c < 0) continue;
|
||||
if (c < 0) { delay(1); continue; }
|
||||
if (prev == MOTA_SEEDER_RSP_MAGIC0 && (uint8_t)c == MOTA_SEEDER_RSP_MAGIC1) { got = true; break; }
|
||||
prev = (uint8_t)c;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ bool SerialMotaSource::readByteT(uint8_t& b) {
|
||||
while ((millis() - t0) < _to) {
|
||||
int c = _io.read();
|
||||
if (c >= 0) { b = (uint8_t)c; return true; }
|
||||
delay(1); // let BLE/WiFi callbacks deliver the response
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -43,7 +44,7 @@ bool SerialMotaSource::txn(uint8_t op, const uint8_t* args, uint8_t arglen,
|
||||
uint8_t prev = 0;
|
||||
while ((millis() - t0) < _to) {
|
||||
int c = _io.read();
|
||||
if (c < 0) continue;
|
||||
if (c < 0) { delay(1); continue; }
|
||||
if (prev == MOTA_SEEDER_RSP_MAGIC0 && (uint8_t)c == MOTA_SEEDER_RSP_MAGIC1) { got = true; break; }
|
||||
prev = (uint8_t)c;
|
||||
}
|
||||
|
||||
@@ -321,8 +321,17 @@ struct OtaContext {
|
||||
FOLDER_LINK_NONE = 0,
|
||||
FOLDER_LINK_SERIAL,
|
||||
FOLDER_LINK_TCP,
|
||||
FOLDER_LINK_BLE,
|
||||
};
|
||||
FolderLink folderLink() const { return _folder_link; }
|
||||
static const char* folderLinkName(FolderLink link) {
|
||||
switch (link) {
|
||||
case FOLDER_LINK_SERIAL: return "serial";
|
||||
case FOLDER_LINK_TCP: return "tcp";
|
||||
case FOLDER_LINK_BLE: return "ble";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
bool folderSourceStats(uint16_t& offered, uint16_t& advertised) const {
|
||||
return manager.sourceStats(_folder_source, offered, advertised);
|
||||
}
|
||||
@@ -337,7 +346,7 @@ struct OtaContext {
|
||||
}
|
||||
if (folder_active && _folder_link != link) {
|
||||
snprintf(msg, cap, "ERR folder already attached via %s",
|
||||
_folder_link == FOLDER_LINK_TCP ? "tcp" : "serial");
|
||||
folderLinkName(_folder_link));
|
||||
return false;
|
||||
}
|
||||
if (folder_active && _folder_source == source) {
|
||||
|
||||
@@ -447,6 +447,7 @@ public:
|
||||
FetchError fetchError() const { return _fetch_error; }
|
||||
uint32_t blocksHave() const { return _have; }
|
||||
uint32_t blocksTotal() const { return _fbc; }
|
||||
uint32_t packetsSent() const { return _packets_sent; }
|
||||
uint8_t fetchPipelineWidth() const { return _pipeline_width; }
|
||||
static constexpr uint8_t fetchPipelineCapacity() { return OTA_FETCH_PIPELINE; }
|
||||
uint32_t fetchRetryTimeoutMs() const;
|
||||
@@ -482,7 +483,9 @@ private:
|
||||
bool expandCatalog();
|
||||
|
||||
bool emit(const uint8_t* b, uint16_t n, bool flood) {
|
||||
return _send && n && _send(_ctx, b, n, flood);
|
||||
const bool sent = _send && n && _send(_ctx, b, n, flood);
|
||||
if (sent) _packets_sent++;
|
||||
return sent;
|
||||
}
|
||||
void handleAdv(const uint8_t* m, uint16_t n); // beacon -> sources table (+ query if interested)
|
||||
void handleQuery(const uint8_t* m, uint16_t n); // serve: reply OTA_HAVE catalog
|
||||
@@ -655,6 +658,7 @@ private:
|
||||
uint32_t _radio_packet_airtime_ms = 0; // measured for MAX_TRANS_UNIT at active SF/BW
|
||||
uint16_t _tx_spacing_permille = 2000; // 1/duty-cycle; default airtime factor 1 => 50% TX
|
||||
uint8_t _observed_path_transmissions = 0; // source + relays; 0 falls back to configured max_hops+1
|
||||
uint32_t _packets_sent = 0; // OTA packets accepted by the radio adapter (wrap-safe)
|
||||
// multi-fragment manifest reassembly (a signed v2 manifest exceeds one packet)
|
||||
uint8_t _mf_buf[OTA_MF_MAXFRAG * OTA_MF_FRAG]; // sized to the fragment cap so no valid manifest is silently dropped
|
||||
uint16_t _mf_retries = 0; // GET_MANIFEST retries while WANT_MANIFEST (give up after a cap)
|
||||
|
||||
@@ -15,6 +15,7 @@ class DisplayDriver {
|
||||
int _w, _h;
|
||||
protected:
|
||||
DisplayDriver(int w, int h) { _w = w; _h = h; }
|
||||
void setDimensions(int w, int h) { _w = w; _h = h; }
|
||||
|
||||
static size_t trimLastUTF8Codepoint(char* str, size_t length) {
|
||||
if (length == 0) return 0;
|
||||
@@ -30,6 +31,11 @@ public:
|
||||
int height() const { return _h; }
|
||||
|
||||
virtual bool isOn() = 0;
|
||||
virtual bool supportsRotation() const { return false; }
|
||||
virtual bool setRotationDegrees(uint16_t degrees) {
|
||||
(void)degrees;
|
||||
return false;
|
||||
}
|
||||
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
|
||||
virtual void forceFullRefresh() {} // next refresh will be full for eink
|
||||
virtual void turnOn() = 0;
|
||||
|
||||
@@ -22,10 +22,34 @@ bool SSD1306Display::begin() {
|
||||
if (_peripher_power) _peripher_power->claim();
|
||||
_isOn = true;
|
||||
}
|
||||
#ifdef DISPLAY_ROTATION
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
#endif
|
||||
return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
const bool started = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS,
|
||||
true, false)
|
||||
&& i2c_probe(Wire, DISPLAY_ADDRESS);
|
||||
if (started) setRotationDegrees(0);
|
||||
return started;
|
||||
}
|
||||
|
||||
bool SSD1306Display::setRotationDegrees(uint16_t degrees) {
|
||||
uint8_t rotation;
|
||||
switch (degrees) {
|
||||
case 0:
|
||||
#ifdef DISPLAY_ROTATION
|
||||
rotation = DISPLAY_ROTATION & 3;
|
||||
#else
|
||||
rotation = 0;
|
||||
#endif
|
||||
break;
|
||||
case 90: rotation = 1; break;
|
||||
case 180: rotation = 2; break;
|
||||
case 270: rotation = 3; break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
display.setRotation(rotation);
|
||||
setDimensions(display.width(), display.height());
|
||||
display.clearDisplay();
|
||||
display.display();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SSD1306Display::turnOn() {
|
||||
|
||||
@@ -32,6 +32,8 @@ public:
|
||||
bool begin();
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
bool supportsRotation() const override { return true; }
|
||||
bool setRotationDegrees(uint16_t degrees) override;
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
|
||||
Reference in new issue
Block a user