From 3ce6b065e46762899ccdfa698fb2c42a37c10a4b Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Sun, 11 Oct 2026 15:07:29 -0400 Subject: [PATCH 1/6] Add Elecrow CrowPanel Advance 5.0 build target New env crowpanel_50_companion_radio_touch for the 800x480 ST7262 RGB panel, GT911 touch and an SX1262 in the wireless slot. Board quirks follow Camillia-MT: 14 MHz pixel clock, internal-RAM draw band, PCA9557 (V1.0) / MCU (V1.1+) backlight controller detection with GT911 reset at 0x5D or 0x14, NSS on GPIO8 (V1.2+), no SD/GPS/battery. Large-screen layouts and UI-size fonts; browser file transfer to the internal tiles partition. Added to the upload helper, release script, flasher manifests, site, DEVICES.md, README and NOTICE. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DEVICES.md | 30 ++ NOTICE | 7 + README.md | 2 + boards/crowpanel_50.json | 45 +++ deploy/flasher/index.html | 7 + deploy/site/docs.html | 4 +- deploy/site/index.html | 6 + platformio.ini | 151 ++++++++++ scripts/build-upload-monitor.sh | 5 + scripts/build/gen-flasher-meta.py | 1 + scripts/build/gen-touch-fonts.sh | 2 +- scripts/release.sh | 2 +- src/helpers/esp32/TouchPrefsStore.cpp | 2 + src/helpers/esp32/WebFileTransferConfig.h | 8 +- src/helpers/input/LgfxGt911Touch.cpp | 19 +- src/main.cpp | 2 +- src/ui-touch/UITask.cpp | 50 ++-- src/ui-touch/device_caps.h | 13 + src/ui-touch/ui_font_18.c | 4 +- src/ui-touch/ui_font_20.c | 4 +- src/ui-touch/ui_font_24.c | 4 +- variants/crowpanel_50/CrowPanel50Board.h | 18 ++ variants/crowpanel_50/CrowPanel50Display.cpp | 294 +++++++++++++++++++ variants/crowpanel_50/CrowPanel50Display.h | 82 ++++++ variants/crowpanel_50/target.cpp | 35 +++ variants/crowpanel_50/target.h | 23 ++ 26 files changed, 787 insertions(+), 33 deletions(-) create mode 100644 boards/crowpanel_50.json create mode 100644 variants/crowpanel_50/CrowPanel50Board.h create mode 100644 variants/crowpanel_50/CrowPanel50Display.cpp create mode 100644 variants/crowpanel_50/CrowPanel50Display.h create mode 100644 variants/crowpanel_50/target.cpp create mode 100644 variants/crowpanel_50/target.h diff --git a/DEVICES.md b/DEVICES.md index 30d5b2c..e51b90d 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -15,6 +15,7 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases | Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one | | Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation | | Elecrow CrowPanel Advance 3.5 | ESP32-S3, external SX1262 | 3.5" ILI9488 480x320 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | Requires an SX1262 module in the LoRa expansion slot; not for other CrowPanel models | +| Elecrow CrowPanel Advance 5.0 | ESP32-S3, external SX1262 | 5" ST7262 RGB 800x480 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | V1.1+ boards; SX1262 in the wireless slot with the function DIP switch on the wireless module; no SD, GPS or battery gauge | | RAK WisMesh Tap V2 (RAK3312) | ESP32-S3, SX1262 | Touch display (LovyanGFX, 30+ fps) | Web flasher | Beta (new in beta_38) | Early community port by Ethac.chen (#136); core mesh, chat and map working | | LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs | | Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported | @@ -124,6 +125,35 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases CrowPanel. Like Wio Tracker L2, it is included in the all-target builds and local beta release scripts, not the two-board stable CI workflow. Select it in the upload helper with `scripts/build-upload-monitor.sh --crowpanel-35`. +- **CrowPanel Advance 5.0**: `crowpanel_50_companion_radio_touch` is a + 16 MB flash / 8 MB OPI PSRAM build for the 800x480 ST7262 RGB panel, the + GT911 touch controller and an SX1262 in the wireless-module slot (J8/J9). The + board quirks follow Camillia-MT's port: + - The panel is driven over the 16-bit RGB bus from a PSRAM framebuffer at a + 14 MHz pixel clock (Elecrow's demo uses 21 MHz); the slower clock leaves + PSRAM bandwidth for WiFi and LVGL so the picture does not drift or tear. + LVGL's draw band stays in internal RAM for the same reason. + - Backlight and touch reset sit behind an I2C controller on GPIO15/16 that + changed between revisions: a PCA9557 at 0x18 on V1.0, a board MCU at 0x30 + from V1.1. The driver detects which one answers, resets the GT911 through + it and accepts the touch controller at either 0x5D or 0x14. Below full + brightness the MCU dims by PWM, which can whine; 100% is steady. + - The function DIP switch on the back routes GPIO 2/4/5/6/19/20 to the + wireless slot, the microSD slot or the speaker/microphone. Set it to the + wireless module (S1=0, S0=1), or the radio is not found (the serial log + says so). The microSD slot is therefore unusable; browser Transfer uses the + internal tiles partition instead, as on the Attaky. + - The radio's chip select is GPIO8 on V1.2/V1.3 boards (current production) + and GPIO0 on V1.0. The build targets GPIO8; a V1.0 board is detected at boot + and logged, and needs `P_LORA_NSS=0`. + - No GPS (no free GPIO pair for a UART; 17/18 are panel data), no battery + voltage sensing and no free user button, so hard lock is disabled and the + screen wakes by touch. + It starts in native 800x480 landscape; Display settings can switch to + 480x800 portrait. It uses the large-screen layouts and UI-size presets of the + Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Display, touch, + backlight and radio still need validation on a physical unit. Select it in + the upload helper with `scripts/build-upload-monitor.sh --crowpanel-50`. ## Requested boards diff --git a/NOTICE b/NOTICE index a62b524..5713fb3 100644 --- a/NOTICE +++ b/NOTICE @@ -97,5 +97,12 @@ Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend) Adapted from src/sd_soft_spi.cpp; integrated with WadaMesh's filesystem API and concurrent storage consumers. +Camillia-MT (CrowPanel Advance 5.0 display, backlight and touch bring-up) + Copyright (c) 2025-2026 Michael A. Cojocari and contributors + GNU General Public License v3.0 or later + The RGB panel configuration, revision detection, backlight controller and + GT911 reset in variants/crowpanel_50/CrowPanel50Display.cpp are adapted + from src/hal/crowpanel_50_display.h and src/hal/hw_crowpanel_50.h. + This list is not exhaustive and is expanded as dependencies are vendored or added. diff --git a/README.md b/README.md index b67f84d..e96baaa 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ per-board status. - LilyGo T-Display P4: built from `tdisplay_p4/` (ESP-IDF); AMOLED + SX1262 by default, with `WADA_P4_LCD=1` for TFT-LCD and `WADA_P4_LR2021=1` for LR2021 (beta) - Attaky Mesh Series: env `attaky_mesh_series_companion_radio_touch` (beta) - Elecrow CrowPanel Advance 3.5: env `crowpanel_35_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the expansion slot) +- Elecrow CrowPanel Advance 5.0: env `crowpanel_50_companion_radio_touch` (beta, hardware validation pending; requires an SX1262 in the wireless slot and the DIP switch set to it) ## Architecture @@ -100,6 +101,7 @@ automatically: pio run -e heltec_v4_tft_companion_radio_usb_tcp_touch # Heltec V4 TFT pio run -e LilyGo_TDeck_companion_radio_touch # LilyGo T-Deck pio run -e crowpanel_35_companion_radio_touch # CrowPanel Advance 3.5 +pio run -e crowpanel_50_companion_radio_touch # CrowPanel Advance 5.0 # or just `pio run` to build the configured default environments ``` diff --git a/boards/crowpanel_50.json b/boards/crowpanel_50.json new file mode 100644 index 0000000..fcec29b --- /dev/null +++ b/boards/crowpanel_50.json @@ -0,0 +1,45 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=0", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "Elecrow CrowPanel Advance 5.0 (N16R8)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": false, + "wait_for_upload_port": false, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.elecrow.com/", + "vendor": "Elecrow" +} diff --git a/deploy/flasher/index.html b/deploy/flasher/index.html index c38dbbf..87abd6c 100644 --- a/deploy/flasher/index.html +++ b/deploy/flasher/index.html @@ -111,6 +111,13 @@ Use Chrome or Edge on desktop (Web Serial). +
+

Elecrow CrowPanel Advance 5.0 Beta

+

5" 800×480 touch display and an SX1262 module in the wireless slot.

+

For the Advance 5.0 (V1.1 or later) only. Hardware validation is pending; set the function DIP switch to the wireless module and attach the SX1262.

+ + Use Chrome or Edge on desktop (Web Serial). +
How it works: plug the device in over USB, click Connect, pick the serial port, diff --git a/deploy/site/docs.html b/deploy/site/docs.html index 0eadfae..787f082 100644 --- a/deploy/site/docs.html +++ b/deploy/site/docs.html @@ -349,7 +349,7 @@
  • Wrong tries slow down: every fifth miss makes you wait, 30 seconds at first and doubling up to 4 minutes, and the count survives a reboot.
  • Change PIN asks for the current one first, and so does turning PIN lock off.
  • -

    The Heltec V4, Heltec V4-R8, Attaky and CrowPanel have no lock screen, so no PIN either.

    +

    The Heltec V4, Heltec V4-R8, Attaky and CrowPanels have no lock screen, so no PIN either.

    The lock screen
    The lock screen
    @@ -493,7 +493,7 @@
  • Aurora: one still sky, drawn once.
  • Moving aurora: the same sky drifting in a slow, seamless loop. It rests while you use the device and while the screen is off, locked or showing the screensaver.
  • Colour: one plain colour, your accent unless you pick another, kept dark enough by night and light enough by day for text to read.
  • -
  • Picture: a JPEG from the file viewer (Set as background), cut to fill the screen and dimmed by night. Not on the Heltec V4 and V4-R8, the CrowPanel or the Attaky.
  • +
  • Picture: a JPEG from the file viewer (Set as background), cut to fill the screen and dimmed by night. Not on the Heltec V4 and V4-R8, the CrowPanels or the Attaky.
  • None: the plain theme, black by night and light grey by day.
  • The background waits for Apply and restart like the colours. High contrast has no background, and the e-paper T-Deck Pro and Max show none.

    diff --git a/deploy/site/index.html b/deploy/site/index.html index 0177957..124cef9 100644 --- a/deploy/site/index.html +++ b/deploy/site/index.html @@ -599,6 +599,7 @@ LilyGoT-Deck MaxE-paper keyboard and touch handheld.ExperimentalBeta HeltecV4-R8V4 with 8 MB PSRAM and an SD card.ExperimentalBeta ElecrowCrowPanel Advance 3.53.5-inch touch panel with an SX1262 module.ExperimentalBeta + ElecrowCrowPanel Advance 5.05-inch 800×480 touch panel with an SX1262 module.ExperimentalBeta @@ -757,6 +758,11 @@ Download .bin

    Beta only. Attach the SX1262 before booting; flash with care and report what works.

    +

    CrowPanel Advance 5.0 (experimental)

    5" 800×480 touch display; requires an SX1262 module in the wireless slot and the function DIP switch set to the wireless module.

    + Partially supportedHardware validation pendingThis build is for the Advance 5.0 (V1.1 and later) with an SX1262 in the wireless slot, not other CrowPanel models. Display, touch, backlight and radio follow the hardware reference; behaviour on a physical unit has not been verified. The DIP switch hands the radio's pins to the microSD slot otherwise, so there is no SD card; there is also no GPS and no battery monitoring. Landscape is the default, with portrait available in Display settings. + + Download .bin +

    Beta only. Set the DIP switch and attach the SX1262 before booting; flash with care and report what works.

    diff --git a/platformio.ini b/platformio.ini index 4d505a0..47eed4b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1464,6 +1464,157 @@ lib_deps = densaugeo/base64 @ ~1.4.0 knolleary/PubSubClient @ ^2.8.0 +; Elecrow CrowPanel Advance 5.0 — ESP32-S3, 800x480 ST7262 RGB panel, GT911 and +; SX1262 in the wireless slot. Board quirks follow Camillia-MT; see DEVICES.md. +; ====================================================================== +[env:crowpanel_50_companion_radio_touch] +platform = platformio/espressif32@6.11.0 +framework = arduino +board = crowpanel_50 +monitor_speed = 115200 +upload_speed = 921600 +board_build.arduino.memory_type = qio_opi +board_build.flash_size = 16MB +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.partitions = partitions_tft_touch.csv +extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_lvgl_anim_uaf.py, pre:scripts/build/patch_lvgl_arabic.py, pre:scripts/build/patch_lgfx_dmadesc.py, pre:scripts/build/perf_lvgl.py, merge-bin.py +custom_lvgl_o2 = yes ; LVGL at -O2 (scripts/build/perf_lvgl.py), like the T-Deck +custom_lvgl_iram = lv_draw_sw_blend.c, lv_draw_label.c ; their hot loops in IRAM + +build_unflags = + -std=gnu++11 + +build_flags = + -I"${platformio.libdeps_dir}/${this.__env__}/MicroNMEA/src" + -I"${platformio.libdeps_dir}/${this.__env__}/LovyanGFX/src" + -I variants/crowpanel_50 + -I include + -I src + -I src/ui-touch + -Wno-deprecated-declarations + -Wno-unused-parameter + -std=gnu++17 + -DNDEBUG + -DRADIOLIB_STATIC_ONLY=1 + -DRADIOLIB_GODMODE=1 + -D MC_VENDORED_TOUCH_APP + -D ESP32_PLATFORM + -D HAS_CROWPANEL_50=1 + -D BOARD_HAS_PSRAM=1 + -D ESP32_CPU_FREQ=240 + -D CORE_DEBUG_LEVEL=0 + -D LORA_FREQ=869.618 + -D LORA_BW=62.5 + -D LORA_SF=8 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + ; SX1262 in the J8/J9 wireless slot. The function DIP switch on the back must + ; select the wireless module (S1=0, S0=1); its other positions hand these pins + ; to the SD card or the speaker/microphone. CS is GPIO8 on V1.2/V1.3 boards + ; (current production); V1.0 boards wire it to GPIO0 and need P_LORA_NSS=0. + -D P_LORA_NSS=8 + -D P_LORA_RESET=19 + -D P_LORA_DIO_1=20 + -D P_LORA_BUSY=2 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=4 + -D P_LORA_MOSI=6 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=3.3 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D RADIOLIB_EXCLUDE_CC1101=1 + -D RADIOLIB_EXCLUDE_RF69=1 + -D RADIOLIB_EXCLUDE_SX1231=1 + -D RADIOLIB_EXCLUDE_SI443X=1 + -D RADIOLIB_EXCLUDE_RFM2X=1 + -D RADIOLIB_EXCLUDE_SX128X=1 + -D RADIOLIB_EXCLUDE_AFSK=1 + -D RADIOLIB_EXCLUDE_AX25=1 + -D RADIOLIB_EXCLUDE_HELLSCHREIBER=1 + -D RADIOLIB_EXCLUDE_MORSE=1 + -D RADIOLIB_EXCLUDE_APRS=1 + -D RADIOLIB_EXCLUDE_BELL=1 + -D RADIOLIB_EXCLUDE_RTTY=1 + -D RADIOLIB_EXCLUDE_SSTV=1 + -D ENV_INCLUDE_GPS=0 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_SHTC3=0 + -D ENV_INCLUDE_SHT4X=0 + -D ENV_INCLUDE_LPS22HB=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_INA226=0 + -D ENV_INCLUDE_INA260=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_BME680=0 + -D ENV_INCLUDE_BMP085=0 + -D PIN_BOARD_SDA=15 + -D PIN_BOARD_SCL=16 + ; No GPS, SD card, battery gauge or spare button: the 16-bit RGB panel takes + ; most GPIOs and the DIP switch gives the rest to the radio. + -D DISPLAY_CLASS=CrowPanel50Display + -D DISPLAY_ROTATION=3 + -D DISPLAY_SCALE_X=1.0 + -D DISPLAY_SCALE_Y=1.0 + -D UI_LVGL=1 + -D HAS_TOUCH_UI=1 + -D FIRMWARE_OTA_ENV='"crowpanel_50_companion_radio_touch"' + -D ENABLE_ADVERT_ON_BOOT=0 + -D LV_CONF_PATH=lv_conf.h + -D LV_CONF_INCLUDE_SIMPLE=1 + -D LV_MEMCPY_MEMSET_STD=1 ; newlib memcpy/memset in LVGL (see include/lv_conf.h) + -D MAX_CONTACTS=2000 + -D MAX_GROUP_CHANNELS=40 + -D ADMIN_PASSWORD='"password"' + -D MULTI_TRANSPORT_COMPANION=1 + -D WADAMESH_FORK_BUILD=1 + -D TCP_PORT=5000 + -D WS_PORT=8765 + -D BLE_PIN_CODE=123456 + -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=1 + -D CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 + -D CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 + +build_src_filter = + +<*.cpp> + +<*.c> + + + + + + + - + + + + + + + +<../variants/crowpanel_50/*.cpp> + +lib_deps = + https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4 + SPI + Wire + SD + jgromes/RadioLib @ ^7.6.0 + rweather/Crypto @ ^0.4.0 + adafruit/RTClib @ ^2.1.3 + melopero/Melopero RV3028 @ ^1.1.0 + electroniccats/CayenneLPP @ 1.6.1 + h2zero/NimBLE-Arduino @ 1.4.3 + stevemarple/MicroNMEA @ ^2.0.6 + ESP32Async/ESPAsyncWebServer @ 3.10.3 + file://arch/esp32/AsyncElegantOTA + lvgl/lvgl @ ^8.3.11 + lovyan03/LovyanGFX @ 1.2.27 + densaugeo/base64 @ ~1.4.0 + knolleary/PubSubClient @ ^2.8.0 + [env:tlora_pager_lr1121_companion_radio_touch] platform = platformio/espressif32@6.11.0 framework = arduino diff --git a/scripts/build-upload-monitor.sh b/scripts/build-upload-monitor.sh index 69a01c6..e753fc9 100755 --- a/scripts/build-upload-monitor.sh +++ b/scripts/build-upload-monitor.sh @@ -15,6 +15,7 @@ HELTEC_R8_ENV="heltec_v4_r8_tft_companion_radio_usb_tcp_touch" ATTAKY_ENV="attaky_mesh_series_companion_radio_touch" WIO_ENV="wio_tracker_l2_companion_radio_touch" CROWPANEL_ENV="crowpanel_35_companion_radio_touch" +CROWPANEL_50_ENV="crowpanel_50_companion_radio_touch" PAGER_LR1121_ENV="tlora_pager_lr1121_companion_radio_touch" PAGER_SX1262_ENV="tlora_pager_sx1262_companion_radio_touch" M9_ENV="ThinkNode_M9_companion_radio_touch" @@ -94,6 +95,7 @@ env_label() { "$ATTAKY_ENV") echo "Attaky Mesh Series" ;; "$WIO_ENV") echo "Seeed Wio Tracker L2" ;; "$CROWPANEL_ENV") echo "Elecrow CrowPanel Advance 3.5" ;; + "$CROWPANEL_50_ENV") echo "Elecrow CrowPanel Advance 5.0" ;; "$PAGER_LR1121_ENV") echo "LilyGo T-LoRa Pager LR1121" ;; "$PAGER_SX1262_ENV") echo "LilyGo T-LoRa Pager SX1262" ;; "$M9_ENV") echo "ThinkNode M9" ;; @@ -120,6 +122,7 @@ Devices: --attaky Attaky Mesh Series --wio Seeed Wio Tracker L2 --crowpanel-35 Elecrow CrowPanel Advance 3.5 + --crowpanel-50 Elecrow CrowPanel Advance 5.0 --pager-lr1121 LilyGo T-LoRa Pager LR1121 --pager-sx1262 LilyGo T-LoRa Pager SX1262 --m9 ThinkNode M9 @@ -171,6 +174,7 @@ prompt_for_device() { "$ATTAKY_ENV" "$WIO_ENV" "$CROWPANEL_ENV" + "$CROWPANEL_50_ENV" "$PAGER_LR1121_ENV" "$PAGER_SX1262_ENV" "$RAK_ENV" @@ -250,6 +254,7 @@ while [ $# -gt 0 ]; do --attaky) select_env "$ATTAKY_ENV" ;; --wio) select_env "$WIO_ENV" ;; --crowpanel-35) select_env "$CROWPANEL_ENV" ;; + --crowpanel-50) select_env "$CROWPANEL_50_ENV" ;; --pager-lr1121) select_env "$PAGER_LR1121_ENV" ;; --pager-sx1262) select_env "$PAGER_SX1262_ENV" ;; --m9) select_env "$M9_ENV" ;; diff --git a/scripts/build/gen-flasher-meta.py b/scripts/build/gen-flasher-meta.py index 51b9bbf..f7c14aa 100644 --- a/scripts/build/gen-flasher-meta.py +++ b/scripts/build/gen-flasher-meta.py @@ -45,6 +45,7 @@ BOARDS = { "manifest-attaky.json": ("wadamesh - Attaky Core (experimental)", "wadamesh-attaky-merged.bin", "ESP32-S3"), "manifest-wio-tracker-l2.json": ("wadamesh - Seeed Wio Tracker L2 (experimental)", "wadamesh-wio-tracker-l2-merged.bin", "ESP32-S3"), "manifest-crowpanel-35.json": ("wadamesh - Elecrow CrowPanel Advance 3.5 (experimental)", "wadamesh-crowpanel-35-merged.bin", "ESP32-S3"), + "manifest-crowpanel-50.json": ("wadamesh - Elecrow CrowPanel Advance 5.0 (experimental)", "wadamesh-crowpanel-50-merged.bin", "ESP32-S3"), "manifest-tdeck-pro-v1-0.json": ("wadamesh - LilyGo T-Deck Pro V1.0 (experimental)", "wadamesh-tdeck-pro-v1-0-merged.bin", "ESP32-S3"), "manifest-tdeck-pro-v1-1.json": ("wadamesh - LilyGo T-Deck Pro V1.1 (experimental)", "wadamesh-tdeck-pro-v1-1-merged.bin", "ESP32-S3"), "manifest-tdeck-max.json": ("wadamesh - LilyGo T-Deck Max (experimental)", "wadamesh-tdeck-max-merged.bin", "ESP32-S3"), diff --git a/scripts/build/gen-touch-fonts.sh b/scripts/build/gen-touch-fonts.sh index b05a3bd..0504828 100755 --- a/scripts/build/gen-touch-fonts.sh +++ b/scripts/build/gen-touch-fonts.sh @@ -414,7 +414,7 @@ generate_fallback_icons() { # some translation units and not others. 18/20/24 are the bigger UI-size presets # (every board with a text-size setting; the V4 has none, its flash is full). # DOC_CAPTURE builds carry them all: they render other boards' sizes and scales for review. -ui_big="defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE)" +ui_big="defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE)" for size in 12 14 16 28; do generate_ui "$size" done diff --git a/scripts/release.sh b/scripts/release.sh index 7341066..4ec4bf1 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -29,7 +29,7 @@ DEST="${WADAMESH_VPS:-}"; DEST_PATH="${WADAMESH_VPS_PATH:-/srv/wadamesh/firmware # env:binname pairs — plain string form (works on macOS's bash 3.2; no associative arrays). # All S3/PIO boards. The T-Display P4 is an ESP32-P4 IDF build (tdisplay_p4/) handled OUT of band # — see the release skill / the P4 build+merge step — because this loop is PlatformIO-only. -ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck ThinkNode_M9_companion_radio_touch:wadamesh-thinknode-m9 rak_tap_v2_companion_radio_touch:wadamesh-rak-tap-v2 heltec_v4_r8_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-r8-tft tlora_pager_lr1121_companion_radio_touch:wadamesh-tlora-pager-lr1121 tlora_pager_sx1262_companion_radio_touch:wadamesh-tlora-pager-sx1262 attaky_mesh_series_companion_radio_touch:wadamesh-attaky wio_tracker_l2_companion_radio_touch:wadamesh-wio-tracker-l2 crowpanel_35_companion_radio_touch:wadamesh-crowpanel-35 LilyGo_TDeck_Pro_v1_0_companion_radio_touch:wadamesh-tdeck-pro-v1-0 LilyGo_TDeck_Pro_v1_1_companion_radio_touch:wadamesh-tdeck-pro-v1-1 LilyGo_TDeck_Max_companion_radio_touch:wadamesh-tdeck-max" +ENVS="heltec_v4_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-tft LilyGo_TDeck_companion_radio_touch:wadamesh-tdeck ThinkNode_M9_companion_radio_touch:wadamesh-thinknode-m9 rak_tap_v2_companion_radio_touch:wadamesh-rak-tap-v2 heltec_v4_r8_tft_companion_radio_usb_tcp_touch:wadamesh-heltec-v4-r8-tft tlora_pager_lr1121_companion_radio_touch:wadamesh-tlora-pager-lr1121 tlora_pager_sx1262_companion_radio_touch:wadamesh-tlora-pager-sx1262 attaky_mesh_series_companion_radio_touch:wadamesh-attaky wio_tracker_l2_companion_radio_touch:wadamesh-wio-tracker-l2 crowpanel_35_companion_radio_touch:wadamesh-crowpanel-35 crowpanel_50_companion_radio_touch:wadamesh-crowpanel-50 LilyGo_TDeck_Pro_v1_0_companion_radio_touch:wadamesh-tdeck-pro-v1-0 LilyGo_TDeck_Pro_v1_1_companion_radio_touch:wadamesh-tdeck-pro-v1-1 LilyGo_TDeck_Max_companion_radio_touch:wadamesh-tdeck-max" # Per-channel destination paths. if [ "$MODE" = "stable" ]; then diff --git a/src/helpers/esp32/TouchPrefsStore.cpp b/src/helpers/esp32/TouchPrefsStore.cpp index f17cf69..3980b7e 100644 --- a/src/helpers/esp32/TouchPrefsStore.cpp +++ b/src/helpers/esp32/TouchPrefsStore.cpp @@ -61,6 +61,8 @@ static const uint8_t DEFAULT_SIG_PROBE_EN = 1; // signal discover static const uint16_t DEFAULT_SIG_POLL_MIN = 5; // minutes between probes #if defined(HAS_CROWPANEL_35) static const uint8_t DEFAULT_UI_ROTATION = 3; // upright ILI9488 landscape +#elif defined(HAS_CROWPANEL_50) +static const uint8_t DEFAULT_UI_ROTATION = 3; // native 800x480 landscape #elif defined(ATTAKY_MESH_SERIES) static const uint8_t DEFAULT_UI_ROTATION = 1; // landscape (LV_DISP_ROT_90), as before it was a setting #else diff --git a/src/helpers/esp32/WebFileTransferConfig.h b/src/helpers/esp32/WebFileTransferConfig.h index afd9829..8831f84 100644 --- a/src/helpers/esp32/WebFileTransferConfig.h +++ b/src/helpers/esp32/WebFileTransferConfig.h @@ -3,14 +3,14 @@ // Browser file transfer requires the shared WebSocket transport and a removable // card backend. Keep internal SPIFFS/LittleFS-only boards out: /transfer can hold // large uploads and is intended to remain user-removable storage. The exception -// is the Attaky Mesh Deck, which has no card slot at all: it transfers to its -// internal LittleFS "tiles" partition (never SPIFFS, which holds the settings, +// is the Attaky Mesh Deck and the CrowPanel Advance 5.0, which have no usable card +// slot: they transfer to the internal LittleFS "tiles" partition (never SPIFFS, which holds the settings, // contacts and chats), within that partition's free space. #if defined(MULTI_TRANSPORT_COMPANION) && \ (defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || \ defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8) || \ defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_TDISPLAY_P4) || \ - defined(HAS_TANMATSU) || defined(ATTAKY_MESH_SERIES)) + defined(HAS_TANMATSU) || defined(ATTAKY_MESH_SERIES) || defined(HAS_CROWPANEL_50)) #define WADA_WEB_FILE_TRANSFER 1 #else #define WADA_WEB_FILE_TRANSFER 0 @@ -24,7 +24,7 @@ #define WADA_WEB_FILE_TRANSFER_SDMMC 0 #endif -#if WADA_WEB_FILE_TRANSFER && defined(ATTAKY_MESH_SERIES) +#if WADA_WEB_FILE_TRANSFER && (defined(ATTAKY_MESH_SERIES) || defined(HAS_CROWPANEL_50)) #define WADA_WEB_FILE_TRANSFER_INTERNAL 1 #else #define WADA_WEB_FILE_TRANSFER_INTERNAL 0 diff --git a/src/helpers/input/LgfxGt911Touch.cpp b/src/helpers/input/LgfxGt911Touch.cpp index 0b9081d..0676d4d 100644 --- a/src/helpers/input/LgfxGt911Touch.cpp +++ b/src/helpers/input/LgfxGt911Touch.cpp @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#if (defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35)) && defined(ESP32) +#if (defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50)) && defined(ESP32) #include #include @@ -11,6 +11,9 @@ #if defined(HAS_CROWPANEL_35) #include extern CrowPanel35Display display; +#elif defined(HAS_CROWPANEL_50) + #include + extern CrowPanel50Display display; #else #include extern WioTrackerL2Display display; @@ -69,6 +72,16 @@ bool heltecV4CapTouchBegin() { nextProbeMs = now + 1000; Wire.beginTransmission(0x5D); if (Wire.endTransmission() != 0) return false; +#elif defined(HAS_CROWPANEL_50) + // The display reset the GT911 and recorded where it answered (0x5D or 0x14). + static uint32_t nextProbeMs = 0; + const uint32_t now = millis(); + if (nextProbeMs && (int32_t)(now - nextProbeMs) < 0) return false; + nextProbeMs = now + 1000; + const uint8_t addr = display.touchAddr(); + if (!addr) return false; + Wire.beginTransmission(addr); + if (Wire.endTransmission() != 0) return false; #endif s_ready = true; return true; @@ -90,6 +103,8 @@ bool heltecV4CapTouchStartBackgroundPoll(uint32_t periodMs) { s_periodMs = periodMs < 4 ? 4 : (periodMs > 100 ? 100 : periodMs); #if defined(HAS_CROWPANEL_35) const char* name = "crow_touch"; +#elif defined(HAS_CROWPANEL_50) + const char* name = "crow50_touch"; #else const char* name = "wio_l2_touch"; #endif @@ -104,6 +119,8 @@ void heltecV4CapTouchSetSlowPoll(bool slow) { s_periodMs = slow ? 50 : 8; } const char* heltecV4CapTouchDebug() { #if defined(HAS_CROWPANEL_35) return s_ready ? "CrowPanel Advance 3.5 GT911" : "CrowPanel GT911 not responding at 0x5D"; +#elif defined(HAS_CROWPANEL_50) + return s_ready ? "CrowPanel Advance 5.0 GT911" : "CrowPanel 5.0 GT911 not responding at 0x5D/0x14"; #else return "Wio Tracker L2 GT911"; #endif diff --git a/src/main.cpp b/src/main.cpp index 0ca7729..680c8b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -874,7 +874,7 @@ void setup() { #endif if (r == 1) display.setDisplayRotation(1); else if (r == 3) display.setDisplayRotation(3); -#if defined(HAS_CROWPANEL_35) +#if defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) else display.setDisplayRotation(0); // restore saved portrait #endif } diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 71f81d7..724034a 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -196,6 +196,8 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM #include #elif defined(HAS_CROWPANEL_35) #include + #elif defined(HAS_CROWPANEL_50) + #include #elif defined(HAS_RAK_TAP_V2) #include // LovyanGFX FSPI on RAK Tap V2 #elif defined(HAS_TDISPLAY_P4) @@ -264,6 +266,8 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM extern WioTrackerL2Display display; #elif defined(HAS_CROWPANEL_35) extern CrowPanel35Display display; + #elif defined(HAS_CROWPANEL_50) + extern CrowPanel50Display display; #elif defined(HAS_RAK_TAP_V2) || defined(HELTEC_LORA_V4_R8) extern LGFXDisplay display; #elif defined(HAS_TDISPLAY_P4) @@ -5102,7 +5106,7 @@ static void lvglFlushImpl(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_col static void applyHardwarePanelRotation(uint8_t lvgl_rot) { if (lvgl_rot == LV_DISP_ROT_90) display.setDisplayRotation(1); else if (lvgl_rot == LV_DISP_ROT_270) display.setDisplayRotation(3); -#if defined(HAS_CROWPANEL_35) +#if defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) else display.setDisplayRotation(0); #elif defined(ATTAKY_MESH_SERIES) else display.setDisplayRotation(DISPLAY_ROTATION); @@ -9029,7 +9033,7 @@ static void kbShowRotateArrows(bool show) { // control. The arrows are not created on these boards; every other reference to // the two pointers is null-guarded, so leaving them null is enough to remove // the buttons everywhere — the same pattern the retired s_kb_alt_btn uses. -#if !defined(HAS_WIO_TRACKER_L2) && !defined(HAS_CROWPANEL_35) +#if !defined(HAS_WIO_TRACKER_L2) && !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) static void kbRotLeftCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; // Tap the left arrow: rotate landscape that way; tap again returns to portrait. @@ -16263,7 +16267,7 @@ static void rotateScreenCycleCb(lv_event_t* e) { #if defined(ESP32) // Simple toggle: portrait <-> the board's upright landscape orientation. const uint8_t cur = touchPrefsGetUiRotation(); -#if defined(HAS_CROWPANEL_35) +#if defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) const uint8_t next = (cur == LV_DISP_ROT_NONE) ? (uint8_t)LV_DISP_ROT_270 : (uint8_t)LV_DISP_ROT_NONE; #else @@ -17472,7 +17476,7 @@ static void buildLockPage() { }); #endif } -#if !defined(HAS_CROWPANEL_35) // no unlock control there, so no lock to put a PIN on +#if !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) // no unlock control there, so no lock to put a PIN on { // PIN lock: off until switched on here. On asks for a new PIN twice; off and Change // ask for the current one first. The switch follows the PIN, not the tap. @@ -31574,7 +31578,7 @@ static volatile uint16_t s_tiles_free_kb = 0xFFFF; static fs::LittleFSFS s_tiles_fs; static bool s_tiles_fs_ready = false; #if WADA_WEB_FILE_TRANSFER_INTERNAL -// File Transfer's storage on the Attaky (declared with the transfer app above). +// File Transfer's storage on the Attaky and CrowPanel 5.0 (declared with the transfer app above). static fs::FS& internalTransferFs() { return s_tiles_fs; } static bool internalTransferFsReady() { return s_tiles_fs_ready; } static size_t internalTransferFsUsed() { return s_tiles_fs.usedBytes(); } @@ -32882,6 +32886,8 @@ static int wifiScanWatchdogSafe(uint32_t cap_ms, uint16_t per_chan_ms = 300) { #define WADA_BOARD_ID "wio-tracker-l2" #elif defined(HAS_CROWPANEL_35) #define WADA_BOARD_ID "crowpanel-35" +#elif defined(HAS_CROWPANEL_50) +#define WADA_BOARD_ID "crowpanel-50" #elif defined(HELTEC_LORA_V4_TFT) #define WADA_BOARD_ID "heltec-v4-tft" #else @@ -50527,7 +50533,7 @@ static void applyVolume(uint8_t pct) { if (pct > 100) pct = 100; s_volume_pct = pct; // play paths read the persisted pref; this keeps the slider live } -#elif defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) +#elif defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) #define HAS_CC_BRIGHTNESS 1 static uint8_t s_brightness_pct = 63; static void applyBrightness(uint8_t pct) { @@ -56223,7 +56229,7 @@ static lv_obj_t* hwIcon(lv_obj_t* parent, const char* glyph, uint32_t col, bool static const lv_font_t* hwFitFont(const char* txt, int w, int h) { const lv_font_t* faces[] = { &lv_font_montserrat_28, -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) &lv_font_montserrat_24, &lv_font_montserrat_20, #endif &g_font_semi_16, &g_font_semi_14, &g_font_12, @@ -64258,7 +64264,7 @@ static void buildUiTree() { }; // Both buttons use the rotation/refresh glyph; left vs. right position // tells the user which way the display will turn. -#if !defined(HAS_WIO_TRACKER_L2) && !defined(HAS_CROWPANEL_35) +#if !defined(HAS_WIO_TRACKER_L2) && !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) s_kb_rot_left_btn = makeRotBtn(LV_SYMBOL_REFRESH, kbRotLeftCb); s_kb_rot_right_btn = makeRotBtn(LV_SYMBOL_REFRESH, kbRotRightCb); #endif @@ -69932,7 +69938,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #else s_lock_on_screen_off = touchPrefsGetLockOnScreenOff(); #endif -#if !defined(HAS_CROWPANEL_35) // no unlock control there, so no lock to put a PIN on +#if !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) // no unlock control there, so no lock to put a PIN on s_pin_on = touchPrefsPinIsSet(); pinRestoreFails(); #endif @@ -70078,6 +70084,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif g_draw_buf_px = 12800; band_psram = true; +#elif defined(HAS_CROWPANEL_50) + // 20 full rows of the 800-px panel in internal RAM, as Camillia-MT runs it: the + // RGB panel already scans its framebuffer out of PSRAM continuously, and a PSRAM + // band on top of that starves the LCD_CAM DMA and tears the picture. + const int draw_band_w = 800; + g_draw_buf_px = draw_band_w * 20; #elif defined(HELTEC_LORA_V4_R8) // The R8's async flush byte-swaps each band into a 6144-px internal DMA buffer // (LGFX_SWAP_BUF_PX) and the band has to fit it, so it stays 240x24; in PSRAM all the @@ -70306,6 +70318,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #elif defined(HAS_CROWPANEL_35) g_lv.disp_drv.hor_res = s_remote_mode ? (s_remote_landscape ? 400 : 240) : (ui_landscape ? 480 : 320); g_lv.disp_drv.ver_res = s_remote_mode ? (s_remote_landscape ? 240 : 400) : (ui_landscape ? 320 : 480); +#elif defined(HAS_CROWPANEL_50) + g_lv.disp_drv.hor_res = s_remote_mode ? (s_remote_landscape ? 400 : 240) : (ui_landscape ? 800 : 480); + g_lv.disp_drv.ver_res = s_remote_mode ? (s_remote_landscape ? 240 : 400) : (ui_landscape ? 480 : 800); #else // Landscape rotates the panel in HARDWARE (smooth — no per-pixel software // rotation each flush), so tell LVGL the already-rotated resolution and let @@ -70378,7 +70393,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // back to LVGL in the panel frame. heltecV4CapTouchSetPointRotation(s_ui_rotation); // LVGL won't, so driver does } -#if defined(HAS_CROWPANEL_35) || defined(ATTAKY_MESH_SERIES) +#if defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) || defined(ATTAKY_MESH_SERIES) else if (!s_remote_mode) { // A file-backed portrait preference may not have been loaded at the boot logo. // The logo reads the legacy NVS copy before main.cpp moves prefs to SPIFFS. On @@ -71706,8 +71721,8 @@ static inline void touchScreenBacklight(bool on) { ledcWrite(kM9BlPwmChannel, 255); // inverted: 255 = 0% conduction = off touchPanelSleep(true); } -#elif defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) - // The display owns its backlight (I2C on Wio, PWM on CrowPanel). +#elif defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) + // The display owns its backlight (I2C on Wio and the CrowPanel 5.0, PWM on the 3.5). if (on) applyBrightness(s_brightness_pct); else display.setBrightness(0); #elif defined(HAS_TDISPLAY_P4) @@ -72189,7 +72204,7 @@ void UITask::wakeScreen() { } void UITask::lockScreen() { -#if defined(HAS_CROWPANEL_35) +#if defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) // No physical unlock button: imported lock preferences must still allow touch wake. sleepScreen(); return; @@ -74405,7 +74420,7 @@ void UITask::loop() { static bool s_con_input_up = false; if (!s_con_input_up) { if (heltecV4CapTouchBegin()) { -#if !defined(HAS_CROWPANEL_35) +#if !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) heltecV4CapTouchStartBackgroundPoll(8); #endif s_con_input_up = true; @@ -75077,7 +75092,7 @@ void UITask::loop() { // thing with or without it. ssaverStart(now, false); _screen_off = true; -#if !defined(HAS_CROWPANEL_35) // no unlock control there; its lockScreen() only sleeps too +#if !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) // no unlock control there; its lockScreen() only sleeps too _manual_lock = s_lock_on_screen_off || s_pin_on; #endif } else @@ -75258,9 +75273,10 @@ void UITask::loop() { } if (g_lv.touch_inited) { pushDiagLine("touch init ok"); - #if defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) + #if defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) // GT911 (LovyanGFX) and the TCA9535/ADS1115 (Wire) share I2C0 on - // Wio's GPIO47/48; the CrowPanel's GT911 shares I2C0 on GPIO15/16. + // Wio's GPIO47/48; the CrowPanels' GT911 shares I2C0 on GPIO15/16 (with + // the 5.0's backlight controller). // Keep every transaction on loopTask so polling cannot interrupt Wire. pushDiagLine("touch inline (shared I2C)"); #elif defined(HAS_TDECK_PRO) diff --git a/src/ui-touch/device_caps.h b/src/ui-touch/device_caps.h index 06e3c61..a684b22 100644 --- a/src/ui-touch/device_caps.h +++ b/src/ui-touch/device_caps.h @@ -36,6 +36,19 @@ #define CAP_LOCK_SCREEN 0 // GPIO0 is LoRa NSS, no separate button to unlock #define CAP_BATTERY 0 // no battery voltage sensing +#elif defined(HAS_CROWPANEL_50) // ===== Elecrow CrowPanel Advance 5.0 (ESP32-S3) ===== + #define CAP_TOUCH 1 // GT911 capacitive touch + #define CAP_ROTATABLE 1 // ST7262 RGB panel, native 800x480 landscape = UI rotation 3 + #define CAP_LARGE_SCREEN 1 // native 800x480: large-panel layouts and the UI-size fonts + // The function DIP switch gives GPIO4/5/6 to the radio, so the card slot is dead + // while the radio works; File Transfer uses the internal tiles partition instead. + #define CAP_SD 0 + #define CAP_FILESYSTEM 0 + #define CAP_GPS 0 // no free GPIO pair for a UART (the RGB bus takes 17/18) + #define CAP_OTA 1 + #define CAP_LOCK_SCREEN 0 // no user button to unlock with + #define CAP_BATTERY 0 // no battery voltage sensing + #elif defined(HAS_WIO_TRACKER_L2) // ===== Seeed Wio Tracker L2 (ESP32-S3) ===== #define CAP_TOUCH 1 #define CAP_ROTATABLE 0 diff --git a/src/ui-touch/ui_font_18.c b/src/ui-touch/ui_font_18.c index 46e6c44..9aa61db 100644 --- a/src/ui-touch/ui_font_18.c +++ b/src/ui-touch/ui_font_18.c @@ -1,4 +1,4 @@ -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) /******************************************************************************* * Size: 18 px @@ -2683,4 +2683,4 @@ lv_font_t lv_font_montserrat_18 = { #endif /*#if WADA_UI_FONT_18_BODY*/ -#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) */ +#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) */ diff --git a/src/ui-touch/ui_font_20.c b/src/ui-touch/ui_font_20.c index c891bcb..9df0e6f 100644 --- a/src/ui-touch/ui_font_20.c +++ b/src/ui-touch/ui_font_20.c @@ -1,4 +1,4 @@ -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) /******************************************************************************* * Size: 20 px @@ -3013,4 +3013,4 @@ lv_font_t lv_font_montserrat_20 = { #endif /*#if WADA_UI_FONT_20_BODY*/ -#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) */ +#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) */ diff --git a/src/ui-touch/ui_font_24.c b/src/ui-touch/ui_font_24.c index 735433d..ccd418a 100644 --- a/src/ui-touch/ui_font_24.c +++ b/src/ui-touch/ui_font_24.c @@ -1,4 +1,4 @@ -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) /******************************************************************************* * Size: 24 px @@ -3708,4 +3708,4 @@ lv_font_t lv_font_montserrat_24 = { #endif /*#if WADA_UI_FONT_24_BODY*/ -#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) */ +#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(DOC_CAPTURE) */ diff --git a/variants/crowpanel_50/CrowPanel50Board.h b/variants/crowpanel_50/CrowPanel50Board.h new file mode 100644 index 0000000..b902f1f --- /dev/null +++ b/variants/crowpanel_50/CrowPanel50Board.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +// Elecrow CrowPanel Advance 5.0" (ESP32-S3-WROOM-1-N16R8). The expansion slot +// is routed to the SX1262 by the DIP switch on the back of the board, not by a +// GPIO, so there is nothing to set up here beyond the shared I2C bus that +// ESP32Board::begin() opens on GPIO15/16 (GT911 + backlight controller). +class CrowPanel50Board : public ESP32Board { +public: + void begin() { + ESP32Board::begin(); + } + + const char* getManufacturerName() const override { return "Elecrow CrowPanel Advance 5.0"; } +}; diff --git a/variants/crowpanel_50/CrowPanel50Display.cpp b/variants/crowpanel_50/CrowPanel50Display.cpp new file mode 100644 index 0000000..d4cf472 --- /dev/null +++ b/variants/crowpanel_50/CrowPanel50Display.cpp @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "CrowPanel50Display.h" + +#include +#include + +// Bus pins, timings and the board controller follow Elecrow's 5.0" factory code +// (LovyanGFX_Driver.h, lesson-05) and Meshtastic's LGFX_ELECROW70, as collected in +// Camillia-MT's hw_crowpanel_50.h / crowpanel_50_display.h. +namespace { +constexpr int kPanelW = 800, kPanelH = 480; +constexpr int kTouchSda = 15, kTouchScl = 16, kTouchInt = 1; + +// 14 MHz rather than Elecrow's 21: the panel scans out of the same PSRAM that +// LVGL, WiFi and the radio stacks use, and the lower clock leaves them the +// bandwidth that otherwise shows up as the picture drifting or tearing. +constexpr uint32_t kPclkHz = 14000000; + +// V1.1+ board MCU: single-byte commands at 0x30. 0 brightest .. 244 dimmest, +// 245 backlight off, 250 wakes the touch panel (V1.2/V1.3 values). +constexpr uint8_t kMcuAddr = 0x30; +constexpr uint8_t kMcuBlMax = 0, kMcuBlMin = 244, kMcuBlOff = 245, kMcuTouchOn = 250; + +// V1.0 PCA9557 (TCA9534 register layout). +constexpr uint8_t kIoexAddr = 0x18; +constexpr uint8_t kIoexBl = 1, kIoexTpRst = 2, kIoexAmpMute = 3, kIoexAmpShut = 4; +constexpr uint8_t kRegOutput = 0x01, kRegConfig = 0x03; +inline uint8_t ioBit(uint8_t pin) { return (uint8_t)(1u << pin); } +} // namespace + +// ---- Board controller ------------------------------------------------------- + +bool CrowPanel50BoardCtl::probe(uint8_t addr) { + Wire.beginTransmission(addr); + return Wire.endTransmission() == 0; +} + +CrowPanel50BoardCtl::Kind CrowPanel50BoardCtl::begin() { + if (probe(kIoexAddr)) { + _kind = PCA9557; + // Levels first, then direction, so no pin glitches on the way to output. + // Elecrow's start-up state: backlight on, touch out of reset, amplifier off. + _out = ioBit(kIoexBl) | ioBit(kIoexTpRst) | ioBit(kIoexAmpShut); + pcaWrite(kRegOutput, _out); + pcaWrite(kRegConfig, (uint8_t)~(ioBit(kIoexBl) | ioBit(kIoexTpRst) | ioBit(kIoexAmpMute) | ioBit(kIoexAmpShut))); + Serial.printf("[crowpanel-50] V1.0 board: PCA9557 at 0x%02X\n", kIoexAddr); + if (P_LORA_NSS != 0) + Serial.println("[crowpanel-50] V1.0 boards wire the radio's CS to GPIO0; this build uses " + "GPIO8 (V1.2+), so the radio will not answer"); + } else if (probe(kMcuAddr)) { + _kind = MCU; + Serial.printf("[crowpanel-50] V1.1+ board: MCU at 0x%02X\n", kMcuAddr); + } else { + _kind = NONE; + Serial.printf("[crowpanel-50] no backlight controller at 0x%02X or 0x%02X\n", kIoexAddr, kMcuAddr); + } + return _kind; +} + +// GT911 address strap: INT held low through the reset gives 0x5D. Elecrow's V1.2 +// code repeats the MCU's wake until the GT911 answers, so this does too, a few +// times. If the strap is missed it comes up at 0x14 instead; either is accepted. +uint8_t CrowPanel50BoardCtl::resetTouch() { + if (_kind == NONE) return probe(0x5D) ? 0x5D : (probe(0x14) ? 0x14 : 0); + for (int attempt = 1; attempt <= 5; ++attempt) { + pinMode(kTouchInt, OUTPUT); + digitalWrite(kTouchInt, LOW); + if (_kind == PCA9557) { + pcaSet(kIoexTpRst, false); + delay(20); + pcaSet(kIoexTpRst, true); + delay(100); + } else { + mcuSendAlways(kMcuTouchOn); + delay(120); + } + pinMode(kTouchInt, INPUT); + delay(100); + const bool at5d = probe(0x5D), at14 = probe(0x14); + if (at5d || at14) { + Serial.printf("[crowpanel-50] GT911 at 0x%02X after %d attempt%s\n", + at5d ? 0x5D : 0x14, attempt, attempt == 1 ? "" : "s"); + return at5d ? 0x5D : 0x14; + } + } + Serial.println("[crowpanel-50] GT911 did not answer at 0x5D or 0x14"); + return 0; +} + +// 0 is off. V1.0's backlight only switches; the MCU dims (255 brightest). Below +// full the MCU's PWM can make the backlight driver whine, so 100% is steady DC. +void CrowPanel50BoardCtl::setBacklight(uint8_t level) { + if (_kind == PCA9557) { + pcaSet(kIoexBl, level > 0); + } else if (_kind == MCU) { + if (level == 0) { mcuSend(kMcuBlOff); return; } + const uint8_t span = kMcuBlMin - kMcuBlMax; + mcuSend((uint8_t)(kMcuBlMin - (uint32_t)(level - 1) * span / 254)); + } +} + +void CrowPanel50BoardCtl::pcaSet(uint8_t pin, bool high) { + const uint8_t next = high ? (uint8_t)(_out | ioBit(pin)) : (uint8_t)(_out & ~ioBit(pin)); + if (next == _out) return; + _out = next; + pcaWrite(kRegOutput, _out); +} + +void CrowPanel50BoardCtl::pcaWrite(uint8_t reg, uint8_t value) { + Wire.beginTransmission(kIoexAddr); + Wire.write(reg); + Wire.write(value); + Wire.endTransmission(); +} + +void CrowPanel50BoardCtl::mcuSend(uint8_t cmd) { + if (cmd == _lastMcu) return; + if (mcuSendAlways(cmd)) _lastMcu = cmd; +} + +bool CrowPanel50BoardCtl::mcuSendAlways(uint8_t cmd) { + Wire.beginTransmission(kMcuAddr); + Wire.write(cmd); + return Wire.endTransmission() == 0; +} + +// ---- Display ------------------------------------------------------------------ + +CrowPanel50Display::CrowPanel50Display() : DisplayDriver(kPanelW, kPanelH) { + { + auto cfg = _panel.config(); + cfg.memory_width = kPanelW; + cfg.memory_height = kPanelH; + cfg.panel_width = kPanelW; + cfg.panel_height = kPanelH; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + cfg.readable = true; // the framebuffer is the only copy of the screen + _panel.config(cfg); + } + { + auto cfg = _panel.config_detail(); + cfg.use_psram = 1; // 800x480x2 = 750 KB + _panel.config_detail(cfg); + } + { + auto cfg = _bus.config(); + cfg.panel = &_panel; + cfg.pin_d0 = GPIO_NUM_21; // B0 + cfg.pin_d1 = GPIO_NUM_47; // B1 + cfg.pin_d2 = GPIO_NUM_48; // B2 + cfg.pin_d3 = GPIO_NUM_45; // B3 + cfg.pin_d4 = GPIO_NUM_38; // B4 + cfg.pin_d5 = GPIO_NUM_9; // G0 + cfg.pin_d6 = GPIO_NUM_10; // G1 + cfg.pin_d7 = GPIO_NUM_11; // G2 + cfg.pin_d8 = GPIO_NUM_12; // G3 + cfg.pin_d9 = GPIO_NUM_13; // G4 + cfg.pin_d10 = GPIO_NUM_14; // G5 + cfg.pin_d11 = GPIO_NUM_7; // R0 + cfg.pin_d12 = GPIO_NUM_17; // R1 + cfg.pin_d13 = GPIO_NUM_18; // R2 + cfg.pin_d14 = GPIO_NUM_3; // R3 + cfg.pin_d15 = GPIO_NUM_46; // R4 + cfg.pin_henable = GPIO_NUM_42; + cfg.pin_vsync = GPIO_NUM_41; + cfg.pin_hsync = GPIO_NUM_40; + cfg.pin_pclk = GPIO_NUM_39; + cfg.freq_write = kPclkHz; + cfg.hsync_polarity = 0; + cfg.hsync_front_porch = 8; + cfg.hsync_pulse_width = 4; + cfg.hsync_back_porch = 8; + cfg.vsync_polarity = 0; + cfg.vsync_front_porch = 8; + cfg.vsync_pulse_width = 4; + cfg.vsync_back_porch = 8; + cfg.pclk_idle_high = 1; + _bus.config(cfg); + } + _panel.setBus(&_bus); + + _light.setController(&_ctl); + _panel.setLight(&_light); + + { + auto cfg = _touch.config(); + cfg.x_min = 0; + cfg.x_max = kPanelW - 1; + cfg.y_min = 0; + cfg.y_max = kPanelH - 1; + cfg.pin_int = -1; // polled; INT is only the address strap + cfg.pin_rst = -1; // reset belongs to the board controller (resetTouch) + cfg.bus_shared = true; // the controller is on the same I2C pins + cfg.offset_rotation = 0; + cfg.i2c_port = 0; + cfg.i2c_addr = 0x5D; + cfg.pin_sda = kTouchSda; + cfg.pin_scl = kTouchScl; + cfg.freq = 400000; + _touch.config(cfg); + _panel.setTouch(&_touch); + } + _lcd.setPanel(&_panel); +} + +bool CrowPanel50Display::begin() { + if (_isOn) return true; + // board.begin() has opened Wire on GPIO15/16 already. + _ctl.begin(); + _touchAddr = _ctl.resetTouch(); + if (_touchAddr) { + auto cfg = _touch.config(); + cfg.i2c_addr = _touchAddr; + _touch.config(cfg); + } + if (!_lcd.init()) { + Serial.println("[crowpanel-50] RGB panel init failed"); + return false; + } + _lcd.setSwapBytes(true); + setDisplayRotation(3); + _lcd.fillScreen(0); + // LovyanGFX's touch init reprograms I2C0 under Wire; take the bus back for the + // backlight controller and the RTC probe, as the CrowPanel 3.5 does. + Wire.end(); + if (!Wire.begin(kTouchSda, kTouchScl, 400000)) { + Serial.println("[crowpanel-50] I2C bus init failed"); + return false; + } + _ctl.setBacklight(_brightness); + _isOn = true; + Serial.printf("[crowpanel-50] display %dx%d, touch %s\n", width(), height(), + _lcd.touch() ? "up" : "not found"); + return true; +} + +void CrowPanel50Display::turnOn() { + if (!_isOn) { _ctl.setBacklight(_brightness); _isOn = true; } +} +void CrowPanel50Display::turnOff() { + if (_isOn) { _ctl.setBacklight(0); _isOn = false; } +} +void CrowPanel50Display::clear() { _lcd.fillScreen(0); } + +// The UI's portrait is 0 and its upright landscape 3 (ROT_270), as on the 3.5. +// The panel itself is native 800x480 landscape at LovyanGFX rotation 0, so map +// 3 to that, 1 to landscape turned 180 degrees and 0 to portrait. LovyanGFX +// rotates the framebuffer writes and the GT911 coordinates together. +void CrowPanel50Display::setDisplayRotation(uint8_t rotation) { + uint8_t r; + switch (rotation) { + case 3: r = 0; break; + case 1: r = 2; break; + default: r = 1; break; + } + _lcd.setRotation(r); + setLogicalSize(_lcd.width(), _lcd.height()); +} + +void CrowPanel50Display::startFrame(ColorVal bkg) { _lcd.fillScreen(bkg); } +void CrowPanel50Display::setTextSize(int sz) { _lcd.setTextSize(sz); } +void CrowPanel50Display::setColor(ColorVal c) { _color = c; _lcd.setTextColor(c); } +void CrowPanel50Display::setCursor(int x, int y) { _lcd.setCursor(x, y); } +void CrowPanel50Display::print(const char* str) { _lcd.print(str); } +void CrowPanel50Display::fillRect(int x, int y, int w, int h) { _lcd.fillRect(x, y, w, h, _color); } +void CrowPanel50Display::drawRect(int x, int y, int w, int h) { _lcd.drawRect(x, y, w, h, _color); } +void CrowPanel50Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { + _lcd.drawXBitmap(x, y, bits, w, h, _color); +} +uint16_t CrowPanel50Display::getTextWidth(const char* str) { return _lcd.textWidth(str); } +void CrowPanel50Display::endFrame() {} + +void CrowPanel50Display::writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels) { + if (!_isOn || !pixels || w <= 0 || h <= 0) return; + _lcd.startWrite(); + _lcd.setAddrWindow(x, y, w, h); + _lcd.writePixels(const_cast(pixels), (uint32_t)w * h); + _lcd.endWrite(); +} + +void CrowPanel50Display::setBrightness(uint8_t value) { + if (value) _brightness = value; + _ctl.setBacklight(value); +} + +bool CrowPanel50Display::getTouchPoint(uint16_t& x, uint16_t& y) { + int32_t tx = 0, ty = 0; + if (!_lcd.getTouch(&tx, &ty)) return false; + x = (uint16_t)tx; + y = (uint16_t)ty; + return true; +} diff --git a/variants/crowpanel_50/CrowPanel50Display.h b/variants/crowpanel_50/CrowPanel50Display.h new file mode 100644 index 0000000..def4818 --- /dev/null +++ b/variants/crowpanel_50/CrowPanel50Display.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#define LGFX_USE_V1 +#include +#include +#include + +// Whatever owns the backlight and the GT911 reset on this board. V1.0 has a +// PCA9557 expander at 0x18; from V1.1 a small board MCU at 0x30 takes both over +// (and the buzzer/speaker). begin() tells them apart by which address answers, +// so one build drives either. Ported from Camillia-MT's crowpanel_50_display.h. +class CrowPanel50BoardCtl { +public: + enum Kind : uint8_t { NONE, PCA9557, MCU }; + + Kind begin(); + uint8_t resetTouch(); // 0x5D / 0x14 the GT911 answered at, 0 if neither + void setBacklight(uint8_t level); + Kind kind() const { return _kind; } + +private: + static bool probe(uint8_t addr); + void pcaSet(uint8_t pin, bool high); + void pcaWrite(uint8_t reg, uint8_t value); + void mcuSend(uint8_t cmd); + bool mcuSendAlways(uint8_t cmd); + + Kind _kind = NONE; + uint8_t _out = 0; + int16_t _lastMcu = -1; +}; + +class CrowPanel50Light : public lgfx::v1::ILight { +public: + void setController(CrowPanel50BoardCtl* ctl) { _ctl = ctl; } + bool init(uint8_t brightness) override { setBrightness(brightness); return true; } + void setBrightness(uint8_t brightness) override { if (_ctl) _ctl->setBacklight(brightness); } + +private: + CrowPanel50BoardCtl* _ctl = nullptr; +}; + +class CrowPanel50Display : public DisplayDriver { +public: + CrowPanel50Display(); + bool begin(); + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; + void setTextSize(int sz) override; + void setColor(ColorVal c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; + + void setDisplayRotation(uint8_t rotation); + void setBrightness(uint8_t value); + bool getTouchPoint(uint16_t& x, uint16_t& y); + void writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels); + uint8_t touchAddr() const { return _touchAddr; } + +private: + lgfx::Bus_RGB _bus; + lgfx::Panel_RGB _panel; + CrowPanel50Light _light; + lgfx::Touch_GT911 _touch; + lgfx::LGFX_Device _lcd; + CrowPanel50BoardCtl _ctl; + uint8_t _touchAddr = 0; + uint8_t _brightness = 255; + bool _isOn = false; + uint16_t _color = 0xffff; +}; diff --git a/variants/crowpanel_50/target.cpp b/variants/crowpanel_50/target.cpp new file mode 100644 index 0000000..0921c9a --- /dev/null +++ b/variants/crowpanel_50/target.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include "target.h" + +CrowPanel50Board board; +static SPIClass radioSpi(FSPI); +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, radioSpi); +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +ClockFloorRTC rtc_clock(fallback_clock); + +// No GPS: every GPIO a UART could use is taken by the RGB panel, the radio or +// the touch bus (an unpinned Serial1 would fall back to GPIO15/16, the GT911). +EnvironmentSensorManager sensors; + +CrowPanel50Display display; + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + if (!radio.std_init(&radioSpi)) { + // The DIP switch on the back shares the slot's pins with the SD card and the + // speaker/microphone; only its wireless-module position connects the radio. + Serial.printf("[crowpanel-50] SX1262 not found (CS%d). Set the function DIP switch to the " + "wireless module and seat the module in J8/J9.\n", P_LORA_NSS); + return false; + } + return true; +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/crowpanel_50/target.h b/variants/crowpanel_50/target.h new file mode 100644 index 0000000..1d28d2f --- /dev/null +++ b/variants/crowpanel_50/target.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include "../../src/helpers/ClockFloorRTC.h" +#include "helpers/sensors/EnvironmentSensorManager.h" +#include "CrowPanel50Board.h" +#include "CrowPanel50Display.h" + +extern CrowPanel50Board board; +extern RADIO_CLASS radio; +extern WRAPPER_CLASS radio_driver; +extern ClockFloorRTC rtc_clock; +extern EnvironmentSensorManager sensors; +extern CrowPanel50Display display; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); From e33eef43dc5e837bbd7334d84d8186181c3befc9 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Sun, 11 Oct 2026 20:00:37 -0400 Subject: [PATCH 2/6] CrowPanel 5: edge fix, Wire touch, composer icons; status bar and keyboard redraw fixes - Panel: clock edge pclk_idle_high=1 (fixes gradient striping), tunable pclk/hporch macros. - Touch: read the GT911 through Wire with a background poll (fixes repeated-start crash). - Chat composer: buttons sized to the input, icon labels in ui_icons_20. - Status bar: chat header uses guarded setters and skips redundant moves/tints, so an open chat no longer repaints the bar every 250 ms. - Keyboard: set/clear PRESSED in a preprocess handler with invalidation off, so a key press repaints only that key instead of the whole keyboard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/ui-touch/UITask.cpp | 191 ++++++++++++------- variants/crowpanel_50/CrowPanel50Display.cpp | 97 ++++++++-- variants/crowpanel_50/CrowPanel50Display.h | 2 + 3 files changed, 212 insertions(+), 78 deletions(-) diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 724034a..a720487 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -2467,9 +2467,16 @@ static inline lv_coord_t modalAvailH() { return lv_disp_get_ver_res(nullptr) - S // kbApplyLayoutForRotation), full CHAT_KB_H in portrait. static inline bool chatLandscape() { return lv_disp_get_hor_res(nullptr) > lv_disp_get_ver_res(nullptr); } static inline lv_coord_t chatScreenW() { return lv_disp_get_hor_res(nullptr); } +// CrowPanel 5.0: all three composer buttons are the text box's height (CHAT_COMP_H +// less the row's 2-px vertical pads), with the 20-px app icons. +#if defined(HAS_CROWPANEL_50) +static constexpr lv_coord_t CHAT_COMP_BTN_CROW50 = CHAT_COMP_H - 4; +#endif static inline lv_coord_t chatComposerChipSz() { #if CAP_ROUND_CORNERS return CHAT_COMP_BTN_P4; +#elif defined(HAS_CROWPANEL_50) + return CHAT_COMP_BTN_CROW50; #elif CAP_LARGE_SCREEN return 56; #elif defined(TLORA_PAGER) @@ -2481,7 +2488,7 @@ static inline lv_coord_t chatComposerChipSz() { static inline lv_coord_t chatComposerSendSz() { #if CAP_ROUND_CORNERS return CHAT_COMP_BTN_P4; -#elif defined(TLORA_PAGER) +#elif defined(TLORA_PAGER) || defined(HAS_CROWPANEL_50) return chatComposerChipSz(); #else return 34; @@ -12681,6 +12688,27 @@ static void composerEditLongPressCb(lv_event_t* e) { txtMenuShow(ta); } +// LVGL's base handler adds/clears LV_STATE_PRESSED on the whole keyboard at +// every key press/release, and because its key styles carry a PRESSED selector +// that invalidates the entire keyboard twice per keystroke. The btnmatrix +// already invalidates just the key it presses and releases, so set the state +// here, ahead of the base handler and with invalidation off; LVGL's own +// add/clear then finds nothing to change. On an 800x480 panel this is the +// difference between redrawing one key and ~200k pixels per keystroke. +static void kbPressStatePreCb(lv_event_t* e) { + lv_obj_t* kb = lv_event_get_current_target(e); + if (lv_event_get_target(e) != kb) return; + const lv_event_code_t code = lv_event_get_code(e); + const bool press = (code == LV_EVENT_PRESSED); + if (!press && code != LV_EVENT_RELEASED && code != LV_EVENT_PRESS_LOST) return; + if (lv_obj_has_state(kb, LV_STATE_PRESSED) == press) return; + lv_disp_t* d = lv_obj_get_disp(kb); + lv_disp_enable_invalidation(d, false); + if (press) lv_obj_add_state(kb, LV_STATE_PRESSED); + else lv_obj_clear_state(kb, LV_STATE_PRESSED); + lv_disp_enable_invalidation(d, true); +} + // On-screen keyboard (V4): backspace with an active selection deletes the whole // highlight instead of one character. Registered BEFORE the keyboard's default // handler (see buildUiTree) so it can replace the default single-char delete — @@ -37802,7 +37830,11 @@ static void makeChatDetail(LvChatPanel& p) { navSetTip(qr_btn, TR("Quick replies")); lv_obj_t* ql = lv_label_create(qr_btn); lv_label_set_text(ql, UI_ICON_MESSAGE_SQUARE_TEXT); +#if defined(HAS_CROWPANEL_50) + lv_obj_set_style_text_font(ql, &ui_icons_20, LV_PART_MAIN); +#else lv_obj_set_style_text_font(ql, &g_font_16, LV_PART_MAIN); +#endif lv_obj_center(ql); #if defined(HAS_TANMATSU) styleChipAsFkey(qr_btn, ql, 0, 0xF5A623, chip_sz, true); // orange △ — quick replies (F2) @@ -37832,7 +37864,11 @@ static void makeChatDetail(LvChatPanel& p) { #else lv_label_set_text(el, UI_ICON_SMILE); #endif +#if defined(HAS_CROWPANEL_50) + lv_obj_set_style_text_font(el, &ui_icons_20, LV_PART_MAIN); +#else lv_obj_set_style_text_font(el, &g_font_16, LV_PART_MAIN); +#endif lv_obj_center(el); #if defined(HAS_TANMATSU) styleChipAsFkey(p.emoji_btn, el, 1, 0xFFD400, chip_sz, false); // yellow □ — emoji picker (F3); keep the colour glyph @@ -37961,7 +37997,7 @@ static void makeChatDetail(LvChatPanel& p) { lv_obj_t* send = lv_btn_create(p.composer_row); p.send_btn = send; -#if defined(TLORA_PAGER) || CAP_ROUND_CORNERS +#if defined(TLORA_PAGER) || CAP_ROUND_CORNERS || defined(HAS_CROWPANEL_50) lv_obj_set_size(send, send_sz, send_sz); #else lv_obj_set_size(send, 30, 30); // a circle; the slot is send_sz wide @@ -37972,7 +38008,11 @@ static void makeChatDetail(LvChatPanel& p) { lv_obj_add_event_cb(send, sendFromPanelCb, LV_EVENT_CLICKED, &p); lv_obj_t* sl = lv_label_create(send); lv_label_set_text(sl, UI_ICON_ARROW_UP); +#if defined(HAS_CROWPANEL_50) + lv_obj_set_style_text_font(sl, &ui_icons_20, LV_PART_MAIN); +#else lv_obj_set_style_text_font(sl, &g_font_16, LV_PART_MAIN); +#endif lv_obj_set_style_text_color(sl, lv_color_hex(COLOR_ON_GLOW), LV_PART_MAIN); lv_obj_center(sl); #if CAP_ROUND_CORNERS @@ -54687,6 +54727,49 @@ static void uiFitTrailingDots(const char* src, const lv_font_t* f, lv_coord_t ma memcpy(out + keep, "...", 4); } +// Set a label's text only when it actually changed. lv_label_set_text always +// invalidates + re-lays-out the label (it does NOT compare), so calling it every +// 250 ms refresh tick with byte-identical text needlessly redraws ~8 labels 4x/s. +// Comparing against the label's CURRENT text also makes this safe across home/tab +// rebuilds (no module-static "last value" that can outlive the widget). +static inline void setLabelIfChanged(lv_obj_t* lbl, const char* txt) { + if (lbl && strcmp(lv_label_get_text(lbl), txt) != 0) lv_label_set_text(lbl, txt); +} +// The same for the bar's other per-tick setters: every LVGL style setter repaints its +// object (and most re-lay it out) even when the value is unchanged, and clearing HIDDEN +// repaints an object that is already visible, so the bar redrew 4 times a second on every +// screen with nothing changing. Each of these compares the live value first. +static inline void sbShow(lv_obj_t* o, bool show) { + if (!o || show != lv_obj_has_flag(o, LV_OBJ_FLAG_HIDDEN)) return; + if (show) lv_obj_clear_flag(o, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN); +} +static inline void sbTranslateY(lv_obj_t* o, lv_coord_t v) { + if (o && lv_obj_get_style_translate_y(o, LV_PART_MAIN) != v) lv_obj_set_style_translate_y(o, v, LV_PART_MAIN); +} +static inline void sbTextColor(lv_obj_t* o, lv_color_t c) { + if (o && lv_obj_get_style_text_color(o, LV_PART_MAIN).full != c.full) lv_obj_set_style_text_color(o, c, LV_PART_MAIN); +} +static inline void sbFont(lv_obj_t* o, const lv_font_t* f, lv_style_selector_t sel) { + if (o && lv_obj_get_style_text_font(o, sel) != f) lv_obj_set_style_text_font(o, f, sel); +} +static inline void sbAlign(lv_obj_t* o, lv_align_t a, lv_coord_t x, lv_coord_t y) { + if (o && (lv_obj_get_style_align(o, LV_PART_MAIN) != a || lv_obj_get_style_x(o, LV_PART_MAIN) != x || + lv_obj_get_style_y(o, LV_PART_MAIN) != y)) + lv_obj_align(o, a, x, y); +} +static inline void sbWidth(lv_obj_t* o, lv_coord_t w) { + if (o && lv_obj_get_style_width(o, LV_PART_MAIN) != w) lv_obj_set_width(o, w); +} +static inline void sbLongMode(lv_obj_t* l, lv_label_long_mode_t m) { + if (l && lv_label_get_long_mode(l) != m) lv_label_set_long_mode(l, m); +} + +static inline void sbSize(lv_obj_t* o, lv_coord_t w, lv_coord_t h) { + if (o && (lv_obj_get_style_width(o, LV_PART_MAIN) != w || lv_obj_get_style_height(o, LV_PART_MAIN) != h)) + lv_obj_set_size(o, w, h); +} + static void chatHeaderApply(bool chat_open) { if (!g_statusbar.chat_avatar) return; lv_obj_t* radios[] = { g_statusbar.conn_icon, g_statusbar.ble_icon, g_statusbar.sd_icon, @@ -54710,7 +54793,9 @@ static void chatHeaderApply(bool chat_open) { } } if (!chat_open) return; - if (g_statusbar.chan_gear) lv_obj_add_flag(g_statusbar.chan_gear, LV_OBJ_FLAG_HIDDEN); + // Every setter below is change-guarded: this runs every 250 ms tick, and LVGL + // repaints an object on any style/size/position write, even an unchanged one. + sbShow(g_statusbar.chan_gear, false); // Avatar and subtitle follow the open thread. static int s_thread = -2; @@ -54751,18 +54836,17 @@ static void chatHeaderApply(bool chat_open) { const lv_coord_t bar_h = statusBarCurH(); const lv_coord_t av_sz = lv_obj_get_style_width(g_statusbar.chat_avatar, LV_PART_MAIN); const lv_coord_t x_av = SC(24), x_text = x_av + av_sz + 8; - lv_obj_set_style_translate_y(g_statusbar.chat_avatar, 0, LV_PART_MAIN); - lv_obj_align(g_statusbar.chat_avatar, LV_ALIGN_LEFT_MID, x_av, 0); + sbTranslateY(g_statusbar.chat_avatar, 0); + sbAlign(g_statusbar.chat_avatar, LV_ALIGN_LEFT_MID, x_av, 0); if (g_statusbar.chat_back) { - lv_obj_set_style_text_color(g_statusbar.chat_back, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); - lv_obj_align(g_statusbar.chat_back, LV_ALIGN_LEFT_MID, 6, 0); + sbTextColor(g_statusbar.chat_back, lv_color_hex(COLOR_TEXT)); + sbAlign(g_statusbar.chat_back, LV_ALIGN_LEFT_MID, 6, 0); } // Right side: battery, then the clock just left of it, centred on the bar. - for (lv_obj_t* o : { g_statusbar.clock, g_statusbar.batt_icon, g_statusbar.batt_pct }) - if (o) lv_obj_set_style_translate_y(o, 0, LV_PART_MAIN); + for (lv_obj_t* o : { g_statusbar.clock, g_statusbar.batt_icon, g_statusbar.batt_pct }) sbTranslateY(o, 0); const char* pct = g_statusbar.batt_pct ? lv_label_get_text(g_statusbar.batt_pct) : ""; const lv_coord_t pct_w = (pct && pct[0]) ? lv_txt_get_width(pct, strlen(pct), &g_font_12, 0, LV_TEXT_FLAG_NONE) + SBX(22) : SBX(20); - lv_obj_align(g_statusbar.clock, LV_ALIGN_RIGHT_MID, -(pct_w + 8), 0); + sbAlign(g_statusbar.clock, LV_ALIGN_RIGHT_MID, -(pct_w + 8), 0); const char* clk = lv_label_get_text(g_statusbar.clock); const lv_coord_t clk_w = lv_txt_get_width(clk, strlen(clk), lv_obj_get_style_text_font(g_statusbar.clock, LV_PART_MAIN), 0, LV_TEXT_FLAG_NONE); const lv_coord_t text_w = lv_obj_get_style_width(g_statusbar.root, LV_PART_MAIN) - x_text - (pct_w + 8 + clk_w + 10); @@ -54771,8 +54855,8 @@ static void chatHeaderApply(bool chat_open) { // wraps one that sizes itself (a narrow panel's subtitle then ran into the list). const lv_coord_t title_h = lv_font_get_line_height(&g_font_semi_14); const lv_coord_t sub_h = lv_font_get_line_height(&g_font_12); - lv_obj_set_style_translate_y(g_statusbar.left_label, 0, LV_PART_MAIN); - lv_obj_set_size(g_statusbar.left_label, LV_MAX((lv_coord_t)40, text_w), title_h); + sbTranslateY(g_statusbar.left_label, 0); + sbSize(g_statusbar.left_label, LV_MAX((lv_coord_t)40, text_w), title_h); if (lv_label_get_long_mode(g_statusbar.left_label) != LV_LABEL_LONG_CLIP) lv_label_set_long_mode(g_statusbar.left_label, LV_LABEL_LONG_CLIP); { @@ -54781,14 +54865,14 @@ static void chatHeaderApply(bool chat_open) { if (strcmp(lv_label_get_text(g_statusbar.left_label), ttl) != 0) lv_label_set_text(g_statusbar.left_label, ttl); } const lv_coord_t top = (bar_h - title_h - sub_h) / 2; - lv_obj_align(g_statusbar.left_label, LV_ALIGN_TOP_LEFT, x_text, top); - lv_obj_set_style_translate_y(g_statusbar.chat_sub, 0, LV_PART_MAIN); - lv_obj_set_size(g_statusbar.chat_sub, LV_MAX((lv_coord_t)40, text_w), sub_h); - lv_obj_align(g_statusbar.chat_sub, LV_ALIGN_TOP_LEFT, x_text, top + title_h); + sbAlign(g_statusbar.left_label, LV_ALIGN_TOP_LEFT, x_text, top); + sbTranslateY(g_statusbar.chat_sub, 0); + sbSize(g_statusbar.chat_sub, LV_MAX((lv_coord_t)40, text_w), sub_h); + sbAlign(g_statusbar.chat_sub, LV_ALIGN_TOP_LEFT, x_text, top + title_h); // The hit area for chat settings: avatar and both lines. - lv_obj_set_style_translate_y(g_statusbar.chat_head, 0, LV_PART_MAIN); - lv_obj_set_size(g_statusbar.chat_head, x_text - x_av + LV_MAX((lv_coord_t)40, text_w), bar_h); - lv_obj_align(g_statusbar.chat_head, LV_ALIGN_LEFT_MID, x_av, 0); + sbTranslateY(g_statusbar.chat_head, 0); + sbSize(g_statusbar.chat_head, x_text - x_av + LV_MAX((lv_coord_t)40, text_w), bar_h); + sbAlign(g_statusbar.chat_head, LV_ALIGN_LEFT_MID, x_av, 0); } // The right-hand indicators, packed leftwards from the battery with one gap between @@ -54825,44 +54909,6 @@ static void statusBarPackRight() { } } -// Set a label's text only when it actually changed. lv_label_set_text always -// invalidates + re-lays-out the label (it does NOT compare), so calling it every -// 250 ms refresh tick with byte-identical text needlessly redraws ~8 labels 4x/s. -// Comparing against the label's CURRENT text also makes this safe across home/tab -// rebuilds (no module-static "last value" that can outlive the widget). -static inline void setLabelIfChanged(lv_obj_t* lbl, const char* txt) { - if (lbl && strcmp(lv_label_get_text(lbl), txt) != 0) lv_label_set_text(lbl, txt); -} -// The same for the bar's other per-tick setters: every LVGL style setter repaints its -// object (and most re-lay it out) even when the value is unchanged, and clearing HIDDEN -// repaints an object that is already visible, so the bar redrew 4 times a second on every -// screen with nothing changing. Each of these compares the live value first. -static inline void sbShow(lv_obj_t* o, bool show) { - if (!o || show != lv_obj_has_flag(o, LV_OBJ_FLAG_HIDDEN)) return; - if (show) lv_obj_clear_flag(o, LV_OBJ_FLAG_HIDDEN); - else lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN); -} -static inline void sbTranslateY(lv_obj_t* o, lv_coord_t v) { - if (o && lv_obj_get_style_translate_y(o, LV_PART_MAIN) != v) lv_obj_set_style_translate_y(o, v, LV_PART_MAIN); -} -static inline void sbTextColor(lv_obj_t* o, lv_color_t c) { - if (o && lv_obj_get_style_text_color(o, LV_PART_MAIN).full != c.full) lv_obj_set_style_text_color(o, c, LV_PART_MAIN); -} -static inline void sbFont(lv_obj_t* o, const lv_font_t* f, lv_style_selector_t sel) { - if (o && lv_obj_get_style_text_font(o, sel) != f) lv_obj_set_style_text_font(o, f, sel); -} -static inline void sbAlign(lv_obj_t* o, lv_align_t a, lv_coord_t x, lv_coord_t y) { - if (o && (lv_obj_get_style_align(o, LV_PART_MAIN) != a || lv_obj_get_style_x(o, LV_PART_MAIN) != x || - lv_obj_get_style_y(o, LV_PART_MAIN) != y)) - lv_obj_align(o, a, x, y); -} -static inline void sbWidth(lv_obj_t* o, lv_coord_t w) { - if (o && lv_obj_get_style_width(o, LV_PART_MAIN) != w) lv_obj_set_width(o, w); -} -static inline void sbLongMode(lv_obj_t* l, lv_label_long_mode_t m) { - if (l && lv_label_get_long_mode(l) != m) lv_label_set_long_mode(l, m); -} - static void updateGlobalStatusBar() { // The drawer's badges are a snapshot taken when the grid was built, so a // message arriving while it is open -- including while the screen was locked @@ -54963,6 +55009,14 @@ static void updateGlobalStatusBar() { // nowhere instead of going back (#308). const bool chat_open = (s_chat_title[0] != '\0') && !s_apppage_title && (s_settings_open_cat < 0); +#if !defined(TLORA_PAGER) && !CAP_ROUND_CORNERS + // An open chat's header (chatHeaderApply, at the end) places the title, back, cog, + // clock and battery itself. Leave them alone here: setting them one way and the + // header the other repainted the whole bar every tick. + const bool chat_hdr = chat_open && g_statusbar.chat_avatar; +#else + const bool chat_hdr = false; +#endif const bool inbox_overview = (getActiveTab() == CHAT_INBOX_TAB_INDEX) && !chat_open && (s_settings_open_cat < 0) && !s_apppage_title; { #if defined(TLORA_PAGER) @@ -55022,6 +55076,10 @@ static void updateGlobalStatusBar() { lv_obj_t* c = lv_obj_get_child(g_statusbar.root, i); if (c == inbox_btns[0] || c == inbox_btns[1] || c == inbox_btns[2]) continue; if (c == g_statusbar.fade || c == g_statusbar.dim) continue; // full-bar backdrops — never shift + if (chat_hdr && (c == g_statusbar.left_label || c == g_statusbar.chat_back || c == g_statusbar.chan_gear || + c == g_statusbar.clock || c == g_statusbar.batt_icon || c == g_statusbar.batt_pct || + c == g_statusbar.chat_avatar || c == g_statusbar.chat_sub || c == g_statusbar.chat_head)) + continue; lv_coord_t t = up; // top row by default if (c == g_statusbar.left_label) { if (s_settings_open_cat >= 0 || s_apppage_title) t = 0; // settings/tool back+title: centred @@ -55047,11 +55105,11 @@ static void updateGlobalStatusBar() { if (g_statusbar.chat_back) { // Re-apply the accent each tick so the chevron tracks the live theme colour (it was // set once at boot to the default accent and never updated on an accent change). - sbTextColor(g_statusbar.chat_back, lv_color_hex(COLOR_ACCENT)); + if (!chat_hdr) sbTextColor(g_statusbar.chat_back, lv_color_hex(COLOR_ACCENT)); sbShow(g_statusbar.chat_back, in_chan_chat); } if (g_statusbar.chan_gear) { - sbShow(g_statusbar.chan_gear, in_chan_chat); + sbShow(g_statusbar.chan_gear, in_chan_chat && !chat_hdr); } // In a chat the thread name (+ unread prefix) is CENTRED on the lower row (the back + // cog sit at the far left); otherwise it's left-aligned per the usual layout. @@ -55090,7 +55148,7 @@ static void updateGlobalStatusBar() { sbWidth(g_statusbar.left_label, 140); sbLongMode(g_statusbar.left_label, LV_LABEL_LONG_DOT); #else - sbAlign(g_statusbar.left_label, LV_ALIGN_CENTER, 0, 0); + if (!chat_hdr) sbAlign(g_statusbar.left_label, LV_ALIGN_CENTER, 0, 0); #endif } else { sbAlign(g_statusbar.left_label, LV_ALIGN_LEFT_MID, 6, 0); @@ -64207,6 +64265,8 @@ static void buildUiTree() { // first, then re-add the default after it. The interceptor runs first and can // stop_processing to replace the default single-char delete when text is selected. lv_obj_remove_event_cb(g_lv.keyboard, lv_keyboard_def_event_cb); + lv_obj_add_event_cb(g_lv.keyboard, kbPressStatePreCb, + (lv_event_code_t)(LV_EVENT_ALL | LV_EVENT_PREPROCESS), nullptr); #if defined(HELTEC_V4_EXPANSION_IO_PIN) && !defined(HELTEC_LORA_V4_R8) lv_obj_add_event_cb(g_lv.keyboard, kbV4SpecialPageCb, LV_EVENT_VALUE_CHANGED, nullptr); #endif @@ -74416,11 +74476,11 @@ void UITask::loop() { // and the keyboard was never even begun. That is why keys did nothing. // // The background task scans touch and (on the T-Deck) the keyboard. - // CrowPanel keeps LovyanGFX and Wire transactions on this thread. + // The CrowPanel 3.5 keeps LovyanGFX and Wire transactions on this thread. static bool s_con_input_up = false; if (!s_con_input_up) { if (heltecV4CapTouchBegin()) { -#if !defined(HAS_CROWPANEL_35) && !defined(HAS_CROWPANEL_50) +#if !defined(HAS_CROWPANEL_35) heltecV4CapTouchStartBackgroundPoll(8); #endif s_con_input_up = true; @@ -75273,11 +75333,12 @@ void UITask::loop() { } if (g_lv.touch_inited) { pushDiagLine("touch init ok"); - #if defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_CROWPANEL_50) + #if defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) // GT911 (LovyanGFX) and the TCA9535/ADS1115 (Wire) share I2C0 on - // Wio's GPIO47/48; the CrowPanels' GT911 shares I2C0 on GPIO15/16 (with - // the 5.0's backlight controller). + // Wio's GPIO47/48; the CrowPanel 3.5's GT911 shares I2C0 on GPIO15/16. // Keep every transaction on loopTask so polling cannot interrupt Wire. + // (The CrowPanel 5.0 reads its GT911 through Wire, whose lock makes the + // background poll below safe: an 800x480 redraw starves inline polling.) pushDiagLine("touch inline (shared I2C)"); #elif defined(HAS_TDECK_PRO) // The e-paper BUSY callback keeps this inline driver polling while the diff --git a/variants/crowpanel_50/CrowPanel50Display.cpp b/variants/crowpanel_50/CrowPanel50Display.cpp index d4cf472..62fbd89 100644 --- a/variants/crowpanel_50/CrowPanel50Display.cpp +++ b/variants/crowpanel_50/CrowPanel50Display.cpp @@ -11,10 +11,24 @@ namespace { constexpr int kPanelW = 800, kPanelH = 480; constexpr int kTouchSda = 15, kTouchScl = 16, kTouchInt = 1; -// 14 MHz rather than Elecrow's 21: the panel scans out of the same PSRAM that -// LVGL, WiFi and the radio stacks use, and the lower clock leaves them the -// bandwidth that otherwise shows up as the picture drifting or tearing. -constexpr uint32_t kPclkHz = 14000000; +// Pixel clock. Elecrow's factory code runs 21 MHz; Meshtastic and Camillia-MT +// run 14 MHz to leave PSRAM bandwidth for LVGL, WiFi and the radio. 12 MHz is +// too slow: the panel drops into its own colour-cycling self-test, which only a +// power cycle clears. The panel timings can be overridden with -D for bring-up +// on other panel revisions. +#ifndef CROWPANEL50_PCLK_HZ +#define CROWPANEL50_PCLK_HZ 14000000 +#endif +// Clock edge (LovyanGFX's pclk_idle_high sets the edge the data changes on). +// Elecrow's 1 is right: with 0 the data changes as the panel samples it, so +// single-pixel detail goes dark and gradients pick up stray lines. +#ifndef CROWPANEL50_PCLK_IDLE_HIGH +#define CROWPANEL50_PCLK_IDLE_HIGH 1 +#endif +#ifndef CROWPANEL50_HPORCH +#define CROWPANEL50_HPORCH 8 +#endif +constexpr uint32_t kPclkHz = CROWPANEL50_PCLK_HZ; // V1.1+ board MCU: single-byte commands at 0x30. 0 brightest .. 244 dimmest, // 245 backlight off, 250 wakes the touch panel (V1.2/V1.3 values). @@ -169,14 +183,14 @@ CrowPanel50Display::CrowPanel50Display() : DisplayDriver(kPanelW, kPanelH) { cfg.pin_pclk = GPIO_NUM_39; cfg.freq_write = kPclkHz; cfg.hsync_polarity = 0; - cfg.hsync_front_porch = 8; + cfg.hsync_front_porch = CROWPANEL50_HPORCH; cfg.hsync_pulse_width = 4; - cfg.hsync_back_porch = 8; + cfg.hsync_back_porch = CROWPANEL50_HPORCH; cfg.vsync_polarity = 0; cfg.vsync_front_porch = 8; cfg.vsync_pulse_width = 4; cfg.vsync_back_porch = 8; - cfg.pclk_idle_high = 1; + cfg.pclk_idle_high = CROWPANEL50_PCLK_IDLE_HIGH; _bus.config(cfg); } _panel.setBus(&_bus); @@ -230,9 +244,30 @@ bool CrowPanel50Display::begin() { return false; } _ctl.setBacklight(_brightness); +#ifdef CROWPANEL50_TESTPATTERN + // Bring-up aid: colour ramps straight to the panel, no LVGL. Clean, even steps mean + // the bus is fine; stray lines or flecks mean data bits are mis-sampled. + { + const int w = _lcd.width(), bh = _lcd.height() / 6; + for (int x = 0; x < w; ++x) { + const uint32_t r5 = x * 32 / w, g6 = x * 64 / w; + _lcd.fillRect(x, 0 * bh, 1, bh, (uint16_t)(r5 << 11)); + _lcd.fillRect(x, 1 * bh, 1, bh, (uint16_t)(g6 << 5)); + _lcd.fillRect(x, 2 * bh, 1, bh, (uint16_t)r5); + _lcd.fillRect(x, 3 * bh, 1, bh, (uint16_t)((r5 << 11) | (g6 << 5) | r5)); + _lcd.fillRect(x, 4 * bh, 1, bh, (uint16_t)((x & 1) ? 0xFFFF : 0x0000)); + const uint8_t r = 0x2D + (0xF4 - 0x2D) * x / w, g = 0xD4 + (0x72 - 0xD4) * x / w, + b = 0xBF + (0xB6 - 0xBF) * x / w; + _lcd.fillRect(x, 5 * bh, 1, _lcd.height() - 5 * bh, _lcd.color565(r, g, b)); + } + Serial.println("[crowpanel-50] test pattern (15 s)"); + delay(15000); + } +#endif _isOn = true; - Serial.printf("[crowpanel-50] display %dx%d, touch %s\n", width(), height(), - _lcd.touch() ? "up" : "not found"); + Serial.printf("[crowpanel-50] display %dx%d, pclk %lu Hz edge %d hporch %d, touch %s\n", + width(), height(), (unsigned long)kPclkHz, CROWPANEL50_PCLK_IDLE_HIGH, + CROWPANEL50_HPORCH, _lcd.touch() ? "up" : "not found"); return true; } @@ -285,10 +320,46 @@ void CrowPanel50Display::setBrightness(uint8_t value) { _ctl.setBacklight(value); } +// Reads the GT911 through Wire rather than LovyanGFX: Wire takes a lock, so the +// background touch task can share the bus with the backlight controller, and +// LovyanGFX drops a pending report whenever reads are more than 128 ms apart. +// Between reports the GT911 holds its last state, so that is what is returned. bool CrowPanel50Display::getTouchPoint(uint16_t& x, uint16_t& y) { - int32_t tx = 0, ty = 0; - if (!_lcd.getTouch(&tx, &ty)) return false; - x = (uint16_t)tx; - y = (uint16_t)ty; + if (!_touchAddr) return false; + uint8_t buf[9] = {0}; + Wire.beginTransmission(_touchAddr); + Wire.write(0x81); + Wire.write(0x4E); + // A STOP, not a repeated start: Wire tracks a repeated start in shared fields + // that are not task-safe, and loopTask's RTC reads use one. + bool ok = Wire.endTransmission() == 0 && + Wire.requestFrom(_touchAddr, (uint8_t)sizeof(buf)) == sizeof(buf); + if (ok) { + for (auto& b : buf) b = Wire.read(); + } + if (ok && (buf[0] & 0x80)) { + const uint8_t points = buf[0] & 0x0F; + if (points > 0 && points <= 5) { + lgfx::touch_point_t tp; + tp.id = buf[1]; + tp.x = (int16_t)(buf[2] | (buf[3] << 8)); + tp.y = (int16_t)(buf[4] | (buf[5] << 8)); + tp.size = (uint16_t)(buf[6] | (buf[7] << 8)); + _lcd.convertRawXY(&tp, 1); + _touchX = (uint16_t)std::max(0, tp.x); + _touchY = (uint16_t)std::max(0, tp.y); + _touchDown = true; + } else { + _touchDown = false; + } + Wire.beginTransmission(_touchAddr); + Wire.write(0x81); + Wire.write(0x4E); + Wire.write(0x00); + Wire.endTransmission(); + } + if (!_touchDown) return false; + x = _touchX; + y = _touchY; return true; } diff --git a/variants/crowpanel_50/CrowPanel50Display.h b/variants/crowpanel_50/CrowPanel50Display.h index def4818..46ae7d0 100644 --- a/variants/crowpanel_50/CrowPanel50Display.h +++ b/variants/crowpanel_50/CrowPanel50Display.h @@ -76,6 +76,8 @@ private: lgfx::LGFX_Device _lcd; CrowPanel50BoardCtl _ctl; uint8_t _touchAddr = 0; + bool _touchDown = false; + uint16_t _touchX = 0, _touchY = 0; uint8_t _brightness = 255; bool _isOn = false; uint16_t _color = 0xffff; From a8a07580880c90c21b328fa1c9c73f1815e19159 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Sun, 11 Oct 2026 20:12:41 -0400 Subject: [PATCH 3/6] CrowPanel 5: experimental LCD clock pause around flash writes Flash writes hold the bus shared with PSRAM, so the RGB panel's DMA underruns and the picture jumps sideways for a frame (confirmed with a flash-write trace while sending messages). This hook stops the LCD clock for the length of each flash write or erase. It's off by default and not yet tested on hardware. Also notes that 12 MHz pclk sends the panel into its self-test pattern. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- variants/crowpanel_50/FlashPanelPause.cpp | 53 +++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 variants/crowpanel_50/FlashPanelPause.cpp diff --git a/variants/crowpanel_50/FlashPanelPause.cpp b/variants/crowpanel_50/FlashPanelPause.cpp new file mode 100644 index 0000000..ffe1e77 --- /dev/null +++ b/variants/crowpanel_50/FlashPanelPause.cpp @@ -0,0 +1,53 @@ +// The ESP32-S3 shares one bus between flash and the octal PSRAM. While a flash +// write or erase holds it, the RGB panel's DMA can't read the framebuffer, the +// LCD FIFO runs dry and the picture jumps sideways until the next frame. The +// IDF 5 fixes (bounce buffers, flash auto-suspend) aren't in this Arduino core, +// so the LCD's clock source is switched off for the length of each flash +// operation: the panel holds its image for those few ms and nothing underruns. +// Experimental and untested: build with +// -DCROWPANEL50_FLASH_PAUSE -Wl,--wrap=esp_flash_write -Wl,--wrap=esp_flash_erase_region +#ifdef CROWPANEL50_FLASH_PAUSE +#include +#include +#include + +namespace { +portMUX_TYPE s_mux = portMUX_INITIALIZER_UNLOCKED; +int s_depth = 0; +uint32_t s_saved = 0; + +void lcdPause() { + portENTER_CRITICAL(&s_mux); + if (s_depth++ == 0) { + s_saved = LCD_CAM.lcd_clock.val; + if (LCD_CAM.lcd_clock.lcd_clk_sel != 0) LCD_CAM.lcd_clock.lcd_clk_sel = 0; + } + portEXIT_CRITICAL(&s_mux); +} + +void lcdResume() { + portENTER_CRITICAL(&s_mux); + if (--s_depth == 0) LCD_CAM.lcd_clock.val = s_saved; + portEXIT_CRITICAL(&s_mux); +} +} // namespace + +extern "C" { +esp_err_t __real_esp_flash_write(esp_flash_t*, const void*, uint32_t, uint32_t); +esp_err_t __real_esp_flash_erase_region(esp_flash_t*, uint32_t, uint32_t); + +esp_err_t __wrap_esp_flash_write(esp_flash_t* chip, const void* buf, uint32_t addr, uint32_t len) { + lcdPause(); + esp_err_t r = __real_esp_flash_write(chip, buf, addr, len); + lcdResume(); + return r; +} + +esp_err_t __wrap_esp_flash_erase_region(esp_flash_t* chip, uint32_t addr, uint32_t len) { + lcdPause(); + esp_err_t r = __real_esp_flash_erase_region(chip, addr, len); + lcdResume(); + return r; +} +} +#endif // CROWPANEL50_FLASH_PAUSE From 0a7a3714f339b75658f45cc18fcca856314d523f Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Mon, 12 Oct 2026 09:07:54 -0400 Subject: [PATCH 4/6] CrowPanel 5: pause the LCD clock during flash writes by default Tested on hardware: while sending messages, the sideways jump during flash writes is now a brief freeze or flicker, with no crash and no panel self-test. The crowpanel_50 env now sets CROWPANEL50_FLASH_PAUSE and wraps esp_flash_write and esp_flash_erase_region. DEVICES.md notes the behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DEVICES.md | 4 ++++ platformio.ini | 5 +++++ variants/crowpanel_50/FlashPanelPause.cpp | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/DEVICES.md b/DEVICES.md index e51b90d..d983abe 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -133,6 +133,10 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases 14 MHz pixel clock (Elecrow's demo uses 21 MHz); the slower clock leaves PSRAM bandwidth for WiFi and LVGL so the picture does not drift or tear. LVGL's draw band stays in internal RAM for the same reason. + - Flash writes (saving chats and settings) take the bus that the panel's DMA + shares with PSRAM, which made the picture jump sideways. The firmware stops + the LCD clock for each flash write or erase, so the screen briefly freezes + or flickers instead. - Backlight and touch reset sit behind an I2C controller on GPIO15/16 that changed between revisions: a PCA9557 at 0x18 on V1.0, a board MCU at 0x30 from V1.1. The driver detects which one answers, resets the GT911 through diff --git a/platformio.ini b/platformio.ini index 47eed4b..f0e1a34 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1560,6 +1560,11 @@ build_flags = ; No GPS, SD card, battery gauge or spare button: the 16-bit RGB panel takes ; most GPIOs and the DIP switch gives the rest to the radio. -D DISPLAY_CLASS=CrowPanel50Display + ; Stop the LCD clock during flash writes so the picture doesn't jump sideways + ; (variants/crowpanel_50/FlashPanelPause.cpp). + -D CROWPANEL50_FLASH_PAUSE + -Wl,--wrap=esp_flash_write + -Wl,--wrap=esp_flash_erase_region -D DISPLAY_ROTATION=3 -D DISPLAY_SCALE_X=1.0 -D DISPLAY_SCALE_Y=1.0 diff --git a/variants/crowpanel_50/FlashPanelPause.cpp b/variants/crowpanel_50/FlashPanelPause.cpp index ffe1e77..8131eac 100644 --- a/variants/crowpanel_50/FlashPanelPause.cpp +++ b/variants/crowpanel_50/FlashPanelPause.cpp @@ -4,7 +4,8 @@ // IDF 5 fixes (bounce buffers, flash auto-suspend) aren't in this Arduino core, // so the LCD's clock source is switched off for the length of each flash // operation: the panel holds its image for those few ms and nothing underruns. -// Experimental and untested: build with +// On hardware the sideways jump becomes a brief freeze or flicker. The +// crowpanel_50 env enables it with // -DCROWPANEL50_FLASH_PAUSE -Wl,--wrap=esp_flash_write -Wl,--wrap=esp_flash_erase_region #ifdef CROWPANEL50_FLASH_PAUSE #include From 320e5cb93bb4134263dc33bcbaf834bdd3b978d0 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Tue, 13 Oct 2026 09:01:59 -0400 Subject: [PATCH 5/6] CrowPanel 5: zero chat_back's shift in the chat header; docs say what was tested chatHeaderApply now resets chat_back's translate_y, which updateGlobalStatusBar skips in a chat, so a chat opened from a settings or app page no longer keeps the half-bar upward shift. DEVICES.md and the site describe the hardware testing, and the site counts 14 boards. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DEVICES.md | 10 +++++++--- deploy/site/index.html | 10 +++++----- src/ui-touch/UITask.cpp | 1 + 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/DEVICES.md b/DEVICES.md index d983abe..df31926 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -15,7 +15,7 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases | Tanmatsu | ESP32-P4 + ESP32-C6, SX1262 | 4" 800x480, 69-key keyboard (no touch) | Tanmatsu app store on the device (runs under the badge.team launcher, not web-flashable) | Store tracks the test channel | Fully supported; LoRa + Wi-Fi + Bluetooth simultaneously, standalone and companion in one | | Elecrow ThinkNode M9 | ESP32-S3, LR1110 | 2.4" 240x320 (no touch), I2C QWERTY + d-pad | Web flasher | Beta (new in beta_38) | Hardware-complete community port by ded (#138): GPS, microSD, buzzer, lock screen, d-pad navigation | | Elecrow CrowPanel Advance 3.5 | ESP32-S3, external SX1262 | 3.5" ILI9488 480x320 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | Requires an SX1262 module in the LoRa expansion slot; not for other CrowPanel models | -| Elecrow CrowPanel Advance 5.0 | ESP32-S3, external SX1262 | 5" ST7262 RGB 800x480 touch (GT911) | Web flasher (beta) | Beta, hardware validation pending | V1.1+ boards; SX1262 in the wireless slot with the function DIP switch on the wireless module; no SD, GPS or battery gauge | +| Elecrow CrowPanel Advance 5.0 | ESP32-S3, external SX1262 | 5" ST7262 RGB 800x480 touch (GT911) | Web flasher (beta) | Beta, hardware-tested (display, touch, radio) | V1.1+ boards; SX1262 in the wireless slot with the function DIP switch on the wireless module; no SD, GPS or battery gauge | | RAK WisMesh Tap V2 (RAK3312) | ESP32-S3, SX1262 | Touch display (LovyanGFX, 30+ fps) | Web flasher | Beta (new in beta_38) | Early community port by Ethac.chen (#136); core mesh, chat and map working | | LilyGo T-Lora Pager | ESP32-S3, LR1121 or SX1262 | 2.33" IPS LCD, 480x222 (no touch), QWERTY + rotary encoder | Web flasher (pick the build matching your radio) | Beta (new in beta_45) | Fully supported; keyboard-first navigation, microSD, map tile packs | | Heltec V4-R8 + Expansion Kit V2 | ESP32-S3 (8 MB octal PSRAM), SX1262 | 2.4" touch (CHSC6x) | Web flasher | Beta (new in beta_45) | As the V4 plus microSD and the Expansion Kit sensors; buzzer supported | @@ -155,8 +155,12 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases screen wakes by touch. It starts in native 800x480 landscape; Display settings can switch to 480x800 portrait. It uses the large-screen layouts and UI-size presets of the - Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Display, touch, - backlight and radio still need validation on a physical unit. Select it in + Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Tested on a physical + unit: a steady picture with smooth gradients, responsive typing with key + highlighting, the chat header and composer, and sending and receiving messages + over the radio (flash writes show as a brief freeze or flicker). Long flash + erases (OTA updates, a first-boot format, a factory reset) are not yet + verified with the LCD clock pause. Select it in the upload helper with `scripts/build-upload-monitor.sh --crowpanel-50`. ## Requested boards diff --git a/deploy/site/index.html b/deploy/site/index.html index 124cef9..bcf7318 100644 --- a/deploy/site/index.html +++ b/deploy/site/index.html @@ -5,13 +5,13 @@ WADAMESH: MeshCore firmware with the whole mesh on the radio - + - + @@ -524,7 +524,7 @@

    The whole mesh, on the radio itself.

    -

    MeshCore firmware with chat, channels, contacts, a map and apps on the device itself, by touch, keyboard or trackball, and a companion radio for the MeshCore app at the same time. Runs on 13 boards, from the LilyGo T-Deck and Heltec V4 to the Tanmatsu.

    +

    MeshCore firmware with chat, channels, contacts, a map and apps on the device itself, by touch, keyboard or trackball, and a companion radio for the MeshCore app at the same time. Runs on 14 boards, from the LilyGo T-Deck and Heltec V4 to the Tanmatsu.

    Offline map @@ -583,7 +583,7 @@
    -

    Runs on 13 boards

    +

    Runs on 14 boards

    From keyboard handhelds to touch panels. Pick yours to jump straight to its installer.

    LilyGoT-Deck / PlusKeyboard, trackball and touchscreen handheld.Fully supportedStable @@ -759,7 +759,7 @@ Download .bin

    Beta only. Attach the SX1262 before booting; flash with care and report what works.

    CrowPanel Advance 5.0 (experimental)

    5" 800×480 touch display; requires an SX1262 module in the wireless slot and the function DIP switch set to the wireless module.

    - Partially supportedHardware validation pendingThis build is for the Advance 5.0 (V1.1 and later) with an SX1262 in the wireless slot, not other CrowPanel models. Display, touch, backlight and radio follow the hardware reference; behaviour on a physical unit has not been verified. The DIP switch hands the radio's pins to the microSD slot otherwise, so there is no SD card; there is also no GPS and no battery monitoring. Landscape is the default, with portrait available in Display settings. + Partially supportedHardware-tested, betaThis build is for the Advance 5.0 (V1.1 and later) with an SX1262 in the wireless slot, not other CrowPanel models. Display, touch, backlight and radio are tested on a physical unit: a steady picture, responsive typing, chat and sending messages. Saving to flash briefly freezes or flickers the screen. The DIP switch hands the radio's pins to the microSD slot otherwise, so there is no SD card; there is also no GPS and no battery monitoring. Landscape is the default, with portrait available in Display settings. Download .bin

    Beta only. Set the DIP switch and attach the SX1262 before booting; flash with care and report what works.

    diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 7da50d7..575d233 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -55161,6 +55161,7 @@ static void chatHeaderApply(bool chat_open) { sbAlign(g_statusbar.chat_avatar, LV_ALIGN_LEFT_MID, x_av, 0); if (g_statusbar.chat_back) { sbTextColor(g_statusbar.chat_back, lv_color_hex(COLOR_TEXT)); + sbTranslateY(g_statusbar.chat_back, 0); sbAlign(g_statusbar.chat_back, LV_ALIGN_LEFT_MID, 6, 0); } // Right side: battery, then the clock just left of it, centred on the bar. From 1826e0f74cccf1eed07e574a722580934cf5c2f1 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Tue, 13 Oct 2026 09:09:54 -0400 Subject: [PATCH 6/6] DEVICES.md: the CrowPanel 5 recovers from long flash erases (full install, OTA) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- DEVICES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DEVICES.md b/DEVICES.md index df31926..4807c4f 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -158,9 +158,9 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases Tanmatsu. Serial uses the board's CH340 USB-to-UART bridge. Tested on a physical unit: a steady picture with smooth gradients, responsive typing with key highlighting, the chat header and composer, and sending and receiving messages - over the radio (flash writes show as a brief freeze or flicker). Long flash - erases (OTA updates, a first-boot format, a factory reset) are not yet - verified with the LCD clock pause. Select it in + over the radio (flash writes show as a brief freeze or flicker), and a fresh + full install and an on-device update, whose long flash erases the panel + recovers from. Select it in the upload helper with `scripts/build-upload-monitor.sh --crowpanel-50`. ## Requested boards