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();