From 372c2282103b28c04a5126c501093e93c21c01a7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 4 Mar 2025 23:09:43 +1100 Subject: [PATCH] * 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; +};