From d8200c35b3c64ff1f438e6ce9cefc47e1ffd4398 Mon Sep 17 00:00:00 2001 From: Confituurke <9595028+Confituurke@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:00:38 +0200 Subject: [PATCH] feat(sensecap-indicator): board skeleton, TCA9535 expander and build target Add the Seeed SenseCAP Indicator D1L as a new touch target. This step builds and boots to a blank screen; the panel, touch and radio drivers follow in their own commits so each can be reviewed against the hardware it drives. The SX1262's NSS, RESET, BUSY and DIO1, the LCD CS/reset and the touch reset all sit on a TCA9535 at 0x20 on the SDA39/SCL40 bus, not on ESP32 GPIOs, so everything after this commit depends on the expander. The env gives the radio lines virtual pin numbers 0x40..0x43 for a radio HAL to map onto expander bits; until that HAL exists the radio is a plain Module that links but cannot drive the chip. The expander driver is header-only so a host test can cover it: - its output and config shadows are seeded from the chip, because a warm reset or the previous firmware may have left pins driven and writing the power-on defaults back would glitch the radio, panel and RP2040 resets; - a failed write rolls both shadows back to the last state written in full, and the next write brings the chip back in line with them; - polarity inversion is cleared when a previous firmware left it set, since it would invert BUSY, DIO1 and the strap; it is written only when it is not already 0; - every read of input port 0 is reported to an observer, so a DIO1 edge is not lost when another reader clears /INT. A five-sample majority of the P1.3 strap selects the TCXO (2.4 V) or crystal radio at run time, so one noisy read cannot pick the wrong oscillator. The expander, the touch driver and the display init serialise on one recursive mutex over the shared Wire bus (IndicatorIo::BusLock). It is a function-local static, so the compiler guards its creation when two tasks reach the first lock together. IndicatorIo::begin() logs the registers it inherited and names the step that failed, since a dead expander takes the panel and radio down with it and is otherwise silent. The board has 8 MB of flash. The partition table has one 4.5 MB factory app slot, a 3.375 MB SPIFFS (as on the 16 MB boards) and a 64 KB coredump, with no map tile partition. The factory slot is larger than the 0x3E0000 OTA slots of the 16 MB boards, so an image that fits those fits here. CAP_OTA is off: with one app slot there is no in-place update. CAP_BATTERY is 0: no battery is fitted. Montserrat 18/20/24 for the UI-size presets are the generated faces; the board joins their gate in ui_font_18/20/24.c, gen-touch-fonts.sh, hwFitFont and luaHostFigureFont. LVGL is built at -O2 with the Arabic and newlib-memcpy settings the other envs use. run-host-tests.sh builds every test/test_*.cpp, so the new expander test gets its include path there in the same commit. build-upload-monitor.sh gets --sensecap-indicator. Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com> --- boards/sensecap_indicator.json | 38 +++ platformio.ini | 157 ++++++++++++ scripts/build-upload-monitor.sh | 5 + scripts/build/gen-touch-fonts.sh | 2 +- scripts/run-host-tests.sh | 1 + src/ui-touch/UITask.cpp | 10 +- src/ui-touch/device_caps.h | 11 + src/ui-touch/ui_font_18.c | 4 +- src/ui-touch/ui_font_20.c | 4 +- src/ui-touch/ui_font_24.c | 4 +- test/test_indicator_expander.cpp | 229 ++++++++++++++++++ .../sensecap_indicator/IndicatorBoard.cpp | 11 + variants/sensecap_indicator/IndicatorBoard.h | 12 + .../sensecap_indicator/IndicatorDisplay.cpp | 25 ++ .../sensecap_indicator/IndicatorDisplay.h | 33 +++ .../sensecap_indicator/IndicatorExpander.h | 148 +++++++++++ variants/sensecap_indicator/IndicatorIo.cpp | 104 ++++++++ variants/sensecap_indicator/IndicatorIo.h | 29 +++ .../sensecap_indicator/IndicatorTouch.cpp | 24 ++ .../partitions_sensecap_indicator.csv | 8 + variants/sensecap_indicator/target.cpp | 37 +++ variants/sensecap_indicator/target.h | 30 +++ 22 files changed, 917 insertions(+), 9 deletions(-) create mode 100644 boards/sensecap_indicator.json create mode 100644 test/test_indicator_expander.cpp create mode 100644 variants/sensecap_indicator/IndicatorBoard.cpp create mode 100644 variants/sensecap_indicator/IndicatorBoard.h create mode 100644 variants/sensecap_indicator/IndicatorDisplay.cpp create mode 100644 variants/sensecap_indicator/IndicatorDisplay.h create mode 100644 variants/sensecap_indicator/IndicatorExpander.h create mode 100644 variants/sensecap_indicator/IndicatorIo.cpp create mode 100644 variants/sensecap_indicator/IndicatorIo.h create mode 100644 variants/sensecap_indicator/IndicatorTouch.cpp create mode 100644 variants/sensecap_indicator/partitions_sensecap_indicator.csv create mode 100644 variants/sensecap_indicator/target.cpp create mode 100644 variants/sensecap_indicator/target.h diff --git a/boards/sensecap_indicator.json b/boards/sensecap_indicator.json new file mode 100644 index 0000000..eb18a5d --- /dev/null +++ b/boards/sensecap_indicator.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-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": "Seeed SenseCAP Indicator D1L (8 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 8388608, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 921600 + }, + "url": "", + "vendor": "Seeed Studio" +} diff --git a/platformio.ini b/platformio.ini index 52637b5..28cd008 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1635,6 +1635,163 @@ lib_deps = densaugeo/base64 @ ~1.4.0 knolleary/PubSubClient @ ^2.8.0 +; ====================================================================== +; Seeed SenseCAP Indicator D1L — 480x480 RGB touch panel, SX1262 behind a TCA9535. +; The radio's NSS/RESET/BUSY/DIO1 are expander pins; P_LORA_* 0x40..0x43 are +; virtual pin numbers the radio HAL maps onto them. 8 MB flash, two OTA slots. +; ====================================================================== +[env:sensecap_indicator_companion_radio_touch] +platform = platformio/espressif32@6.11.0 +framework = arduino +board = sensecap_indicator +monitor_speed = 115200 +upload_speed = 921600 +board_build.arduino.memory_type = qio_opi +board_build.flash_size = 8MB +board_upload.flash_size = 8MB +board_upload.maximum_size = 8388608 +board_build.partitions = variants/sensecap_indicator/partitions_sensecap_indicator.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_meshcore_txt_timestamp.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 +; No custom_lvgl_iram: it costs +5.9 KB IRAM, and with this board's 23 KB internal draw +; band (see UITask::begin) that would leave less internal heap than the T-Deck's. + +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/sensecap_indicator + -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_SENSECAP_INDICATOR=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 + -D P_LORA_NSS=0x40 + -D P_LORA_RESET=0x41 + -D P_LORA_BUSY=0x42 + -D P_LORA_DIO_1=0x43 + -D P_LORA_SCLK=41 + -D P_LORA_MISO=47 + -D P_LORA_MOSI=48 + -D SX126X_DIO2_AS_RF_SWITCH=true + ; The radio strap picks TCXO (2.4 V) or crystal at run time. The inner quotes + ; keep the parentheses away from the shell that runs the compiler. + -D SX126X_DIO3_TCXO_VOLTAGE="'indicatorTcxoVoltage()'" + -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=39 + -D PIN_BOARD_SCL=40 + -D PIN_USER_BTN=38 + -D PIN_TFT_LEDA_CTL=-1 + -D PIN_TFT_RST=-1 + -D PIN_TFT_DC=-1 + -D DISPLAY_CLASS=IndicatorDisplay + -D DISPLAY_ROTATION=0 + -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='"sensecap_indicator_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) + ; The UI-size presets use Montserrat 18/20/24. Those are generated fonts + ; (src/ui-touch/ui_font_NN.c, gated on this board's macro in the generator), not + ; LVGL's: never force LV_FONT_MONTSERRAT_NN=1 here, it would link LVGL's copy too. + -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/sensecap_indicator/*.cpp> + +lib_deps = + https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.18.0 + SPI + Wire + SD_MMC + 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 e753fc9..daeb68f 100755 --- a/scripts/build-upload-monitor.sh +++ b/scripts/build-upload-monitor.sh @@ -16,6 +16,7 @@ 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" +INDICATOR_ENV="sensecap_indicator_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" @@ -96,6 +97,7 @@ env_label() { "$WIO_ENV") echo "Seeed Wio Tracker L2" ;; "$CROWPANEL_ENV") echo "Elecrow CrowPanel Advance 3.5" ;; "$CROWPANEL_50_ENV") echo "Elecrow CrowPanel Advance 5.0" ;; + "$INDICATOR_ENV") echo "Seeed SenseCAP Indicator D1L" ;; "$PAGER_LR1121_ENV") echo "LilyGo T-LoRa Pager LR1121" ;; "$PAGER_SX1262_ENV") echo "LilyGo T-LoRa Pager SX1262" ;; "$M9_ENV") echo "ThinkNode M9" ;; @@ -123,6 +125,7 @@ Devices: --wio Seeed Wio Tracker L2 --crowpanel-35 Elecrow CrowPanel Advance 3.5 --crowpanel-50 Elecrow CrowPanel Advance 5.0 + --sensecap-indicator Seeed SenseCAP Indicator D1L --pager-lr1121 LilyGo T-LoRa Pager LR1121 --pager-sx1262 LilyGo T-LoRa Pager SX1262 --m9 ThinkNode M9 @@ -175,6 +178,7 @@ prompt_for_device() { "$WIO_ENV" "$CROWPANEL_ENV" "$CROWPANEL_50_ENV" + "$INDICATOR_ENV" "$PAGER_LR1121_ENV" "$PAGER_SX1262_ENV" "$RAK_ENV" @@ -255,6 +259,7 @@ while [ $# -gt 0 ]; do --wio) select_env "$WIO_ENV" ;; --crowpanel-35) select_env "$CROWPANEL_ENV" ;; --crowpanel-50) select_env "$CROWPANEL_50_ENV" ;; + --sensecap-indicator) select_env "$INDICATOR_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-touch-fonts.sh b/scripts/build/gen-touch-fonts.sh index a7bd3db..01da1b9 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE)" +ui_big="defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE)" for size in 12 14 16 28; do generate_ui "$size" done diff --git a/scripts/run-host-tests.sh b/scripts/run-host-tests.sh index 6ba47cc..e534423 100755 --- a/scripts/run-host-tests.sh +++ b/scripts/run-host-tests.sh @@ -25,6 +25,7 @@ extra_flags() { # Its subject is a board variant header, not shared src/. test_crowpanel_sd_codec) echo "-I variants/crowpanel_35" ;; test_m9_keyboard) echo "-I test/m9_keyboard_fakes" ;; + test_indicator_expander) echo "-I variants/sensecap_indicator" ;; *) echo "" ;; esac } diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 55e91cd..b21c168 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -211,6 +211,8 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM #include #elif defined(TLORA_PAGER) #include + #elif defined(HAS_SENSECAP_INDICATOR) + #include #elif defined(HAS_WIO_TRACKER_L2) #include #include @@ -285,6 +287,8 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM extern TDeckProDisplay display; #elif defined(TLORA_PAGER) extern ST7796LCDDisplay display; + #elif defined(HAS_SENSECAP_INDICATOR) + extern IndicatorDisplay display; #elif defined(HAS_WIO_TRACKER_L2) extern WioTrackerL2Display display; #elif defined(HAS_CROWPANEL_35) @@ -34537,6 +34541,8 @@ static int wifiScanWatchdogSafe(uint32_t cap_ms, uint16_t per_chan_ms = 300) { #endif #elif defined(ATTAKY_MESH_SERIES) #define WADA_BOARD_ID "attaky" +#elif defined(HAS_SENSECAP_INDICATOR) +#define WADA_BOARD_ID "sensecap-indicator" #elif defined(HAS_WIO_TRACKER_L2) #define WADA_BOARD_ID "wio-tracker-l2" #elif defined(HAS_CROWPANEL_35) @@ -59066,7 +59072,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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || 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_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE) &lv_font_montserrat_24, &lv_font_montserrat_20, #endif &g_font_semi_16, &g_font_semi_14, &g_font_12, @@ -68764,7 +68770,7 @@ bool luaHostScreenOn() { // It never comes out smaller than the regular 16, which on a scaled-up UI already is // Montserrat 24. Older firmware maps any size above 16 to 16, so apps stay readable. static const lv_font_t* luaHostFigureFont() { -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE) if (lv_font_get_line_height(&lv_font_montserrat_24) > lv_font_get_line_height(&g_font_16)) return &lv_font_montserrat_24; #endif diff --git a/src/ui-touch/device_caps.h b/src/ui-touch/device_caps.h index c4b40bf..87ebc20 100644 --- a/src/ui-touch/device_caps.h +++ b/src/ui-touch/device_caps.h @@ -49,6 +49,17 @@ #define CAP_LOCK_SCREEN 0 // no user button to unlock with #define CAP_BATTERY 0 // no battery voltage sensing +#elif defined(HAS_SENSECAP_INDICATOR) // ===== Seeed SenseCAP Indicator D1L (ESP32-S3) ===== + #define CAP_TOUCH 1 + #define CAP_ROTATABLE 0 + #define CAP_LARGE_SCREEN 1 // 480x480 + #define CAP_SD 0 + #define CAP_FILESYSTEM 1 + #define CAP_GPS 0 + #define CAP_OTA 0 // one factory app slot, no in-place update + #define CAP_LOCK_SCREEN 0 + #define CAP_BATTERY 0 // no battery fitted + #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 6dd0522..906cc5a 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) */ +#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE) */ diff --git a/src/ui-touch/ui_font_20.c b/src/ui-touch/ui_font_20.c index b0388c6..840d3d4 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) */ +#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE) */ diff --git a/src/ui-touch/ui_font_24.c b/src/ui-touch/ui_font_24.c index bf34783..ef18fb7 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || 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(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(DOC_CAPTURE) */ +#endif /* defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_CROWPANEL_P4_101) || defined(HAS_CROWPANEL_50) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_SENSECAP_INDICATOR) || defined(DOC_CAPTURE) */ diff --git a/test/test_indicator_expander.cpp b/test/test_indicator_expander.cpp new file mode 100644 index 0000000..d7d6704 --- /dev/null +++ b/test/test_indicator_expander.cpp @@ -0,0 +1,229 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// +// Host test for the SenseCAP Indicator TCA9535 driver (IndicatorExpander.h): the +// shadow registers, the write order, the rollback on a failed write and the +// radio-strap vote. +// +// c++ -std=c++17 -Wall -Wextra -I src -I variants/sensecap_indicator test/test_indicator_expander.cpp -o /tmp/ind_exp && /tmp/ind_exp + +#include +#include +#include + +#include "IndicatorExpander.h" + +using namespace indicator; + +// Eight registers of a TCA9535 and a log of every write that reached it. +struct FakeBus : ExpanderBus { + struct Write { uint8_t reg; uint8_t bytes[2]; size_t n; }; + + uint8_t regs[8] = {}; + bool reads[8] = {}; + Write log[32] = {}; + size_t writes = 0; + int fail_write_at = 0; // 0: never fail; N: the Nth write from now fails + + bool read(uint8_t reg, uint8_t* data, size_t n) override { + for (size_t i = 0; i < n; ++i) { + data[i] = regs[reg + i]; + reads[reg + i] = true; + } + return true; + } + + bool write(uint8_t reg, const uint8_t* data, size_t n) override { + if (fail_write_at > 0 && --fail_write_at == 0) return false; + Write& w = log[writes++]; + w.reg = reg; + w.n = n; + for (size_t i = 0; i < n; ++i) { w.bytes[i] = data[i]; regs[reg + i] = data[i]; } + return true; + } +}; + +static void seedChip(FakeBus& bus, uint8_t out0, uint8_t out1, uint8_t cfg0, uint8_t cfg1) { + bus.regs[0x02] = out0; bus.regs[0x03] = out1; + bus.regs[0x06] = cfg0; bus.regs[0x07] = cfg1; +} + +static void seeds_shadow_from_chip_not_defaults() { + FakeBus bus; + seedChip(bus, 0x5A, 0x0F, 0x4C, 0xFF); + Tca9535 chip(bus); + assert(chip.begin()); + assert(chip.output(0) == 0x5A); + assert(chip.output(1) == 0x0F); + assert(chip.config(0) == 0x4C); + assert(chip.config(1) == 0xFF); + assert(bus.reads[0x00]); // /INT released + assert(bus.writes == 0); +} + +static void latch_before_direction() { + FakeBus bus; + seedChip(bus, 0x00, 0x00, 0xFF, 0xFF); + Tca9535 chip(bus); + assert(chip.begin()); + assert(chip.setOutput(pins::kLcdCs, true)); + assert(bus.writes == 2); + assert(bus.log[0].reg == 0x02); + assert(bus.log[1].reg == 0x06); + assert(chip.output(0) & 0x10); + assert(!(chip.config(0) & 0x10)); +} + +static void output_pin_writes_level_only() { + FakeBus bus; + seedChip(bus, 0x00, 0x00, 0xFF, 0xFF); + Tca9535 chip(bus); + assert(chip.begin()); + assert(chip.setOutput(pins::kLoraNss, true)); + assert(bus.writes == 2); + // Already an output: the level changes, the config register is left alone. + assert(chip.setOutput(pins::kLoraNss, false)); + assert(chip.setOutput(pins::kLoraNss, true)); + assert(bus.writes == 4); + assert(bus.log[2].reg == 0x02 && bus.log[3].reg == 0x02); + assert(bus.regs[0x02] & 0x01); + assert(!(bus.regs[0x06] & 0x01)); +} + +static void port1_untouched_except_written_pin() { + FakeBus bus; + seedChip(bus, 0x00, 0xA4, 0xFF, 0xF7); + Tca9535 chip(bus); + assert(chip.begin()); + assert(chip.setOutput(pins::kRp2040Reset, true)); + assert(chip.output(1) == 0xA5); + assert(chip.config(1) == 0xF6); + assert(bus.regs[0x03] == 0xA5); + assert(bus.regs[0x07] == 0xF6); + assert((bus.regs[0x03] & 0xFE) == (0xA4 & 0xFE)); + assert((bus.regs[0x07] & 0xFE) == (0xF7 & 0xFE)); +} + +static void failed_write_rolls_back_shadow() { + FakeBus bus; + seedChip(bus, 0x01, 0x00, 0xFF, 0xFF); + Tca9535 chip(bus); + assert(chip.begin()); + const uint8_t out0 = chip.output(0), out1 = chip.output(1); + const uint8_t cfg0 = chip.config(0), cfg1 = chip.config(1); + + bus.fail_write_at = 1; + assert(!chip.setOutput(pins::kLoraNss, false)); + assert(chip.output(0) == out0 && chip.output(1) == out1); + assert(chip.config(0) == cfg0 && chip.config(1) == cfg1); + + // The next write starts from the restored shadow, not from the half-applied one. + assert(chip.setOutput(pins::kLcdCs, true)); + assert(bus.writes == 2); + assert(bus.log[0].reg == 0x02 && bus.log[0].bytes[0] == (out0 | 0x10)); + assert(bus.log[1].reg == 0x06 && bus.log[1].bytes[0] == (cfg0 & (uint8_t)~0x10)); +} + +static void failed_config_write_rolls_back_both_shadows() { + FakeBus bus; + seedChip(bus, 0x01, 0x00, 0xFF, 0xFF); + Tca9535 chip(bus); + assert(chip.begin()); + const uint8_t out0 = chip.output(0), out1 = chip.output(1); + const uint8_t cfg0 = chip.config(0), cfg1 = chip.config(1); + + // The output write (0x02) lands, the config write (0x06) fails. + bus.fail_write_at = 2; + assert(!chip.setOutput(pins::kLcdReset, true)); + assert(bus.writes == 1 && bus.log[0].reg == 0x02); + assert(chip.output(0) == out0 && chip.output(1) == out1); + assert(chip.config(0) == cfg0 && chip.config(1) == cfg1); + + // The next write starts from the restored shadow: LCD RESET is not carried + // over from the half-applied call, and the chip's output latch is rewritten. + assert(chip.setOutput(pins::kLcdCs, true)); + assert(bus.writes == 3); + assert(bus.log[1].reg == 0x02 && bus.log[1].bytes[0] == (out0 | 0x10)); + assert(bus.log[1].bytes[1] == out1); + assert(bus.log[2].reg == 0x06 && bus.log[2].bytes[0] == (cfg0 & (uint8_t)~0x10)); + assert(bus.log[2].bytes[1] == cfg1); + assert(bus.regs[0x02] == (out0 | 0x10)); +} + +static uint8_t s_seen = 0; +static int s_calls = 0; +static void* s_ctx = nullptr; + +static void observer(uint8_t value, void* ctx) { + s_seen = value; + s_ctx = ctx; + ++s_calls; +} + +static void port0_observer_sees_every_read() { + FakeBus bus; + seedChip(bus, 0x00, 0x00, 0xFF, 0xFF); + Tca9535 chip(bus); + assert(chip.begin()); + int token = 0; + chip.setPort0Observer(observer, &token); + + bus.regs[0x00] = 0x0C; // BUSY and DIO1 high + bool high = false; + assert(chip.readPin(pins::kLoraBusy, high)); + assert(high); + assert(s_calls == 1 && s_seen == 0x0C && s_ctx == &token); + + bus.regs[0x00] = 0x08; + uint8_t value = 0; + assert(chip.readPort0(value)); + assert(value == 0x08); + assert(s_calls == 2 && s_seen == 0x08); + + // A port-1 pin read covers both input bytes, so port 0 is reported too. + bus.regs[0x00] = 0x48; + bus.regs[0x01] = 0x08; + assert(chip.readPin(pins::kRadioStrap, high)); + assert(high); + assert(s_calls == 3 && s_seen == 0x48); +} + +static void strap_majority_vote() { + const bool a[5] = {true, true, false, false, true}; + const bool b[5] = {true, false, false, false, true}; + const bool c[5] = {true, true, true, false, false}; + assert(strapMajority(a)); + assert(!strapMajority(b)); + assert(strapMajority(c)); +} + +static void clears_polarity_inversion_only_when_set() { + FakeBus clean; + seedChip(clean, 0xFF, 0xFF, 0x4C, 0xFE); + Tca9535 a(clean); + assert(a.begin()); + assert(clean.writes == 0); // already 0: nothing written + + FakeBus inverted; + seedChip(inverted, 0xFF, 0xFF, 0x4C, 0xFE); + inverted.regs[0x04] = 0x0C; inverted.regs[0x05] = 0x08; + Tca9535 b(inverted); + assert(b.begin()); + assert(inverted.writes == 1); + assert(inverted.log[0].reg == 0x04 && inverted.log[0].n == 2); + assert(inverted.regs[0x04] == 0x00 && inverted.regs[0x05] == 0x00); + assert(b.output(0) == 0xFF && b.config(0) == 0x4C); // shadows still from the chip +} + +int main() { + seeds_shadow_from_chip_not_defaults(); + clears_polarity_inversion_only_when_set(); + latch_before_direction(); + output_pin_writes_level_only(); + port1_untouched_except_written_pin(); + failed_write_rolls_back_shadow(); + failed_config_write_rolls_back_both_shadows(); + port0_observer_sees_every_read(); + strap_majority_vote(); + printf("all indicator expander tests passed\n"); + return 0; +} diff --git a/variants/sensecap_indicator/IndicatorBoard.cpp b/variants/sensecap_indicator/IndicatorBoard.cpp new file mode 100644 index 0000000..10143e3 --- /dev/null +++ b/variants/sensecap_indicator/IndicatorBoard.cpp @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "IndicatorBoard.h" + +#include + +#include "IndicatorIo.h" + +void IndicatorBoard::begin() { + ESP32Board::begin(); // starts Wire on PIN_BOARD_SDA/PIN_BOARD_SCL + if (!IndicatorIo::begin()) Serial.println("[indicator] expander unavailable"); +} diff --git a/variants/sensecap_indicator/IndicatorBoard.h b/variants/sensecap_indicator/IndicatorBoard.h new file mode 100644 index 0000000..2606603 --- /dev/null +++ b/variants/sensecap_indicator/IndicatorBoard.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +class IndicatorBoard : public ESP32Board { +public: + void begin(); + uint16_t getBattMilliVolts() override { return 0; } // mains-powered, no battery + bool isExternalPowered() override { return true; } + const char* getManufacturerName() const override { return "Seeed SenseCAP Indicator"; } +}; diff --git a/variants/sensecap_indicator/IndicatorDisplay.cpp b/variants/sensecap_indicator/IndicatorDisplay.cpp new file mode 100644 index 0000000..af64f97 --- /dev/null +++ b/variants/sensecap_indicator/IndicatorDisplay.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "IndicatorDisplay.h" + +// Placeholder until the panel driver lands: the target links and boots to a +// blank screen. + +bool IndicatorDisplay::begin() { return false; } + +void IndicatorDisplay::turnOn() {} +void IndicatorDisplay::turnOff() {} +void IndicatorDisplay::clear() {} +void IndicatorDisplay::startFrame(ColorVal) {} +void IndicatorDisplay::setTextSize(int) {} +void IndicatorDisplay::setColor(ColorVal) {} +void IndicatorDisplay::setCursor(int, int) {} +void IndicatorDisplay::print(const char*) {} +void IndicatorDisplay::fillRect(int, int, int, int) {} +void IndicatorDisplay::drawRect(int, int, int, int) {} +void IndicatorDisplay::drawXbm(int, int, const uint8_t*, int, int) {} +uint16_t IndicatorDisplay::getTextWidth(const char*) { return 0; } +void IndicatorDisplay::endFrame() {} + +void IndicatorDisplay::writePixelsRGB565(int, int, int, int, const uint16_t*) {} +void IndicatorDisplay::setDisplayRotation(uint8_t) {} +void IndicatorDisplay::setBrightness(uint8_t) {} diff --git a/variants/sensecap_indicator/IndicatorDisplay.h b/variants/sensecap_indicator/IndicatorDisplay.h new file mode 100644 index 0000000..b2fa624 --- /dev/null +++ b/variants/sensecap_indicator/IndicatorDisplay.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +// 480x480 ST7701S RGB panel. +class IndicatorDisplay : public DisplayDriver { +public: + IndicatorDisplay() : DisplayDriver(480, 480) {} + 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 writePixelsRGB565(int x, int y, int w, int h, const uint16_t* px); + void setDisplayRotation(uint8_t rotation); + void setBrightness(uint8_t brightness); + +private: + bool _isOn = false; +}; diff --git a/variants/sensecap_indicator/IndicatorExpander.h b/variants/sensecap_indicator/IndicatorExpander.h new file mode 100644 index 0000000..be7135f --- /dev/null +++ b/variants/sensecap_indicator/IndicatorExpander.h @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +// TCA9535 I/O expander on the SenseCAP Indicator. It carries the SX1262 control +// lines, the LCD and touch resets, the RP2040 reset and the radio strap, so the +// radio and the panel both depend on it. Header-only and hardware-free: the I2C +// transport is an ExpanderBus, which the host test replaces with a fake. + +#include +#include + +namespace indicator { + +// Pin n is port n>>3, bit n&7. +namespace pins { +constexpr uint8_t kLoraNss = 0; +constexpr uint8_t kLoraReset = 1; +constexpr uint8_t kLoraBusy = 2; // input +constexpr uint8_t kLoraDio1 = 3; // input +constexpr uint8_t kLcdCs = 4; +constexpr uint8_t kLcdReset = 5; +constexpr uint8_t kTouchInt = 6; // input +constexpr uint8_t kTouchReset = 7; +constexpr uint8_t kRp2040Reset = 8; +constexpr uint8_t kRadioStrap = 11; // input, high = TCXO +} // namespace pins + +struct ExpanderBus { + virtual bool read(uint8_t reg, uint8_t* data, size_t n) = 0; + virtual bool write(uint8_t reg, const uint8_t* data, size_t n) = 0; + virtual ~ExpanderBus() = default; +}; + +class Tca9535 { +public: + explicit Tca9535(ExpanderBus& bus) : _bus(bus) {} + + // The shadows come from the chip, not from the power-on defaults: a warm reset + // or a previous firmware may have left pins driven, and writing 0xFF/0x00 back + // would glitch them. Polarity inversion is cleared, since a previous firmware + // that set it would invert BUSY, DIO1 and the strap; it changes no output, and + // it is written only when it is not already 0. The input read releases /INT. + bool begin() { + uint8_t out[2], cfg[2], pol[2], in[2]; + if (!_bus.read(kRegOutput0, out, 2) || !_bus.read(kRegConfig0, cfg, 2) || + !_bus.read(kRegPolarity0, pol, 2)) return false; + _out[0] = out[0]; _out[1] = out[1]; + _cfg[0] = cfg[0]; _cfg[1] = cfg[1]; + static constexpr uint8_t kNoInversion[2] = {0x00, 0x00}; + if ((pol[0] | pol[1]) && !_bus.write(kRegPolarity0, kNoInversion, 2)) return false; + return readInputs(in); + } + + // Latch the level before turning the pin into an output, so it never drives + // the stale level for a moment. + bool setOutput(uint8_t pin, bool high) { + if (pin > 15) return false; + const uint8_t port = pin >> 3; + const uint8_t mask = (uint8_t)(1U << (pin & 7)); + const uint8_t out[2] = {_out[0], _out[1]}; + const uint8_t cfg[2] = {_cfg[0], _cfg[1]}; + if (high) _out[port] |= mask; + else _out[port] &= (uint8_t)~mask; + _cfg[port] &= (uint8_t)~mask; + // A pin that already is an output needs only the level (the radio's NSS + // toggles on every SPI command). + if (_bus.write(kRegOutput0, _out, 2) && + (_cfg[port] == cfg[port] || _bus.write(kRegConfig0, _cfg, 2))) + return true; + _out[0] = out[0]; _out[1] = out[1]; + _cfg[0] = cfg[0]; _cfg[1] = cfg[1]; + return false; + } + + bool setInput(uint8_t pin) { + if (pin > 15) return false; + const uint8_t cfg[2] = {_cfg[0], _cfg[1]}; + _cfg[pin >> 3] |= (uint8_t)(1U << (pin & 7)); + if (_bus.write(kRegConfig0, _cfg, 2)) return true; + _cfg[0] = cfg[0]; _cfg[1] = cfg[1]; + return false; + } + + bool readPort0(uint8_t& value) { + if (!_bus.read(kRegInput0, &value, 1)) return false; + notify(value); + return true; + } + + // A port-1 pin reads both input bytes, so a port-0 change that this read + // clears from /INT still reaches the observer. + bool readPin(uint8_t pin, bool& high) { + if (pin > 15) return false; + const uint8_t mask = (uint8_t)(1U << (pin & 7)); + if (pin < 8) { + uint8_t value = 0; + if (!readPort0(value)) return false; + high = (value & mask) != 0; + return true; + } + uint8_t in[2]; + if (!readInputs(in)) return false; + high = (in[1] & mask) != 0; + return true; + } + + uint8_t output(uint8_t port) const { return _out[port & 1]; } + uint8_t config(uint8_t port) const { return _cfg[port & 1]; } + + // Every read of input port 0 reports the raw byte here. Reading the port clears + // /INT, so whoever watches DIO1 must see the reads other callers make too. + void setPort0Observer(void (*fn)(uint8_t value, void* ctx), void* ctx) { + _observer = fn; + _observerCtx = ctx; + } + +private: + static constexpr uint8_t kRegInput0 = 0x00; + static constexpr uint8_t kRegOutput0 = 0x02; + static constexpr uint8_t kRegPolarity0 = 0x04; + static constexpr uint8_t kRegConfig0 = 0x06; + + bool readInputs(uint8_t in[2]) { + if (!_bus.read(kRegInput0, in, 2)) return false; + notify(in[0]); + return true; + } + + void notify(uint8_t value) { + if (_observer) _observer(value, _observerCtx); + } + + ExpanderBus& _bus; + uint8_t _out[2] = {0, 0}; + uint8_t _cfg[2] = {0xFF, 0xFF}; + void (*_observer)(uint8_t value, void* ctx) = nullptr; + void* _observerCtx = nullptr; +}; + +// The strap is read five times; three or more highs win, so one noisy read +// cannot flip the TCXO selection. +inline bool strapMajority(const bool samples[5]) { + int high = 0; + for (int i = 0; i < 5; ++i) high += samples[i] ? 1 : 0; + return high >= 3; +} + +} // namespace indicator diff --git a/variants/sensecap_indicator/IndicatorIo.cpp b/variants/sensecap_indicator/IndicatorIo.cpp new file mode 100644 index 0000000..36e90fe --- /dev/null +++ b/variants/sensecap_indicator/IndicatorIo.cpp @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "IndicatorIo.h" + +#include +#include +#include +#include + +namespace { + +constexpr uint8_t kAddress = 0x20; + +// Created on first use; the compiler guards the local static's initialisation, +// so two tasks that reach the first BusLock together still get one mutex. +SemaphoreHandle_t busMutex() { + static StaticSemaphore_t storage; + static const SemaphoreHandle_t handle = xSemaphoreCreateRecursiveMutexStatic(&storage); + return handle; +} + +class WireBus : public indicator::ExpanderBus { +public: + bool read(uint8_t reg, uint8_t* data, size_t n) override { + IndicatorIo::BusLock lock; + Wire.beginTransmission(kAddress); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return false; + if (Wire.requestFrom((int)kAddress, (int)n) != n) return false; + for (size_t i = 0; i < n; ++i) data[i] = (uint8_t)Wire.read(); + return true; + } + + bool write(uint8_t reg, const uint8_t* data, size_t n) override { + IndicatorIo::BusLock lock; + Wire.beginTransmission(kAddress); + Wire.write(reg); + for (size_t i = 0; i < n; ++i) Wire.write(data[i]); + return Wire.endTransmission() == 0; + } +}; + +WireBus s_bus; +indicator::Tca9535 s_chip(s_bus); +bool s_ready = false; +bool s_tcxo = false; + +} // namespace + +namespace IndicatorIo { + +BusLock::BusLock() { xSemaphoreTakeRecursive(busMutex(), portMAX_DELAY); } +BusLock::~BusLock() { xSemaphoreGiveRecursive(busMutex()); } + +bool begin() { + using namespace indicator::pins; + BusLock lock; + s_ready = false; + Wire.setClock(400000); + + if (!s_chip.begin()) { + Serial.println("[indicator] expander: seed read failed (no ack at 0x20?)"); + return false; + } + // What the previous firmware or a warm reset left behind. + Serial.printf("[indicator] expander seeded out=%02X/%02X cfg=%02X/%02X\n", s_chip.output(0), + s_chip.output(1), s_chip.config(0), s_chip.config(1)); + + // NSS high first, so the SX1262 ignores the panel-init bytes on the shared lines. + if (!s_chip.setOutput(kLoraNss, true) || !s_chip.setOutput(kLcdCs, true) || + !s_chip.setOutput(kLcdReset, true) || !s_chip.setOutput(kTouchReset, true) || + !s_chip.setOutput(kRp2040Reset, true)) { + Serial.println("[indicator] expander: output config failed"); + return false; + } + if (!s_chip.setInput(kLoraBusy) || !s_chip.setInput(kLoraDio1) || + !s_chip.setInput(kTouchInt) || !s_chip.setInput(kRadioStrap)) { + Serial.println("[indicator] expander: input config failed"); + return false; + } + + bool samples[5]; + for (int i = 0; i < 5; ++i) { + if (!s_chip.readPin(kRadioStrap, samples[i])) { + Serial.println("[indicator] expander: strap read failed"); + return false; + } + delay(1); + } + s_tcxo = indicator::strapMajority(samples); + + s_ready = true; + Serial.printf("[indicator] expander out=%02X/%02X cfg=%02X/%02X tcxo=%d\n", + s_chip.output(0), s_chip.output(1), s_chip.config(0), s_chip.config(1), + s_tcxo ? 1 : 0); + return true; +} + +bool ready() { return s_ready; } + +indicator::Tca9535& chip() { return s_chip; } + +bool radioHasTcxo() { return s_tcxo; } + +} // namespace IndicatorIo diff --git a/variants/sensecap_indicator/IndicatorIo.h b/variants/sensecap_indicator/IndicatorIo.h new file mode 100644 index 0000000..c1e60d6 --- /dev/null +++ b/variants/sensecap_indicator/IndicatorIo.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#include "IndicatorExpander.h" + +namespace IndicatorIo { + +bool begin(); +bool ready(); +indicator::Tca9535& chip(); + +// One recursive mutex over Wire on SDA39/SCL40, taken by every expander transfer, +// the touch driver and the display init. The core's RTC probe uses Wire without +// it; Wire's own lock covers each of those transactions. Hold it across a sequence of chip() +// calls that must not interleave with another task's. +class BusLock { +public: + BusLock(); + ~BusLock(); + BusLock(const BusLock&) = delete; + BusLock& operator=(const BusLock&) = delete; +}; + +// The radio strap, read in begin(): true for the TCXO radio, false for the crystal. +bool radioHasTcxo(); + +} // namespace IndicatorIo diff --git a/variants/sensecap_indicator/IndicatorTouch.cpp b/variants/sensecap_indicator/IndicatorTouch.cpp new file mode 100644 index 0000000..4eda51c --- /dev/null +++ b/variants/sensecap_indicator/IndicatorTouch.cpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#if defined(HAS_SENSECAP_INDICATOR) && defined(ESP32) + +#include +#include + +// Placeholder until the FT6336 reader lands: the touch API the UI calls, with no +// touch behind it. + +bool heltecV4CapTouchBegin() { return false; } +int heltecV4CapTouchCheck() { return BUTTON_EVENT_NONE; } +bool heltecV4CapTouchPopTap(uint16_t*, uint16_t*) { return false; } +bool heltecV4CapTouchGetLive(uint16_t*, uint16_t*) { return false; } +bool heltecV4CapTouchPopSwipe(int8_t*, int8_t*) { return false; } +bool heltecV4CapTouchStartBackgroundPoll(uint32_t) { return false; } +bool heltecV4CapTouchIsAsyncPolling() { return false; } +bool heltecV4CapTouchIsSwiping() { return false; } +void heltecV4CapTouchSetRotation(uint8_t) {} +void heltecV4CapTouchSetPointRotation(uint8_t) {} +void heltecV4CapTouchSetSlowPoll(bool) {} +const char* heltecV4CapTouchDebug() { return "SenseCAP Indicator (no touch yet)"; } +void heltecV4CapTouchGetRaw(uint16_t* x, uint16_t* y) { if (x) *x = 0; if (y) *y = 0; } + +#endif diff --git a/variants/sensecap_indicator/partitions_sensecap_indicator.csv b/variants/sensecap_indicator/partitions_sensecap_indicator.csv new file mode 100644 index 0000000..baa0661 --- /dev/null +++ b/variants/sensecap_indicator/partitions_sensecap_indicator.csv @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SenseCAP Indicator D1L: 8 MB flash, one factory app slot, no map tile partition. +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +factory, app, factory, 0x10000, 0x480000, +spiffs, data, spiffs, 0x490000, 0x360000, +coredump, data, coredump, 0x7F0000, 0x10000, diff --git a/variants/sensecap_indicator/target.cpp b/variants/sensecap_indicator/target.cpp new file mode 100644 index 0000000..e0baff0 --- /dev/null +++ b/variants/sensecap_indicator/target.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include + +#include "target.h" +#include "IndicatorIo.h" + +IndicatorBoard board; + +// The radio's NSS/RESET/BUSY/DIO1 sit on the TCA9535; the P_LORA_* values are +// virtual pins that a HAL resolves to expander bits. Until that HAL exists this +// is a plain Module, which links but cannot drive the radio. +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); + +EnvironmentSensorManager sensors; + +IndicatorDisplay display; +MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + +float indicatorTcxoVoltage() { + return IndicatorIo::radioHasTcxo() ? 2.4f : 0.0f; +} + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + return radio.std_init(&radioSpi); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/sensecap_indicator/target.h b/variants/sensecap_indicator/target.h new file mode 100644 index 0000000..7bd46a6 --- /dev/null +++ b/variants/sensecap_indicator/target.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +// The env sets SX126X_DIO3_TCXO_VOLTAGE=indicatorTcxoVoltage(): the strap picks +// TCXO or crystal at run time, and CustomSX1262.h expands it inline. +float indicatorTcxoVoltage(); + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include "../../src/helpers/ClockFloorRTC.h" +#include "helpers/sensors/EnvironmentSensorManager.h" + +#include "IndicatorBoard.h" +#include "IndicatorDisplay.h" + +extern IndicatorBoard board; +extern RADIO_CLASS radio; +extern WRAPPER_CLASS radio_driver; +extern ClockFloorRTC rtc_clock; +extern EnvironmentSensorManager sensors; +extern IndicatorDisplay display; +extern MomentaryButton user_btn; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity();