From ea9a4dcd3af187bb56d5ced63c4cc506d77dc68f Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 3 Mar 2025 15:30:50 +1100 Subject: [PATCH 1/7] * room server: adding post, was not sending Acks on retries. * room server and repeater: now does NOT send Acks for TXT_TYPE_CLI_DATA commands --- examples/simple_repeater/main.cpp | 30 ++++++++++++++++------------ examples/simple_room_server/main.cpp | 25 ++++++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 2c35106c..d47a6e02 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -407,27 +407,34 @@ protected: if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); - } else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks + } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks + bool is_retry = (sender_timestamp == client->last_timestamp); client->last_timestamp = sender_timestamp; client->last_activity = getRTCClock()->getCurrentTime(); // len can be > original length, but 'text' will be padded with zeroes data[len] = 0; // need to make a C string again, with null terminator - uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it - mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE); + if (flags == TXT_TYPE_PLAIN) { // for legacy CLI, send Acks + uint32_t ack_hash; // calc truncated hash of the message timestamp + text + sender pub_key, to prove to sender that we got it + mesh::Utils::sha256((uint8_t *) &ack_hash, 4, data, 5 + strlen((char *)&data[5]), client->id.pub_key, PUB_KEY_SIZE); - mesh::Packet* ack = createAck(ack_hash); - if (ack) { - if (client->out_path_len < 0) { - sendFlood(ack); - } else { - sendDirect(ack, client->out_path, client->out_path_len); + mesh::Packet* ack = createAck(ack_hash); + if (ack) { + if (client->out_path_len < 0) { + sendFlood(ack); + } else { + sendDirect(ack, client->out_path, client->out_path_len); + } } } uint8_t temp[166]; - _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); + if (is_retry) { + temp[0] = 0; + } else { + _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); + } int text_len = strlen((char *) &temp[5]); if (text_len > 0) { uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); @@ -438,9 +445,6 @@ protected: memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN - // calc expected ACK reply - //mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { if (client->out_path_len < 0) { diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index 4ef72e16..cce7e3d4 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -372,7 +372,8 @@ protected: if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported command flags received: flags=%02x", (uint32_t)flags); - } else if (sender_timestamp > client->last_timestamp) { // prevent replay attacks + } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks, but send Acks for retries + bool is_retry = (sender_timestamp == client->last_timestamp); client->last_timestamp = sender_timestamp; uint32_t now = getRTCClock()->getCurrentTimeUnique(); @@ -389,19 +390,26 @@ protected: bool send_ack; if (flags == TXT_TYPE_CLI_DATA) { if (client->is_admin) { - _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); - temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN) - send_ack = true; + if (is_retry) { + temp[5] = 0; // no reply + } else { + _cli.handleCommand(sender_timestamp, (const char *) &data[5], (char *) &temp[5]); + temp[4] = (TXT_TYPE_CLI_DATA << 2); // attempt and flags, (NOTE: legacy was: TXT_TYPE_PLAIN) + } + send_ack = false; } else { temp[5] = 0; // no reply send_ack = false; // and no ACK... user shoudn't be sending these } } else { // TXT_TYPE_PLAIN - addPost(client, (const char *) &data[5]); + if (!is_retry) { + addPost(client, (const char *) &data[5]); + } temp[5] = 0; // no reply (ACK is enough) send_ack = true; } + uint32_t delay_millis; if (send_ack) { mesh::Packet* ack = createAck(ack_hash); if (ack) { @@ -411,6 +419,9 @@ protected: sendDirect(ack, client->out_path, client->out_path_len); } } + delay_millis = REPLY_DELAY_MILLIS; + } else { + delay_millis = 0; } int text_len = strlen((char *) &temp[5]); @@ -427,9 +438,9 @@ protected: auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { if (client->out_path_len < 0) { - sendFlood(reply, REPLY_DELAY_MILLIS); + sendFlood(reply, delay_millis); } else { - sendDirect(reply, client->out_path, client->out_path_len, REPLY_DELAY_MILLIS); + sendDirect(reply, client->out_path, client->out_path_len, delay_millis); } } } From 72c7cebbbb64b5f4c0e28a2b497e5251a0854408 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 3 Mar 2025 19:51:45 +1100 Subject: [PATCH 2/7] * misc --- src/helpers/BaseChatMesh.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 9835bb7f..b61a0d37 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -104,7 +104,6 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // len can be > original length, but 'text' will be padded with zeroes data[len] = 0; // need to make a C string again, with null terminator - //if ( ! alreadyReceived timestamp ) { if (flags == TXT_TYPE_PLAIN) { onMessageRecv(from, packet->isRouteFlood() ? packet->path_len : 0xFF, timestamp, (const char *) &data[5]); // let UI know @@ -131,7 +130,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender // NOTE: no ack expected for CLI_DATA replies if (packet->isRouteFlood()) { - // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK + // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra) mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0); if (path) sendFlood(path); } From 68770d7728bbed8445c4c0f4065925841248b183 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 4 Mar 2025 18:29:16 +1100 Subject: [PATCH 3/7] * 'last_snr' added to repeater stats. --- examples/simple_repeater/main.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d47a6e02..d2a9626f 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -103,7 +103,8 @@ struct RepeaterStats { uint32_t total_up_time_secs; uint32_t n_sent_flood, n_sent_direct; uint32_t n_recv_flood, n_recv_direct; - uint16_t n_full_events, reserved1; + uint16_t n_full_events; + int16_t last_snr; // x 4 uint16_t n_direct_dups, n_flood_dups; }; @@ -171,7 +172,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { stats.n_recv_flood = getNumRecvFlood(); stats.n_recv_direct = getNumRecvDirect(); stats.n_full_events = getNumFullEvents(); - stats.reserved1 = 0; + stats.last_snr = (int16_t)(my_radio->getLastSNR() * 4); stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); From 372c2282103b28c04a5126c501093e93c21c01a7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 4 Mar 2025 23:09:43 +1100 Subject: [PATCH 4/7] * new ui/DisplayDriver classes (just SSD1306Display impl for now) * companion radio: now with optional UITask (enabled by DISPLAY_CLASS config in target/env) --- examples/companion_radio/UITask.cpp | 110 ++++++++++++++++++++++++++++ examples/companion_radio/UITask.h | 22 ++++++ examples/companion_radio/main.cpp | 27 +++++++ platformio.ini | 13 +++- src/helpers/HeltecV3Board.h | 2 + src/helpers/ui/DisplayDriver.h | 27 +++++++ src/helpers/ui/SSD1306Display.cpp | 56 ++++++++++++++ src/helpers/ui/SSD1306Display.h | 37 ++++++++++ 8 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 examples/companion_radio/UITask.cpp create mode 100644 examples/companion_radio/UITask.h create mode 100644 src/helpers/ui/DisplayDriver.h create mode 100644 src/helpers/ui/SSD1306Display.cpp create mode 100644 src/helpers/ui/SSD1306Display.h diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp new file mode 100644 index 00000000..b30c34b1 --- /dev/null +++ b/examples/companion_radio/UITask.cpp @@ -0,0 +1,110 @@ +#include "UITask.h" +#include +#include + +#define AUTO_OFF_MILLIS 15000 // 15 seconds + +// 'meshcore', 128x13px +static const uint8_t meshcore_logo [] PROGMEM = { + 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, + 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, + 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, + 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, + 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, + 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, + 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, + 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, + 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, +}; + +void UITask::begin(const char* node_name, const char* build_date) { + _prevBtnState = HIGH; + _auto_off = millis() + AUTO_OFF_MILLIS; + clearMsgPreview(); + _node_name = node_name; + _build_date = build_date; + _display->turnOn(); +} + +void UITask::clearMsgPreview() { + _origin[0] = 0; + _msg[0] = 0; +} + +void UITask::showMsgPreview(uint8_t path_len, const char* from_name, const char* text) { + if (path_len == 0xFF) { + sprintf(_origin, "(F) %s", from_name); + } else { + sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name); + } + StrHelper::strncpy(_msg, text, sizeof(_msg)); + + if (!_display->isOn()) _display->turnOn(); + _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer +} + +void UITask::renderCurrScreen() { + char tmp[80]; + if (_origin[0] && _msg[0]) { + // render message preview + _display->setCursor(0, 0); + _display->setTextSize(1); + _display->print(_node_name); + + _display->setCursor(0, 12); + _display->print(_origin); + _display->setCursor(0, 24); + _display->print(_msg); + + //_display->setCursor(100, 9); TODO + //_display->setTextSize(2); + //_display->printf("%d", msgs); + } else { + // render 'home' screen + _display->drawXbm(0, 0, meshcore_logo, 128, 13); + _display->setCursor(0, 20); + _display->setTextSize(1); + _display->print(_node_name); + + sprintf(tmp, "Build: %s", _build_date); + _display->setCursor(0, 32); + _display->print(tmp); + //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); + //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); + } +} + +void UITask::loop() { + if (millis() >= _next_read) { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState != _prevBtnState) { + if (btnState == LOW) { // pressed? + if (_display->isOn()) { + clearMsgPreview(); + } else { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } + _prevBtnState = btnState; + } + _next_read = millis() + 100; // 10 reads per second + } + + if (_display->isOn()) { + if (millis() >= _next_refresh) { + _display->startFrame(); + renderCurrScreen(); + _display->endFrame(); + + _next_refresh = millis() + 1000; // refresh every second + } + if (millis() > _auto_off) { + _display->turnOff(); + } + } +} diff --git a/examples/companion_radio/UITask.h b/examples/companion_radio/UITask.h new file mode 100644 index 00000000..89c58763 --- /dev/null +++ b/examples/companion_radio/UITask.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +class UITask { + DisplayDriver* _display; + unsigned long _next_read, _next_refresh, _auto_off; + int _prevBtnState; + const char* _node_name; + const char* _build_date; + char _origin[62]; + char _msg[80]; + + void renderCurrScreen(); +public: + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + void begin(const char* node_name, const char* build_date); + + void clearMsgPreview(); + void showMsgPreview(uint8_t path_len, const char* from_name, const char* text); + void loop(); +}; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index f81b9ffc..69f3df4e 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -84,6 +84,15 @@ #error "need to provide a 'board' object" #endif +#ifdef DISPLAY_CLASS + #include + + static DISPLAY_CLASS display; + + #include "UITask.h" + static UITask ui_task(display); +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -471,6 +480,9 @@ protected: } else { soundBuzzer(); } + #ifdef DISPLAY_CLASS + ui_task.showMsgPreview(path_len, from.name, text); + #endif } void onMessageRecv(const ContactInfo& from, uint8_t path_len, uint32_t sender_timestamp, const char *text) override { @@ -510,6 +522,9 @@ protected: } else { soundBuzzer(); } + #ifdef DISPLAY_CLASS + ui_task.showMsgPreview(in_path_len < 0 ? 0xFF : in_path_len, "Public", text); + #endif } void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) override { @@ -1077,6 +1092,10 @@ public: } else if (!_serial->isWriteBusy()) { checkConnections(); } + + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif } }; @@ -1132,6 +1151,10 @@ void setup() { float tcxo = 1.6f; #endif +#ifdef DISPLAY_CLASS + display.begin(); +#endif + #if defined(NRF52_PLATFORM) SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); SPI.begin(); @@ -1190,6 +1213,10 @@ void setup() { #else #error "need to define filesystem" #endif + +#ifdef DISPLAY_CLASS + ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE); +#endif } void loop() { diff --git a/platformio.ini b/platformio.ini index df6d2093..58d1e172 100644 --- a/platformio.ini +++ b/platformio.ini @@ -113,10 +113,14 @@ build_flags = -D P_LORA_TX_LED=35 -D PIN_BOARD_SDA=17 -D PIN_BOARD_SCL=18 + -D PIN_USER_BTN=0 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=130.0f ; for best TX power! build_src_filter = ${esp32_base.build_src_filter} +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 [env:Heltec_v3_repeater] extends = Heltec_lora32_v3 @@ -162,11 +166,12 @@ build_flags = ${Heltec_lora32_v3.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 + -D DISPLAY_CLASS=SSD1306Display ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 -build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/companion_radio/main.cpp> +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/companion_radio> lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -177,13 +182,14 @@ build_flags = ${Heltec_lora32_v3.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 + -D DISPLAY_CLASS=SSD1306Display -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 ; -D ENABLE_PRIVATE_KEY_IMPORT=1 ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/companion_radio/main.cpp> +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + +<../examples/companion_radio> lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -194,6 +200,7 @@ build_flags = ${Heltec_lora32_v3.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=1 + -D DISPLAY_CLASS=SSD1306Display -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID="\"myssid\"" -D WIFI_PWD="\"mypwd\"" @@ -201,7 +208,7 @@ build_flags = ; -D ENABLE_PRIVATE_KEY_EXPORT=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/companion_radio/main.cpp> +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + +<../examples/companion_radio> lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/src/helpers/HeltecV3Board.h b/src/helpers/HeltecV3Board.h index 232ed414..ce742efa 100644 --- a/src/helpers/HeltecV3Board.h +++ b/src/helpers/HeltecV3Board.h @@ -17,6 +17,7 @@ #define PIN_ADC_CTRL_ACTIVE LOW #define PIN_ADC_CTRL_INACTIVE HIGH #define PIN_LED_BUILTIN 35 +#define PIN_VEXT_EN 36 #include "ESP32Board.h" @@ -28,6 +29,7 @@ public: ESP32Board::begin(); pinMode(PIN_ADC_CTRL, OUTPUT); + //pinMode(PIN_VEXT_EN, OUTPUT); esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h new file mode 100644 index 00000000..1c8bebc7 --- /dev/null +++ b/src/helpers/ui/DisplayDriver.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +class DisplayDriver { + int _w, _h; +protected: + DisplayDriver(int w, int h) { _w = w; _h = h; } +public: + enum Color { DARK, LIGHT }; + + int width() const { return _w; } + int height() const { return _h; } + + virtual bool isOn() = 0; + virtual void turnOn() = 0; + virtual void turnOff() = 0; + virtual void startFrame(Color bkg = DARK) = 0; + virtual void setTextSize(int sz) = 0; + virtual void setColor(Color c) = 0; + virtual void setCursor(int x, int y) = 0; + virtual void print(const char* str) = 0; + virtual void fillRect(int x, int y, int w, int h) = 0; + virtual void drawRect(int x, int y, int w, int h) = 0; + virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0; + virtual void endFrame() = 0; +}; diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp new file mode 100644 index 00000000..84db7d13 --- /dev/null +++ b/src/helpers/ui/SSD1306Display.cpp @@ -0,0 +1,56 @@ +#include "SSD1306Display.h" + +bool SSD1306Display::begin() { + return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS); +} + +void SSD1306Display::turnOn() { + display.ssd1306_command(SSD1306_DISPLAYON); + _isOn = true; +} + +void SSD1306Display::turnOff() { + display.ssd1306_command(SSD1306_DISPLAYOFF); + _isOn = false; +} + +void SSD1306Display::startFrame(Color bkg) { + display.clearDisplay(); // TODO: apply 'bkg' + _color = SSD1306_WHITE; + display.setTextColor(_color); + display.setTextSize(1); + display.cp437(true); // Use full 256 char 'Code Page 437' font +} + +void SSD1306Display::setTextSize(int sz) { + display.setTextSize(sz); +} + +void SSD1306Display::setColor(Color c) { + _color = (c == LIGHT) ? SSD1306_WHITE : SSD1306_BLACK; + display.setTextColor(_color); +} + +void SSD1306Display::setCursor(int x, int y) { + display.setCursor(x, y); +} + +void SSD1306Display::print(const char* str) { + display.print(str); +} + +void SSD1306Display::fillRect(int x, int y, int w, int h) { + display.fillRect(x, y, w, h, _color); +} + +void SSD1306Display::drawRect(int x, int y, int w, int h) { + display.drawRect(x, y, w, h, _color); +} + +void SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + display.drawBitmap(x, y, bits, w, h, SSD1306_WHITE); +} + +void SSD1306Display::endFrame() { + display.display(); +} diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h new file mode 100644 index 00000000..a685eedf --- /dev/null +++ b/src/helpers/ui/SSD1306Display.h @@ -0,0 +1,37 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include +#include + +#ifndef PIN_OLED_RESET + #define PIN_OLED_RESET 21 // Reset pin # (or -1 if sharing Arduino reset pin) +#endif + +#ifndef DISPLAY_ADDRESS + #define DISPLAY_ADDRESS 0x3C +#endif + +class SSD1306Display : public DisplayDriver { + Adafruit_SSD1306 display; + bool _isOn; + uint8_t _color; + +public: + SSD1306Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { _isOn = false; } + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void startFrame(Color bkg = DARK) override; + void setTextSize(int sz) override; + void setColor(Color c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + void endFrame() override; +}; From c2ae34314eaa7c28481acd65c1cc0b44de5bb7ab Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 5 Mar 2025 12:52:29 +1100 Subject: [PATCH 5/7] * basic UITask added to repeater and room server (only HeltecV3 targets enable so far) --- examples/simple_repeater/UITask.cpp | 79 ++++++++++++++++++++++++++ examples/simple_repeater/UITask.h | 18 ++++++ examples/simple_repeater/main.cpp | 21 +++++++ examples/simple_room_server/UITask.cpp | 79 ++++++++++++++++++++++++++ examples/simple_room_server/UITask.h | 18 ++++++ examples/simple_room_server/main.cpp | 22 +++++++ platformio.ini | 7 ++- 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 examples/simple_repeater/UITask.cpp create mode 100644 examples/simple_repeater/UITask.h create mode 100644 examples/simple_room_server/UITask.cpp create mode 100644 examples/simple_room_server/UITask.h diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp new file mode 100644 index 00000000..e8a204e3 --- /dev/null +++ b/examples/simple_repeater/UITask.cpp @@ -0,0 +1,79 @@ +#include "UITask.h" +#include + +#define AUTO_OFF_MILLIS 20000 // 20 seconds + +// 'meshcore', 128x13px +static const uint8_t meshcore_logo [] PROGMEM = { + 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, + 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, + 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, + 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, + 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, + 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, + 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, + 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, + 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, +}; + +void UITask::begin(const char* node_name, const char* build_date) { + _prevBtnState = HIGH; + _auto_off = millis() + AUTO_OFF_MILLIS; + _node_name = node_name; + _build_date = build_date; + _display->turnOn(); +} + +void UITask::renderCurrScreen() { + char tmp[80]; + // render 'home' screen + _display->drawXbm(0, 0, meshcore_logo, 128, 13); + _display->setCursor(0, 20); + _display->setTextSize(1); + _display->print(_node_name); + + sprintf(tmp, "Build: %s", _build_date); + _display->setCursor(0, 32); + _display->print(tmp); + _display->setCursor(0, 43); + _display->print("< Repeater >"); + //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); + //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); +} + +void UITask::loop() { +#ifdef PIN_USER_BTN + if (millis() >= _next_read) { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState != _prevBtnState) { + if (btnState == LOW) { // pressed? + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } + _prevBtnState = btnState; + } + _next_read = millis() + 200; // 5 reads per second + } +#endif + + if (_display->isOn()) { + if (millis() >= _next_refresh) { + _display->startFrame(); + renderCurrScreen(); + _display->endFrame(); + + _next_refresh = millis() + 1000; // refresh every second + } + if (millis() > _auto_off) { + _display->turnOff(); + } + } +} diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h new file mode 100644 index 00000000..e58d6111 --- /dev/null +++ b/examples/simple_repeater/UITask.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class UITask { + DisplayDriver* _display; + unsigned long _next_read, _next_refresh, _auto_off; + int _prevBtnState; + const char* _node_name; + const char* _build_date; + + void renderCurrScreen(); +public: + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + void begin(const char* node_name, const char* build_date); + + void loop(); +}; \ No newline at end of file diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d2a9626f..10776e3e 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -84,6 +84,15 @@ #error "need to provide a 'board' object" #endif +#ifdef DISPLAY_CLASS + #include + + static DISPLAY_CLASS display; + + #include "UITask.h" + static UITask ui_task(display); +#endif + #define PACKET_LOG_FILE "/packet_log" /* ------------------------------ Code -------------------------------- */ @@ -529,6 +538,7 @@ public: const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } + const char* getNodeName() { return _prefs.node_name; } void savePrefs() override { #if defined(NRF52_PLATFORM) @@ -604,6 +614,9 @@ public: updateAdvertTimer(); // schedule next local advert } + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif } }; @@ -649,6 +662,10 @@ void setup() { float tcxo = 1.6f; #endif +#ifdef DISPLAY_CLASS + display.begin(); +#endif + #if defined(NRF52_PLATFORM) SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); SPI.begin(); @@ -701,6 +718,10 @@ void setup() { the_mesh.begin(fs); +#ifdef DISPLAY_CLASS + ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE); +#endif + // send out initial Advertisement to the mesh the_mesh.sendSelfAdvertisement(2000); } diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp new file mode 100644 index 00000000..2f1d8ae5 --- /dev/null +++ b/examples/simple_room_server/UITask.cpp @@ -0,0 +1,79 @@ +#include "UITask.h" +#include + +#define AUTO_OFF_MILLIS 20000 // 20 seconds + +// 'meshcore', 128x13px +static const uint8_t meshcore_logo [] PROGMEM = { + 0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, + 0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe, + 0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc, + 0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00, + 0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00, + 0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8, + 0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0, + 0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00, + 0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00, + 0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8, + 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, +}; + +void UITask::begin(const char* node_name, const char* build_date) { + _prevBtnState = HIGH; + _auto_off = millis() + AUTO_OFF_MILLIS; + _node_name = node_name; + _build_date = build_date; + _display->turnOn(); +} + +void UITask::renderCurrScreen() { + char tmp[80]; + // render 'home' screen + _display->drawXbm(0, 0, meshcore_logo, 128, 13); + _display->setCursor(0, 20); + _display->setTextSize(1); + _display->print(_node_name); + + sprintf(tmp, "Build: %s", _build_date); + _display->setCursor(0, 32); + _display->print(tmp); + _display->setCursor(0, 43); + _display->print("< Room Server >"); + //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); + //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); +} + +void UITask::loop() { +#ifdef PIN_USER_BTN + if (millis() >= _next_read) { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState != _prevBtnState) { + if (btnState == LOW) { // pressed? + if (_display->isOn()) { + // TODO: any action ? + } else { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + } + _prevBtnState = btnState; + } + _next_read = millis() + 200; // 5 reads per second + } +#endif + + if (_display->isOn()) { + if (millis() >= _next_refresh) { + _display->startFrame(); + renderCurrScreen(); + _display->endFrame(); + + _next_refresh = millis() + 1000; // refresh every second + } + if (millis() > _auto_off) { + _display->turnOff(); + } + } +} diff --git a/examples/simple_room_server/UITask.h b/examples/simple_room_server/UITask.h new file mode 100644 index 00000000..e58d6111 --- /dev/null +++ b/examples/simple_room_server/UITask.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class UITask { + DisplayDriver* _display; + unsigned long _next_read, _next_refresh, _auto_off; + int _prevBtnState; + const char* _node_name; + const char* _build_date; + + void renderCurrScreen(); +public: + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + void begin(const char* node_name, const char* build_date); + + void loop(); +}; \ No newline at end of file diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index cce7e3d4..ed1ab345 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -88,6 +88,15 @@ #error "need to provide a 'board' object" #endif +#ifdef DISPLAY_CLASS + #include + + static DISPLAY_CLASS display; + + #include "UITask.h" + static UITask ui_task(display); +#endif + /* ------------------------------ Code -------------------------------- */ struct ClientInfo { @@ -570,6 +579,7 @@ public: const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } + const char* getNodeName() { return _prefs.node_name; } void savePrefs() override { #if defined(NRF52_PLATFORM) @@ -664,6 +674,10 @@ public: updateAdvertTimer(); // schedule next local advert } + #ifdef DISPLAY_CLASS + ui_task.loop(); + #endif + // TODO: periodically check for OLD/inactive entries in known_clients[], and evict } }; @@ -710,6 +724,10 @@ void setup() { float tcxo = 1.6f; #endif +#ifdef DISPLAY_CLASS + display.begin(); +#endif + #if defined(NRF52_PLATFORM) SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); SPI.begin(); @@ -761,6 +779,10 @@ void setup() { the_mesh.begin(fs); +#ifdef DISPLAY_CLASS + ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE); +#endif + // send out initial Advertisement to the mesh the_mesh.sendSelfAdvertisement(2000); } diff --git a/platformio.ini b/platformio.ini index 58d1e172..2457a306 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,19 +126,21 @@ lib_deps = extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} + -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME="\"Heltec Repeater\"" -D ADVERT_LAT=-37.0 -D ADVERT_LON=145.0 -D ADMIN_PASSWORD="\"password\"" -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_repeater/main.cpp> +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_repeater> [env:Heltec_v3_room_server] extends = Heltec_lora32_v3 -build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_room_server/main.cpp> +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_room_server> build_flags = ${Heltec_lora32_v3.build_flags} + -D DISPLAY_CLASS=SSD1306Display -D ADVERT_NAME="\"Heltec Room\"" -D ADVERT_LAT=-37.0 -D ADVERT_LON=145.0 @@ -526,6 +528,7 @@ board_build.ldscript = boards/nrf52840_s140_v7.ld build_flags = ${nrf52840_t1000e.build_flags} -Ivariants/t1000-e -DT1000_E + -D PIN_USER_BTN=6 -D RADIO_CLASS=CustomLR1110 -D WRAPPER_CLASS=CustomLR1110Wrapper build_src_filter = ${nrf52840_t1000e.build_src_filter} From 86681364bd873e04ecf05835d9a2188f5e12990c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 5 Mar 2025 15:47:29 +1100 Subject: [PATCH 6/7] * companion: UITask now shows BLE PIN (when no connection) * companion, HeltecV3: new installs, now chooses random BLE PIN * companion: prefs load/save improvements (sanitises bad values) --- examples/companion_radio/UITask.cpp | 15 +++++-- examples/companion_radio/UITask.h | 7 ++- examples/companion_radio/main.cpp | 67 ++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp index b30c34b1..f003aa52 100644 --- a/examples/companion_radio/UITask.cpp +++ b/examples/companion_radio/UITask.cpp @@ -21,12 +21,13 @@ static const uint8_t meshcore_logo [] PROGMEM = { 0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8, }; -void UITask::begin(const char* node_name, const char* build_date) { +void UITask::begin(const char* node_name, const char* build_date, uint32_t pin_code) { _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; clearMsgPreview(); _node_name = node_name; _build_date = build_date; + _pin_code = pin_code; _display->turnOn(); } @@ -73,8 +74,16 @@ void UITask::renderCurrScreen() { sprintf(tmp, "Build: %s", _build_date); _display->setCursor(0, 32); _display->print(tmp); - //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); - //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); + + if (_connected) { + //_display->printf("freq : %03.2f sf %d\n", _prefs.freq, _prefs.sf); + //_display->printf("bw : %03.2f cr %d\n", _prefs.bw, _prefs.cr); + } else { + _display->setTextSize(2); + _display->setCursor(0, 43); + sprintf(tmp, "Pin:%d", _pin_code); + _display->print(tmp); + } } } diff --git a/examples/companion_radio/UITask.h b/examples/companion_radio/UITask.h index 89c58763..f1a63225 100644 --- a/examples/companion_radio/UITask.h +++ b/examples/companion_radio/UITask.h @@ -6,6 +6,8 @@ class UITask { DisplayDriver* _display; unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; + bool _connected; + uint32_t _pin_code; const char* _node_name; const char* _build_date; char _origin[62]; @@ -13,9 +15,10 @@ class UITask { void renderCurrScreen(); public: - UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } - void begin(const char* node_name, const char* build_date); + UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; _connected = false; } + void begin(const char* node_name, const char* build_date, uint32_t pin_code); + void setHasConnection(bool connected) { _connected = connected; } void clearMsgPreview(); void showMsgPreview(uint8_t path_len, const char* from_name, const char* text); void loop(); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 69f3df4e..1f528439 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -187,6 +187,7 @@ struct NodePrefs { // persisted to file uint8_t tx_power_dbm; uint8_t unused[3]; float rx_delay_base; + uint32_t ble_pin; }; class MyMesh : public BaseChatMesh { @@ -219,6 +220,11 @@ class MyMesh : public BaseChatMesh { if (!_identity_store->load("_main", self_id)) { self_id = mesh::LocalIdentity(&trng); // create new random identity saveMainIdentity(self_id); + + #if defined(BLE_PIN_CODE) && defined(DISPLAY_CLASS) + // start with randomly assigned BLE pin + _prefs.ble_pin = trng.nextInt(100000, 999999); + #endif } } @@ -611,6 +617,9 @@ public: _prefs.bw = LORA_BW; _prefs.cr = LORA_CR; _prefs.tx_power_dbm = LORA_TX_POWER; + #ifdef BLE_PIN_CODE + _prefs.ble_pin = BLE_PIN_CODE; + #endif //_prefs.rx_delay_base = 10.0f; enable once new algo fixed } @@ -631,7 +640,34 @@ public: if (_fs->exists("/node_prefs")) { File file = _fs->open("/node_prefs"); if (file) { - file.read((uint8_t *) &_prefs, sizeof(_prefs)); + uint8_t pad[8]; + + file.read((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0 + file.read((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4 + file.read(pad, 4); // 36 + file.read((uint8_t *) &_prefs.node_lat, sizeof(_prefs.node_lat)); // 40 + file.read((uint8_t *) &_prefs.node_lon, sizeof(_prefs.node_lon)); // 48 + file.read((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56 + file.read((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60 + file.read((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61 + file.read((uint8_t *) &_prefs.reserved1, sizeof(_prefs.reserved1)); // 62 + file.read((uint8_t *) &_prefs.reserved2, sizeof(_prefs.reserved2)); // 63 + file.read((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64 + file.read((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + file.read((uint8_t *) _prefs.unused, sizeof(_prefs.unused)); // 69 + file.read((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + file.read(pad, 4); // 76 + file.read((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 + + // sanitise bad pref values + _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); + _prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f); + _prefs.freq = constrain(_prefs.freq, 400.0f, 2500.0f); + _prefs.bw = constrain(_prefs.bw, 62.5f, 500.0f); + _prefs.sf = constrain(_prefs.sf, 7, 12); + _prefs.cr = constrain(_prefs.cr, 5, 8); + _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER); + file.close(); } } @@ -650,6 +686,7 @@ public: } const char* getNodeName() { return _prefs.node_name; } + uint32_t getBLEPin() { return _prefs.ble_pin; } void startInterface(BaseSerialInterface& serial) { _serial = &serial; @@ -664,7 +701,26 @@ public: File file = _fs->open("/node_prefs", "w", true); #endif if (file) { - file.write((const uint8_t *)&_prefs, sizeof(_prefs)); + uint8_t pad[8]; + memset(pad, 0, sizeof(pad)); + + file.write((uint8_t *) &_prefs.airtime_factor, sizeof(float)); // 0 + file.write((uint8_t *) _prefs.node_name, sizeof(_prefs.node_name)); // 4 + file.write(pad, 4); // 36 + file.write((uint8_t *) &_prefs.node_lat, sizeof(_prefs.node_lat)); // 40 + file.write((uint8_t *) &_prefs.node_lon, sizeof(_prefs.node_lon)); // 48 + file.write((uint8_t *) &_prefs.freq, sizeof(_prefs.freq)); // 56 + file.write((uint8_t *) &_prefs.sf, sizeof(_prefs.sf)); // 60 + file.write((uint8_t *) &_prefs.cr, sizeof(_prefs.cr)); // 61 + file.write((uint8_t *) &_prefs.reserved1, sizeof(_prefs.reserved1)); // 62 + file.write((uint8_t *) &_prefs.reserved2, sizeof(_prefs.reserved2)); // 63 + file.write((uint8_t *) &_prefs.bw, sizeof(_prefs.bw)); // 64 + file.write((uint8_t *) &_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 + file.write((uint8_t *) _prefs.unused, sizeof(_prefs.unused)); // 69 + file.write((uint8_t *) &_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 + file.write(pad, 4); // 76 + file.write((uint8_t *) &_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 + file.close(); } } @@ -1094,6 +1150,7 @@ public: } #ifdef DISPLAY_CLASS + ui_task.setHasConnection(_serial->isConnected()); ui_task.loop(); #endif } @@ -1189,7 +1246,7 @@ void setup() { #ifdef BLE_PIN_CODE char dev_name[32+10]; sprintf(dev_name, "MeshCore-%s", the_mesh.getNodeName()); - serial_interface.begin(dev_name, BLE_PIN_CODE); + serial_interface.begin(dev_name, the_mesh.getBLEPin()); #else pinMode(WB_IO2, OUTPUT); serial_interface.begin(Serial); @@ -1205,7 +1262,7 @@ void setup() { #elif defined(BLE_PIN_CODE) char dev_name[32+10]; sprintf(dev_name, "MeshCore-%s", the_mesh.getNodeName()); - serial_interface.begin(dev_name, BLE_PIN_CODE); + serial_interface.begin(dev_name, the_mesh.getBLEPin()); #else serial_interface.begin(Serial); #endif @@ -1215,7 +1272,7 @@ void setup() { #endif #ifdef DISPLAY_CLASS - ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE); + ui_task.begin(the_mesh.getNodeName(), FIRMWARE_BUILD_DATE, the_mesh.getBLEPin()); #endif } From 01d84d5d3e803e16d0a9e45e395989edf7df58fe Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 5 Mar 2025 16:39:45 +1100 Subject: [PATCH 7/7] * repeater and room server: CommonCLI now handles load/save of Prefs. Now sanitise bad prefs values. --- examples/simple_repeater/main.cpp | 19 +------ examples/simple_room_server/main.cpp | 19 +------ src/helpers/CommonCLI.cpp | 83 ++++++++++++++++++++++++++++ src/helpers/CommonCLI.h | 3 + 4 files changed, 90 insertions(+), 34 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 10776e3e..08fca56e 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -519,13 +519,7 @@ public: mesh::Mesh::begin(); _fs = fs; // load persisted prefs - if (_fs->exists("/node_prefs")) { - File file = _fs->open("/node_prefs"); - if (file) { - file.read((uint8_t *) &_prefs, sizeof(_prefs)); - file.close(); - } - } + _cli.loadPrefs(_fs); _phy->setFrequency(_prefs.freq); _phy->setSpreadingFactor(_prefs.sf); @@ -541,16 +535,7 @@ public: const char* getNodeName() { return _prefs.node_name; } void savePrefs() override { -#if defined(NRF52_PLATFORM) - File file = _fs->open("/node_prefs", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } -#else - File file = _fs->open("/node_prefs", "w", true); -#endif - if (file) { - file.write((const uint8_t *)&_prefs, sizeof(_prefs)); - file.close(); - } + _cli.savePrefs(_fs); } bool formatFileSystem() override { diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index ed1ab345..9b33c794 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -560,13 +560,7 @@ public: mesh::Mesh::begin(); _fs = fs; // load persisted prefs - if (_fs->exists("/node_prefs")) { - File file = _fs->open("/node_prefs"); - if (file) { - file.read((uint8_t *) &_prefs, sizeof(_prefs)); - file.close(); - } - } + _cli.loadPrefs(_fs); _phy->setFrequency(_prefs.freq); _phy->setSpreadingFactor(_prefs.sf); @@ -582,16 +576,7 @@ public: const char* getNodeName() { return _prefs.node_name; } void savePrefs() override { -#if defined(NRF52_PLATFORM) - File file = _fs->open("/node_prefs", FILE_O_WRITE); - if (file) { file.seek(0); file.truncate(); } -#else - File file = _fs->open("/node_prefs", "w", true); -#endif - if (file) { - file.write((const uint8_t *)&_prefs, sizeof(_prefs)); - file.close(); - } + _cli.savePrefs(_fs); } bool formatFileSystem() override { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b436a1da..21cd690f 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -13,6 +13,89 @@ static uint32_t _atoi(const char* sp) { return n; } +void CommonCLI::loadPrefs(FILESYSTEM* fs) { + if (fs->exists("/node_prefs")) { + File file = fs->open("/node_prefs"); + if (file) { + uint8_t pad[8]; + + file.read((uint8_t *) &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 + file.read((uint8_t *) &_prefs->node_name, sizeof(_prefs->node_name)); // 4 + file.read(pad, 4); // 36 + file.read((uint8_t *) &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 + file.read((uint8_t *) &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 + file.read((uint8_t *) &_prefs->password[0], sizeof(_prefs->password)); // 56 + file.read((uint8_t *) &_prefs->freq, sizeof(_prefs->freq)); // 72 + file.read((uint8_t *) &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 + file.read((uint8_t *) &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 + file.read((uint8_t *) &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 + file.read((uint8_t *) &_prefs->unused, sizeof(_prefs->unused)); // 79 + file.read((uint8_t *) &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 + file.read((uint8_t *) &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 + file.read((uint8_t *) &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 + file.read((uint8_t *) &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 + file.read(pad, 4); // 108 + file.read((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112 + file.read((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113 + file.read((uint8_t *) &_prefs->reserved1, sizeof(_prefs->reserved1)); // 114 + file.read((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115 + file.read((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116 + file.read(pad, 4); // 120 + + // sanitise bad pref values + _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); + _prefs->tx_delay_factor = constrain(_prefs->tx_delay_factor, 0, 2.0f); + _prefs->direct_tx_delay_factor = constrain(_prefs->direct_tx_delay_factor, 0, 2.0f); + _prefs->airtime_factor = constrain(_prefs->airtime_factor, 0, 9.0f); + _prefs->freq = constrain(_prefs->freq, 400.0f, 2500.0f); + _prefs->bw = constrain(_prefs->bw, 62.5f, 500.0f); + _prefs->sf = constrain(_prefs->sf, 7, 12); + _prefs->cr = constrain(_prefs->cr, 5, 8); + _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, 1, 30); + + file.close(); + } + } +} + +void CommonCLI::savePrefs(FILESYSTEM* fs) { +#if defined(NRF52_PLATFORM) + File file = fs->open("/node_prefs", FILE_O_WRITE); + if (file) { file.seek(0); file.truncate(); } +#else + File file = fs->open("/node_prefs", "w", true); +#endif + if (file) { + uint8_t pad[8]; + memset(pad, 0, sizeof(pad)); + + file.write((uint8_t *) &_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 + file.write((uint8_t *) &_prefs->node_name, sizeof(_prefs->node_name)); // 4 + file.write(pad, 4); // 36 + file.write((uint8_t *) &_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 + file.write((uint8_t *) &_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 + file.write((uint8_t *) &_prefs->password[0], sizeof(_prefs->password)); // 56 + file.write((uint8_t *) &_prefs->freq, sizeof(_prefs->freq)); // 72 + file.write((uint8_t *) &_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 + file.write((uint8_t *) &_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 + file.write((uint8_t *) &_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 + file.write((uint8_t *) &_prefs->unused, sizeof(_prefs->unused)); // 79 + file.write((uint8_t *) &_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 + file.write((uint8_t *) &_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 + file.write((uint8_t *) &_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 + file.write((uint8_t *) &_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 + file.write(pad, 4); // 108 + file.write((uint8_t *) &_prefs->sf, sizeof(_prefs->sf)); // 112 + file.write((uint8_t *) &_prefs->cr, sizeof(_prefs->cr)); // 113 + file.write((uint8_t *) &_prefs->reserved1, sizeof(_prefs->reserved1)); // 114 + file.write((uint8_t *) &_prefs->reserved2, sizeof(_prefs->reserved2)); // 115 + file.write((uint8_t *) &_prefs->bw, sizeof(_prefs->bw)); // 116 + file.write(pad, 4); // 120 + + file.close(); + } +} + #define MIN_LOCAL_ADVERT_INTERVAL 60 void CommonCLI::checkAdvertInterval() { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f384bd7f..ec038d07 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -1,6 +1,7 @@ #pragma once #include "Mesh.h" +#include struct NodePrefs { // persisted to file float airtime_factor; @@ -54,5 +55,7 @@ public: CommonCLI(mesh::MainBoard& board, mesh::Mesh* mesh, NodePrefs* prefs, CommonCLICallbacks* callbacks) : _board(&board), _mesh(mesh), _prefs(prefs), _callbacks(callbacks) { } + void loadPrefs(FILESYSTEM* _fs); + void savePrefs(FILESYSTEM* _fs); void handleCommand(uint32_t sender_timestamp, const char* command, char* reply); };