From f0163ef5fabc7cdc7aa6dc942d90258abee5042d Mon Sep 17 00:00:00 2001 From: Jukka Vaisanen Date: Thu, 20 Aug 2026 15:00:00 +0300 Subject: [PATCH] Added variant with minimal 64x32 UI for Wireless Stick V3 Add a companion-radio target for the Heltec Wireless Stick V3. Make SSD1306 dimensions configurable with 128x64 defaults, and use a dedicated 64x32 UI for Bluetooth status, message count, and node identity. --- examples/companion_radio/ui-64x32/UITask.cpp | 117 +++++++++++++++++++ examples/companion_radio/ui-64x32/UITask.h | 36 ++++++ src/helpers/ui/SSD1306Display.h | 12 +- variants/heltec_v3/platformio.ini | 26 +++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 examples/companion_radio/ui-64x32/UITask.cpp create mode 100644 examples/companion_radio/ui-64x32/UITask.h diff --git a/examples/companion_radio/ui-64x32/UITask.cpp b/examples/companion_radio/ui-64x32/UITask.cpp new file mode 100644 index 0000000000..853c69d9ec --- /dev/null +++ b/examples/companion_radio/ui-64x32/UITask.cpp @@ -0,0 +1,117 @@ +#include "UITask.h" + +#include "../MyMesh.h" +#include "target.h" + +#define UI_REFRESH_MILLIS 250 +#define UI_FOOTER_ALTERNATE_MILLIS 2000 + +void UITask::begin(DisplayDriver *display, SensorManager *sensors, NodePrefs *node_prefs) { + (void)sensors; + (void)node_prefs; + + _display = display; + _started_at = millis(); + +#ifdef PIN_USER_BTN + user_btn.begin(); +#endif + + wakeDisplay(); +} + +void UITask::wakeDisplay() { + if (_display == NULL) return; + + if (!_display->isOn()) { + _display->turnOn(); + } + _auto_off = millis() + AUTO_OFF_MILLIS; + _footer_started_at = millis(); + _next_refresh = 0; +} + +void UITask::render() { + char status[16]; + char messages[16]; + char node_id[10]; + char node_name[32]; + + if (hasConnection()) { + strcpy(status, "CONNECTED"); + } else if (!isBluetoothEnabled()) { + strcpy(status, "BT OFF"); + } else if (the_mesh.getBLEPin() != 0) { + snprintf(status, sizeof(status), "PIN %06lu", (unsigned long)the_mesh.getBLEPin()); + } else { + strcpy(status, "READY"); + } + + snprintf(messages, sizeof(messages), "MSG: %d", _msgcount); + + strcpy(node_id, "ID "); + mesh::Utils::toHex(&node_id[3], the_mesh.self_id.pub_key, 3); + _display->translateUTF8ToBlocks(node_name, the_mesh.getNodeName(), sizeof(node_name)); + + _display->startFrame(); + _display->setColor(UIColor::primary_txt); + _display->setTextSize(1); + _display->drawTextCentered(_display->width() / 2, 0, status); + _display->drawTextCentered(_display->width() / 2, 12, messages); + bool show_id = ((millis() - _footer_started_at) / UI_FOOTER_ALTERNATE_MILLIS) % 2; + if (show_id) { + _display->drawTextCentered(_display->width() / 2, 24, node_id); + } else if (_display->getTextWidth(node_name) <= _display->width()) { + _display->drawTextCentered(_display->width() / 2, 24, node_name); + } else { + _display->drawTextEllipsized(0, 24, _display->width(), node_name); + } + _display->endFrame(); +} + +void UITask::msgRead(int msgcount) { + _msgcount = msgcount; + _next_refresh = 0; +} + +void UITask::newMsg(uint8_t path_len, const char *from_name, const char *text, int msgcount) { + (void)path_len; + (void)from_name; + (void)text; + + _msgcount = msgcount; + wakeDisplay(); +} + +void UITask::notify(UIEventType type) { + (void)type; +} + +void UITask::loop() { +#ifdef PIN_USER_BTN + int event = user_btn.check(); + if (event == BUTTON_EVENT_CLICK || event == BUTTON_EVENT_DOUBLE_CLICK || + event == BUTTON_EVENT_TRIPLE_CLICK) { + if (_display != NULL && _display->isOn()) { + _display->turnOff(); + } else { + wakeDisplay(); + } + } else if (event == BUTTON_EVENT_LONG_PRESS && millis() - _started_at < 8000) { + the_mesh.enterCLIRescue(); + } +#endif + + if (_display == NULL || !_display->isOn()) return; + + if (millis() >= _next_refresh) { + render(); + _next_refresh = millis() + UI_REFRESH_MILLIS; + } + +#if AUTO_OFF_MILLIS > 0 + if (millis() >= _auto_off) { + _display->turnOff(); + } +#endif +} diff --git a/examples/companion_radio/ui-64x32/UITask.h b/examples/companion_radio/ui-64x32/UITask.h new file mode 100644 index 0000000000..c086b37347 --- /dev/null +++ b/examples/companion_radio/ui-64x32/UITask.h @@ -0,0 +1,36 @@ +#pragma once + +#include "../AbstractUITask.h" +#include "../NodePrefs.h" + +#include +#include +#include + +#ifndef AUTO_OFF_MILLIS +#define AUTO_OFF_MILLIS 15000 +#endif + +class UITask : public AbstractUITask { + DisplayDriver *_display; + unsigned long _next_refresh; + unsigned long _auto_off; + unsigned long _started_at; + unsigned long _footer_started_at; + int _msgcount; + + void render(); + void wakeDisplay(); + +public: + UITask(mesh::MainBoard *board, MultiSerialInterface *serial) + : AbstractUITask(board, serial), _display(NULL), _next_refresh(0), _auto_off(0), _started_at(0), + _footer_started_at(0), _msgcount(0) {} + + void begin(DisplayDriver *display, SensorManager *sensors, NodePrefs *node_prefs); + + void msgRead(int msgcount) override; + void newMsg(uint8_t path_len, const char *from_name, const char *text, int msgcount) override; + void notify(UIEventType type = UIEventType::none) override; + void loop() override; +}; diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 5dabbc58c0..e4584d3ff4 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -15,6 +15,14 @@ #define DISPLAY_ADDRESS 0x3C #endif +#ifndef SSD1306_WIDTH + #define SSD1306_WIDTH 128 +#endif + +#ifndef SSD1306_HEIGHT + #define SSD1306_HEIGHT 64 +#endif + class SSD1306Display : public DisplayDriver { Adafruit_SSD1306 display; bool _isOn; @@ -23,8 +31,8 @@ class SSD1306Display : public DisplayDriver { bool i2c_probe(TwoWire& wire, uint8_t addr); public: - SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), - display(128, 64, &Wire, PIN_OLED_RESET), + SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(SSD1306_WIDTH, SSD1306_HEIGHT), + display(SSD1306_WIDTH, SSD1306_HEIGHT, &Wire, PIN_OLED_RESET), _peripher_power(peripher_power) { _isOn = false; diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 65e636eb4e..4d7c2ac0a8 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -321,6 +321,32 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_WS_V3_companion_radio_ble] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -I examples/companion_radio/ui-64x32 + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D SSD1306_WIDTH=64 + -D SSD1306_HEIGHT=32 + -D BLE_PIN_CODE=123456 ; dynamic, random PIN displayed by the 64x32 UI + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-64x32/*.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_WSL3_companion_radio_usb] extends = Heltec_lora32_v3 build_flags =