From 68726a1b0e184432f4bbf56a0e2f61c11592a7f1 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 19 Aug 2025 15:06:43 -0400 Subject: [PATCH 01/15] Docker: fix web assets location (#7683) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6a2ddeece..b1e151ac7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,7 +61,7 @@ RUN apt-get update && apt-get --no-install-recommends -y install \ # Fetch compiled binary from the builder COPY --from=builder /tmp/firmware/release/meshtasticd /usr/bin/ -COPY --from=builder /tmp/web /usr/share/meshtasticd/ +COPY --from=builder /tmp/web /usr/share/meshtasticd/web/ # Copy config templates COPY ./bin/config.d /etc/meshtasticd/available.d From 9654f5b21867d2f911b264006e1bc36ee39ac34a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:13:25 -0500 Subject: [PATCH 02/15] Update platform-native digest to 37d9864 (#7684) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- arch/portduino/portduino.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index 33a4c4d05..20b3f8e3d 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -2,7 +2,7 @@ [portduino_base] platform = # renovate: datasource=git-refs depName=platform-native packageName=https://github.com/meshtastic/platform-native gitBranch=develop - https://github.com/meshtastic/platform-native/archive/cd32f4ed20812d1fe9c8f74c0b6e80dc93dfce54.zip + https://github.com/meshtastic/platform-native/archive/37d986499ce24511952d7146db72d667c6bdaff7.zip framework = arduino build_src_filter = From eb6ef1cbea08bab0619a001a9d3c95a9887a5069 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:13:53 -0500 Subject: [PATCH 03/15] Update meshtastic-esp8266-oled-ssd1306 digest to 9573abb (#7686) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 528563def..3d1360cc4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -60,7 +60,7 @@ monitor_speed = 115200 monitor_filters = direct lib_deps = # renovate: datasource=git-refs depName=meshtastic-esp8266-oled-ssd1306 packageName=https://github.com/meshtastic/esp8266-oled-ssd1306 gitBranch=master - https://github.com/meshtastic/esp8266-oled-ssd1306/archive/0119501e9983bd894830b02f545c377ee08d66fe.zip + https://github.com/meshtastic/esp8266-oled-ssd1306/archive/9573abb64dc9c94f3051348f2bf4fc5cedf03c22.zip # renovate: datasource=git-refs depName=meshtastic-OneButton packageName=https://github.com/meshtastic/OneButton gitBranch=master https://github.com/meshtastic/OneButton/archive/fa352d668c53f290cfa480a5f79ad422cd828c70.zip # renovate: datasource=git-refs depName=meshtastic-arduino-fsm packageName=https://github.com/meshtastic/arduino-fsm gitBranch=master From 1c1462e7766a5b0658ad3226dc52218fcb7d50a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:14:12 -0500 Subject: [PATCH 04/15] Update meshtastic/device-ui digest to 8f5094b (#7633) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 3d1360cc4..ac30eb32e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/0cd108ff783539e41ef38258ba2784ab3b1bdc97.zip + https://github.com/meshtastic/device-ui/archive/8f5094b248c15ea2f9acf19cedfef6d2248fc1ff.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 5de61b1a3ddfd3a177fb7bcbf48c2f51f9080309 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 19 Aug 2025 14:15:05 -0500 Subject: [PATCH 05/15] Only gate PKC behind the simradio CLI flag (#7681) * Only gate PKC behind the simradio CLI flag * Hide router.cpp simradio check behind #if ARCH_PORTDUINO --- src/mesh/Router.cpp | 6 ++++-- src/platform/portduino/PortduinoGlue.cpp | 5 ++--- src/platform/portduino/PortduinoGlue.h | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index a54dcb976..cceacfe9e 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -523,8 +523,10 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // is not in the local nodedb // First, only PKC encrypt packets we are originating if (isFromUs(p) && - // Don't use PKC with simulator - radioType != SIM_RADIO && +#if ARCH_PORTDUINO + // Sim radio via the cli flag skips PKC + !portduino_config.force_simradio && +#endif // Don't use PKC with Ham mode !owner.is_licensed && // Don't use PKC if it's not explicitly requested and a non-primary channel is requested diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index ac4e79af1..3753c944c 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -34,7 +34,6 @@ std::ofstream traceFile; Ch341Hal *ch341Hal = nullptr; char *configPath = nullptr; char *optionMac = nullptr; -bool forceSimulated = false; bool verboseEnabled = false; const char *argp_program_version = optstr(APP_VERSION); @@ -67,7 +66,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state) configPath = arg; break; case 's': - forceSimulated = true; + portduino_config.force_simradio = true; break; case 'h': optionMac = arg; @@ -190,7 +189,7 @@ void portduinoSetup() YAML::Node yamlConfig; - if (forceSimulated == true) { + if (portduino_config.force_simradio == true) { settingsMap[use_simradio] = true; } else if (configPath != nullptr) { if (loadConfig(configPath)) { diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 64277322a..6e450c90e 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -134,4 +134,5 @@ extern struct portduino_config_struct { bool has_rfswitch_table = false; uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; Module::RfSwitchMode_t rfswitch_table[8]; + bool force_simradio = false; } portduino_config; \ No newline at end of file From c19f573b49f6c2d75f3a33a66efbee0fee983836 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 19 Aug 2025 20:10:47 -0500 Subject: [PATCH 06/15] Fix TLS port bug on default mqtt validation --- src/mqtt/MQTT.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index d94aeff95..7f7a9d511 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -279,6 +279,8 @@ struct PubSubConfig { // Defaults static constexpr uint16_t defaultPort = 1883; + static constexpr uint16_t defaultPortTls = 8883; + uint16_t serverPort = defaultPort; String serverAddr = default_mqtt_address; const char *mqttUsername = default_mqtt_username; @@ -641,7 +643,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC } const bool defaultServer = isDefaultServer(parsed.serverAddr); - if (defaultServer && parsed.serverPort != PubSubConfig::defaultPort) { + if (defaultServer && !IS_ONE_OF(parsed.serverPort, PubSubConfig::defaultPort, PubSubConfig::defaultPortTls)) { const char *warning = "Invalid MQTT config: default server address must not have a port specified"; LOG_ERROR(warning); #if !IS_RUNNING_TESTS From f413c49555fdf95730b265072a819ba78e637f23 Mon Sep 17 00:00:00 2001 From: Wilson Date: Wed, 20 Aug 2025 11:52:10 +0800 Subject: [PATCH 07/15] Add Meshtiny device (#7676) * Add Meshtiny device - nRF52 OLED upDown encoder * Update platformio.ini * Update platformio.ini * Add GPS Exclude to Meshtiny. --------- Co-authored-by: Ben Meadors --- boards/meshtiny.json | 52 ++++++ variants/nrf52840/meshtiny/platformio.ini | 19 +++ variants/nrf52840/meshtiny/variant.cpp | 54 ++++++ variants/nrf52840/meshtiny/variant.h | 199 ++++++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 boards/meshtiny.json create mode 100644 variants/nrf52840/meshtiny/platformio.ini create mode 100644 variants/nrf52840/meshtiny/variant.cpp create mode 100644 variants/nrf52840/meshtiny/variant.h diff --git a/boards/meshtiny.json b/boards/meshtiny.json new file mode 100644 index 000000000..1473388ea --- /dev/null +++ b/boards/meshtiny.json @@ -0,0 +1,52 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "MeshTiny", + "mcu": "nrf52840", + "variant": "meshtiny", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "freertos"], + "name": "MeshTiny", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://github.com/meshtastic/firmware", + "vendor": "MTools Tec" +} diff --git a/variants/nrf52840/meshtiny/platformio.ini b/variants/nrf52840/meshtiny/platformio.ini new file mode 100644 index 000000000..ef744a1c3 --- /dev/null +++ b/variants/nrf52840/meshtiny/platformio.ini @@ -0,0 +1,19 @@ +; MeshTiny - Custom device based on GAT562 with encoder and buzzer support +[env:meshtiny] +extends = nrf52840_base +board = meshtiny +board_level = extra +build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHTINY + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 + -D INPUTDRIVER_ENCODER_TYPE=2 + -D INPUTDRIVER_ENCODER_UP=4 + -D INPUTDRIVER_ENCODER_DOWN=26 + -D INPUTDRIVER_ENCODER_BTN=28 + -D USE_PIN_BUZZER=PIN_BUZZER + -D MESHTASTIC_EXCLUDE_GPS=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny> +lib_deps = + ${nrf52840_base.lib_deps} diff --git a/variants/nrf52840/meshtiny/variant.cpp b/variants/nrf52840/meshtiny/variant.cpp new file mode 100644 index 000000000..2e8b00e4b --- /dev/null +++ b/variants/nrf52840/meshtiny/variant.cpp @@ -0,0 +1,54 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); + + // Initialize Encoder pins + pinMode(INPUTDRIVER_ENCODER_UP, INPUT_PULLUP); + pinMode(INPUTDRIVER_ENCODER_DOWN, INPUT_PULLUP); + pinMode(INPUTDRIVER_ENCODER_BTN, INPUT_PULLUP); + + // Initialize Buzzer pin + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); +} diff --git a/variants/nrf52840/meshtiny/variant.h b/variants/nrf52840/meshtiny/variant.h new file mode 100644 index 000000000..83ad4c5b9 --- /dev/null +++ b/variants/nrf52840/meshtiny/variant.h @@ -0,0 +1,199 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_MESHTINY_ +#define _VARIANT_MESHTINY_ + +#define MESHTINY + +// #define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Encoder + */ +#define INPUTDRIVER_ENCODER_TYPE 2 +#define INPUTDRIVER_ENCODER_UP 26 +#define INPUTDRIVER_ENCODER_DOWN 4 +#define INPUTDRIVER_ENCODER_BTN 28 + +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +/* + * Buzzer - PWM + */ +#define PIN_BUZZER 30 + +/* + * Buttons + */ + +#define PIN_BUTTON1 9 +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +#define HAS_SCREEN 1 +#define USE_SSD1306 + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 22 // Changed from 26 to avoid conflict with encoder +#define PIN_QSPI_IO0 27 // Changed from 30 to avoid conflict with buzzer +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 21 // Changed from 28 to avoid conflict with encoder button +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +#define SX126X_POWER_EN (37) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM + +#define PIN_3V3_EN (34) + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 890357d579b1905889d160e78a5e9948510a6425 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 05:53:20 -0500 Subject: [PATCH 08/15] Update protobufs (#7693) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/config.pb.h | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/protobufs b/protobufs index be5137698..8985852d7 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit be5137698027f9e9fe6e68d5d5d638049f61ba8f +Subproject commit 8985852d752de3f7210f9a4a3e0923120ec438b3 diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 8a68197f0..67d461611 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -207,10 +207,10 @@ typedef enum _meshtastic_Config_DisplayConfig_OledType { meshtastic_Config_DisplayConfig_OledType_OLED_SSD1306 = 1, /* Default / Autodetect */ meshtastic_Config_DisplayConfig_OledType_OLED_SH1106 = 2, - /* Can not be auto detected but set by proto. Used for 128x128 screens */ - meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3, /* Can not be auto detected but set by proto. Used for 128x64 screens */ - meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64 = 4 + meshtastic_Config_DisplayConfig_OledType_OLED_SH1107 = 3, + /* Can not be auto detected but set by proto. Used for 128x128 screens */ + meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 = 4 } meshtastic_Config_DisplayConfig_OledType; typedef enum _meshtastic_Config_DisplayConfig_DisplayMode { @@ -682,8 +682,8 @@ extern "C" { #define _meshtastic_Config_DisplayConfig_DisplayUnits_ARRAYSIZE ((meshtastic_Config_DisplayConfig_DisplayUnits)(meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL+1)) #define _meshtastic_Config_DisplayConfig_OledType_MIN meshtastic_Config_DisplayConfig_OledType_OLED_AUTO -#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64 -#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_64+1)) +#define _meshtastic_Config_DisplayConfig_OledType_MAX meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128 +#define _meshtastic_Config_DisplayConfig_OledType_ARRAYSIZE ((meshtastic_Config_DisplayConfig_OledType)(meshtastic_Config_DisplayConfig_OledType_OLED_SH1107_128_128+1)) #define _meshtastic_Config_DisplayConfig_DisplayMode_MIN meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT #define _meshtastic_Config_DisplayConfig_DisplayMode_MAX meshtastic_Config_DisplayConfig_DisplayMode_COLOR From 11309662a98cccc1a03e5c528a4f3258a1ce1ef6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 09:08:14 -0400 Subject: [PATCH 09/15] Update platformio/espressif32 to v6.12.0 (#7523) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- arch/esp32/esp32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index 8990053eb..ffeaaf4cb 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -4,7 +4,7 @@ extends = arduino_base custom_esp32_kind = esp32 platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.11.0 + platformio/espressif32@6.12.0 build_src_filter = ${arduino_base.build_src_filter} - - - - - From 57e1725419009a3f10067577de8a14329fd2a5c0 Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 20 Aug 2025 10:10:39 -0400 Subject: [PATCH 10/15] Revert "Update platformio/espressif32 to v6.12.0 (#7523)" (#7695) This reverts commit 11309662a98cccc1a03e5c528a4f3258a1ce1ef6. --- arch/esp32/esp32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index ffeaaf4cb..8990053eb 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -4,7 +4,7 @@ extends = arduino_base custom_esp32_kind = esp32 platform = # renovate: datasource=custom.pio depName=platformio/espressif32 packageName=platformio/platform/espressif32 - platformio/espressif32@6.12.0 + platformio/espressif32@6.11.0 build_src_filter = ${arduino_base.build_src_filter} - - - - - From 5ce47045e75d967fa0f7bddb0c62eaddece9694d Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 20 Aug 2025 12:51:14 -0500 Subject: [PATCH 11/15] Add SDL option to BaseUI on Native (#7568) * Add SDL option to BaseUI on Native * Update to latest LovyanGFX PR and use LGFX_SDL define * Move SDL backend to native-sdl target --- src/graphics/TFTDisplay.cpp | 62 +++++++++++++++++++++++- src/graphics/TFTDisplay.h | 1 + src/main.cpp | 8 ++- variants/native/portduino/platformio.ini | 20 +++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 24ea6c47a..f8787612f 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -667,15 +667,19 @@ static LGFX *tft = nullptr; static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h #elif ARCH_PORTDUINO #include // Graphics and font library for ST7735 driver chip +#if defined(LGFX_SDL) +#include +#endif class LGFX : public lgfx::LGFX_Device { - lgfx::Panel_Device *_panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::ITouch *_touch_instance; public: + lgfx::Panel_Device *_panel_instance; + LGFX(void) { if (settingsMap[displayPanel] == st7789) @@ -694,6 +698,11 @@ class LGFX : public lgfx::LGFX_Device _panel_instance = new lgfx::Panel_ILI9488; else if (settingsMap[displayPanel] == hx8357d) _panel_instance = new lgfx::Panel_HX8357D; +#if defined(LGFX_SDL) + else if (settingsMap[displayPanel] == x11) { + _panel_instance = new lgfx::Panel_sdl; + } +#endif else { _panel_instance = new lgfx::Panel_NULL; LOG_ERROR("Unknown display panel configured!"); @@ -754,7 +763,13 @@ class LGFX : public lgfx::LGFX_Device _touch_instance->config(touch_cfg); _panel_instance->setTouch(_touch_instance); } - +#if defined(LGFX_SDL) + if (settingsMap[displayPanel] == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)_panel_instance; + sdl_panel_->setup(); + sdl_panel_->addKeyCodeMapping(SDLK_RETURN, SDL_SCANCODE_KP_ENTER); + } +#endif setPanel(_panel_instance); // Sets the panel to use. } }; @@ -1060,6 +1075,49 @@ void TFTDisplay::display(bool fromBlank) } } +void TFTDisplay::sdlLoop() +{ +#if defined(LGFX_SDL) + static int lastPressed = 0; + static int shuttingDown = false; + if (settingsMap[displayPanel] == x11) { + lgfx::Panel_sdl *sdl_panel_ = (lgfx::Panel_sdl *)tft->_panel_instance; + if (sdl_panel_->loop() && !shuttingDown) { + LOG_WARN("Window Closed!"); + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SHUTDOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } + + // debounce + if (lastPressed != 0 && !lgfx::v1::gpio_in(lastPressed)) + return; + if (!lgfx::v1::gpio_in(37)) { + lastPressed = 37; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_RIGHT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(36)) { + lastPressed = 36; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_UP, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(38)) { + lastPressed = 38; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_DOWN, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(39)) { + lastPressed = 39; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_LEFT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (!lgfx::v1::gpio_in(SDL_SCANCODE_KP_ENTER)) { + lastPressed = SDL_SCANCODE_KP_ENTER; + InputEvent event = {.inputEvent = (input_broker_event)INPUT_BROKER_SELECT, .kbchar = 0, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else { + lastPressed = 0; + } + } +#endif +} + // Send a command to the display (low level function) void TFTDisplay::sendCommand(uint8_t com) { diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 38cd53ebb..60adfdf7c 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -23,6 +23,7 @@ class TFTDisplay : public OLEDDisplay // Write the buffer to the display memory virtual void display() override { display(false); }; virtual void display(bool fromBlank); + void sdlLoop(); // Turn the display upside down virtual void flipScreenVertically(); diff --git a/src/main.cpp b/src/main.cpp index c53877e37..ef5f5a721 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1562,7 +1562,13 @@ void loop() #endif service->loop(); - +#if defined(LGFX_SDL) + if (screen) { + auto dispdev = screen->getDisplayDevice(); + if (dispdev) + static_cast(dispdev)->sdlLoop(); + } +#endif long delayMsec = mainController.runOrDelay(); // We want to sleep as long as possible here - because it saves power diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index b8452bb48..62942a80e 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -25,7 +25,6 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio -D USE_X11=1 -D HAS_TFT=1 -D HAS_SCREEN=1 - -D LV_CACHE_DEF_SIZE=6291456 -D LV_BUILD_TEST=0 -D LV_USE_LIBINPUT=1 @@ -41,6 +40,25 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio build_src_filter = ${native_base.build_src_filter} +[env:native-sdl] +extends = native_base +build_type = release +lib_deps = + ${env.lib_deps} + ${networking_base.lib_deps} + ${radiolib_base.lib_deps} + ${environmental_base.lib_deps} + # renovate: datasource=custom.pio depName=rweather/Crypto packageName=rweather/library/Crypto + rweather/Crypto@0.4.0 + # renovate: datasource=git-refs depName=libch341-spi-userspace packageName=https://github.com/pine64/libch341-spi-userspace gitBranch=main + https://github.com/pine64/libch341-spi-userspace/archive/af9bc27c9c30fa90772279925b7c5913dff789b4.zip + # renovate: datasource=custom.pio depName=adafruit/Adafruit seesaw Library packageName=adafruit/library/Adafruit seesaw Library + adafruit/Adafruit seesaw Library@1.7.9 + https://github.com/jp-bennett/LovyanGFX/archive/7458f84a126c1f8fdc7b038074f71be903f6e4c0.zip +build_flags = ${native_base.build_flags} + !pkg-config --cflags --libs sdl2 --silence-errors || : + -D LGFX_SDL=1 + [env:native-fb] extends = native_base build_type = release From ce75bf4496fe8900dbe2d443cedef77c53e4fc5a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 20 Aug 2025 14:18:20 -0500 Subject: [PATCH 12/15] Initial stab at rak6421 autoconf (#7691) * Initial stab at rak6421 autoconf * trunk * Add crc check to eeprom autoconf * Trunk again --- ...421.yaml => lora-RAK6421-13300-slot1.yaml} | 11 +-- bin/config.d/lora-RAK6421-13300-slot2.yaml | 8 ++ src/mesh/NodeDB.cpp | 6 +- src/platform/portduino/PortduinoGlue.cpp | 98 +++++++++++++++++-- src/platform/portduino/PortduinoGlue.h | 15 ++- 5 files changed, 113 insertions(+), 25 deletions(-) rename bin/config.d/{lora-RAK6421.yaml => lora-RAK6421-13300-slot1.yaml} (56%) create mode 100644 bin/config.d/lora-RAK6421-13300-slot2.yaml diff --git a/bin/config.d/lora-RAK6421.yaml b/bin/config.d/lora-RAK6421-13300-slot1.yaml similarity index 56% rename from bin/config.d/lora-RAK6421.yaml rename to bin/config.d/lora-RAK6421-13300-slot1.yaml index bbf38a474..6f65f9ccd 100644 --- a/bin/config.d/lora-RAK6421.yaml +++ b/bin/config.d/lora-RAK6421-13300-slot1.yaml @@ -9,13 +9,4 @@ Lora: DIO3_TCXO_VOLTAGE: true DIO2_AS_RF_SWITCH: true spidev: spidev0.0 - # CS: 8 - - - ### RAK13300in Slot 2 pins -# IRQ: 18 #IO6 -# Reset: 24 # IO4 -# Busy: 19 # IO5 -# # Ant_sw: 23 # IO3 -# spidev: spidev0.1 -# # CS: 7 \ No newline at end of file + # CS: 8 \ No newline at end of file diff --git a/bin/config.d/lora-RAK6421-13300-slot2.yaml b/bin/config.d/lora-RAK6421-13300-slot2.yaml new file mode 100644 index 000000000..cbc794d39 --- /dev/null +++ b/bin/config.d/lora-RAK6421-13300-slot2.yaml @@ -0,0 +1,8 @@ +Lora: + ### RAK13300in Slot 2 pins + IRQ: 18 #IO6 + Reset: 24 # IO4 + Busy: 19 # IO5 + # Ant_sw: 23 # IO3 + spidev: spidev0.1 + # CS: 7 \ No newline at end of file diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 97a1e463c..18014eb02 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -225,7 +225,11 @@ NodeDB::NodeDB() memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end)); myNodeInfo.device_id.size = 16; // Uncomment below to print the device id - +#elif ARCH_PORTDUINO + if (portduino_config.has_device_id) { + memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16); + myNodeInfo.device_id.size = 16; + } #else // FIXME - implement for other platforms #endif diff --git a/src/platform/portduino/PortduinoGlue.cpp b/src/platform/portduino/PortduinoGlue.cpp index 3753c944c..929a45d09 100644 --- a/src/platform/portduino/PortduinoGlue.cpp +++ b/src/platform/portduino/PortduinoGlue.cpp @@ -10,6 +10,7 @@ #include "linux/gpio/LinuxGPIOPin.h" #include "meshUtils.h" #include "yaml-cpp/yaml.h" +#include #include #include #include @@ -253,16 +254,95 @@ void portduinoSetup() std::cout << "autoconf: Could not locate CH341 device" << std::endl; } // Try Pi HAT+ - std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; - if (access("/proc/device-tree/hat/product", R_OK) == 0) { - std::ifstream hatProductFile("/proc/device-tree/hat/product"); - if (hatProductFile.is_open()) { - hatProductFile.read(autoconf_product, 95); - hatProductFile.close(); + if (strlen(autoconf_product) < 6) { + std::cout << "autoconf: Looking for Pi HAT+..." << std::endl; + if (access("/proc/device-tree/hat/product", R_OK) == 0) { + std::ifstream hatProductFile("/proc/device-tree/hat/product"); + if (hatProductFile.is_open()) { + hatProductFile.read(autoconf_product, 95); + hatProductFile.close(); + } + std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl; + } else { + std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl; + } + } + // attempt to load autoconf data from an EEPROM on 0x50 + // RAK6421-13300-S1:aabbcc123456:5ba85807d92138b7519cfb60460573af:3061e8d8 + // :mac address :<16 random unique bytes in hexidecimal> : crc32 + // crc32 is calculated on the eeprom string up to but not including the final colon + if (strlen(autoconf_product) < 6) { + try { + char *mac_start = nullptr; + char *devID_start = nullptr; + char *crc32_start = nullptr; + Wire.begin(); + Wire.beginTransmission(0x50); + Wire.write(0x0); + Wire.write(0x0); + Wire.endTransmission(); + Wire.requestFrom((uint8_t)0x50, (uint8_t)75); + uint8_t i = 0; + delay(100); + std::string autoconf_raw; + while (Wire.available() && i < sizeof(autoconf_product)) { + autoconf_product[i] = Wire.read(); + if (autoconf_product[i] == 0xff) { + autoconf_product[i] = 0x0; + break; + } + autoconf_raw += autoconf_product[i]; + if (autoconf_product[i] == ':') { + autoconf_product[i] = 0x0; + if (mac_start == nullptr) { + mac_start = autoconf_product + i + 1; + } else if (devID_start == nullptr) { + devID_start = autoconf_product + i + 1; + } else if (crc32_start == nullptr) { + crc32_start = autoconf_product + i + 1; + } + } + i++; + } + if (crc32_start != nullptr && strlen(crc32_start) == 8) { + std::string crc32_str(crc32_start); + uint32_t crc32_value = 0; + + // convert crc32 ascii to raw uint32 + for (int j = 0; j < 4; j++) { + crc32_value += std::stoi(crc32_str.substr(j * 2, 2), nullptr, 16) << (3 - j) * 8; + } + std::cout << "autoconf: Found eeprom crc " << crc32_start << std::endl; + + // set the autoconf string to blank and short circuit + if (crc32_value != crc32Buffer(autoconf_raw.c_str(), i - 9)) { + std::cout << "autoconf: crc32 mismatch, dropping " << std::endl; + autoconf_product[0] = 0x0; + } else { + std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl; + if (mac_start != nullptr) { + std::cout << "autoconf: Found mac data " << mac_start << std::endl; + if (strlen(mac_start) == 12) + settingsStrings[mac_address] = std::string(mac_start); + } + if (devID_start != nullptr) { + std::cout << "autoconf: Found deviceid data " << devID_start << std::endl; + if (strlen(devID_start) == 32) { + std::string devID_str(devID_start); + for (int j = 0; j < 16; j++) { + portduino_config.device_id[j] = std::stoi(devID_str.substr(j * 2, 2), nullptr, 16); + } + portduino_config.has_device_id = true; + } + } + } + } else { + std::cout << "autoconf: crc32 missing " << std::endl; + autoconf_product[0] = 0x0; + } + } catch (...) { + std::cout << "autoconf: Could not locate EEPROM" << std::endl; } - std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl; - } else { - std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl; } // Load the config file based on the product string if (strlen(autoconf_product) > 0) { diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 6e450c90e..8c36a1180 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -10,11 +10,14 @@ // Product strings for auto-configuration // {"PRODUCT_STRING", "CONFIG.YAML"} // YAML paths are relative to `meshtastic/available.d` -inline const std::unordered_map configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, - {"MESHSTICK", "lora-meshstick-1262.yaml"}, - {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, - {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, - {"POWERPI", "lora-MeshAdv-900M30S.yaml"}}; +inline const std::unordered_map configProducts = { + {"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, + {"MESHSTICK", "lora-meshstick-1262.yaml"}, + {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, + {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, + {"POWERPI", "lora-MeshAdv-900M30S.yaml"}, + {"RAK6421-13300-S1", "lora-RAK6421-13300-slot1.yaml"}, + {"RAK6421-13300-S2", "lora-RAK6421-13300-slot2.yaml"}}; enum configNames { default_gpiochip, @@ -135,4 +138,6 @@ extern struct portduino_config_struct { uint32_t rfswitch_dio_pins[5] = {RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; Module::RfSwitchMode_t rfswitch_table[8]; bool force_simradio = false; + bool has_device_id = false; + uint8_t device_id[16] = {0}; } portduino_config; \ No newline at end of file From 7574bfb7cbeaaf48ce29ef73ac2fcd38692186a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:18:33 -0500 Subject: [PATCH 13/15] Update meshtastic/device-ui digest to 3dc7cf3 (#7698) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ac30eb32e..cce4d2dcf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/8f5094b248c15ea2f9acf19cedfef6d2248fc1ff.zip + https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From fe3f14a63e2dbd160eabfcb8fdf1fb7cfb8eb1db Mon Sep 17 00:00:00 2001 From: Wilson Date: Thu, 21 Aug 2025 18:01:31 +0800 Subject: [PATCH 14/15] Add on-screen keyboard implementation on Trackball device. (#7625) * Add on-screen keyboard implementation on Wio Tracker L1. * Update On-Screen Keyboard to new layout. * The on-screen keyboard dynamically adjusts the key size based on the screen. * Improve input box display on small screens. * Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens. * Optimize the text alignment of numeric keys on ssd1306. --------- Co-authored-by: Ben Meadors --- src/buzz/BuzzerFeedbackThread.cpp | 1 + src/graphics/Screen.cpp | 75 ++- src/graphics/Screen.h | 4 +- src/graphics/VirtualKeyboard.cpp | 738 +++++++++++++++++++++ src/graphics/VirtualKeyboard.h | 80 +++ src/graphics/draw/MenuHandler.cpp | 3 + src/graphics/draw/NotificationRenderer.cpp | 111 +++- src/graphics/draw/NotificationRenderer.h | 6 + src/input/InputBroker.h | 1 + src/input/TrackballInterruptBase.cpp | 76 ++- src/input/TrackballInterruptBase.h | 13 +- src/input/TrackballInterruptImpl1.cpp | 7 +- src/main.cpp | 6 + src/main.h | 1 + src/modules/CannedMessageModule.cpp | 136 +++- 15 files changed, 1227 insertions(+), 31 deletions(-) create mode 100644 src/graphics/VirtualKeyboard.cpp create mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index ce762c764..838224c69 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -28,6 +28,7 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa71e17d8..5e29814cb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -216,6 +216,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t ui->update(); } +void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback) +{ + LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); + + if (NotificationRenderer::virtualKeyboard) { + delete NotificationRenderer::virtualKeyboard; + NotificationRenderer::virtualKeyboard = nullptr; + } + + NotificationRenderer::textInputCallback = nullptr; + + NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); + if (header) { + NotificationRenderer::virtualKeyboard->setHeader(header); + } + if (initialText) { + NotificationRenderer::virtualKeyboard->setInputText(initialText); + } + + // Set up callback with safer cleanup mechanism + NotificationRenderer::textInputCallback = textCallback; + NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); + + // Store the message and set the expiration timestamp (use same pattern as other notifications) + strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; + + // Set the overlay using the same pattern as other notification types + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); +} + static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -713,13 +751,19 @@ int32_t Screen::runOnce() handleSetOn(false); break; case Cmd::ON_PRESS: - handleOnPress(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleOnPress(); + } break; case Cmd::SHOW_PREV_FRAME: - handleShowPrevFrame(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleShowPrevFrame(); + } break; case Cmd::SHOW_NEXT_FRAME: - handleShowNextFrame(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleShowNextFrame(); + } break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away @@ -741,7 +785,9 @@ int32_t Screen::runOnce() NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - setFrames(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } break; case Cmd::NOOP: break; @@ -777,6 +823,7 @@ int32_t Screen::runOnce() if (showingNormalScreen) { // standard screen loop handling here if (config.display.auto_screen_carousel_secs > 0 && + NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead @@ -867,6 +914,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Called when a frame should be added / removed, or custom frames should be cleared void Screen::setFrames(FrameFocus focus) { + // Block setFrames calls when virtual keyboard is active to prevent overlay interference + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return; + } + uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter @@ -1313,6 +1365,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return 0; + } + if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) @@ -1335,6 +1392,16 @@ int Screen::handleInputEvent(const InputEvent *event) if (!screenOn) return 0; + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + return 0; + } + #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 265900131..0f100d455 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -12,7 +12,7 @@ #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { -enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; struct BannerOverlayOptions { const char *message; @@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); + void showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp new file mode 100644 index 000000000..84d5551cb --- /dev/null +++ b/src/graphics/VirtualKeyboard.cpp @@ -0,0 +1,738 @@ +#include "VirtualKeyboard.h" +#include "configuration.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "main.h" +#include +#include + +namespace graphics +{ + +VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) +{ + initializeKeyboard(); + // Set cursor to H(2, 5) + cursorRow = 2; + cursorCol = 5; +} + +VirtualKeyboard::~VirtualKeyboard() {} + +void VirtualKeyboard::initializeKeyboard() +{ + // New 4 row, 11 column keyboard layout: + static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; + + // Derive layout dimensions and assert they match the configured keyboard grid + constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); + constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); + static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); + static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); + + // Initialize all keys to empty first + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; + } + } + + // Fill keyboard from the 2D layout + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + char ch = LAYOUT[row][col]; + // No empty slots in the simplified layout + + VirtualKeyType type = VK_CHAR; + if (ch == '\b') { + type = VK_BACKSPACE; + } else if (ch == '\n') { + type = VK_ENTER; + } else if (ch == '\x1b') { // ESC + type = VK_ESC; + } else if (ch == ' ') { + type = VK_SPACE; + } + + // Make action keys wider to fit text while keeping the last column aligned + uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; + keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; + } + } +} + +void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) +{ + // Repeat ticking is driven by NotificationRenderer once per frame + // Base styles + display->setColor(WHITE); + display->setFont(FONT_SMALL); + + // Screen geometry + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels + // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide + const bool isWide = screenW >= 200; + + // Determine last-column label max width + display->setFont(FONT_SMALL); + const int wENTER = display->getStringWidth("ENTER"); + int lastColLabelW = wENTER; // ENTER is usually the widest + // Smaller padding on very small screens to avoid excessive whitespace + const int lastColPad = (screenW <= 128 ? 2 : 6); + const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys + + // Always reserve width for the rightmost text column to avoid overlap on small screens + int cellW = 0; + int leftoverW = 0; + { + const int leftCols = KEYBOARD_COLS - 1; // 10 input characters + int usableW = screenW - reservedLastColW; + if (usableW < leftCols) { + // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) + usableW = leftCols; + } + cellW = usableW / leftCols; + leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) + } + + // Dynamic key geometry + int cellH = KEY_HEIGHT; + int keyboardStartY = 0; + if (screenH <= 64) { + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); + const int gapBelowHeader = 0; + const int singleLineBoxHeight = FONT_HEIGHT_SMALL; + const int gapAboveKeyboard = 0; + keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; + if (keyboardStartY < 0) + keyboardStartY = 0; + if (keyboardStartY > screenH) + keyboardStartY = screenH; + int keyboardHeight = screenH - keyboardStartY; + cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); + } else if (isWide) { + // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. + cellH = std::max((int)KEY_HEIGHT, cellW); + + // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. + // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 + display->setFont(FONT_SMALL); + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); + const int headerToBoxGap = 1; + const int gapAboveKb = 1; + const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom + int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); + int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; + if (maxCellHAllowed < (int)KEY_HEIGHT) + maxCellHAllowed = KEY_HEIGHT; + if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { + cellH = maxCellHAllowed; + } + // Keyboard placement from bottom for wide screens + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } else { + // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom + cellH = KEY_HEIGHT; + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } + + // Draw input area above keyboard + drawInputArea(display, offsetX, offsetY, keyboardStartY); + + // Precompute per-column x and width with leftover distributed over left columns for even spacing + int colX[KEYBOARD_COLS]; + int colW[KEYBOARD_COLS]; + int runningX = offsetX; + for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { + int wcol = cellW + (col < leftoverW ? 1 : 0); + colX[col] = runningX; + colW[col] = wcol; + runningX += wcol; + } + // Last column + colX[KEYBOARD_COLS - 1] = runningX; + colW[KEYBOARD_COLS - 1] = reservedLastColW; + + // Draw keyboard grid + for (int row = 0; row < KEYBOARD_ROWS; row++) { + for (int col = 0; col < KEYBOARD_COLS; col++) { + const VirtualKey &k = keyboard[row][col]; + if (k.character != 0 || k.type != VK_CHAR) { + const bool isLastCol = (col == KEYBOARD_COLS - 1); + int x = colX[col]; + int w = colW[col]; + int y = offsetY + keyboardStartY + row * cellH; + int h = cellH; + bool selected = (row == cursorRow && col == cursorCol); + drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); + } + } + } +} + +void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) +{ + display->setColor(WHITE); + + const int screenWidth = display->getWidth(); + const int screenHeight = display->getHeight(); + // Use the standard small font metrics for input box sizing (restore original size) + const int inputLineH = FONT_HEIGHT_SMALL; + + // Header uses the standard small (which may be larger on big screens) + display->setFont(FONT_SMALL); + int headerHeight = 0; + if (!headerText.empty()) { + // Draw header and reserve exact font height (plus a tighter gap) to maximize input area + display->drawString(offsetX + 2, offsetY, headerText.c_str()); + if (screenHeight <= 64) { + headerHeight = FONT_HEIGHT_SMALL - 2; // 11px + } else { + headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in + } + } + + const int boxX = offsetX; + const int boxWidth = screenWidth; + int boxY; + int boxHeight; + if (screenHeight <= 64) { + const int gapBelowHeader = 0; + const int fixedBoxHeight = inputLineH; + const int gapAboveKeyboard = 0; + boxY = offsetY + headerHeight + gapBelowHeader; + boxHeight = fixedBoxHeight; + if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { + int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; + boxHeight = std::max(1, fixedBoxHeight - over); + } + } else { + const int gapBelowHeader = 1; + int gapAboveKeyboard = 1; + int tmpBoxY = offsetY + headerHeight + gapBelowHeader; + const int minBoxHeight = inputLineH + 2; + int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; + if (availableH < minBoxHeight) + availableH = minBoxHeight; + boxY = tmpBoxY; + boxHeight = availableH; + } + + // Draw box border + display->drawRect(boxX, boxY, boxWidth, boxHeight); + + display->setFont(FONT_SMALL); + + // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis + const int textX = boxX + 2; + const int maxTextWidth = boxWidth - 4; + const int maxLines = (boxHeight - 2) / inputLineH; + if (maxLines >= 2) { + // Inner bounds for caret clamping + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Wrap text greedily into lines that fit maxTextWidth + std::vector lines; + { + std::string remaining = inputText; + while (!remaining.empty()) { + int bestLen = 0; + for (int len = 1; len <= (int)remaining.size(); ++len) { + int w = display->getStringWidth(remaining.substr(0, len).c_str()); + if (w <= maxTextWidth) + bestLen = len; + else + break; + } + if (bestLen == 0) { + // At least show one character to make progress + bestLen = 1; + } + lines.emplace_back(remaining.substr(0, bestLen)); + remaining.erase(0, bestLen); + } + } + + const bool scrolledUp = ((int)lines.size() > maxLines); + int caretX = textX; + int caretY = innerTop; + + // Leave a small top gap to render '...' without replacing the first line + const int topInset = 2; + const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height + int lineY = innerTop + topInset; + + if (scrolledUp) { + // Draw three small dots centered horizontally, vertically at the midpoint of the gap + // between the inner top and the first line's top baseline. This avoids using a tall glyph. + const int firstLineTop = lineY; // baseline top for the first visible line + const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested + const int centerX = boxX + boxWidth / 2; + const int dotSpacing = 3; // px between dots + const int dotSize = 1; // small square dot + display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); + display->fillRect(centerX, gapMidY, dotSize, dotSize); + display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); + } + + // How many lines fit with our top inset and tighter step + const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); + const int linesToShow = std::min((int)lines.size(), linesCapacity); + const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; + + for (int i = 0; i < linesToShow; ++i) { + const std::string &chunk = lines[startIndex + i]; + display->drawString(textX, lineY, chunk.c_str()); + caretX = textX + display->getStringWidth(chunk.c_str()); + caretY = lineY; + lineY += lineStep; + } + + // Draw caret at end of the last visible line + int caretPadY = 2; + if (boxHeight >= inputLineH + 4) + caretPadY = 3; + int cursorTop = caretY + caretPadY; + // Use lineStep so caret height matches the row spacing + int cursorH = lineStep - caretPadY * 2; + if (cursorH < 1) + cursorH = 1; + // Clamp vertical bounds to stay inside the inner rect + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + // Only draw if cursor is inside inner bounds + if (caretX >= innerLeft && caretX <= innerRight) { + display->drawVerticalLine(caretX, cursorTop, cursorH); + } + } else { + std::string displayText = inputText; + int textW = display->getStringWidth(displayText.c_str()); + std::string scrolled = displayText; + if (textW > maxTextWidth) { + // Trim from the left until it fits + while (textW > maxTextWidth && !scrolled.empty()) { + scrolled.erase(0, 1); + textW = display->getStringWidth(scrolled.c_str()); + } + // Add leading ellipsis and ensure it still fits + if (scrolled != displayText) { + scrolled = "..." + scrolled; + textW = display->getStringWidth(scrolled.c_str()); + // If adding ellipsis causes overflow, trim more after the ellipsis + while (textW > maxTextWidth && scrolled.size() > 3) { + scrolled.erase(3, 1); // remove chars after the ellipsis + textW = display->getStringWidth(scrolled.c_str()); + } + } + } else { + // Keep textW in sync with what we draw + textW = display->getStringWidth(scrolled.c_str()); + } + + int textY; + if (screenHeight <= 64) { + textY = boxY + (boxHeight - inputLineH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Center text vertically within inner box for single-line, then clamp so it never overlaps borders + int innerH = innerBottom - innerTop + 1; + textY = innerTop + std::max(0, (innerH - inputLineH) / 2); + // Clamp fully inside the inner rect + if (textY < innerTop) + textY = innerTop; + int maxTop = innerBottom - inputLineH + 1; + if (textY > maxTop) + textY = maxTop; + } + + if (!scrolled.empty()) { + display->drawString(textX, textY, scrolled.c_str()); + } + + int cursorX = textX + textW; + if (screenHeight > 64) { + const int innerRight = boxX + boxWidth - 2; + if (cursorX > innerRight) + cursorX = innerRight; + } + + int cursorTop, cursorH; + if (screenHeight <= 64) { + cursorH = 10; + cursorTop = boxY + (boxHeight - cursorH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + cursorTop = boxY + 2; + cursorH = boxHeight - 4; + if (cursorH < 1) + cursorH = 1; + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + + if (cursorX < innerLeft || cursorX > innerRight) + return; + } + + display->drawVerticalLine(cursorX, cursorTop, cursorH); + } +} + +void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, + uint8_t height, bool isLastCol) +{ + // Draw key content + display->setFont(FONT_SMALL); + const int fontH = FONT_HEIGHT_SMALL; + // Build label and metrics first + std::string keyText; + if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { + // Keep literal text labels for the action keys on the rightmost column + keyText = (key.type == VK_BACKSPACE) ? "BACK" + : (key.type == VK_ENTER) ? "ENTER" + : (key.type == VK_SPACE) ? "SPACE" + : (key.type == VK_ESC) ? "ESC" + : ""; + } else { + char c = getCharForKey(key, false); + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + } + + int textWidth = display->getStringWidth(keyText.c_str()); + // Label alignment + // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. + // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. + int textX; + if (isLastCol) { + const int rightPad = 1; + textX = x + width - textWidth - rightPad; + if (textX < x) + textX = x; // guard + } else { + if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { + textX = x + (width - textWidth + 1) / 2; + } else { + textX = x + (width - textWidth) / 2; + } + } + int contentTop = y; + int contentH = height; + if (selected) { + display->setColor(WHITE); + bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); + + if (display->getHeight() <= 64 && !isAction) { + display->fillRect(x, y, width, height); + } else if (isAction) { + const int padX = 1; + const int padY = 2; + int hlW = textWidth + padX * 2; + int hlX = textX - padX; + + if (hlX < x) { + hlW -= (x - hlX); + hlX = x; + } + int maxW = (x + width) - hlX; + if (hlW > maxW) + hlW = maxW; + if (hlW < 1) + hlW = 1; + + int hlH = std::min(fontH + padY * 2, (int)height); + int hlY = y + (height - hlH) / 2; + display->fillRect(hlX, hlY, hlW, hlH); + contentTop = hlY; + contentH = hlH; + } else { + display->fillRect(x, y, width, height); + } + display->setColor(BLACK); + } else { + display->setColor(WHITE); + } + + int centeredTextY; + if (display->getHeight() <= 64) { + centeredTextY = y + (height - fontH) / 2; + } else { + centeredTextY = contentTop + (contentH - fontH) / 2; + } + if (display->getHeight() > 64) { + if (centeredTextY < contentTop) + centeredTextY = contentTop; + if (centeredTextY + fontH > contentTop + contentH) + centeredTextY = std::max(contentTop, contentTop + contentH - fontH); + } + + if (display->getHeight() <= 64 && keyText.size() == 1) { + char ch = keyText[0]; + if (ch == '.' || ch == ',' || ch == ';') { + centeredTextY -= 1; + } + } + display->drawString(textX, centeredTextY, keyText.c_str()); +} + +char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) +{ + if (key.type != VK_CHAR) { + return key.character; + } + + char c = key.character; + + // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings + if (isLongPress && c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } + + return c; +} + +void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) +{ + resetTimeout(); + // wrap around rows and cols in the 4x11 grid + int r = (int)cursorRow + dRow; + int c = (int)cursorCol + dCol; + if (r < 0) + r = KEYBOARD_ROWS - 1; + else if (r >= KEYBOARD_ROWS) + r = 0; + if (c < 0) + c = KEYBOARD_COLS - 1; + else if (c >= KEYBOARD_COLS) + c = 0; + cursorRow = (uint8_t)r; + cursorCol = (uint8_t)c; +} + +void VirtualKeyboard::moveCursorUp() +{ + moveCursorDelta(-1, 0); +} +void VirtualKeyboard::moveCursorDown() +{ + moveCursorDelta(1, 0); +} +void VirtualKeyboard::moveCursorLeft() +{ + resetTimeout(); + + if (cursorCol > 0) { + cursorCol--; + } else { + if (cursorRow > 0) { + cursorRow--; + cursorCol = KEYBOARD_COLS - 1; + } else { + cursorRow = KEYBOARD_ROWS - 1; + cursorCol = KEYBOARD_COLS - 1; + } + } +} +void VirtualKeyboard::moveCursorRight() +{ + resetTimeout(); + + if (cursorCol < KEYBOARD_COLS - 1) { + cursorCol++; + } else { + if (cursorRow < KEYBOARD_ROWS - 1) { + cursorRow++; + cursorCol = 0; + } else { + cursorRow = 0; + cursorCol = 0; + } + } +} + +void VirtualKeyboard::handlePress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert lowercase character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char + return; + } + + // Handle non-character keys immediately + switch (key.type) { + case VK_BACKSPACE: + deleteCharacter(); + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + std::function callback = onTextEntered; + onTextEntered = nullptr; + inputText = ""; + callback(""); + } + return; + default: + break; + } +} + +void VirtualKeyboard::handleLongPress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert uppercase/alternate character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char + return; + } + + switch (key.type) { + case VK_BACKSPACE: + // One-shot: delete up to 5 characters on long press + for (int i = 0; i < 5; ++i) { + if (inputText.empty()) + break; + deleteCharacter(); + } + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + onTextEntered(""); + } + break; + default: + break; + } +} + +void VirtualKeyboard::insertCharacter(char c) +{ + if (inputText.length() < 160) { // Reasonable text length limit + inputText += c; + } +} + +void VirtualKeyboard::deleteCharacter() +{ + if (!inputText.empty()) { + inputText.pop_back(); + } +} + +void VirtualKeyboard::submitText() +{ + LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); + + // Only submit if text is not empty + if (!inputText.empty() && onTextEntered) { + // Store callback and text to submit before clearing callback + std::function callback = onTextEntered; + std::string textToSubmit = inputText; + onTextEntered = nullptr; + // Don't clear inputText here - let the calling module handle cleanup + // inputText = ""; // Removed: keep text visible until module cleans up + callback(textToSubmit); + } else if (inputText.empty()) { + // For empty text, just ignore the submission - don't clear callback + // This keeps the virtual keyboard responsive for further input + LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); + } else { + // No callback available + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } +} + +void VirtualKeyboard::setInputText(const std::string &text) +{ + inputText = text; +} + +std::string VirtualKeyboard::getInputText() const +{ + return inputText; +} + +void VirtualKeyboard::setHeader(const std::string &header) +{ + headerText = header; +} + +void VirtualKeyboard::setCallback(std::function callback) +{ + onTextEntered = callback; +} + +void VirtualKeyboard::resetTimeout() +{ + lastActivityTime = millis(); +} + +bool VirtualKeyboard::isTimedOut() const +{ + return (millis() - lastActivityTime) > TIMEOUT_MS; +} + +} // namespace graphics diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h new file mode 100644 index 000000000..169163b57 --- /dev/null +++ b/src/graphics/VirtualKeyboard.h @@ -0,0 +1,80 @@ +#pragma once + +#include "configuration.h" +#include +#include +#include + +namespace graphics +{ + +enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; + +struct VirtualKey { + char character; + VirtualKeyType type; + uint8_t x; + uint8_t y; + uint8_t width; + uint8_t height; +}; + +class VirtualKeyboard +{ + public: + VirtualKeyboard(); + ~VirtualKeyboard(); + + void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); + void setInputText(const std::string &text); + std::string getInputText() const; + void setHeader(const std::string &header); + void setCallback(std::function callback); + + // Navigation methods for encoder input + void moveCursorUp(); + void moveCursorDown(); + void moveCursorLeft(); + void moveCursorRight(); + void handlePress(); + void handleLongPress(); + + // Timeout management + void resetTimeout(); + bool isTimedOut() const; + + private: + static const uint8_t KEYBOARD_ROWS = 4; + static const uint8_t KEYBOARD_COLS = 11; + static const uint8_t KEY_WIDTH = 9; + static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays + static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom + + VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; + + std::string inputText; + std::string headerText; + std::function onTextEntered; + + uint8_t cursorRow; + uint8_t cursorCol; + + // Timeout management for auto-exit + uint32_t lastActivityTime; + static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout + + void initializeKeyboard(); + void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, + bool isLastCol); + void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); + + // Unified cursor movement helper + void moveCursorDelta(int dRow, int dCol); + + char getCharForKey(const VirtualKey &key, bool isLongPress = false); + void insertCharacter(char c); + void deleteCharacter(); + void submitText(); +}; + +} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 512f650ec..e02948864 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -10,7 +10,10 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" #include "main.h" +#include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3d635e588..221d95075 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -38,6 +38,8 @@ bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; +VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; +std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { @@ -89,14 +91,33 @@ void NotificationRenderer::resetBanner() void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') - resetBanner(); - if (!isOverlayBannerShowing() || pauseBanner) + // Handle text_input notifications first - they have their own timeout/banner logic + if (current_notification_type == notificationTypeEnum::text_input) { + // Check for timeout and reset if needed for text input + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + return; + } + drawTextInput(display, state); return; + } + + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + } + + // Exit if no banner is showing or banner is paused + if (!isOverlayBannerShowing() || pauseBanner) { + return; + } + switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; + case notificationTypeEnum::text_input: + // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. + break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -575,6 +596,90 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } +void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + if (virtualKeyboard) { + // Check for timeout and auto-exit if needed + if (virtualKeyboard->isTimedOut()) { + LOG_INFO("Virtual keyboard timeout - auto-exiting"); + // Cancel virtual keyboard - call callback with empty string to indicate timeout + auto callback = textInputCallback; // Store callback before clearing + + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + + // Call callback after cleanup + if (callback) { + callback(""); + } + + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Handle input events for virtual keyboard navigation + if (inEvent.inputEvent != INPUT_BROKER_NONE) { + if (inEvent.inputEvent == INPUT_BROKER_UP) { + virtualKeyboard->moveCursorUp(); + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { + virtualKeyboard->moveCursorDown(); + } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { + virtualKeyboard->moveCursorLeft(); + } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { + virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + // Long press UP = move left + virtualKeyboard->moveCursorLeft(); + } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + // Long press DOWN = move right + virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + virtualKeyboard->handlePress(); + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { + virtualKeyboard->handleLongPress(); + } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { + // Cancel virtual keyboard - call callback with empty string + auto callback = textInputCallback; // Store callback before clearing + + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + + // Call callback after cleanup + if (callback) { + callback(""); + } + + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Reset input event after processing + inEvent.inputEvent = INPUT_BROKER_NONE; + } + + // Clear the display and draw virtual keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + virtualKeyboard->draw(display, 0, 0); + } else { + // If virtualKeyboard is null, reset the banner to avoid getting stuck + resetBanner(); + } +} + bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 9c30b329c..edb069513 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -3,6 +3,9 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" +#include "graphics/VirtualKeyboard.h" +#include +#include #define MAX_LINES 5 namespace graphics @@ -22,6 +25,8 @@ class NotificationRenderer static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; + static VirtualKeyboard *virtualKeyboard; + static std::function textInputCallback; static bool pauseBanner; @@ -30,6 +35,7 @@ class NotificationRenderer static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 4487fa662..012a403f5 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,6 +4,7 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, + INPUT_BROKER_SELECT_LONG, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index d41ad2fd6..4c8ce6409 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,12 +1,14 @@ #include "TrackballInterruptBase.h" #include "configuration.h" +extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), - void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) + input_broker_event eventRight, input_broker_event eventPressed, + input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); @@ -40,9 +43,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } - LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, - pinPress); - + LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, + this->_pinLeft, this->_pinRight, pinPress); + osk_found = true; this->setInterval(100); } @@ -50,10 +53,47 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = INPUT_BROKER_NONE; + + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = millis() - pressStartTime; + bool buttonStillPressed = false; + +#if defined(T_DECK) + buttonStillPressed = (this->action == TB_ACTION_PRESSED); +#else + buttonStillPressed = !digitalRead(_pinPress); +#endif + + if (!buttonStillPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION) { + // Short press + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastLongPressEventTime = 0; + this->action = TB_ACTION_NONE; + } else if (pressDuration >= LONG_PRESS_DURATION) { + // Long press detected + uint32_t currentTime = millis(); + // Only trigger long press event if enough time has passed since the last one + if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventPressedLong; + lastLongPressEventTime = currentTime; + } + this->action = TB_ACTION_PRESSED_LONG; + } + } + #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED) { - // LOG_DEBUG("Trackball event Press"); - e.inputEvent = this->_eventPressed; + if (this->action == TB_ACTION_PRESSED && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce() e.inputEvent = this->_eventRight; } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { - // LOG_DEBUG("Trackball event Press"); - e.inputEvent = this->_eventPressed; + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce() e.kbchar = 0x00; this->notifyObservers(&e); } - lastEvent = action; - this->action = TB_ACTION_NONE; - return 100; + // Only update lastEvent for non-press actions or completed press actions + if (this->action != TB_ACTION_PRESSED || !pressDetected) { + lastEvent = action; + if (!pressDetected) { + this->action = TB_ACTION_NONE; + } + } + + return 50; // Check more frequently for better long press detection } void TrackballInterruptBase::intPressHandler() diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 92db8720e..38be22f20 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable, public con explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), - void (*onIntPress)()); + input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable, public con enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, + TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, @@ -46,12 +47,20 @@ class TrackballInterruptBase : public Observable, public con volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + // Long press detection for press button + uint32_t pressStartTime = 0; + bool pressDetected = false; + uint32_t lastLongPressEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = 500; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events + private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 896238f38..594facdeb 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, - TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, - TrackballInterruptImpl1::handleIntPressed); + eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, + TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, + TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } diff --git a/src/main.cpp b/src/main.cpp index ef5f5a721..23ffa6b6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -191,6 +191,8 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; uint8_t kb_model; // global bool to record that a kb is present bool kb_found = false; +// global bool to record that on-screen keyboard (OSK) is present +bool osk_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -1412,6 +1414,10 @@ void setup() #endif #endif +#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) + osk_found = true; +#endif + #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/main.h b/src/main.h index 3568daad2..2ddd4862f 100644 --- a/src/main.h +++ b/src/main.h @@ -32,6 +32,7 @@ extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; +extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index d40dcd24f..76b950322 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,12 +13,16 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -38,6 +42,7 @@ extern ScanI2C::DeviceAddress cardkb_found; extern bool graphics::isMuted; +extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; @@ -151,10 +156,13 @@ int CannedMessageModule::splitConfiguredMessages() int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; - #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a keyboard + // Add a "Free Text" entry at the top if using a touch screen virtual keyboard tempMessages[tempCount++] = "[-- Free Text --]"; +#else + if (osk_found && screen) { + tempMessages[tempCount++] = "[-- Free Text --]"; + } #endif // First message always starts at buffer start @@ -341,6 +349,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state + // Virtual keyboard mode: Show virtual keyboard and handle input + // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; @@ -627,6 +637,56 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo notifyObservers(&e); return true; } +#else + if (strcmp(current, "[-- Free Text --]") == 0) { + if (osk_found && screen) { + char headerBuffer[64]; + if (this->dest == NODENUM_BROADCAST) { + snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); + } else { + snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); + } + screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { + if (!text.empty()) { + this->freetext = text.c_str(); + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + currentMessageIndex = -1; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + setIntervalFromNow(500); + return; + } else { + // Don't delete virtual keyboard immediately - it might still be executing + // Instead, just clear the callback and reset banner to stop input processing + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + + // Return to inactive state + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Force display update to show normal screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + // Schedule cleanup for next loop iteration to ensure safe deletion + setIntervalFromNow(50); + return; + } + }); + + return true; + } + } #endif // Normal canned message selection @@ -943,12 +1003,54 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + // Clean up virtual keyboard if needed when going inactive + if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { + LOG_INFO("Performing delayed virtual keyboard cleanup"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + } + temporaryMessage = ""; return INT32_MAX; } + // Handle delayed virtual keyboard message sending + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + // Virtual keyboard message sending case - text was not empty + if (this->freetext.length() > 0) { + LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); + sendText(this->dest, this->channel, this->freetext.c_str(), true); + + // Clean up virtual keyboard after sending + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard after message send"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + } + + // Clear payload to indicate virtual keyboard processing is complete + // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds + this->payload = 0; + } else { + // Empty message, just go inactive + LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); + return 2000; + } + UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && + this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -958,6 +1060,18 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); + } + // Handle SENDING_ACTIVE state transition after virtual keyboard message + else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { + // This happens after virtual keyboard message sending is complete + LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + temporaryMessage = ""; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -966,9 +1080,23 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + // Clean up virtual keyboard if it exists during timeout + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard due to module timeout"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + } + this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->payload == 0) { + // [Exit] button pressed - return to inactive state + LOG_INFO("Processing [Exit] action - returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; From 1daf5aad1ff4a84071e3e09cc05252ab21811963 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 21 Aug 2025 06:29:23 -0500 Subject: [PATCH 15/15] Revert "Add on-screen keyboard implementation on Trackball device. (#7625)" (#7704) This reverts commit fe3f14a63e2dbd160eabfcb8fdf1fb7cfb8eb1db. --- src/buzz/BuzzerFeedbackThread.cpp | 1 - src/graphics/Screen.cpp | 75 +-- src/graphics/Screen.h | 4 +- src/graphics/VirtualKeyboard.cpp | 738 --------------------- src/graphics/VirtualKeyboard.h | 80 --- src/graphics/draw/MenuHandler.cpp | 3 - src/graphics/draw/NotificationRenderer.cpp | 109 +-- src/graphics/draw/NotificationRenderer.h | 6 - src/input/InputBroker.h | 1 - src/input/TrackballInterruptBase.cpp | 76 +-- src/input/TrackballInterruptBase.h | 13 +- src/input/TrackballInterruptImpl1.cpp | 7 +- src/main.cpp | 6 - src/main.h | 1 - src/modules/CannedMessageModule.cpp | 136 +--- 15 files changed, 30 insertions(+), 1226 deletions(-) delete mode 100644 src/graphics/VirtualKeyboard.cpp delete mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 838224c69..ce762c764 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -28,7 +28,6 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: - case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5e29814cb..fa71e17d8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -216,44 +216,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t ui->update(); } -void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback) -{ - LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - - if (NotificationRenderer::virtualKeyboard) { - delete NotificationRenderer::virtualKeyboard; - NotificationRenderer::virtualKeyboard = nullptr; - } - - NotificationRenderer::textInputCallback = nullptr; - - NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); - if (header) { - NotificationRenderer::virtualKeyboard->setHeader(header); - } - if (initialText) { - NotificationRenderer::virtualKeyboard->setInputText(initialText); - } - - // Set up callback with safer cleanup mechanism - NotificationRenderer::textInputCallback = textCallback; - NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); - - // Store the message and set the expiration timestamp (use same pattern as other notifications) - strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; - - // Set the overlay using the same pattern as other notification types - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); -} - static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -751,19 +713,13 @@ int32_t Screen::runOnce() handleSetOn(false); break; case Cmd::ON_PRESS: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleOnPress(); - } + handleOnPress(); break; case Cmd::SHOW_PREV_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowPrevFrame(); - } + handleShowPrevFrame(); break; case Cmd::SHOW_NEXT_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowNextFrame(); - } + handleShowNextFrame(); break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away @@ -785,9 +741,7 @@ int32_t Screen::runOnce() NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - setFrames(); - } + setFrames(); break; case Cmd::NOOP: break; @@ -823,7 +777,6 @@ int32_t Screen::runOnce() if (showingNormalScreen) { // standard screen loop handling here if (config.display.auto_screen_carousel_secs > 0 && - NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead @@ -914,11 +867,6 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Called when a frame should be added / removed, or custom frames should be cleared void Screen::setFrames(FrameFocus focus) { - // Block setFrames calls when virtual keyboard is active to prevent overlay interference - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return; - } - uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter @@ -1365,11 +1313,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return 0; - } - if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) @@ -1392,16 +1335,6 @@ int Screen::handleInputEvent(const InputEvent *event) if (!screenOn) return 0; - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } - #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 0f100d455..265900131 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -12,7 +12,7 @@ #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { -enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; struct BannerOverlayOptions { const char *message; @@ -313,8 +313,6 @@ class Screen : public concurrency::OSThread void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); - void showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp deleted file mode 100644 index 84d5551cb..000000000 --- a/src/graphics/VirtualKeyboard.cpp +++ /dev/null @@ -1,738 +0,0 @@ -#include "VirtualKeyboard.h" -#include "configuration.h" -#include "graphics/Screen.h" -#include "graphics/ScreenFonts.h" -#include "graphics/SharedUIDisplay.h" -#include "main.h" -#include -#include - -namespace graphics -{ - -VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) -{ - initializeKeyboard(); - // Set cursor to H(2, 5) - cursorRow = 2; - cursorCol = 5; -} - -VirtualKeyboard::~VirtualKeyboard() {} - -void VirtualKeyboard::initializeKeyboard() -{ - // New 4 row, 11 column keyboard layout: - static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, - {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; - - // Derive layout dimensions and assert they match the configured keyboard grid - constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); - constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); - static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); - static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); - - // Initialize all keys to empty first - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; - } - } - - // Fill keyboard from the 2D layout - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - char ch = LAYOUT[row][col]; - // No empty slots in the simplified layout - - VirtualKeyType type = VK_CHAR; - if (ch == '\b') { - type = VK_BACKSPACE; - } else if (ch == '\n') { - type = VK_ENTER; - } else if (ch == '\x1b') { // ESC - type = VK_ESC; - } else if (ch == ' ') { - type = VK_SPACE; - } - - // Make action keys wider to fit text while keeping the last column aligned - uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; - keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; - } - } -} - -void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) -{ - // Repeat ticking is driven by NotificationRenderer once per frame - // Base styles - display->setColor(WHITE); - display->setFont(FONT_SMALL); - - // Screen geometry - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); - - // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels - // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide - const bool isWide = screenW >= 200; - - // Determine last-column label max width - display->setFont(FONT_SMALL); - const int wENTER = display->getStringWidth("ENTER"); - int lastColLabelW = wENTER; // ENTER is usually the widest - // Smaller padding on very small screens to avoid excessive whitespace - const int lastColPad = (screenW <= 128 ? 2 : 6); - const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys - - // Always reserve width for the rightmost text column to avoid overlap on small screens - int cellW = 0; - int leftoverW = 0; - { - const int leftCols = KEYBOARD_COLS - 1; // 10 input characters - int usableW = screenW - reservedLastColW; - if (usableW < leftCols) { - // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) - usableW = leftCols; - } - cellW = usableW / leftCols; - leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) - } - - // Dynamic key geometry - int cellH = KEY_HEIGHT; - int keyboardStartY = 0; - if (screenH <= 64) { - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); - const int gapBelowHeader = 0; - const int singleLineBoxHeight = FONT_HEIGHT_SMALL; - const int gapAboveKeyboard = 0; - keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; - if (keyboardStartY < 0) - keyboardStartY = 0; - if (keyboardStartY > screenH) - keyboardStartY = screenH; - int keyboardHeight = screenH - keyboardStartY; - cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); - } else if (isWide) { - // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. - cellH = std::max((int)KEY_HEIGHT, cellW); - - // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. - // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 - display->setFont(FONT_SMALL); - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); - const int headerToBoxGap = 1; - const int gapAboveKb = 1; - const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom - int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); - int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; - if (maxCellHAllowed < (int)KEY_HEIGHT) - maxCellHAllowed = KEY_HEIGHT; - if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { - cellH = maxCellHAllowed; - } - // Keyboard placement from bottom for wide screens - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } else { - // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom - cellH = KEY_HEIGHT; - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } - - // Draw input area above keyboard - drawInputArea(display, offsetX, offsetY, keyboardStartY); - - // Precompute per-column x and width with leftover distributed over left columns for even spacing - int colX[KEYBOARD_COLS]; - int colW[KEYBOARD_COLS]; - int runningX = offsetX; - for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { - int wcol = cellW + (col < leftoverW ? 1 : 0); - colX[col] = runningX; - colW[col] = wcol; - runningX += wcol; - } - // Last column - colX[KEYBOARD_COLS - 1] = runningX; - colW[KEYBOARD_COLS - 1] = reservedLastColW; - - // Draw keyboard grid - for (int row = 0; row < KEYBOARD_ROWS; row++) { - for (int col = 0; col < KEYBOARD_COLS; col++) { - const VirtualKey &k = keyboard[row][col]; - if (k.character != 0 || k.type != VK_CHAR) { - const bool isLastCol = (col == KEYBOARD_COLS - 1); - int x = colX[col]; - int w = colW[col]; - int y = offsetY + keyboardStartY + row * cellH; - int h = cellH; - bool selected = (row == cursorRow && col == cursorCol); - drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); - } - } - } -} - -void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) -{ - display->setColor(WHITE); - - const int screenWidth = display->getWidth(); - const int screenHeight = display->getHeight(); - // Use the standard small font metrics for input box sizing (restore original size) - const int inputLineH = FONT_HEIGHT_SMALL; - - // Header uses the standard small (which may be larger on big screens) - display->setFont(FONT_SMALL); - int headerHeight = 0; - if (!headerText.empty()) { - // Draw header and reserve exact font height (plus a tighter gap) to maximize input area - display->drawString(offsetX + 2, offsetY, headerText.c_str()); - if (screenHeight <= 64) { - headerHeight = FONT_HEIGHT_SMALL - 2; // 11px - } else { - headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in - } - } - - const int boxX = offsetX; - const int boxWidth = screenWidth; - int boxY; - int boxHeight; - if (screenHeight <= 64) { - const int gapBelowHeader = 0; - const int fixedBoxHeight = inputLineH; - const int gapAboveKeyboard = 0; - boxY = offsetY + headerHeight + gapBelowHeader; - boxHeight = fixedBoxHeight; - if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { - int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; - boxHeight = std::max(1, fixedBoxHeight - over); - } - } else { - const int gapBelowHeader = 1; - int gapAboveKeyboard = 1; - int tmpBoxY = offsetY + headerHeight + gapBelowHeader; - const int minBoxHeight = inputLineH + 2; - int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; - if (availableH < minBoxHeight) - availableH = minBoxHeight; - boxY = tmpBoxY; - boxHeight = availableH; - } - - // Draw box border - display->drawRect(boxX, boxY, boxWidth, boxHeight); - - display->setFont(FONT_SMALL); - - // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis - const int textX = boxX + 2; - const int maxTextWidth = boxWidth - 4; - const int maxLines = (boxHeight - 2) / inputLineH; - if (maxLines >= 2) { - // Inner bounds for caret clamping - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Wrap text greedily into lines that fit maxTextWidth - std::vector lines; - { - std::string remaining = inputText; - while (!remaining.empty()) { - int bestLen = 0; - for (int len = 1; len <= (int)remaining.size(); ++len) { - int w = display->getStringWidth(remaining.substr(0, len).c_str()); - if (w <= maxTextWidth) - bestLen = len; - else - break; - } - if (bestLen == 0) { - // At least show one character to make progress - bestLen = 1; - } - lines.emplace_back(remaining.substr(0, bestLen)); - remaining.erase(0, bestLen); - } - } - - const bool scrolledUp = ((int)lines.size() > maxLines); - int caretX = textX; - int caretY = innerTop; - - // Leave a small top gap to render '...' without replacing the first line - const int topInset = 2; - const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height - int lineY = innerTop + topInset; - - if (scrolledUp) { - // Draw three small dots centered horizontally, vertically at the midpoint of the gap - // between the inner top and the first line's top baseline. This avoids using a tall glyph. - const int firstLineTop = lineY; // baseline top for the first visible line - const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested - const int centerX = boxX + boxWidth / 2; - const int dotSpacing = 3; // px between dots - const int dotSize = 1; // small square dot - display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); - display->fillRect(centerX, gapMidY, dotSize, dotSize); - display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); - } - - // How many lines fit with our top inset and tighter step - const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); - const int linesToShow = std::min((int)lines.size(), linesCapacity); - const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; - - for (int i = 0; i < linesToShow; ++i) { - const std::string &chunk = lines[startIndex + i]; - display->drawString(textX, lineY, chunk.c_str()); - caretX = textX + display->getStringWidth(chunk.c_str()); - caretY = lineY; - lineY += lineStep; - } - - // Draw caret at end of the last visible line - int caretPadY = 2; - if (boxHeight >= inputLineH + 4) - caretPadY = 3; - int cursorTop = caretY + caretPadY; - // Use lineStep so caret height matches the row spacing - int cursorH = lineStep - caretPadY * 2; - if (cursorH < 1) - cursorH = 1; - // Clamp vertical bounds to stay inside the inner rect - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - // Only draw if cursor is inside inner bounds - if (caretX >= innerLeft && caretX <= innerRight) { - display->drawVerticalLine(caretX, cursorTop, cursorH); - } - } else { - std::string displayText = inputText; - int textW = display->getStringWidth(displayText.c_str()); - std::string scrolled = displayText; - if (textW > maxTextWidth) { - // Trim from the left until it fits - while (textW > maxTextWidth && !scrolled.empty()) { - scrolled.erase(0, 1); - textW = display->getStringWidth(scrolled.c_str()); - } - // Add leading ellipsis and ensure it still fits - if (scrolled != displayText) { - scrolled = "..." + scrolled; - textW = display->getStringWidth(scrolled.c_str()); - // If adding ellipsis causes overflow, trim more after the ellipsis - while (textW > maxTextWidth && scrolled.size() > 3) { - scrolled.erase(3, 1); // remove chars after the ellipsis - textW = display->getStringWidth(scrolled.c_str()); - } - } - } else { - // Keep textW in sync with what we draw - textW = display->getStringWidth(scrolled.c_str()); - } - - int textY; - if (screenHeight <= 64) { - textY = boxY + (boxHeight - inputLineH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Center text vertically within inner box for single-line, then clamp so it never overlaps borders - int innerH = innerBottom - innerTop + 1; - textY = innerTop + std::max(0, (innerH - inputLineH) / 2); - // Clamp fully inside the inner rect - if (textY < innerTop) - textY = innerTop; - int maxTop = innerBottom - inputLineH + 1; - if (textY > maxTop) - textY = maxTop; - } - - if (!scrolled.empty()) { - display->drawString(textX, textY, scrolled.c_str()); - } - - int cursorX = textX + textW; - if (screenHeight > 64) { - const int innerRight = boxX + boxWidth - 2; - if (cursorX > innerRight) - cursorX = innerRight; - } - - int cursorTop, cursorH; - if (screenHeight <= 64) { - cursorH = 10; - cursorTop = boxY + (boxHeight - cursorH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - cursorTop = boxY + 2; - cursorH = boxHeight - 4; - if (cursorH < 1) - cursorH = 1; - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - - if (cursorX < innerLeft || cursorX > innerRight) - return; - } - - display->drawVerticalLine(cursorX, cursorTop, cursorH); - } -} - -void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, - uint8_t height, bool isLastCol) -{ - // Draw key content - display->setFont(FONT_SMALL); - const int fontH = FONT_HEIGHT_SMALL; - // Build label and metrics first - std::string keyText; - if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { - // Keep literal text labels for the action keys on the rightmost column - keyText = (key.type == VK_BACKSPACE) ? "BACK" - : (key.type == VK_ENTER) ? "ENTER" - : (key.type == VK_SPACE) ? "SPACE" - : (key.type == VK_ESC) ? "ESC" - : ""; - } else { - char c = getCharForKey(key, false); - if (c >= 'a' && c <= 'z') { - c = c - 'a' + 'A'; - } - keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); - } - - int textWidth = display->getStringWidth(keyText.c_str()); - // Label alignment - // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. - // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. - int textX; - if (isLastCol) { - const int rightPad = 1; - textX = x + width - textWidth - rightPad; - if (textX < x) - textX = x; // guard - } else { - if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { - textX = x + (width - textWidth + 1) / 2; - } else { - textX = x + (width - textWidth) / 2; - } - } - int contentTop = y; - int contentH = height; - if (selected) { - display->setColor(WHITE); - bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); - - if (display->getHeight() <= 64 && !isAction) { - display->fillRect(x, y, width, height); - } else if (isAction) { - const int padX = 1; - const int padY = 2; - int hlW = textWidth + padX * 2; - int hlX = textX - padX; - - if (hlX < x) { - hlW -= (x - hlX); - hlX = x; - } - int maxW = (x + width) - hlX; - if (hlW > maxW) - hlW = maxW; - if (hlW < 1) - hlW = 1; - - int hlH = std::min(fontH + padY * 2, (int)height); - int hlY = y + (height - hlH) / 2; - display->fillRect(hlX, hlY, hlW, hlH); - contentTop = hlY; - contentH = hlH; - } else { - display->fillRect(x, y, width, height); - } - display->setColor(BLACK); - } else { - display->setColor(WHITE); - } - - int centeredTextY; - if (display->getHeight() <= 64) { - centeredTextY = y + (height - fontH) / 2; - } else { - centeredTextY = contentTop + (contentH - fontH) / 2; - } - if (display->getHeight() > 64) { - if (centeredTextY < contentTop) - centeredTextY = contentTop; - if (centeredTextY + fontH > contentTop + contentH) - centeredTextY = std::max(contentTop, contentTop + contentH - fontH); - } - - if (display->getHeight() <= 64 && keyText.size() == 1) { - char ch = keyText[0]; - if (ch == '.' || ch == ',' || ch == ';') { - centeredTextY -= 1; - } - } - display->drawString(textX, centeredTextY, keyText.c_str()); -} - -char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) -{ - if (key.type != VK_CHAR) { - return key.character; - } - - char c = key.character; - - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); - } - - return c; -} - -void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) -{ - resetTimeout(); - // wrap around rows and cols in the 4x11 grid - int r = (int)cursorRow + dRow; - int c = (int)cursorCol + dCol; - if (r < 0) - r = KEYBOARD_ROWS - 1; - else if (r >= KEYBOARD_ROWS) - r = 0; - if (c < 0) - c = KEYBOARD_COLS - 1; - else if (c >= KEYBOARD_COLS) - c = 0; - cursorRow = (uint8_t)r; - cursorCol = (uint8_t)c; -} - -void VirtualKeyboard::moveCursorUp() -{ - moveCursorDelta(-1, 0); -} -void VirtualKeyboard::moveCursorDown() -{ - moveCursorDelta(1, 0); -} -void VirtualKeyboard::moveCursorLeft() -{ - resetTimeout(); - - if (cursorCol > 0) { - cursorCol--; - } else { - if (cursorRow > 0) { - cursorRow--; - cursorCol = KEYBOARD_COLS - 1; - } else { - cursorRow = KEYBOARD_ROWS - 1; - cursorCol = KEYBOARD_COLS - 1; - } - } -} -void VirtualKeyboard::moveCursorRight() -{ - resetTimeout(); - - if (cursorCol < KEYBOARD_COLS - 1) { - cursorCol++; - } else { - if (cursorRow < KEYBOARD_ROWS - 1) { - cursorRow++; - cursorCol = 0; - } else { - cursorRow = 0; - cursorCol = 0; - } - } -} - -void VirtualKeyboard::handlePress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert lowercase character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char - return; - } - - // Handle non-character keys immediately - switch (key.type) { - case VK_BACKSPACE: - deleteCharacter(); - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - std::function callback = onTextEntered; - onTextEntered = nullptr; - inputText = ""; - callback(""); - } - return; - default: - break; - } -} - -void VirtualKeyboard::handleLongPress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert uppercase/alternate character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char - return; - } - - switch (key.type) { - case VK_BACKSPACE: - // One-shot: delete up to 5 characters on long press - for (int i = 0; i < 5; ++i) { - if (inputText.empty()) - break; - deleteCharacter(); - } - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - onTextEntered(""); - } - break; - default: - break; - } -} - -void VirtualKeyboard::insertCharacter(char c) -{ - if (inputText.length() < 160) { // Reasonable text length limit - inputText += c; - } -} - -void VirtualKeyboard::deleteCharacter() -{ - if (!inputText.empty()) { - inputText.pop_back(); - } -} - -void VirtualKeyboard::submitText() -{ - LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); - - // Only submit if text is not empty - if (!inputText.empty() && onTextEntered) { - // Store callback and text to submit before clearing callback - std::function callback = onTextEntered; - std::string textToSubmit = inputText; - onTextEntered = nullptr; - // Don't clear inputText here - let the calling module handle cleanup - // inputText = ""; // Removed: keep text visible until module cleans up - callback(textToSubmit); - } else if (inputText.empty()) { - // For empty text, just ignore the submission - don't clear callback - // This keeps the virtual keyboard responsive for further input - LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); - } else { - // No callback available - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } -} - -void VirtualKeyboard::setInputText(const std::string &text) -{ - inputText = text; -} - -std::string VirtualKeyboard::getInputText() const -{ - return inputText; -} - -void VirtualKeyboard::setHeader(const std::string &header) -{ - headerText = header; -} - -void VirtualKeyboard::setCallback(std::function callback) -{ - onTextEntered = callback; -} - -void VirtualKeyboard::resetTimeout() -{ - lastActivityTime = millis(); -} - -bool VirtualKeyboard::isTimedOut() const -{ - return (millis() - lastActivityTime) > TIMEOUT_MS; -} - -} // namespace graphics diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h deleted file mode 100644 index 169163b57..000000000 --- a/src/graphics/VirtualKeyboard.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "configuration.h" -#include -#include -#include - -namespace graphics -{ - -enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; - -struct VirtualKey { - char character; - VirtualKeyType type; - uint8_t x; - uint8_t y; - uint8_t width; - uint8_t height; -}; - -class VirtualKeyboard -{ - public: - VirtualKeyboard(); - ~VirtualKeyboard(); - - void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); - void setInputText(const std::string &text); - std::string getInputText() const; - void setHeader(const std::string &header); - void setCallback(std::function callback); - - // Navigation methods for encoder input - void moveCursorUp(); - void moveCursorDown(); - void moveCursorLeft(); - void moveCursorRight(); - void handlePress(); - void handleLongPress(); - - // Timeout management - void resetTimeout(); - bool isTimedOut() const; - - private: - static const uint8_t KEYBOARD_ROWS = 4; - static const uint8_t KEYBOARD_COLS = 11; - static const uint8_t KEY_WIDTH = 9; - static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays - static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom - - VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; - - std::string inputText; - std::string headerText; - std::function onTextEntered; - - uint8_t cursorRow; - uint8_t cursorCol; - - // Timeout management for auto-exit - uint32_t lastActivityTime; - static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout - - void initializeKeyboard(); - void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, - bool isLastCol); - void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); - - // Unified cursor movement helper - void moveCursorDelta(int dRow, int dCol); - - char getCharForKey(const VirtualKey &key, bool isLongPress = false); - void insertCharacter(char c); - void deleteCharacter(); - void submitText(); -}; - -} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e02948864..512f650ec 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -10,10 +10,7 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" -#include "input/RotaryEncoderInterruptImpl1.h" -#include "input/UpDownInterruptImpl1.h" #include "main.h" -#include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 221d95075..3d635e588 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -38,8 +38,6 @@ bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; -VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; -std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { @@ -91,33 +89,14 @@ void NotificationRenderer::resetBanner() void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - // Handle text_input notifications first - they have their own timeout/banner logic - if (current_notification_type == notificationTypeEnum::text_input) { - // Check for timeout and reset if needed for text input - if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); - return; - } - drawTextInput(display, state); - return; - } - - if (millis() > alertBannerUntil && alertBannerUntil > 0) { + if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') resetBanner(); - } - - // Exit if no banner is showing or banner is paused - if (!isOverlayBannerShowing() || pauseBanner) { + if (!isOverlayBannerShowing() || pauseBanner) return; - } - switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; - case notificationTypeEnum::text_input: - // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. - break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -596,90 +575,6 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } -void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - if (virtualKeyboard) { - // Check for timeout and auto-exit if needed - if (virtualKeyboard->isTimedOut()) { - LOG_INFO("Virtual keyboard timeout - auto-exiting"); - // Cancel virtual keyboard - call callback with empty string to indicate timeout - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Handle input events for virtual keyboard navigation - if (inEvent.inputEvent != INPUT_BROKER_NONE) { - if (inEvent.inputEvent == INPUT_BROKER_UP) { - virtualKeyboard->moveCursorUp(); - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { - virtualKeyboard->moveCursorDown(); - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { - // Long press UP = move left - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { - // Long press DOWN = move right - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - virtualKeyboard->handlePress(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { - virtualKeyboard->handleLongPress(); - } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { - // Cancel virtual keyboard - call callback with empty string - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Reset input event after processing - inEvent.inputEvent = INPUT_BROKER_NONE; - } - - // Clear the display and draw virtual keyboard - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - virtualKeyboard->draw(display, 0, 0); - } else { - // If virtualKeyboard is null, reset the banner to avoid getting stuck - resetBanner(); - } -} - bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index edb069513..9c30b329c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -3,9 +3,6 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" -#include "graphics/VirtualKeyboard.h" -#include -#include #define MAX_LINES 5 namespace graphics @@ -25,8 +22,6 @@ class NotificationRenderer static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; - static VirtualKeyboard *virtualKeyboard; - static std::function textInputCallback; static bool pauseBanner; @@ -35,7 +30,6 @@ class NotificationRenderer static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 012a403f5..4487fa662 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,7 +4,6 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, - INPUT_BROKER_SELECT_LONG, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index 4c8ce6409..d41ad2fd6 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,14 +1,12 @@ #include "TrackballInterruptBase.h" #include "configuration.h" -extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, - input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) + input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -20,7 +18,6 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); @@ -43,9 +40,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } - LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, - this->_pinLeft, this->_pinRight, pinPress); - osk_found = true; + LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, + pinPress); + this->setInterval(100); } @@ -53,47 +50,10 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = INPUT_BROKER_NONE; - - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = millis() - pressStartTime; - bool buttonStillPressed = false; - -#if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); -#else - buttonStillPressed = !digitalRead(_pinPress); -#endif - - if (!buttonStillPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION) { - // Short press - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastLongPressEventTime = 0; - this->action = TB_ACTION_NONE; - } else if (pressDuration >= LONG_PRESS_DURATION) { - // Long press detected - uint32_t currentTime = millis(); - // Only trigger long press event if enough time has passed since the last one - if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventPressedLong; - lastLongPressEventTime = currentTime; - } - this->action = TB_ACTION_PRESSED_LONG; - } - } - #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press + if (this->action == TB_ACTION_PRESSED) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -108,11 +68,9 @@ int32_t TrackballInterruptBase::runOnce() e.inputEvent = this->_eventRight; } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -133,16 +91,10 @@ int32_t TrackballInterruptBase::runOnce() e.kbchar = 0x00; this->notifyObservers(&e); } + lastEvent = action; + this->action = TB_ACTION_NONE; - // Only update lastEvent for non-press actions or completed press actions - if (this->action != TB_ACTION_PRESSED || !pressDetected) { - lastEvent = action; - if (!pressDetected) { - this->action = TB_ACTION_NONE; - } - } - - return 50; // Check more frequently for better long press detection + return 100; } void TrackballInterruptBase::intPressHandler() diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 38be22f20..92db8720e 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable, public con explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); + input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -33,7 +33,6 @@ class TrackballInterruptBase : public Observable, public con enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, - TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, @@ -47,20 +46,12 @@ class TrackballInterruptBase : public Observable, public con volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - // Long press detection for press button - uint32_t pressStartTime = 0; - bool pressDetected = false; - uint32_t lastLongPressEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = 500; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events - private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 594facdeb..896238f38 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -13,12 +13,11 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, - TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, - TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); + eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, + TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, + TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } diff --git a/src/main.cpp b/src/main.cpp index 23ffa6b6d..ef5f5a721 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -191,8 +191,6 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; uint8_t kb_model; // global bool to record that a kb is present bool kb_found = false; -// global bool to record that on-screen keyboard (OSK) is present -bool osk_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -1414,10 +1412,6 @@ void setup() #endif #endif -#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) - osk_found = true; -#endif - #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/main.h b/src/main.h index 2ddd4862f..3568daad2 100644 --- a/src/main.h +++ b/src/main.h @@ -32,7 +32,6 @@ extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; -extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 76b950322..d40dcd24f 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,16 +13,12 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" -#include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control -#if HAS_TRACKBALL -#include "input/TrackballInterruptImpl1.h" -#endif #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -42,7 +38,6 @@ extern ScanI2C::DeviceAddress cardkb_found; extern bool graphics::isMuted; -extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; @@ -156,13 +151,10 @@ int CannedMessageModule::splitConfiguredMessages() int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; + #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a touch screen virtual keyboard + // Add a "Free Text" entry at the top if using a keyboard tempMessages[tempCount++] = "[-- Free Text --]"; -#else - if (osk_found && screen) { - tempMessages[tempCount++] = "[-- Free Text --]"; - } #endif // First message always starts at buffer start @@ -349,8 +341,6 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state - // Virtual keyboard mode: Show virtual keyboard and handle input - // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; @@ -637,56 +627,6 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo notifyObservers(&e); return true; } -#else - if (strcmp(current, "[-- Free Text --]") == 0) { - if (osk_found && screen) { - char headerBuffer[64]; - if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); - } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); - } - screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { - if (!text.empty()) { - this->freetext = text.c_str(); - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; - currentMessageIndex = -1; - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - setIntervalFromNow(500); - return; - } else { - // Don't delete virtual keyboard immediately - it might still be executing - // Instead, just clear the callback and reset banner to stop input processing - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - - // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Force display update to show normal screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - // Schedule cleanup for next loop iteration to ensure safe deletion - setIntervalFromNow(50); - return; - } - }); - - return true; - } - } #endif // Normal canned message selection @@ -1003,54 +943,12 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { - // Clean up virtual keyboard if needed when going inactive - if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { - LOG_INFO("Performing delayed virtual keyboard cleanup"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - } - temporaryMessage = ""; return INT32_MAX; } - // Handle delayed virtual keyboard message sending - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - // Virtual keyboard message sending case - text was not empty - if (this->freetext.length() > 0) { - LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); - sendText(this->dest, this->channel, this->freetext.c_str(), true); - - // Clean up virtual keyboard after sending - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard after message send"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - } - - // Clear payload to indicate virtual keyboard processing is complete - // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds - this->payload = 0; - } else { - // Empty message, just go inactive - LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - return 2000; - } - UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && - this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -1060,18 +958,6 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); - } - // Handle SENDING_ACTIVE state transition after virtual keyboard message - else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - // This happens after virtual keyboard message sending is complete - LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -1080,23 +966,9 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - // Clean up virtual keyboard if it exists during timeout - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - } - this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == 0) { - // [Exit] button pressed - return to inactive state - LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;