diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 9bb747e79..efdbbe6e2 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -56,6 +56,8 @@ #define CMD_SEND_ANON_REQ 57 #define CMD_SET_AUTOADD_CONFIG 58 #define CMD_GET_AUTOADD_CONFIG 59 +#define CMD_GET_DISPLAY_SETTINGS 60 +#define CMD_SET_DISPLAY_SETTINGS 61 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -88,6 +90,7 @@ #define RESP_CODE_TUNING_PARAMS 23 #define RESP_CODE_STATS 24 // v8+, second byte is stats type #define RESP_CODE_AUTOADD_CONFIG 25 +#define RESP_CODE_DISPLAY_SETTINGS 26 #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -115,6 +118,10 @@ #define PUSH_CODE_CONTROL_DATA 0x8E // v8+ #define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest #define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full +#define PUSH_CODE_DISPLAY_SETTINGS 0x91 + +#define DISPLAY_SETTING_SCREEN 0x01 +#define DISPLAY_SETTING_BRIGHTNESS 0x02 #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 @@ -152,6 +159,36 @@ void MyMesh::writeDisabledFrame() { _serial->writeFrame(buf, 1); } +void MyMesh::pushDisplaySettings() { +#ifdef DISPLAY_CLASS + if (!_serial || !_serial->isConnected()) { + return; + } + + uint8_t mask = 0; + if (board.supportsDisplaySettings()) { + mask |= DISPLAY_SETTING_SCREEN; + } + if (board.supportsDisplayBrightness()) { + mask |= DISPLAY_SETTING_BRIGHTNESS; + } + if (mask == 0) { + return; + } + + int i = 0; + out_frame[i++] = PUSH_CODE_DISPLAY_SETTINGS; + out_frame[i++] = mask; + if (mask & DISPLAY_SETTING_SCREEN) { + out_frame[i++] = board.getDisplayEnabled() ? 1 : 0; + } + if (mask & DISPLAY_SETTING_BRIGHTNESS) { + out_frame[i++] = board.getDisplayBrightness(); + } + _serial->writeFrame(out_frame, i); +#endif +} + void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) { int i = 0; out_frame[i++] = code; @@ -1632,6 +1669,75 @@ void MyMesh::handleCmdFrame(size_t len) { } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); } + } else if (cmd_frame[0] == CMD_GET_DISPLAY_SETTINGS) { +#ifdef DISPLAY_CLASS + uint8_t mask = 0; + if (board.supportsDisplaySettings()) { + mask |= DISPLAY_SETTING_SCREEN; + } + if (board.supportsDisplayBrightness()) { + mask |= DISPLAY_SETTING_BRIGHTNESS; + } + if (mask == 0) { + writeDisabledFrame(); + } else { + int i = 0; + out_frame[i++] = RESP_CODE_DISPLAY_SETTINGS; + out_frame[i++] = mask; + if (mask & DISPLAY_SETTING_SCREEN) { + out_frame[i++] = board.getDisplayEnabled() ? 1 : 0; + } + if (mask & DISPLAY_SETTING_BRIGHTNESS) { + out_frame[i++] = board.getDisplayBrightness(); + } + _serial->writeFrame(out_frame, i); + } +#else + writeDisabledFrame(); +#endif + } else if (cmd_frame[0] == CMD_SET_DISPLAY_SETTINGS && len >= 2) { +#ifdef DISPLAY_CLASS + uint8_t mask = cmd_frame[1]; + bool screen_supported = board.supportsDisplaySettings(); + bool brightness_supported = board.supportsDisplayBrightness(); + if (mask == 0 || + ((mask & DISPLAY_SETTING_SCREEN) && !screen_supported) || + ((mask & DISPLAY_SETTING_BRIGHTNESS) && !brightness_supported)) { + writeDisabledFrame(); + } else { + int expected = 2; + if (mask & DISPLAY_SETTING_SCREEN) expected += 1; + if (mask & DISPLAY_SETTING_BRIGHTNESS) expected += 1; + if (len < expected) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else { + int idx = 2; + if (mask & DISPLAY_SETTING_SCREEN) { + bool enabled = cmd_frame[idx++] != 0; + board.setDisplayEnabled(enabled); + if (enabled) { + display.turnOn(); + if (brightness_supported) { + display.setBrightness(board.getDisplayBrightness()); + } + } else { + display.turnOff(); + } + } + if (mask & DISPLAY_SETTING_BRIGHTNESS) { + uint8_t brightness = cmd_frame[idx++]; + board.setDisplayBrightness(brightness); + if (display.isOn()) { + display.setBrightness(brightness); + } + } + writeOKFrame(); + pushDisplaySettings(); + } + } +#else + writeDisabledFrame(); +#endif } else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) { // FUTURE use: uint8_t reserved = cmd_frame[1]; uint8_t *pub_key = &cmd_frame[2]; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 95265a19a..cdab7b602 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -90,6 +90,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { void begin(bool has_display); void startInterface(BaseSerialInterface &serial); + void pushDisplaySettings(); const char *getNodeName(); NodePrefs *getNodePrefs(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0690b45ac..52b752ab8 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -88,16 +88,26 @@ class HomeScreen : public UIScreen { #if UI_SENSORS_PAGE == 1 SENSORS, #endif + #ifdef HELTEC_V3_SCREEN_LED_CONTROL + SETTINGS, + #endif SHUTDOWN, Count // keep as last }; UITask* _task; + mesh::MainBoard* _board; mesh::RTCClock* _rtc; SensorManager* _sensors; NodePrefs* _node_prefs; uint8_t _page; bool _shutdown_init; +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + bool _in_settings_submenu; // true when inside light settings submenu + uint8_t _display_menu_item; // 0=back, 1=screen, 2=led, 3=brightness + bool _brightness_popup; + uint8_t _brightness_popup_item; // 0=back, 1=low, 2=medium, 3=high +#endif AdvertPath recent[UI_RECENT_LIST_SIZE]; @@ -161,9 +171,13 @@ class HomeScreen : public UIScreen { } public: - HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs) - : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), - _shutdown_init(false), sensors_lpp(200) { } + HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs, mesh::MainBoard* board) + : _task(task), _board(board), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), + _shutdown_init(false) +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + , _in_settings_submenu(false), _display_menu_item(0), _brightness_popup(false), _brightness_popup_item(0) +#endif + , sensors_lpp(200) { } void poll() override { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released @@ -383,6 +397,83 @@ class HomeScreen : public UIScreen { } if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; +#endif +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + } else if (_page == HomePage::SETTINGS) { + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + + if (!_in_settings_submenu) { + // Main settings page - show icon/title + display.drawTextCentered(display.width() / 2, 26, "Light Settings"); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 50, "hold:enter"); + } else { + // Inside submenu - show options + // Get current screen and LED states + bool screen_enabled = true; + bool led_enabled = true; + if (_board && _board->supportsDisplaySettings()) { + screen_enabled = _board->getDisplayEnabled(); + } + if (_sensors != NULL) { + int num = _sensors->getNumSettings(); + for (int i = 0; i < num; i++) { + const char* name = _sensors->getSettingName(i); + if (strcmp(name, "led") == 0) { + led_enabled = (strcmp(_sensors->getSettingValue(i), "1") == 0); + } + } + } + + // Show menu with selection indicator (>) + int y = 18; + display.setCursor(0, y); + if (_display_menu_item == 0) display.print(">"); + display.print("Back"); + if (_display_menu_item == 0) display.print("<"); + + y += 12; + display.setCursor(0, y); + if (_display_menu_item == 1) display.print(">"); + display.print("Screen: "); + display.print(screen_enabled ? "ON" : "OFF"); + if (_display_menu_item == 1) display.print("<"); + + y += 12; + display.setCursor(0, y); + if (_display_menu_item == 2) display.print(">"); + display.print("LED: "); + display.print(led_enabled ? "ON" : "OFF"); + if (_display_menu_item == 2) display.print("<"); + + y += 12; + display.setCursor(0, y); + if (_display_menu_item == 3) display.print(">"); + display.print("Brightness: (hold)"); + if (_display_menu_item == 3) display.print("<"); + + // Brightness popup window + if (_brightness_popup) { + int box_w = display.width() - 12; + int box_h = 48; + int box_x = 6; + int box_y = 12; + display.setColor(DisplayDriver::DARK); + display.fillRect(box_x, box_y, box_w, box_h); + display.setColor(DisplayDriver::LIGHT); + display.drawRect(box_x, box_y, box_w, box_h); + + const char* labels[4] = {"Back", "Low", "Medium", "High"}; + for (int i = 0; i < 4; i++) { + int y = box_y + 4 + (i * 11); + display.setCursor(box_x + 4, y); + if (_brightness_popup_item == i) display.print(">"); + display.print(labels[i]); + if (_brightness_popup_item == i) display.print("<"); + } + } + } #endif } else if (_page == HomePage::SHUTDOWN) { display.setColor(DisplayDriver::GREEN); @@ -403,12 +494,66 @@ class HomeScreen : public UIScreen { return true; } if (c == KEY_NEXT || c == KEY_RIGHT) { +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // On SETTINGS page, use NEXT to scroll menu items only if in submenu + if (_page == HomePage::SETTINGS && _in_settings_submenu) { + if (_brightness_popup) { + _brightness_popup_item = (_brightness_popup_item + 1) % 4; + return true; + } + _display_menu_item = (_display_menu_item + 1) % 4; // 0=back, 1=screen, 2=led, 3=brightness + return true; + } +#endif _page = (_page + 1) % HomePage::Count; if (_page == HomePage::RECENT) { _task->showAlert("Recent adverts", 800); } +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + if (_page == HomePage::SETTINGS) { + _in_settings_submenu = false; + _display_menu_item = 0; + } +#endif + return true; + } +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + if (c == KEY_ENTER && _page == HomePage::SETTINGS) { + if (!_in_settings_submenu) { + _in_settings_submenu = true; + _display_menu_item = 0; + _brightness_popup = false; + _brightness_popup_item = 0; + } else { + if (_brightness_popup) { + if (_brightness_popup_item == 0) { + _brightness_popup = false; + } else if (_brightness_popup_item == 1) { + _task->setBrightnessLevel(1); + _brightness_popup = false; + } else if (_brightness_popup_item == 2) { + _task->setBrightnessLevel(2); + _brightness_popup = false; + } else if (_brightness_popup_item == 3) { + _task->setBrightnessLevel(3); + _brightness_popup = false; + } + return true; + } + if (_display_menu_item == 0) { + _in_settings_submenu = false; + } else if (_display_menu_item == 1) { + _task->toggleScreen(); + } else if (_display_menu_item == 2) { + _task->toggleLED(); + } else if (_display_menu_item == 3) { + _brightness_popup = true; + _brightness_popup_item = 0; + } + } return true; } +#endif if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { if (_task->isSerialEnabled()) { // toggle Bluetooth on/off _task->disableSerial(); @@ -567,7 +712,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif if (_display != NULL) { - _display->turnOn(); + if (_board && _board->supportsDisplayBrightness()) { + _display->setBrightness(_board->getDisplayBrightness()); + } + if (_board && _board->supportsDisplaySettings() && !_board->getDisplayEnabled()) { + _display->turnOff(); + } else { + _display->turnOn(); + } } #ifdef PIN_BUZZER @@ -583,7 +735,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _alert_expiry = 0; splash = new SplashScreen(this); - home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); + home = new HomeScreen(this, &rtc_clock, sensors, node_prefs, _board); msg_preview = new MsgPreviewScreen(this, &rtc_clock); setCurrScreen(splash); } @@ -637,9 +789,21 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i setCurrScreen(msg_preview); if (_display != NULL) { +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // Check if screen is enabled via board settings + bool screen_enabled = true; + if (_board && _board->supportsDisplaySettings()) { + screen_enabled = _board->getDisplayEnabled(); + } + + if (!_display->isOn() && !hasConnection() && screen_enabled) { + _display->turnOn(); // only turn on if screen is enabled + } +#else if (!_display->isOn() && !hasConnection()) { _display->turnOn(); } +#endif if (_display->isOn()) { _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer _next_refresh = 100; // trigger refresh @@ -802,6 +966,12 @@ void UITask::loop() { _next_refresh = _alert_expiry; // will need refresh when alert is dismissed } else { _next_refresh = millis() + delay_millis; +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // After alert expires, check if screen should be off + if (_board && _board->supportsDisplaySettings() && !_board->getDisplayEnabled()) { + _display->turnOff(); + } +#endif } _display->endFrame(); } @@ -844,10 +1014,23 @@ void UITask::loop() { char UITask::checkDisplayOn(char c) { if (_display != NULL) { +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // Check if screen is enabled via board settings + bool screen_enabled = true; + if (_board && _board->supportsDisplaySettings()) { + screen_enabled = _board->getDisplayEnabled(); + } + + if (!_display->isOn() && screen_enabled) { + _display->turnOn(); // turn display on only if enabled + c = 0; + } +#else if (!_display->isOn()) { _display->turnOn(); // turn display on and consume event c = 0; } +#endif _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer _next_refresh = 0; // trigger refresh } @@ -864,14 +1047,42 @@ char UITask::handleLongPress(char c) { char UITask::handleDoubleClick(char c) { MESH_DEBUG_PRINTLN("UITask: double click triggered"); +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // Emergency restore: If screen setting is OFF, double-click turns it back ON + if (_board && _board->supportsDisplaySettings()) { + if (!_board->getDisplayEnabled()) { + _board->setDisplayEnabled(true); + if (_display != NULL) { + _display->turnOn(); + } + showAlert("Screen: ON", 1000); + the_mesh.pushDisplaySettings(); + c = 0; // consume event + } + } +#endif checkDisplayOn(c); return c; } char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // Failsafe: Always turn screen on with triple-click + if (_display != NULL && !_display->isOn()) { + if (_board && _board->supportsDisplaySettings()) { + _board->setDisplayEnabled(true); + } + _display->turnOn(); + showAlert("Screen: ON", 1000); + the_mesh.pushDisplaySettings(); + } else { + toggleBuzzer(); + } +#else checkDisplayOn(c); toggleBuzzer(); +#endif c = 0; return c; } @@ -927,3 +1138,72 @@ void UITask::toggleBuzzer() { _next_refresh = 0; // trigger refresh #endif } +#ifdef HELTEC_V3_SCREEN_LED_CONTROL +void UITask::toggleScreen() { + if (_board && _board->supportsDisplaySettings()) { + bool screen_enabled = _board->getDisplayEnabled(); + if (screen_enabled) { + _board->setDisplayEnabled(false); + if (_display) { + _display->turnOn(); + } + notify(UIEventType::ack); + showAlert("Dbl-click to turn on", 2500); + } else { + _board->setDisplayEnabled(true); + if (_display) { + _display->turnOn(); + } + notify(UIEventType::ack); + showAlert("Screen: ON", 800); + } + the_mesh.pushDisplaySettings(); + _next_refresh = 0; + } +} + +void UITask::toggleLED() { + if (_sensors != NULL) { + // toggle LED on/off + int num = _sensors->getNumSettings(); + for (int i = 0; i < num; i++) { + if (strcmp(_sensors->getSettingName(i), "led") == 0) { + if (strcmp(_sensors->getSettingValue(i), "1") == 0) { + _sensors->setSettingValue("led", "0"); + notify(UIEventType::ack); + } else { + _sensors->setSettingValue("led", "1"); + notify(UIEventType::ack); + } + showAlert(_sensors->getSettingValue(i)[0] == '1' ? "LED: ON" : "LED: OFF", 800); + _next_refresh = 0; + break; + } + } + } +} + +void UITask::setBrightnessLevel(int level) { + if (_board && _board->supportsDisplayBrightness()) { + int value = 255; + const char* label = "High"; + if (level == 1) { + value = 1; + label = "Low"; + } else if (level == 2) { + value = 80; + label = "Medium"; + } + + _board->setDisplayBrightness((uint8_t)value); + if (_display != NULL) { + _display->setBrightness((uint8_t)value); + } + the_mesh.pushDisplaySettings(); + char msg[20]; + sprintf(msg, "Bright: %s", label); + showAlert(msg, 800); + _next_refresh = 0; + } +} +#endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 02c3cafbd..f2029c62f 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -81,6 +81,11 @@ class UITask : public AbstractUITask { void toggleBuzzer(); bool getGPSState(); void toggleGPS(); +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + void toggleScreen(); + void toggleLED(); + void setBrightnessLevel(int level); +#endif // from AbstractUITask diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 6d957cc09..2cb425e23 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1168,6 +1168,92 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + } else if (strcmp(command, "status") == 0) { + // Show current status of screen and LED settings + const char* screen_status = "unknown"; + const char* led_status = "unknown"; + bool has_status = false; + + if (board.supportsDisplaySettings()) { + screen_status = board.getDisplayEnabled() ? "ON" : "OFF"; + has_status = true; + } + + int num = sensors.getNumSettings(); + for (int i = 0; i < num; i++) { + const char* name = sensors.getSettingName(i); + if (strcmp(name, "led") == 0) { + led_status = (strcmp(sensors.getSettingValue(i), "1") == 0) ? "ON" : "OFF"; + has_status = true; + break; + } + } + + if (has_status) { + sprintf(reply, "Screen: %s | LED: %s", screen_status, led_status); + } else { + strcpy(reply, "No screen/LED settings available"); + } + } else if (strcmp(command, "screen on") == 0) { + // Convenience command for "sensor set screen 1" + if (board.supportsDisplaySettings()) { + board.setDisplayEnabled(true); +#ifdef DISPLAY_CLASS + display.turnOn(); +#endif + strcpy(reply, "OK - Screen enabled"); + } else { + strcpy(reply, "Err - Screen setting not available"); + } + } else if (strcmp(command, "screen off") == 0) { + // Convenience command for "sensor set screen 0" + if (board.supportsDisplaySettings()) { + board.setDisplayEnabled(false); +#ifdef DISPLAY_CLASS + display.turnOff(); +#endif + strcpy(reply, "OK - Screen disabled"); + } else { + strcpy(reply, "Err - Screen setting not available"); + } + } else if (strcmp(command, "led on") == 0) { + // Convenience command for "sensor set led 1" + if (sensors.setSettingValue("led", "1")) { + strcpy(reply, "OK - LED enabled"); + } else { + strcpy(reply, "Err - LED setting not available"); + } + } else if (strcmp(command, "led off") == 0) { + // Convenience command for "sensor set led 0" + if (sensors.setSettingValue("led", "0")) { + strcpy(reply, "OK - LED disabled"); + } else { + strcpy(reply, "Err - LED setting not available"); + } + } else if (strcmp(command, "brightness") == 0) { + if (board.supportsDisplayBrightness()) { + sprintf(reply, "Brightness: %u", board.getDisplayBrightness()); + } else { + strcpy(reply, "Err - Brightness setting not available"); + } + } else if (strncmp(command, "brightness ", 11) == 0) { + const char* val = command + 11; + if (board.supportsDisplayBrightness()) { + int brightness = atoi(val); + if (brightness < 0) brightness = 0; + if (brightness > 255) brightness = 255; + board.setDisplayBrightness((uint8_t)brightness); +#ifdef DISPLAY_CLASS + if (display.isOn()) { + display.setBrightness((uint8_t)brightness); + } +#endif + strcpy(reply, "OK - Brightness updated"); + } else { + strcpy(reply, "Err - Brightness setting not available"); + } +#endif } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index d096d14b2..b90a775e7 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -2,6 +2,9 @@ #include #include +static char alert_msg[64] = ""; +static unsigned long alert_until = 0; + #define AUTO_OFF_MILLIS 20000 // 20 seconds #define BOOT_SCREEN_MILLIS 4000 // 4 seconds @@ -26,7 +29,19 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi _prevBtnState = HIGH; _auto_off = millis() + AUTO_OFF_MILLIS; _node_prefs = node_prefs; +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + bool screen_enabled = true; + if (_board && _board->supportsDisplaySettings()) { + screen_enabled = _board->getDisplayEnabled(); + } + if (screen_enabled) { + _display->turnOn(); + } else { + _display->turnOff(); + } +#else _display->turnOn(); +#endif // strip off dash and commit hash by changing dash to null terminator // e.g: v1.2.3-abcdef -> v1.2.3 @@ -78,24 +93,68 @@ void UITask::renderCurrScreen() { sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); _display->print(tmp); } + + // Show alert if active + if (millis() < alert_until && alert_msg[0] != '\0') { + _display->setColor(DisplayDriver::YELLOW); + _display->setTextSize(1); + _display->drawTextCentered(_display->width() / 2, 50, alert_msg); + } +} + +void UITask::showAlert(const char* msg, int duration_ms) { + strncpy(alert_msg, msg, sizeof(alert_msg) - 1); + alert_msg[sizeof(alert_msg) - 1] = '\0'; + alert_until = millis() + duration_ms; + _next_refresh = 0; // trigger immediate refresh } 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(); + int ev = user_btn.check(); + if (ev == BUTTON_EVENT_CLICK) { + if (!_display->isOn()) { +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + // Check if screen is enabled before turning on + bool screen_enabled = true; + if (_board && _board->supportsDisplaySettings()) { + screen_enabled = _board->getDisplayEnabled(); + } + if (screen_enabled) { + _display->turnOn(); + } +#else + _display->turnOn(); +#endif + } + _auto_off = millis() + AUTO_OFF_MILLIS; +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + // Toggle screen + if (_board && _board->supportsDisplaySettings()) { + bool enabled = _board->getDisplayEnabled(); + _board->setDisplayEnabled(!enabled); + showAlert(enabled ? "Screen: OFF" : "Screen: ON", 1500); + if (!enabled) { + _display->turnOn(); + } + _next_refresh = 0; + } + } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { + // Toggle LED + if (_sensors != NULL) { + int num = _sensors->getNumSettings(); + for (int i = 0; i < num; i++) { + if (strcmp(_sensors->getSettingName(i), "led") == 0) { + bool enabled = (strcmp(_sensors->getSettingValue(i), "1") == 0); + _sensors->setSettingValue("led", enabled ? "0" : "1"); + showAlert(enabled ? "LED: OFF" : "LED: ON", 800); + _next_refresh = 0; + break; } - _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer } - _prevBtnState = btnState; } - _next_read = millis() + 200; // 5 reads per second +#endif } #endif @@ -105,6 +164,12 @@ void UITask::loop() { renderCurrScreen(); _display->endFrame(); +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + if (_board && _board->supportsDisplaySettings() && !_board->getDisplayEnabled()) { + _display->turnOff(); + } +#endif + _next_refresh = millis() + 1000; // refresh every second } if (millis() > _auto_off) { diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index a27259f11..1a00c126b 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -1,18 +1,42 @@ #pragma once #include +#include #include +#include +#include class UITask { DisplayDriver* _display; +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + mesh::MainBoard* _board; + SensorManager* _sensors; +#endif unsigned long _next_read, _next_refresh, _auto_off; int _prevBtnState; NodePrefs* _node_prefs; char _version_info[32]; +#ifdef PIN_USER_BTN + MomentaryButton user_btn; +#endif void renderCurrScreen(); + void showAlert(const char* msg, int duration_ms); public: - UITask(DisplayDriver& display) : _display(&display) { _next_read = _next_refresh = 0; } + UITask(DisplayDriver& display +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + , mesh::MainBoard* board + , SensorManager* sensors +#endif + ) : _display(&display) +#ifdef HELTEC_V3_SCREEN_LED_CONTROL + , _board(board) + , _sensors(sensors) +#endif +#ifdef PIN_USER_BTN + , user_btn(PIN_USER_BTN, 1000, true) +#endif + { _next_read = _next_refresh = 0; } void begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version); void loop(); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index d55d61186..1ae436ce3 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -5,7 +5,11 @@ #ifdef DISPLAY_CLASS #include "UITask.h" - static UITask ui_task(display); + #ifdef HELTEC_V3_SCREEN_LED_CONTROL + static UITask ui_task(display, &board, &sensors); + #else + static UITask ui_task(display); + #endif #endif StdRNG fast_rng; @@ -34,10 +38,21 @@ void setup() { #ifdef DISPLAY_CLASS if (display.begin()) { + #ifdef HELTEC_V3_SCREEN_LED_CONTROL + if (board.supportsDisplaySettings() && !board.getDisplayEnabled()) { + display.turnOff(); + } else { + display.startFrame(); + display.setCursor(0, 0); + display.print("Please wait..."); + display.endFrame(); + } + #else display.startFrame(); display.setCursor(0, 0); display.print("Please wait..."); display.endFrame(); + #endif } #endif diff --git a/platformio.ini b/platformio.ini index 743e357af..95ac09c05 100644 --- a/platformio.ini +++ b/platformio.ini @@ -49,6 +49,7 @@ build_src_filter = + + + + + + ; ----------------- ESP32 --------------------- diff --git a/src/MeshCore.h b/src/MeshCore.h index f194cdeb4..2d2cd9f33 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -57,6 +57,14 @@ class MainBoard { virtual uint8_t getStartupReason() const = 0; virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported + // Optional display control interface (boards without displays keep defaults) + virtual bool supportsDisplaySettings() const { return false; } + virtual bool supportsDisplayBrightness() const { return false; } + virtual bool getDisplayEnabled() const { return true; } + virtual bool setDisplayEnabled(bool enabled) { (void)enabled; return false; } + virtual uint8_t getDisplayBrightness() const { return 255; } + virtual bool setDisplayBrightness(uint8_t brightness) { (void)brightness; return false; } + // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } virtual uint16_t getBootVoltage() { return 0; } diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index ec63c1912..2510db975 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -23,6 +23,7 @@ class DisplayDriver { virtual void setCursor(int x, int y) = 0; virtual void print(const char* str) = 0; virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override + virtual void setBrightness(uint8_t brightness) { (void)brightness; } 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; diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 4e7fd10ad..7c43e19f8 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -11,18 +11,45 @@ bool SSD1306Display::begin() { if (_peripher_power) _peripher_power->claim(); _isOn = true; } + delay(100); #ifdef DISPLAY_ROTATION display.setRotation(DISPLAY_ROTATION); #endif - return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS); + bool ok = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false); + if (!ok) { + delay(50); + ok = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false); + } + if (ok) { + ok = i2c_probe(Wire, DISPLAY_ADDRESS); + } + if (ok) { + _initialized = true; + setBrightness(_brightness); + } else { + if (_peripher_power) _peripher_power->release(); + _isOn = false; + _initialized = false; + } + return ok; } void SSD1306Display::turnOn() { + if (!_initialized) { + begin(); + return; + } + if (_peripher_power && !_isOn) { + _peripher_power->claim(); + delay(10); + } display.ssd1306_command(SSD1306_DISPLAYON); if (!_isOn) { - if (_peripher_power) _peripher_power->claim(); _isOn = true; } + display.ssd1306_command(SSD1306_CHARGEPUMP); + display.ssd1306_command(0x14); + setBrightness(_brightness); } void SSD1306Display::turnOff() { @@ -63,6 +90,26 @@ void SSD1306Display::print(const char* str) { display.print(str); } +void SSD1306Display::setBrightness(uint8_t brightness) { + _brightness = brightness; + + uint8_t contrast = brightness; + uint8_t precharge = (brightness == 0) ? 0x00 : 0xF1; + uint8_t comdetect = (brightness >= 200) ? 0x60 : 0x40; + + display.ssd1306_command(SSD1306_DISPLAYON); + display.ssd1306_command(SSD1306_CHARGEPUMP); + display.ssd1306_command(0x14); + display.ssd1306_command(SSD1306_SETPRECHARGE); + display.ssd1306_command(precharge); + display.ssd1306_command(SSD1306_SETCONTRAST); + display.ssd1306_command(contrast); + display.ssd1306_command(SSD1306_SETVCOMDETECT); + display.ssd1306_command(comdetect); + display.ssd1306_command(SSD1306_DISPLAYALLON_RESUME); + display.ssd1306_command(SSD1306_NORMALDISPLAY); +} + void SSD1306Display::fillRect(int x, int y, int w, int h) { display.fillRect(x, y, w, h, _color); } diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index d843da85b..06e9b8a5b 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -18,7 +18,9 @@ class SSD1306Display : public DisplayDriver { Adafruit_SSD1306 display; bool _isOn; + bool _initialized; uint8_t _color; + uint8_t _brightness; RefCountedDigitalPin* _peripher_power; bool i2c_probe(TwoWire& wire, uint8_t addr); @@ -28,6 +30,8 @@ class SSD1306Display : public DisplayDriver { _peripher_power(peripher_power) { _isOn = false; + _initialized = false; + _brightness = 0xFF; } bool begin(); @@ -40,6 +44,7 @@ class SSD1306Display : public DisplayDriver { void setColor(Color c) override; void setCursor(int x, int y) override; void print(const char* str) override; + void setBrightness(uint8_t brightness) 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; diff --git a/variants/heltec_v3/DISPLAY_LED_CONTROL.md b/variants/heltec_v3/DISPLAY_LED_CONTROL.md new file mode 100644 index 000000000..cf9f45fcd --- /dev/null +++ b/variants/heltec_v3/DISPLAY_LED_CONTROL.md @@ -0,0 +1,115 @@ +# Display and LED Control for Heltec V3 + +The Heltec V3 variant now supports user-configurable settings to enable/disable the screen and LED lights through CLI commands. + +## Available Settings + +### 1. Screen Control (Repeaters/Clients with Display) +- **Setting name**: `screen` +- **Values**: `0` (disabled) or `1` (enabled) +- **Default**: Enabled +- **Purpose**: Control the OLED display power + +### 2. LED Control (All devices) +- **Setting name**: `led` +- **Values**: `0` (disabled) or `1` (enabled) +- **Default**: Enabled +- **Purpose**: Control the TX indicator LED (pin 35) + +## CLI Commands + +### View Current Settings +```bash +sensor list +``` +This will show all available settings and their current values. + +### Get Specific Setting +```bash +sensor get screen +sensor get led +``` + +### Change Settings +```bash +# Disable screen +sensor set screen 0 + +# Enable screen +sensor set screen 1 + +# Disable LED +sensor set led 0 + +# Enable LED +sensor set led 1 +``` + +## Using in Application Code + +### Check if Screen is Enabled +```cpp +#include "target.h" + +void setup() { + board.begin(); + + #ifdef DISPLAY_CLASS + if (board.screen_enabled && display.begin()) { + // Display is enabled and initialized + display.startFrame(); + display.print("Screen is on!"); + display.endFrame(); + } + #endif +} +``` + +### Control LED with Settings +```cpp +void onTransmit() { + // LED will only turn on if led_enabled is true + board.setLED(true); + delay(100); + board.setLED(false); +} +``` + +### Disable Display at Runtime +```cpp +void loop() { + #ifdef DISPLAY_CLASS + if (!board.screen_enabled && display.isOn()) { + display.turnOff(); // Turn off if setting was changed + } else if (board.screen_enabled && !display.isOn()) { + display.turnOn(); // Turn on if setting was changed + } + #endif +} +``` + +## Power Saving Benefits + +Disabling the screen and LED can significantly reduce power consumption: +- **Screen**: ~15-20mA when active +- **LED**: ~5-10mA when lit +- **Combined savings**: Up to 30mA when both disabled + +This is especially useful for: +- Solar-powered nodes +- Battery-operated devices +- Stealth deployments where visual indicators are not desired + +## Hardware Details + +- **Screen Power Pin**: GPIO 36 (`PIN_VEXT_EN`) +- **LED Pin**: GPIO 35 (`P_LORA_TX_LED`) +- **Display Type**: SSD1306 OLED (128x64) +- **I2C Address**: 0x3C + +## Notes + +1. Settings are managed through the board's `getNumSettings()`, `getSettingName()`, `getSettingValue()`, and `setSettingValue()` methods +2. The display power is controlled via the `RefCountedDigitalPin periph_power` to safely share power control with other peripherals +3. LED control uses direct GPIO manipulation for minimal overhead +4. Settings can be persisted by saving node preferences after modification diff --git a/variants/heltec_v3/HeltecV3Board.h b/variants/heltec_v3/HeltecV3Board.h index afdaf6398..c1c2ceb1c 100644 --- a/variants/heltec_v3/HeltecV3Board.h +++ b/variants/heltec_v3/HeltecV3Board.h @@ -4,6 +4,11 @@ #include #include +#if defined(ESP32) + #include "nvs_flash.h" + #include "nvs.h" +#endif + // built-ins #ifndef PIN_VBAT_READ // set in platformio.ini for boards like Heltec Wireless Paper (20) #define PIN_VBAT_READ 1 @@ -19,15 +24,93 @@ class HeltecV3Board : public ESP32Board { private: bool adc_active_state; + bool initNvs() { + #if defined(ESP32) + static bool nvs_ready = false; + if (nvs_ready) return true; + + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + nvs_flash_erase(); + err = nvs_flash_init(); + } + nvs_ready = (err == ESP_OK); + return nvs_ready; + #else + return false; + #endif + } + + bool readNvsU8(const char* key, uint8_t& value) { + #if defined(ESP32) + if (!initNvs()) return false; + nvs_handle_t handle; + if (nvs_open("heltec_v3", NVS_READWRITE, &handle) != ESP_OK) return false; + esp_err_t err = nvs_get_u8(handle, key, &value); + nvs_close(handle); + return err == ESP_OK; + #else + (void)key; + (void)value; + return false; + #endif + } + + void writeNvsU8(const char* key, uint8_t value) { + #if defined(ESP32) + if (!initNvs()) return; + nvs_handle_t handle; + if (nvs_open("heltec_v3", NVS_READWRITE, &handle) != ESP_OK) return; + nvs_set_u8(handle, key, value); + nvs_commit(handle); + nvs_close(handle); + #else + (void)key; + (void)value; + #endif + } public: RefCountedDigitalPin periph_power; + bool screen_enabled; + bool led_enabled; + uint8_t screen_brightness; + mutable char brightness_str[4]; - HeltecV3Board() : periph_power(PIN_VEXT_EN) { } + #ifdef PIN_VEXT_EN_ACTIVE + HeltecV3Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE), screen_enabled(true), led_enabled(true), screen_brightness(255) { + #else + HeltecV3Board() : periph_power(PIN_VEXT_EN), screen_enabled(true), led_enabled(true), screen_brightness(255) { + #endif + brightness_str[0] = '2'; + brightness_str[1] = '5'; + brightness_str[2] = '5'; + brightness_str[3] = 0; + } void begin() { ESP32Board::begin(); + // Load settings from NVS (ESP32) + uint8_t val; + if (readNvsU8("screen", val)) { + screen_enabled = (val != 0); + } + if (readNvsU8("led", val)) { + led_enabled = (val != 0); + } + if (readNvsU8("brightness", val)) { + screen_brightness = val; + } else { + screen_brightness = 255; + writeNvsU8("brightness", screen_brightness); + } + if (screen_brightness == 0) { + screen_brightness = 255; + writeNvsU8("brightness", screen_brightness); + } + Serial.printf("Loaded: screen=%d led=%d brightness=%u\n", screen_enabled, led_enabled, screen_brightness); + // Auto-detect correct ADC_CTRL pin polarity (different for boards >3.2) pinMode(PIN_ADC_CTRL, INPUT); adc_active_state = !digitalRead(PIN_ADC_CTRL); @@ -37,6 +120,12 @@ class HeltecV3Board : public ESP32Board { periph_power.begin(); + // Initialize LED pin + #ifdef P_LORA_TX_LED + pinMode(P_LORA_TX_LED, OUTPUT); + digitalWrite(P_LORA_TX_LED, LOW); // Start with LED off + #endif + esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { long wakeup_source = esp_sleep_get_ext1_wakeup_status(); @@ -94,4 +183,83 @@ class HeltecV3Board : public ESP32Board { const char* getManufacturerName() const override { return "Heltec V3"; } + + bool supportsDisplaySettings() const override { return true; } + bool supportsDisplayBrightness() const override { return true; } + bool getDisplayEnabled() const override { return screen_enabled; } + bool setDisplayEnabled(bool enabled) override { + screen_enabled = enabled; + writeNvsU8("screen", enabled ? 1 : 0); + Serial.printf("Saved: screen=%d\n", enabled ? 1 : 0); + return true; + } + uint8_t getDisplayBrightness() const override { return screen_brightness; } + bool setDisplayBrightness(uint8_t brightness) override { + if (brightness == 0) { + brightness = 1; + } + screen_brightness = brightness; + writeNvsU8("brightness", screen_brightness); + Serial.printf("Saved: brightness=%u\n", screen_brightness); + return true; + } + + // Settings support for LED control + int getNumSettings() const { + return 1; // led only + } + + const char* getSettingName(int i) const { + if (i == 0) return "led"; + return NULL; + } + + const char* getSettingValue(int i) const { + if (i == 0) return led_enabled ? "1" : "0"; + return NULL; + } + + bool setSettingValue(const char* name, const char* value) { + bool enable = (strcmp(value, "1") == 0); + bool changed = false; + + if (strcmp(name, "led") == 0) { + led_enabled = enable; + #ifdef P_LORA_TX_LED + digitalWrite(P_LORA_TX_LED, enable ? LOW : LOW); // Keep off when disabled + #endif + changed = true; + } + + if (changed) { + writeNvsU8(name, enable ? 1 : 0); + Serial.printf("Saved: %s=%d\n", name, enable); + return true; + } + + return false; + } + + // Helper to control LED based on settings + void setLED(bool on) { + #ifdef P_LORA_TX_LED + if (led_enabled && on) { + digitalWrite(P_LORA_TX_LED, HIGH); + } else { + digitalWrite(P_LORA_TX_LED, LOW); + } + #endif + } + + // Override transmit callbacks to respect LED setting + #ifdef P_LORA_TX_LED + void onBeforeTransmit() override { + if (led_enabled) { + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on only if enabled + } + } + void onAfterTransmit() override { + digitalWrite(P_LORA_TX_LED, LOW); // always turn off + } + #endif }; diff --git a/variants/heltec_v3/HeltecV3SSD1306Display.cpp b/variants/heltec_v3/HeltecV3SSD1306Display.cpp new file mode 100644 index 000000000..2ca4958e0 --- /dev/null +++ b/variants/heltec_v3/HeltecV3SSD1306Display.cpp @@ -0,0 +1,112 @@ +#include "HeltecV3SSD1306Display.h" + +bool HeltecV3SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { + wire.beginTransmission(addr); + uint8_t error = wire.endTransmission(); + return (error == 0); +} + +bool HeltecV3SSD1306Display::begin() { + if (!_isOn) { + 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); +} + +void HeltecV3SSD1306Display::turnOn() { + display.ssd1306_command(SSD1306_DISPLAYON); + if (!_isOn) { + if (_peripher_power) _peripher_power->claim(); + _isOn = true; + } + // Re-enable charge pump and brightness after power cycling + display.ssd1306_command(SSD1306_CHARGEPUMP); + display.ssd1306_command(0x14); + setBrightness(_brightness); +} + +void HeltecV3SSD1306Display::turnOff() { + display.ssd1306_command(SSD1306_DISPLAYOFF); + if (_isOn) { + if (_peripher_power) _peripher_power->release(); + _isOn = false; + } +} + +void HeltecV3SSD1306Display::clear() { + display.clearDisplay(); + display.display(); +} + +void HeltecV3SSD1306Display::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 HeltecV3SSD1306Display::setTextSize(int sz) { + display.setTextSize(sz); +} + +void HeltecV3SSD1306Display::setColor(Color c) { + _color = (c != 0) ? SSD1306_WHITE : SSD1306_BLACK; + display.setTextColor(_color); +} + +void HeltecV3SSD1306Display::setCursor(int x, int y) { + display.setCursor(x, y); +} + +void HeltecV3SSD1306Display::print(const char* str) { + display.print(str); +} + +void HeltecV3SSD1306Display::setBrightness(uint8_t brightness) { + _brightness = brightness; + + uint8_t contrast = brightness; + uint8_t precharge = (brightness == 0) ? 0x00 : 0xF1; + // Slightly higher VCOMH can increase perceived brightness on some panels + uint8_t comdetect = (brightness >= 200) ? 0x60 : 0x40; + + display.ssd1306_command(SSD1306_DISPLAYON); + display.ssd1306_command(SSD1306_CHARGEPUMP); + display.ssd1306_command(0x14); + display.ssd1306_command(SSD1306_SETPRECHARGE); + display.ssd1306_command(precharge); + display.ssd1306_command(SSD1306_SETCONTRAST); + display.ssd1306_command(contrast); + display.ssd1306_command(SSD1306_SETVCOMDETECT); + display.ssd1306_command(comdetect); + display.ssd1306_command(SSD1306_DISPLAYALLON_RESUME); + display.ssd1306_command(SSD1306_NORMALDISPLAY); +} + +void HeltecV3SSD1306Display::fillRect(int x, int y, int w, int h) { + display.fillRect(x, y, w, h, _color); +} + +void HeltecV3SSD1306Display::drawRect(int x, int y, int w, int h) { + display.drawRect(x, y, w, h, _color); +} + +void HeltecV3SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + display.drawBitmap(x, y, bits, w, h, SSD1306_WHITE); +} + +uint16_t HeltecV3SSD1306Display::getTextWidth(const char* str) { + int16_t x1, y1; + uint16_t w, h; + display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); + return w; +} + +void HeltecV3SSD1306Display::endFrame() { + display.display(); +} diff --git a/variants/heltec_v3/HeltecV3SSD1306Display.h b/variants/heltec_v3/HeltecV3SSD1306Display.h new file mode 100644 index 000000000..44c9756d3 --- /dev/null +++ b/variants/heltec_v3/HeltecV3SSD1306Display.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#define SSD1306_NO_SPLASH +#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 HeltecV3SSD1306Display : public DisplayDriver { + Adafruit_SSD1306 display; + bool _isOn; + uint8_t _color; + uint8_t _brightness; + RefCountedDigitalPin* _peripher_power; + + bool i2c_probe(TwoWire& wire, uint8_t addr); +public: + HeltecV3SSD1306Display(RefCountedDigitalPin* peripher_power=nullptr) : DisplayDriver(128, 64), + display(128, 64, &Wire, PIN_OLED_RESET), + _peripher_power(peripher_power) + { + _isOn = false; + _brightness = 0xFF; + } + + bool begin(); + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() 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 setBrightness(uint8_t brightness); + 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; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; +}; diff --git a/variants/heltec_v3/HeltecV3SensorManager.h b/variants/heltec_v3/HeltecV3SensorManager.h new file mode 100644 index 000000000..fe6f91657 --- /dev/null +++ b/variants/heltec_v3/HeltecV3SensorManager.h @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include "HeltecV3Board.h" + +#ifdef DISPLAY_CLASS + #include +#endif + +class HeltecV3SensorManager : public SensorManager { + HeltecV3Board* _board; + EnvironmentSensorManager* _env_sensors; + #ifdef DISPLAY_CLASS + DisplayDriver* _display; + #endif + +public: + HeltecV3SensorManager(HeltecV3Board& board) : _board(&board), _env_sensors(nullptr) { + #ifdef DISPLAY_CLASS + _display = nullptr; + #endif + } + + HeltecV3SensorManager(HeltecV3Board& board, EnvironmentSensorManager* env) + : _board(&board), _env_sensors(env) { + #ifdef DISPLAY_CLASS + _display = nullptr; + #endif + } + + #ifdef DISPLAY_CLASS + void setDisplay(DisplayDriver* display) { + _display = display; + if (_display) { + if (_board->getDisplayEnabled()) { + _display->turnOn(); + } else { + _display->turnOff(); + } + if (_board->supportsDisplayBrightness()) { + _display->setBrightness(_board->getDisplayBrightness()); + } + } + } + #endif + + bool begin() override { + if (_env_sensors) { + return _env_sensors->begin(); + } + return true; + } + + bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override { + if (_env_sensors) { + return _env_sensors->querySensors(requester_permissions, telemetry); + } + return false; + } + + void loop() override { + if (_env_sensors) { + _env_sensors->loop(); + } + } + + LocationProvider* getLocationProvider() override { + if (_env_sensors) { + return _env_sensors->getLocationProvider(); + } + return nullptr; + } + + int getNumSettings() const override { + int base_count = _env_sensors ? _env_sensors->getNumSettings() : 0; + return base_count + _board->getNumSettings(); + } + + const char* getSettingName(int i) const override { + int base_count = _env_sensors ? _env_sensors->getNumSettings() : 0; + if (i < base_count && _env_sensors) { + return _env_sensors->getSettingName(i); + } + return _board->getSettingName(i - base_count); + } + + const char* getSettingValue(int i) const override { + int base_count = _env_sensors ? _env_sensors->getNumSettings() : 0; + if (i < base_count && _env_sensors) { + return _env_sensors->getSettingValue(i); + } + return _board->getSettingValue(i - base_count); + } + + bool setSettingValue(const char* name, const char* value) override { + // Try environment sensors first + if (_env_sensors && _env_sensors->setSettingValue(name, value)) { + return true; + } + // Then try board settings + return _board->setSettingValue(name, value); + } + + const char* getSettingByKey(const char* name) const { + if (_env_sensors) { + const char* result = _env_sensors->getSettingByKey(name); + if (result) return result; + } + + // Check board settings + for (int i = 0; i < _board->getNumSettings(); i++) { + const char* setting_name = _board->getSettingName(i); + if (setting_name && strcmp(setting_name, name) == 0) { + return _board->getSettingValue(i); + } + } + return nullptr; + } +}; diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 6b61eff5d..32bdcfe5f 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${sensor_base.build_flags} -I variants/heltec_v3 -D HELTEC_LORA_V3 + -D HELTEC_V3_SCREEN_LED_CONTROL -D ESP32_CPU_FREQ=80 -D P_LORA_DIO_1=14 -D P_LORA_NSS=8 @@ -22,6 +23,7 @@ build_flags = -D PIN_BOARD_SCL=18 -D PIN_USER_BTN=0 -D PIN_VEXT_EN=36 + -D PIN_VEXT_EN_ACTIVE=LOW -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 @@ -35,12 +37,29 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} ${sensor_base.lib_deps} + adafruit/Adafruit GFX Library @ ^1.11.11 + adafruit/Adafruit SSD1306 @ ^2.5.9 [env:Heltec_v3_repeater] extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -D DISPLAY_CLASS=SSD1306Display + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 -D ADVERT_NAME='"Heltec Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -49,8 +68,9 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} - + - +<../examples/simple_repeater> + -<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/ui-new/*.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} @@ -61,6 +81,21 @@ extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -D DISPLAY_CLASS=SSD1306Display + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 -D ADVERT_NAME='"RS232 Bridge"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -74,8 +109,9 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + - + - +<../examples/simple_repeater> + -<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/ui-new/*.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} @@ -85,6 +121,21 @@ extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} -D DISPLAY_CLASS=SSD1306Display + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 -D ADVERT_NAME='"ESPNow Bridge"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -96,8 +147,9 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + - + - +<../examples/simple_repeater> + -<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/ui-new/*.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} @@ -115,7 +167,6 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} - + +<../examples/simple_room_server> lib_deps = ${Heltec_lora32_v3.lib_deps} @@ -143,13 +194,27 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 ; 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/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> + +<../examples/companion_radio/ui-new/UITask.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -162,6 +227,21 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 -D BLE_PIN_CODE=123456 ; dynamic, random PIN -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 @@ -169,11 +249,10 @@ build_flags = ; -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-new/*.cpp> + +<../examples/companion_radio/ui-new/UITask.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -186,6 +265,21 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 -D WIFI_DEBUG_LOGGING=1 -D WIFI_SSID='"myssid"' -D WIFI_PWD='"mypwd"' @@ -193,11 +287,10 @@ build_flags = ; -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-new/*.cpp> + +<../examples/companion_radio/ui-new/UITask.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -216,7 +309,6 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} - + +<../examples/simple_sensor> lib_deps = ${Heltec_lora32_v3.lib_deps} @@ -234,7 +326,9 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} - +<../examples/simple_repeater> + -<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/ui-new/*.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} @@ -257,7 +351,9 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + - +<../examples/simple_repeater> + -<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/ui-new/*.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} @@ -278,7 +374,9 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + - +<../examples/simple_repeater> + -<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/ui-new/*.cpp> + +<../examples/simple_repeater/*.cpp> lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} diff --git a/variants/heltec_v3/target.cpp b/variants/heltec_v3/target.cpp index 78b881972..6a9cfc8ea 100644 --- a/variants/heltec_v3/target.cpp +++ b/variants/heltec_v3/target.cpp @@ -18,17 +18,22 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS #include MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); - EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); + EnvironmentSensorManager env_sensors = EnvironmentSensorManager(nmea); + HeltecV3SensorManager sensors = HeltecV3SensorManager(board, &env_sensors); #else - EnvironmentSensorManager sensors; + HeltecV3SensorManager sensors = HeltecV3SensorManager(board); #endif #ifdef DISPLAY_CLASS - DISPLAY_CLASS display; + DISPLAY_CLASS display(&(board.periph_power)); MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif bool radio_init() { + #ifdef DISPLAY_CLASS + sensors.setDisplay(&display); + #endif + fallback_clock.begin(); rtc_clock.begin(Wire); diff --git a/variants/heltec_v3/target.h b/variants/heltec_v3/target.h index 739aecfe0..ae2b6898a 100644 --- a/variants/heltec_v3/target.h +++ b/variants/heltec_v3/target.h @@ -3,11 +3,12 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include +#include "HeltecV3Board.h" #include #include #include #include +#include "HeltecV3SensorManager.h" #ifdef DISPLAY_CLASS #include #include @@ -16,7 +17,7 @@ extern HeltecV3Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; -extern EnvironmentSensorManager sensors; +extern HeltecV3SensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display;