mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-10-06 03:27:23 +00:00
feat(crowpanel-35): add Elecrow CrowPanel 3.5" (ESP32-S3, ILI9488 + GT911) touch target
- New crowpanel_35 variant: display, SD, board and target files, plus board JSON - Portrait mode is drawn rotated 180°; landscape stays at rotation 3 - Share the GT911 touch driver between Wio Tracker L2 and CrowPanel (WioTrackerL2Touch.cpp -> src/helpers/input/LgfxGt911Touch.cpp) - platformio envs, flasher/site entries, release and merge-bin support - Tests for the CrowPanel SD codec and merge-bin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0132MkJ7RUV32mV9F7o4PJMm
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f7402e628c
commit
dbecef0b6f
+26
@@ -14,6 +14,7 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
| Heltec V4 + TFT | ESP32-S3, SX1262 | 2.4" 240x320 touch (CHSC6x) | Web flasher | Stable | Fully supported; Expansion Kit sensors, V4.3 high-gain RX toggle |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -98,6 +99,31 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
|
||||
- **Seeed Wio Tracker L2**: pre-release touch target under active bring-up. Builds are for
|
||||
development and hardware validation only; no public release artifact is
|
||||
promised until the port is verified.
|
||||
- **CrowPanel Advance 3.5**: `crowpanel_35_companion_radio_touch` is a
|
||||
16 MB flash / 8 MB OPI PSRAM build for the ILI9488/GT911 panel and an SX1262
|
||||
in the expansion slot. It starts upright in 480x320 landscape; Display
|
||||
settings can switch to 320x480 portrait, rotated 180 degrees. The selected
|
||||
orientation persists across reboots; landscape is only the default when no
|
||||
saved choice exists.
|
||||
It uses the same compact font and control sizing as Wio Tracker L2.
|
||||
Fit the radio before booting. GPIO0 is shared by BOOT and the radio's chip
|
||||
select, so BOOT is not configured as a wake/unlock input on stock wiring.
|
||||
Screen-off remains touch-wakeable; hard lock is disabled without a separate
|
||||
unlock button. See the
|
||||
[official schematic](https://github.com/Elecrow-RD/CrowPanel-Advance-HMI-ESP32-AI-Display/blob/master/3.5/schematic/ESP32%20Display%203.5%20inch%20V1.0.sch)
|
||||
and [reference radio pin map](https://github.com/meshtastic/firmware/blob/master/variants/esp32s3/elecrow_panel/variant.h).
|
||||
An optional external GPS can be connected to UART1 (CrowPanel RX GPIO18 /
|
||||
TX GPIO17, 9600 baud). Data
|
||||
can use a FAT32 microSD card for data, offline maps, File Manager and browser
|
||||
Transfer. The card uses software SPI (SCLK 5, MISO 4, MOSI 6, CS 7), leaving
|
||||
the display and radio SPI hosts untouched; missing or unreadable cards fall
|
||||
back to SPIFFS. The dedicated LittleFS partition remains the internal tile
|
||||
cache. Cards are never auto-formatted; format them on a computer. Serial
|
||||
uses the board's USB-to-UART bridge. Battery voltage monitoring is not
|
||||
supported. Display, touch, radio and SD still need validation on a physical
|
||||
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`.
|
||||
|
||||
## Requested boards
|
||||
|
||||
|
||||
@@ -91,5 +91,11 @@ driver, variants/tdisplay_p4/P4Audio.cpp)
|
||||
LovyanGFX (RAK WisMesh Tap V2 display backend)
|
||||
Copyright (c) lovyan03 — FreeBSD License — https://github.com/lovyan03/LovyanGFX
|
||||
|
||||
Camillia-MT (CrowPanel Advance 3.5 software-SPI microSD backend)
|
||||
Copyright (c) 2025-2026 Michael A. Cojocari and contributors
|
||||
GNU General Public License v3.0 or later
|
||||
Adapted from src/sd_soft_spi.cpp; integrated with WadaMesh's filesystem
|
||||
API and concurrent storage consumers.
|
||||
|
||||
This list is not exhaustive and is expanded as dependencies are vendored or
|
||||
added.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<p align="center"><b>A real touchscreen UI for your mesh radio.</b> · open source · GPL-3.0</p>
|
||||
|
||||
Touch-UI [MeshCore](https://github.com/meshcore-dev/MeshCore) companion-radio
|
||||
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and eight other boards
|
||||
firmware for the **LilyGo T-Deck / T-Deck Plus**, **Heltec V4 + TFT** and other boards
|
||||
(ESP32-S3).
|
||||
|
||||
An LVGL touch UI — map, chat, contacts, channels, settings — split out of
|
||||
@@ -29,6 +29,7 @@ per-board status.
|
||||
- Heltec V4-R8 + Expansion Kit V2 — env `heltec_v4_r8_tft_companion_radio_usb_tcp_touch` (beta)
|
||||
- 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)
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -98,7 +99,8 @@ automatically:
|
||||
```bash
|
||||
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
|
||||
# or just `pio run` to build both
|
||||
pio run -e crowpanel_35_companion_radio_touch # CrowPanel Advance 3.5
|
||||
# or just `pio run` to build the configured default environments
|
||||
```
|
||||
|
||||
Flash with the NVS-preserving 4-component chain (bootloader / partitions /
|
||||
|
||||
@@ -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", "gps"],
|
||||
"debug": {
|
||||
"default_tool": "esp-builtin",
|
||||
"onboard_tools": ["esp-builtin"],
|
||||
"openocd_target": "esp32s3.cfg"
|
||||
},
|
||||
"frameworks": [
|
||||
"arduino",
|
||||
"espidf"
|
||||
],
|
||||
"name": "Elecrow CrowPanel Advance 3.5 (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"
|
||||
}
|
||||
@@ -104,6 +104,13 @@
|
||||
<esp-web-install-button manifest="./manifest-tlora-pager-sx1262.json"></esp-web-install-button>
|
||||
<span class="unsupported">Use Chrome or Edge on desktop (Web Serial).</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Elecrow CrowPanel Advance 3.5 <span class="beta">Beta</span></h2>
|
||||
<p>3.5" touch display, microSD, and an SX1262 module in the LoRa expansion slot.</p>
|
||||
<p class="warn">For the <b>Advance 3.5</b> only. Hardware validation is pending; the SX1262 module must be attached.</p>
|
||||
<esp-web-install-button manifest="https://firmware.wadamesh.com/latest-beta/manifest-crowpanel-35.json"></esp-web-install-button>
|
||||
<span class="unsupported">Use Chrome or Edge on desktop (Web Serial).</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
<b>How it works:</b> plug the device in over USB, click <b>Connect</b>, pick the serial port,
|
||||
|
||||
@@ -517,6 +517,11 @@
|
||||
<esp-web-install-button id="inst-m9" manifest="https://firmware.wadamesh.com/latest-beta/manifest-thinknode-m9.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="thinknode-m9" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-thinknode-m9-merged.bin"><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" data-dl-ver></span></a>
|
||||
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">New board: beta channel only until the next stable.</p></div>
|
||||
<div class="board" data-search="elecrow crowpanel advance 3.5 esp32-s3 ili9488 gt911 sx1262 touchscreen lora"><h4>CrowPanel Advance 3.5 <span style="font-weight:600;color:var(--mut)">(experimental)</span></h4><p>3.5" 480×320 touch display; requires an SX1262 module in the LoRa expansion slot.</p>
|
||||
<span class="sup part" tabindex="0"><span class="dot"></span>Partially supported<span class="pop"><b>Hardware validation pending</b>This build is for the Advance 3.5 with an external SX1262, not other CrowPanel models. Display, touch, radio and software-SPI microSD are ported from the hardware reference; behaviour on a physical unit has not been verified. Landscape is the default, with portrait available in Display settings. No battery monitoring.</span></span>
|
||||
<esp-web-install-button id="inst-crowpanel-35" manifest="https://firmware.wadamesh.com/latest-beta/manifest-crowpanel-35.json"></esp-web-install-button>
|
||||
<a class="dl2" data-dl-board="crowpanel-35" data-dl-betaonly download href="https://firmware.wadamesh.com/latest-beta/wadamesh-crowpanel-35-merged.bin"><span class="dl2-row"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v12M7 11l5 5 5-5M5 21h14"/></svg>Download .bin</span><span class="dlver" data-dl-ver></span></a>
|
||||
<p data-wm-betaonly style="color:var(--mut);font-size:.78rem;margin:6px 0 0">Beta only. Attach the SX1262 before booting; flash with care and report what works.</p></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
+18
-19
@@ -77,25 +77,24 @@ def merge_bin_action(source, target, env):
|
||||
"$ESP32_APP_OFFSET",
|
||||
source[0].get_abspath(),
|
||||
]
|
||||
merge_cmd = " ".join(
|
||||
[
|
||||
'"$PYTHONEXE"',
|
||||
'"$OBJCOPY"',
|
||||
"--chip",
|
||||
board_config.get("build.mcu", "esp32"),
|
||||
"merge_bin",
|
||||
"-o",
|
||||
merged_bin,
|
||||
"--flash_mode",
|
||||
board_config.get("build.flash_mode", "dio"),
|
||||
"--flash_freq",
|
||||
"${__get_board_f_flash(__env__)}",
|
||||
"--flash_size",
|
||||
board_config.get("upload.flash_size", "4MB"),
|
||||
*flash_images,
|
||||
]
|
||||
)
|
||||
env.Execute(merge_cmd)
|
||||
merge_cmd = [
|
||||
"$PYTHONEXE",
|
||||
"$OBJCOPY",
|
||||
"--chip",
|
||||
board_config.get("build.mcu", "esp32"),
|
||||
"merge_bin",
|
||||
"-o",
|
||||
merged_bin,
|
||||
"--flash_mode",
|
||||
board_config.get("build.flash_mode", "dio"),
|
||||
"--flash_freq",
|
||||
"${__get_board_f_flash(__env__)}",
|
||||
"--flash_size",
|
||||
board_config.get("upload.flash_size", "4MB"),
|
||||
*flash_images,
|
||||
]
|
||||
merge_args = [env.subst(str(arg)) for arg in merge_cmd]
|
||||
return subprocess.run(merge_args, env=env["ENV"]).returncode
|
||||
|
||||
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", copy_app_bin_to_out)
|
||||
|
||||
+151
@@ -1299,6 +1299,157 @@ lib_deps =
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
knolleary/PubSubClient @ ^2.8.0
|
||||
|
||||
; Elecrow CrowPanel Advance 3.5 — ESP32-S3, ILI9488/GT911 and SX1262 expansion.
|
||||
; ======================================================================
|
||||
[env:crowpanel_35_companion_radio_touch]
|
||||
platform = platformio/espressif32@6.11.0
|
||||
framework = arduino
|
||||
board = crowpanel_35
|
||||
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_lgfx_dmadesc.py, merge-bin.py
|
||||
|
||||
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_35
|
||||
-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_35=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
|
||||
; GPIO0 is shared by BOOT and LoRa chip select; leave the wake input disabled.
|
||||
-D P_LORA_NSS=0
|
||||
-D P_LORA_RESET=2
|
||||
-D P_LORA_DIO_1=1
|
||||
-D P_LORA_BUSY=46
|
||||
-D P_LORA_SCLK=10
|
||||
-D P_LORA_MISO=9
|
||||
-D P_LORA_MOSI=3
|
||||
-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=1
|
||||
-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
|
||||
; Core calls Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX), i.e. (ESP RX, ESP TX).
|
||||
-D PIN_GPS_TX=18
|
||||
-D PIN_GPS_RX=17
|
||||
-D GPS_BAUD_RATE=9600
|
||||
-D PIN_BOARD_SDA=15
|
||||
-D PIN_BOARD_SCL=16
|
||||
; Software-SPI SD: the radio and display already occupy both SPI hosts.
|
||||
-D PIN_SD_CS=7
|
||||
-D PIN_SD_SCLK=5
|
||||
-D PIN_SD_MISO=4
|
||||
-D PIN_SD_MOSI=6
|
||||
-D DISPLAY_CLASS=CrowPanel35Display
|
||||
-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_35_companion_radio_touch"'
|
||||
-D ENABLE_ADVERT_ON_BOOT=0
|
||||
-D LV_CONF_PATH=lv_conf.h
|
||||
-D LV_CONF_INCLUDE_SIMPLE=1
|
||||
-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>
|
||||
+<helpers/*.cpp>
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/input/*.cpp>
|
||||
-<helpers/input/TDeckTouch.cpp>
|
||||
+<helpers/ui/UIColorPalette.cpp>
|
||||
+<ui-touch/*.cpp>
|
||||
+<ui-touch/*.c>
|
||||
+<../variants/crowpanel_35/*.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
|
||||
|
||||
@@ -14,6 +14,7 @@ HELTEC_ENV="heltec_v4_tft_companion_radio_usb_tcp_touch"
|
||||
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"
|
||||
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"
|
||||
@@ -92,6 +93,7 @@ env_label() {
|
||||
"$HELTEC_R8_ENV") echo "Heltec V4-R8" ;;
|
||||
"$ATTAKY_ENV") echo "Attaky Mesh Series" ;;
|
||||
"$WIO_ENV") echo "Seeed Wio Tracker L2" ;;
|
||||
"$CROWPANEL_ENV") echo "Elecrow CrowPanel Advance 3.5" ;;
|
||||
"$PAGER_LR1121_ENV") echo "LilyGo T-LoRa Pager LR1121" ;;
|
||||
"$PAGER_SX1262_ENV") echo "LilyGo T-LoRa Pager SX1262" ;;
|
||||
"$M9_ENV") echo "ThinkNode M9" ;;
|
||||
@@ -117,6 +119,7 @@ Devices:
|
||||
--heltec-r8 Heltec V4-R8
|
||||
--attaky Attaky Mesh Series
|
||||
--wio Seeed Wio Tracker L2
|
||||
--crowpanel-35 Elecrow CrowPanel Advance 3.5
|
||||
--pager-lr1121 LilyGo T-LoRa Pager LR1121
|
||||
--pager-sx1262 LilyGo T-LoRa Pager SX1262
|
||||
--m9 ThinkNode M9
|
||||
@@ -167,6 +170,7 @@ prompt_for_device() {
|
||||
"$HELTEC_R8_ENV"
|
||||
"$ATTAKY_ENV"
|
||||
"$WIO_ENV"
|
||||
"$CROWPANEL_ENV"
|
||||
"$PAGER_LR1121_ENV"
|
||||
"$PAGER_SX1262_ENV"
|
||||
"$RAK_ENV"
|
||||
@@ -245,6 +249,7 @@ while [ $# -gt 0 ]; do
|
||||
--heltec-r8) select_env "$HELTEC_R8_ENV" ;;
|
||||
--attaky) select_env "$ATTAKY_ENV" ;;
|
||||
--wio) select_env "$WIO_ENV" ;;
|
||||
--crowpanel-35) select_env "$CROWPANEL_ENV" ;;
|
||||
--pager-lr1121) select_env "$PAGER_LR1121_ENV" ;;
|
||||
--pager-sx1262) select_env "$PAGER_SX1262_ENV" ;;
|
||||
--m9) select_env "$M9_ENV" ;;
|
||||
|
||||
@@ -44,6 +44,7 @@ BOARDS = {
|
||||
"manifest-tlora-pager-sx1262.json": ("wadamesh - LilyGo T-LoRa Pager (SX1262)", "wadamesh-tlora-pager-sx1262-merged.bin", "ESP32-S3"),
|
||||
"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-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"),
|
||||
|
||||
+2
-2
@@ -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 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 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
|
||||
@@ -53,7 +53,7 @@ fi
|
||||
mkdir -p "$ARCH/$TAG" "$FEED"
|
||||
|
||||
if [ "$MODE" = "beta" ]; then
|
||||
# 2b. TEST build: compile both boards (tag + version embedded) into the beta archive + feed.
|
||||
# 2b. TEST build: compile all PlatformIO boards (tag + version embedded) into the beta archive + feed.
|
||||
# Firmware data (About screen + the app's device info) is derived here, never
|
||||
# hand-maintained: the tag, the core version actually pinned in platformio.ini,
|
||||
# and today's build date. Every staged binary is verified to carry them below —
|
||||
|
||||
+11
-3
@@ -3,7 +3,11 @@
|
||||
#include "DataStore.h"
|
||||
#include <helpers/AdvertDataHelpers.h> // ADV_TYPE_NONE (blank-record skip in loadContacts)
|
||||
#if defined(ESP32)
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
#include <CrowPanel35SD.h>
|
||||
#else
|
||||
#include <SD.h>
|
||||
#endif
|
||||
#include "helpers/esp32/WdtHeavyGuard.h" // suspend core-0 idle WDT during the (SPIFFS-GC-prone) contact write
|
||||
#endif
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_WIO_TRACKER_L2)
|
||||
@@ -1127,9 +1131,13 @@ bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
|
||||
// Creates the folders, repoints _fs + the identity store, and sets the path
|
||||
// prefix so every subsequent read/write/exists/remove lands under /meshcomod.
|
||||
bool DataStore::useSdStorage() {
|
||||
if (!SD.exists("/meshcomod")) SD.mkdir("/meshcomod");
|
||||
if (!SD.exists("/meshcomod/bl")) SD.mkdir("/meshcomod/bl");
|
||||
if (!SD.exists("/meshcomod/identity")) SD.mkdir("/meshcomod/identity");
|
||||
static const char* const directories[] = { "/meshcomod", "/meshcomod/bl", "/meshcomod/identity" };
|
||||
for (const char* path : directories) {
|
||||
if (!SD.exists(path) && !SD.mkdir(path)) {
|
||||
Serial.printf("[STORE] SD data directory unavailable: %s\n", path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
strncpy(_root, "/meshcomod", sizeof(_root) - 1);
|
||||
_root[sizeof(_root) - 1] = '\0';
|
||||
_fs = &SD;
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
#if defined(ESP32)
|
||||
|
||||
#include <FS.h>
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
#include <CrowPanel35SD.h>
|
||||
#else
|
||||
#include <SD.h>
|
||||
#endif
|
||||
#include <SPIFFS.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/queue.h>
|
||||
|
||||
@@ -57,6 +57,11 @@ static const uint32_t DEFAULT_ACCENT = 0x15B6A6u; // brand teal
|
||||
static const bool DEFAULT_DC_SHOW = true;
|
||||
static const uint8_t DEFAULT_SIG_PROBE_EN = 1; // signal discover probe ON
|
||||
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
|
||||
#else
|
||||
static const uint8_t DEFAULT_UI_ROTATION = 0;
|
||||
#endif
|
||||
|
||||
using TouchCfg = TouchPrefsSchema::Config;
|
||||
|
||||
@@ -91,7 +96,7 @@ static void cfgSetDefaults(TouchCfg& c) {
|
||||
c.kb_layout = DEFAULT_KB_LAYOUT;
|
||||
c.kb_secondary = DEFAULT_KB_SECONDARY;
|
||||
c.ui_lang = 0;
|
||||
c.ui_rotation = 0;
|
||||
c.ui_rotation = DEFAULT_UI_ROTATION;
|
||||
c.dc_show = DEFAULT_DC_SHOW ? 1 : 0;
|
||||
c.use_miles = 0;
|
||||
c.tiles_from_sd = 0;
|
||||
@@ -356,7 +361,7 @@ static void cfgLoadOrMigrate() {
|
||||
if (s_prefs.isKey(LK_USE_MILES)) s_cfg.use_miles = s_prefs.getBool(LK_USE_MILES, false) ? 1 : 0;
|
||||
if (s_prefs.isKey(LK_TILES_FROM_SD))s_cfg.tiles_from_sd= s_prefs.getBool(LK_TILES_FROM_SD, false) ? 1 : 0;
|
||||
if (s_prefs.isKey(LK_UI_LANG)) s_cfg.ui_lang = s_prefs.getUChar(LK_UI_LANG, 0);
|
||||
if (s_prefs.isKey(LK_UI_ROTATION)) s_cfg.ui_rotation = s_prefs.getUChar(LK_UI_ROTATION, 0);
|
||||
if (s_prefs.isKey(LK_UI_ROTATION)) s_cfg.ui_rotation = s_prefs.getUChar(LK_UI_ROTATION, DEFAULT_UI_ROTATION);
|
||||
if (s_prefs.isKey(LK_BATT_FULL)) s_cfg.batt_full_mv = s_prefs.getUShort(LK_BATT_FULL, 0);
|
||||
if (s_prefs.isKey(LK_GPS_BAUD)) s_cfg.gps_baud = s_prefs.getUInt(LK_GPS_BAUD, 0);
|
||||
// Enabled-layout mask: legacy used 0xFFFF as the "never written" sentinel and
|
||||
@@ -1628,15 +1633,18 @@ bool touchPrefsSetLangFile(const char* code) {
|
||||
|
||||
uint8_t touchPrefsGetUiRotation() {
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
uint8_t r = s_cfg.ui_rotation; // default = portrait
|
||||
return (r <= 3) ? r : 0;
|
||||
const uint8_t r = s_cfg.ui_rotation;
|
||||
return (s_cfg_loaded && r <= 3) ? r : DEFAULT_UI_ROTATION;
|
||||
}
|
||||
|
||||
bool touchPrefsSetUiRotation(uint8_t rot) {
|
||||
if (rot > 3) rot = 0;
|
||||
if (!s_begun) touchPrefsBegin();
|
||||
const uint8_t previous = s_cfg.ui_rotation;
|
||||
s_cfg.ui_rotation = rot;
|
||||
return cfgFlush();
|
||||
if (cfgFlush()) return true;
|
||||
s_cfg.ui_rotation = previous;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t touchPrefsGetBattFullMv() {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#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_TDISPLAY_P4) || \
|
||||
defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(HAS_TDISPLAY_P4) || \
|
||||
defined(HAS_TANMATSU))
|
||||
#define WADA_WEB_FILE_TRANSFER 1
|
||||
#else
|
||||
|
||||
+34
-7
@@ -1,15 +1,20 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#if defined(HAS_WIO_TRACKER_L2) && defined(ESP32)
|
||||
#if (defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35)) && defined(ESP32)
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <helpers/input/HeltecV4CapTouch.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
|
||||
#include "WioTrackerL2Display.h"
|
||||
|
||||
extern WioTrackerL2Display display;
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
#include <CrowPanel35Display.h>
|
||||
extern CrowPanel35Display display;
|
||||
#else
|
||||
#include <WioTrackerL2Display.h>
|
||||
extern WioTrackerL2Display display;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
bool s_ready = false;
|
||||
@@ -56,7 +61,18 @@ void pollTask(void*) {
|
||||
}
|
||||
}
|
||||
|
||||
bool heltecV4CapTouchBegin() { s_ready = true; return true; }
|
||||
bool heltecV4CapTouchBegin() {
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
static uint32_t nextProbeMs = 0;
|
||||
const uint32_t now = millis();
|
||||
if (nextProbeMs && (int32_t)(now - nextProbeMs) < 0) return false;
|
||||
nextProbeMs = now + 1000;
|
||||
Wire.beginTransmission(0x5D);
|
||||
if (Wire.endTransmission() != 0) return false;
|
||||
#endif
|
||||
s_ready = true;
|
||||
return true;
|
||||
}
|
||||
int heltecV4CapTouchCheck() { if (!s_ready) return BUTTON_EVENT_NONE; poll(); return BUTTON_EVENT_NONE; }
|
||||
bool heltecV4CapTouchPopTap(uint16_t* x, uint16_t* y) {
|
||||
if (!s_tapPending) return false; s_tapPending = false;
|
||||
@@ -72,7 +88,12 @@ bool heltecV4CapTouchPopSwipe(int8_t* x, int8_t* y) {
|
||||
bool heltecV4CapTouchStartBackgroundPoll(uint32_t periodMs) {
|
||||
if (s_async || !s_ready) return false;
|
||||
s_periodMs = periodMs < 4 ? 4 : (periodMs > 100 ? 100 : periodMs);
|
||||
if (xTaskCreatePinnedToCore(pollTask, "wio_l2_touch", 3072, nullptr, 2, &s_task, 0) != pdPASS) return false;
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
const char* name = "crow_touch";
|
||||
#else
|
||||
const char* name = "wio_l2_touch";
|
||||
#endif
|
||||
if (xTaskCreatePinnedToCore(pollTask, name, 3072, nullptr, 2, &s_task, 0) != pdPASS) return false;
|
||||
s_async = true; return true;
|
||||
}
|
||||
bool heltecV4CapTouchIsAsyncPolling() { return s_async; }
|
||||
@@ -80,7 +101,13 @@ bool heltecV4CapTouchIsSwiping() { return s_swiping; }
|
||||
void heltecV4CapTouchSetRotation(uint8_t) {}
|
||||
void heltecV4CapTouchSetPointRotation(uint8_t) {}
|
||||
void heltecV4CapTouchSetSlowPoll(bool slow) { s_periodMs = slow ? 50 : 8; }
|
||||
const char* heltecV4CapTouchDebug() { return "Wio Tracker L2 GT911"; }
|
||||
const char* heltecV4CapTouchDebug() {
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
return s_ready ? "CrowPanel Advance 3.5 GT911" : "CrowPanel GT911 not responding at 0x5D";
|
||||
#else
|
||||
return "Wio Tracker L2 GT911";
|
||||
#endif
|
||||
}
|
||||
void heltecV4CapTouchGetRaw(uint16_t* x, uint16_t* y) { if (x) *x = s_x; if (y) *y = s_y; }
|
||||
|
||||
#endif
|
||||
+27
-5
@@ -56,9 +56,13 @@ static uint32_t _atoi(const char* sp) {
|
||||
#include <SD_MMC.h>
|
||||
#include <WioTrackerL2Io.h>
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_CROWPANEL_35)
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
#include <CrowPanel35SD.h>
|
||||
#else
|
||||
#include <SD.h>
|
||||
#include "SdFastClock.h" // post-mount operating-clock raise (SD_SPI_FAST_HZ boards)
|
||||
#endif
|
||||
#include <Preferences.h>
|
||||
#if defined(TLORA_PAGER)
|
||||
#include <mbedtls/sha256.h>
|
||||
@@ -270,7 +274,7 @@ extern volatile uint8_t g_wifi_last_disc_reason;
|
||||
|
||||
#include "esp_task_wdt.h" // task-watchdog reconfigure — see setup() (GH #56)
|
||||
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_CROWPANEL_35)
|
||||
// ---- SPIFFS -> SD migration (fixes the beta_36 "lost my profile" upgrades) ----
|
||||
// Users who flipped "Store data on SD" before beta_36 ran with the toggle IGNORED
|
||||
// (the flag never survived a reboot), so their identity/prefs/contacts kept living
|
||||
@@ -858,6 +862,9 @@ void setup() {
|
||||
uint8_t r = touchPrefsGetUiRotation();
|
||||
if (r == 1) display.setDisplayRotation(1);
|
||||
else if (r == 3) display.setDisplayRotation(3);
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
else display.setDisplayRotation(0); // restore saved portrait
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if defined(HAS_TDECK_PRO)
|
||||
@@ -1082,8 +1089,9 @@ void setup() {
|
||||
Serial.println("[BOOT] wio-l2 SD_MMC unavailable; using SPIFFS");
|
||||
}
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_CROWPANEL_35)
|
||||
{
|
||||
#if !defined(HAS_CROWPANEL_35)
|
||||
#if defined(TLORA_PAGER)
|
||||
extern SPIClass* tloraPagerSharedSPI(); // display/radio/SD shared bus
|
||||
#elif defined(HELTEC_LORA_V4_R8)
|
||||
@@ -1092,6 +1100,7 @@ void setup() {
|
||||
extern SPIClass* m9SharedSPI(); // radio/display/SD shared bus
|
||||
#else
|
||||
extern SPIClass* tdeckSharedSPI(); // LoRa SPI bus
|
||||
#endif
|
||||
#endif
|
||||
bool setup_done = false;
|
||||
{ Preferences _p; if (_p.begin("touch", true)) {
|
||||
@@ -1115,6 +1124,7 @@ void setup() {
|
||||
// device has no usable SPIFFS, the user opted in, or it's a brand-new device.
|
||||
bool want_full_sd = !spiffs_ok || use_sd_pref || fresh_install;
|
||||
|
||||
#if !defined(HAS_CROWPANEL_35)
|
||||
#if defined(TLORA_PAGER)
|
||||
SPIClass* _spi = tloraPagerSharedSPI();
|
||||
#elif defined(HELTEC_LORA_V4_R8)
|
||||
@@ -1123,9 +1133,17 @@ void setup() {
|
||||
SPIClass* _spi = m9SharedSPI();
|
||||
#else
|
||||
SPIClass* _spi = tdeckSharedSPI();
|
||||
#endif
|
||||
#endif
|
||||
bool sd_mounted = false;
|
||||
#if defined(TLORA_PAGER)
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
sdMountDiagBegin();
|
||||
const bool begin_ok = SD.begin();
|
||||
sd_mounted = begin_ok && SD.cardType() != CARD_NONE;
|
||||
sdMountDiagAttempt(0, begin_ok, sd_mounted);
|
||||
sdMountDiagSetMounted(sd_mounted, 0);
|
||||
if (!sd_mounted) Serial.println("[BOOT] CrowPanel SD unavailable; using SPIFFS");
|
||||
#elif defined(TLORA_PAGER)
|
||||
sdMountDiagBegin();
|
||||
if (!_spi) {
|
||||
Serial.println("[BOOT] SD: shared SPI unavailable");
|
||||
@@ -1317,6 +1335,10 @@ void setup() {
|
||||
if (adopt) {
|
||||
sd_storage = store.useSdStorage();
|
||||
g_full_data_on_sd = sd_storage;
|
||||
if (!sd_storage) {
|
||||
g_contacts_on_sd = false;
|
||||
Serial.println("[BOOT] SD data directory setup failed; using SPIFFS");
|
||||
}
|
||||
// On a genuine first run, persist the auto-pick so the "Store data on SD"
|
||||
// toggle reflects it and the choice sticks on every later boot.
|
||||
if (fresh_install && sd_storage && !use_sd_pref) {
|
||||
@@ -1402,7 +1424,7 @@ void setup() {
|
||||
#if defined(HAS_WIO_TRACKER_L2)
|
||||
SdNvsPrefs::useFile(sd_storage ? (fs::FS*)&SD_MMC : (fs::FS*)&SPIFFS,
|
||||
sd_storage ? "/meshcomod" : "/prefs");
|
||||
#elif defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#elif defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_CROWPANEL_35)
|
||||
SdNvsPrefs::useFile(sd_storage ? (fs::FS*)&SD : (fs::FS*)&SPIFFS,
|
||||
sd_storage ? "/meshcomod" : "/prefs");
|
||||
#else
|
||||
|
||||
+149
-71
@@ -71,7 +71,9 @@ static void* wadaMp3Scratch() { return s_wada_mp3_scratch; }
|
||||
static inline esp_err_t esp_core_dump_image_erase() { return ESP_FAIL; }
|
||||
#endif
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
#include <CrowPanel35SD.h>
|
||||
#elif defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#include <SD.h> // microSD — T-Deck/M9 on the LoRa SPI, V4-R8 on the TFT SPI
|
||||
#include "SdFastClock.h" // post-mount operating-clock raise (SD_SPI_FAST_HZ boards)
|
||||
#include "sd_diskio.h" // internal Arduino-SD drive helpers (sdcard_init / sd_*_raw)
|
||||
@@ -186,6 +188,8 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM
|
||||
#elif defined(HAS_WIO_TRACKER_L2)
|
||||
#include <WioTrackerL2Display.h>
|
||||
#include <WioTrackerL2Io.h>
|
||||
#elif defined(HAS_CROWPANEL_35)
|
||||
#include <CrowPanel35Display.h>
|
||||
#elif defined(HAS_RAK_TAP_V2)
|
||||
#include <LGFXDisplay.h> // LovyanGFX FSPI on RAK Tap V2
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
@@ -252,6 +256,8 @@ static_assert(ChannelSenderSplit::kMaxWireName >= (size_t)UITask::MAX_SENDER_NAM
|
||||
extern ST7796LCDDisplay display;
|
||||
#elif defined(HAS_WIO_TRACKER_L2)
|
||||
extern WioTrackerL2Display display;
|
||||
#elif defined(HAS_CROWPANEL_35)
|
||||
extern CrowPanel35Display display;
|
||||
#elif defined(HAS_RAK_TAP_V2) || defined(HELTEC_LORA_V4_R8)
|
||||
extern LGFXDisplay display;
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
@@ -1185,8 +1191,8 @@ static inline bool luaAudioStorageBusy() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_LORA_V4_R8) || defined(HAS_THINKNODE_M9) || defined(HAS_TDECK_PRO)
|
||||
static bool fmSdTryMount(); // non-audio SD targets — fwd decl for sdRestoreRun
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
static bool fmSdTryMount(); // storage callers precede the mount implementation
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER)
|
||||
static constexpr int kI2sSampleRate = 16000;
|
||||
@@ -1195,9 +1201,6 @@ static constexpr i2s_port_t kI2sPort = I2S_NUM_0;
|
||||
// beeping while tiles are downloading — the I2S DMA buffers + Wi-Fi RX DMA + a
|
||||
// tile decode all contend for the scarce internal DMA RAM, and this build is
|
||||
// already tight enough that tile downloads can OOM-reboot on their own.
|
||||
#if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER)
|
||||
static bool fmSdTryMount(); // defined far below (microSD mount)
|
||||
#endif
|
||||
#if defined(HAS_TDECK_GT911)
|
||||
// I2S is installed ON DEMAND for the duration of a tone and uninstalled after.
|
||||
// Holding the driver resident permanently kept ~2 KB of internal DMA RAM, which
|
||||
@@ -2760,9 +2763,14 @@ void sdMountDiagSetMounted(bool mounted, uint32_t hz) {
|
||||
}
|
||||
|
||||
static void sdDiagClockText(uint32_t hz, char* out, size_t cap) {
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
(void)hz;
|
||||
snprintf(out, cap, "software SPI");
|
||||
#else
|
||||
if (hz >= 1000000) snprintf(out, cap, "%lu MHz", (unsigned long)(hz / 1000000));
|
||||
else if (hz > 0) snprintf(out, cap, "%lu kHz", (unsigned long)(hz / 1000));
|
||||
else snprintf(out, cap, "unknown");
|
||||
#endif
|
||||
}
|
||||
static uint32_t g_draw_buf_px = 240 * LV_DRAW_BUF_LINES; // actual buffer size in px; shrinks if the full alloc fails at boot
|
||||
#if CAP_LARGE_SCREEN
|
||||
@@ -4049,6 +4057,9 @@ static void lvglFlush(lv_disp_drv_t* disp_drv, const lv_area_t* area, lv_color_t
|
||||
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)
|
||||
else display.setDisplayRotation(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// In-chat action openers, forward-declared here UNCONDITIONALLY — their click-callbacks
|
||||
@@ -7449,13 +7460,12 @@ static void kbShowRotateArrows(bool show) {
|
||||
}
|
||||
}
|
||||
|
||||
// The Wio Tracker L2 panel is fixed 320x240 LANDSCAPE (CAP_ROTATABLE 0), so
|
||||
// "rotate for landscape typing" has nothing to offer there — it would turn a
|
||||
// landscape keyboard into a portrait one. The arrows are not created on that
|
||||
// board and these, their only callers, go with them. Every other reference to
|
||||
// The Wio panel is fixed landscape; CrowPanel defaults to landscape and rotates
|
||||
// through Display settings. Neither needs a second keyboard-only orientation
|
||||
// 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)
|
||||
#if !defined(HAS_WIO_TRACKER_L2) && !defined(HAS_CROWPANEL_35)
|
||||
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.
|
||||
@@ -13794,7 +13804,7 @@ static void useSdStorageToggleCb(lv_event_t* e) {
|
||||
: TR("Data -> internal on reboot"), 1800);
|
||||
}
|
||||
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
// "Copy internal data to SD": recovery for the beta_36 upgrades where the live
|
||||
// profile was orphaned on internal flash while the honored SD toggle adopted an
|
||||
// empty card. Pager resumes only onto a card with no identity or the identical
|
||||
@@ -14280,11 +14290,19 @@ static const char* uiRotationLabel() {
|
||||
static void rotateScreenCycleCb(lv_event_t* e) {
|
||||
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
|
||||
#if defined(ESP32)
|
||||
// Simple toggle: portrait <-> landscape (90 degrees). One tap each way.
|
||||
// Simple toggle: portrait <-> the board's upright landscape orientation.
|
||||
const uint8_t cur = touchPrefsGetUiRotation();
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
const uint8_t next = (cur == LV_DISP_ROT_NONE) ? (uint8_t)LV_DISP_ROT_270
|
||||
: (uint8_t)LV_DISP_ROT_NONE;
|
||||
#else
|
||||
const uint8_t next = (cur == LV_DISP_ROT_NONE) ? (uint8_t)LV_DISP_ROT_90
|
||||
: (uint8_t)LV_DISP_ROT_NONE;
|
||||
touchPrefsSetUiRotation(next);
|
||||
#endif
|
||||
if (!touchPrefsSetUiRotation(next)) {
|
||||
if (g_lv.task) g_lv.task->showAlert(TR("Save failed"), 1200);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// Same as the theme and language switches: reboot on a timer so the notice is
|
||||
// actually painted first, rather than going dark under the user's finger.
|
||||
@@ -15667,7 +15685,7 @@ static void buildDeviceSettings(int sec) {
|
||||
lv_obj_add_event_cb(sw, useSdStorageToggleCb, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
y += LV_MAX(40, h + 12);
|
||||
}
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
/* Where contacts ACTUALLY live this boot. The toggle above is only an intent — if the
|
||||
card failed to mount at boot (cold/slow card), contacts silently stay on internal flash
|
||||
even with it ON. This line shows the truth and flags that mismatch. */
|
||||
@@ -15707,7 +15725,7 @@ static void buildDeviceSettings(int sec) {
|
||||
fresh-identity) card. This copies EVERYTHING from internal flash over the
|
||||
card's copies and reboots into the restored profile. This is a SPIFFS->SD
|
||||
recovery on T-Deck, V4-R8 and Pager; Tanmatsu uses SD_MMC with no SPIFFS. */
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
{
|
||||
lv_obj_t* b = lv_btn_create(body);
|
||||
lv_obj_set_size(b, lv_pct(96), SC(30));
|
||||
@@ -22574,8 +22592,8 @@ static char s_fm_path[160] = {0}; // current dir within s_fm_fs (e.g.
|
||||
// a generic fs::FS*; only &SD is real microSD I/O (Internal = SPIFFS). Browsing
|
||||
// (fmRefresh) and the file open/save paths call this; mutations re-list via
|
||||
// fmRefresh, so they blip the LED too.
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
static inline bool fmIsSd(fs::FS* fs) { return fs == &SD; } // Arduino SD (T-Deck/pager/M9 LoRa bus, V4-R8 TFT bus)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
static inline bool fmIsSd(fs::FS* fs) { return fs == &SD; } // hardware or software SPI SD
|
||||
#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_WIO_TRACKER_L2)
|
||||
static inline bool fmIsSd(fs::FS* fs) { return fs == &SD_MMC; } // microSD on SDMMC slot 0
|
||||
#else
|
||||
@@ -24035,7 +24053,14 @@ static void fmFmtSize64(uint64_t bytes, char* out, size_t outsz) {
|
||||
else snprintf(out, outsz, "%.1f GB", bytes / (1024.0 * 1024 * 1024));
|
||||
}
|
||||
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8) // microSD mount/format helpers — Arduino SD on the shared SPI bus
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
static uint8_t sdDriveNumber() { return SD.driveNumber(); }
|
||||
#else
|
||||
struct SdDriveAccess : fs::SDFS {
|
||||
static uint8_t of(const fs::SDFS& sd) { return sd.*(&SdDriveAccess::_pdrv); }
|
||||
};
|
||||
static uint8_t sdDriveNumber() { return SdDriveAccess::of(SD); }
|
||||
// One shared-SPI accessor per board: the T-Deck/M9 expose their pre-begun SPIClass
|
||||
// via tdeckSharedSPI()/m9SharedSPI(); the V4-R8's microSD shares its TFT FSPI bus
|
||||
// (heltecV4R8SharedSPI()); the pager accessor returns the same TFT_eSPI SPIClass
|
||||
@@ -24049,6 +24074,7 @@ static inline SPIClass* sdSharedSPI() { return m9SharedSPI(); }
|
||||
#else
|
||||
static inline SPIClass* sdSharedSPI() { return tdeckSharedSPI(); }
|
||||
#endif
|
||||
#endif // !HAS_CROWPANEL_35
|
||||
// Mount the microSD on its shared SPI bus. Safe to call repeatedly (no-op
|
||||
// once mounted). SD.begin's internal spi.begin() is a no-op because the bus is
|
||||
// already initialised by the radio/display, so those pins are untouched.
|
||||
@@ -24060,6 +24086,14 @@ static bool sdAdoptLiveMount() {
|
||||
sdMountDiagSetMounted(true, g_sd_operating_hz);
|
||||
return true;
|
||||
}
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
static bool sdBeginTracked() {
|
||||
const bool begin_ok = SD.begin();
|
||||
const bool card_ready = begin_ok && SD.cardType() != CARD_NONE;
|
||||
sdMountDiagAttempt(0, begin_ok, card_ready);
|
||||
return card_ready;
|
||||
}
|
||||
#else
|
||||
static bool sdBeginTracked(SPIClass* spi, uint32_t hz, bool* begin_ok_out = nullptr) {
|
||||
const bool begin_ok = spi && SD.begin(PIN_SD_CS, *spi, hz, "/sd", 6);
|
||||
const bool card_ready = begin_ok && SD.cardType() != CARD_NONE;
|
||||
@@ -24067,6 +24101,7 @@ static bool sdBeginTracked(SPIClass* spi, uint32_t hz, bool* begin_ok_out = null
|
||||
sdMountDiagAttempt(hz, begin_ok, card_ready);
|
||||
return card_ready;
|
||||
}
|
||||
#endif
|
||||
#if defined(TLORA_PAGER)
|
||||
static bool sdPagerBeginWithPowerRecovery(SPIClass* spi, bool* begin_ok_out) {
|
||||
bool begin_ok = false;
|
||||
@@ -24108,10 +24143,17 @@ static bool fmSdTryMount() {
|
||||
// the multi-second mount ladder while the card is out, freezing the UI.
|
||||
// Explicit user retries clear the backoff first (fmSdMountOrFormatCb).
|
||||
if (s_sd_retry_after_ms && millis() < s_sd_retry_after_ms) return false;
|
||||
#if defined(TLORA_PAGER)
|
||||
#if defined(TLORA_PAGER) || defined(HAS_CROWPANEL_35)
|
||||
if (sdRuntimeLifecycleBusy()) return false;
|
||||
#endif
|
||||
#if defined(TLORA_PAGER)
|
||||
if (!board.sdCardPresent()) return false;
|
||||
#endif
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
sdMountDiagBegin();
|
||||
const bool mounted = sdBeginTracked();
|
||||
const uint32_t mounted_hz = 0;
|
||||
#else
|
||||
SPIClass* spi = sdSharedSPI();
|
||||
if (!spi) return false;
|
||||
sdMountDiagBegin();
|
||||
@@ -24190,6 +24232,7 @@ static bool fmSdTryMount() {
|
||||
if (mounted) g_sd_operating_hz = mounted_hz;
|
||||
}
|
||||
#endif
|
||||
#endif // !HAS_CROWPANEL_35
|
||||
if (mounted) {
|
||||
s_sd_mounted = true;
|
||||
s_sd_size = SD.cardSize();
|
||||
@@ -24240,10 +24283,14 @@ static void fmSdUnmount() {
|
||||
// (opendir fails -> operator bool false). Healthy-card cost is sub-ms; a dead
|
||||
// card rides out the SPI timeouts once, so callers rate-limit the probe.
|
||||
static bool sdProbeAlive() {
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
return SD.probeAlive(); // Explicit CMD13; a cached FAT root is not a card probe.
|
||||
#else
|
||||
File d = SD.open("/");
|
||||
const bool ok = d && d.isDirectory();
|
||||
if (d) d.close();
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
// Discriminator for hot READ paths (map tile lookups run up to five SD opens
|
||||
// per tile on the loop task): a missing FILE on a healthy card must stay cheap
|
||||
@@ -24265,24 +24312,20 @@ static void fmSdClickCb(lv_event_t* e) {
|
||||
if (s_sd_mounted) fmOpenStorage(&SD, "SD", "/");
|
||||
}
|
||||
|
||||
#if defined(TLORA_PAGER)
|
||||
// Recovery entry for a card the Pager can SEE (card-detect asserted) but cannot
|
||||
// mount: exFAT/NTFS, or a damaged FAT. Without this the roots page renders no SD
|
||||
// row at all (fmShowRoots below), so there is no way to tell "no card" from "card
|
||||
// the firmware won't take", and no way to retry without a reboot.
|
||||
//
|
||||
// Deliberately NON-destructive. It clears the mount backoff and makes exactly the
|
||||
// one vendor-compatible 4 MHz attempt fmSdTryMount() already makes — it does not
|
||||
// add a retry ladder and never tears down the live shared display/radio bus. If
|
||||
// that attempt fails the card needs formatting, and on this board that is a
|
||||
// deliberate host-side task: see the format-helper guard below for why the in-app
|
||||
// formatter stays off for the Pager.
|
||||
static void fmSdPagerRetryMountCb(lv_event_t* e) {
|
||||
#if defined(TLORA_PAGER) || defined(HAS_CROWPANEL_35)
|
||||
// These ports offer only a non-destructive mount retry. Formatting stays on
|
||||
// the computer, rather than using the hardware-SPI formatter on an untested bus.
|
||||
static void fmSdRetryMountCb(lv_event_t* e) {
|
||||
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
|
||||
s_sd_retry_after_ms = 0; // explicit user retry — bypass the backoff
|
||||
if (fmSdTryMount() && s_sd_mounted) { fmShowRoots(); return; }
|
||||
if (g_lv.task)
|
||||
if (g_lv.task) {
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
g_lv.task->showAlert(TR("SD unavailable - insert a FAT32 card and retry"), 3600);
|
||||
#else
|
||||
g_lv.task->showAlert(TR("SD unreadable - format it as FAT32 on a computer"), 3600);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -25727,6 +25770,19 @@ static void fmShowRoots() {
|
||||
fmStyleRow(sd, COLOR_SUB);
|
||||
lv_obj_add_event_cb(sd, fmSdMountOrFormatCb, LV_EVENT_CLICKED, nullptr);
|
||||
}
|
||||
#elif defined(HAS_CROWPANEL_35)
|
||||
if ((s_sd_mounted || millis() >= s_sd_retry_after_ms) && fmSdTryMount()) {
|
||||
char sdl[48], cs[16];
|
||||
fmFmtSize64(s_sd_size, cs, sizeof cs);
|
||||
snprintf(sdl, sizeof sdl, TR("SD card %s"), cs);
|
||||
lv_obj_t* sd = lv_list_add_btn(s_fm_list, LV_SYMBOL_SD_CARD, sdl);
|
||||
fmStyleRow(sd, COLOR_TEXT);
|
||||
lv_obj_add_event_cb(sd, fmSdClickCb, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
} else {
|
||||
lv_obj_t* sd = lv_list_add_btn(s_fm_list, LV_SYMBOL_SD_CARD, TR("SD card (tap to mount)"));
|
||||
fmStyleRow(sd, COLOR_SUB);
|
||||
lv_obj_add_event_cb(sd, fmSdRetryMountCb, LV_EVENT_CLICKED, nullptr);
|
||||
}
|
||||
#elif defined(TLORA_PAGER) // microSD row — browse + mount recovery, no in-app format
|
||||
// The card-detect line makes "not present" unambiguous, unlike the T-Deck (no detect
|
||||
// pin at all) -- so unlike its always-shown "tap to mount/format" fallback row, a bare
|
||||
@@ -25745,7 +25801,7 @@ static void fmShowRoots() {
|
||||
// absent case above, and gives a retry that bypasses the mount backoff.
|
||||
lv_obj_t* sd = lv_list_add_btn(s_fm_list, LV_SYMBOL_SD_CARD, TR("SD card (unreadable - tap to retry)"));
|
||||
fmStyleRow(sd, COLOR_SUB);
|
||||
lv_obj_add_event_cb(sd, fmSdPagerRetryMountCb, LV_EVENT_CLICKED, nullptr);
|
||||
lv_obj_add_event_cb(sd, fmSdRetryMountCb, LV_EVENT_CLICKED, nullptr);
|
||||
}
|
||||
#endif
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_WIO_TRACKER_L2)
|
||||
@@ -26386,7 +26442,7 @@ static ReaderLocalResult readerReadLocal(const char* url, uint8_t* raw, size_t c
|
||||
s_reader_sd_busy = false;
|
||||
storage_claimed = false;
|
||||
};
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
s_reader_sd_busy = true;
|
||||
s_reader_sd_owner = xTaskGetCurrentTaskHandle();
|
||||
storage_claimed = true;
|
||||
@@ -29496,7 +29552,7 @@ static void makeHome(lv_obj_t* tab) {
|
||||
lv_obj_set_ext_click_area(s_home_chart_legend, 8);
|
||||
lv_obj_add_event_cb(s_home_chart_legend, homeChartClickedCb, LV_EVENT_CLICKED, nullptr);
|
||||
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TANMATSU) || defined(TLORA_PAGER) || defined(HAS_RAK_TAP_V2) || defined(HAS_THINKNODE_M9) || defined(HAS_WIO_TRACKER_L2) || defined(ATTAKY_MESH_SERIES) || defined(HAS_TDISPLAY_P4)
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TANMATSU) || defined(TLORA_PAGER) || defined(HAS_RAK_TAP_V2) || defined(HAS_THINKNODE_M9) || defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35) || defined(ATTAKY_MESH_SERIES) || defined(HAS_TDISPLAY_P4)
|
||||
// Landscape boards keep the chart clear of the right-hand button strip.
|
||||
const int chart_w = home_land ? (cw - RSTRIP) : cw;
|
||||
#else
|
||||
@@ -32116,6 +32172,8 @@ static int wifiScanWatchdogSafe(uint32_t cap_ms, uint16_t per_chan_ms = 300) {
|
||||
#define WADA_BOARD_ID "attaky"
|
||||
#elif defined(HAS_WIO_TRACKER_L2)
|
||||
#define WADA_BOARD_ID "wio-tracker-l2"
|
||||
#elif defined(HAS_CROWPANEL_35)
|
||||
#define WADA_BOARD_ID "crowpanel-35"
|
||||
#elif defined(HELTEC_LORA_V4_TFT)
|
||||
#define WADA_BOARD_ID "heltec-v4-tft"
|
||||
#else
|
||||
@@ -33795,7 +33853,7 @@ static void renderMapTiles() {
|
||||
} else
|
||||
#endif
|
||||
if (!s_tiles_fs_ready) {
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9)
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HAS_CROWPANEL_35)
|
||||
// Pager included: under the launcher there's no "tiles" partition, so point the user at the
|
||||
// microSD fallback rather than the (launcher-wrong) "reflash the tiles partition" advice.
|
||||
// M9 included: its cache PREFERS the built-in 16 GB microSD (every unit ships with one), so
|
||||
@@ -38196,6 +38254,9 @@ static void makeSettings(lv_obj_t* tab) {
|
||||
const lv_coord_t card_h = landscape ? 54 : 46;
|
||||
|
||||
for (int c = 0; c < CAT_COUNT; ++c) {
|
||||
#if !CAP_BATTERY
|
||||
if (c == CAT_BATTERY) continue;
|
||||
#endif
|
||||
#if !CAP_LOCK_SCREEN
|
||||
if (c == CAT_LOCK) continue; // lock screen is a T-Deck / Tanmatsu feature
|
||||
#endif
|
||||
@@ -47644,7 +47705,7 @@ static void openPowerMenu() {
|
||||
// ROM force-download leaves a COM that esptool cannot open on HW CDC — hide the entry.
|
||||
const int card_w = (sw - 40 > 240) ? 240 : (sw - 40);
|
||||
const int p_bh = 34, p_y0 = 28, p_step = 40, card_h = p_y0 + 3 * p_step + 8;
|
||||
#elif defined(HAS_THINKNODE_M9) || defined(HAS_WIO_TRACKER_L2)
|
||||
#elif defined(HAS_THINKNODE_M9) || defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35)
|
||||
// Power-off row hidden (see below) — 3 rows: Reboot / Download / Cancel.
|
||||
const int card_w = (sw - 40 > 240) ? 240 : (sw - 40);
|
||||
const int p_bh = 34, p_y0 = 28, p_step = 40, card_h = p_y0 + 3 * p_step + 8;
|
||||
@@ -47977,7 +48038,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)
|
||||
#elif defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35)
|
||||
#define HAS_CC_BRIGHTNESS 1
|
||||
static uint8_t s_brightness_pct = 63;
|
||||
static void applyBrightness(uint8_t pct) {
|
||||
@@ -49716,14 +49777,6 @@ static lv_timer_t* s_usbfiles_timer = nullptr;
|
||||
static bool s_usbfiles_map_dirty = false; // tiles or the card changed: re-check the map
|
||||
static void closeUsbFilesPage();
|
||||
|
||||
#if CAP_SD
|
||||
// FatFs drive of the Arduino SD mount, for the session's fast folder listings.
|
||||
// Same pointer-to-member route as luaHostSdClearAttributes (further down).
|
||||
struct UsbFilesSdDrive : fs::SDFS {
|
||||
static uint8_t of(const fs::SDFS& sd) { return sd.*(&UsbFilesSdDrive::_pdrv); }
|
||||
};
|
||||
#endif
|
||||
|
||||
class UiUsbFilesHost : public UsbFilesHost {
|
||||
public:
|
||||
bool root(UsbFiles::Root r, UsbFilesRootInfo* out, bool mount) override {
|
||||
@@ -49737,7 +49790,7 @@ class UiUsbFilesHost : public UsbFilesHost {
|
||||
const bool up = mount ? fmSdTryMount() : s_sd_mounted;
|
||||
if (up && SD.cardType() != CARD_NONE) {
|
||||
out->fs = &SD;
|
||||
snprintf(out->drive, sizeof out->drive, "%u:", (unsigned)UsbFilesSdDrive::of(SD));
|
||||
snprintf(out->drive, sizeof out->drive, "%u:", (unsigned)sdDriveNumber());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -51541,6 +51594,10 @@ static void buildGlobalStatusBar() {
|
||||
// the bar root's NAV_SKIP into its children, so each indicator opts out individually.)
|
||||
lv_obj_add_flag(g_statusbar.batt_icon, NAV_SKIP_FLAG);
|
||||
lv_obj_add_flag(g_statusbar.batt_pct, NAV_SKIP_FLAG);
|
||||
#if !CAP_BATTERY
|
||||
lv_obj_add_flag(g_statusbar.batt_icon, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(g_statusbar.batt_pct, LV_OBJ_FLAG_HIDDEN);
|
||||
#endif
|
||||
|
||||
g_statusbar.clock = lv_label_create(g_statusbar.root);
|
||||
lv_label_set_text(g_statusbar.clock, "--:--");
|
||||
@@ -55337,7 +55394,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)
|
||||
#if !defined(HAS_WIO_TRACKER_L2) && !defined(HAS_CROWPANEL_35)
|
||||
s_kb_rot_left_btn = makeRotBtn(LV_SYMBOL_REFRESH, kbRotLeftCb);
|
||||
s_kb_rot_right_btn = makeRotBtn(LV_SYMBOL_REFRESH, kbRotRightCb);
|
||||
#endif
|
||||
@@ -57657,13 +57714,10 @@ bool luaHostSdReadFailed() { return sdReadFailedCardDead(); }
|
||||
// Declared by hand like f_mkfs above: ff.h drags FatFs' BYTE/WORD typedefs into
|
||||
// this file. FRESULT is an int-sized enum and FR_OK is 0.
|
||||
extern "C" int f_chmod(const char* path, uint8_t attr, uint8_t mask);
|
||||
struct SdPdrvAccess : fs::SDFS {
|
||||
static uint8_t of(const fs::SDFS& sd) { return sd.*(&SdPdrvAccess::_pdrv); }
|
||||
};
|
||||
bool luaHostSdClearAttributes(const char* path) {
|
||||
if (!s_sd_mounted || !path || path[0] != '/') return false;
|
||||
char ffpath[200];
|
||||
const int n = snprintf(ffpath, sizeof ffpath, "%u:%s", (unsigned)SdPdrvAccess::of(SD), path);
|
||||
const int n = snprintf(ffpath, sizeof ffpath, "%u:%s", (unsigned)sdDriveNumber(), path);
|
||||
if (n <= 0 || n >= (int)sizeof ffpath) return false;
|
||||
markSdIo();
|
||||
constexpr uint8_t kAmRdo = 0x01, kAmHid = 0x02, kAmSys = 0x04; // FatFs AM_RDO/AM_HID/AM_SYS
|
||||
@@ -58361,7 +58415,7 @@ static bool uiDataFsIsSdCard() {
|
||||
if (!uiDataFsReady()) return false;
|
||||
#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_WIO_TRACKER_L2)
|
||||
return s_ui_data_fs == &SD_MMC;
|
||||
#elif defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#elif CAP_SD || defined(TLORA_PAGER)
|
||||
return s_ui_data_fs == &SD;
|
||||
#else
|
||||
return false;
|
||||
@@ -58378,7 +58432,7 @@ static File uiDataOpen(const char* name, const char* mode) {
|
||||
if (!uiDataFsReady()) return File();
|
||||
char p[80]; snprintf(p, sizeof p, "%s%s", s_ui_data_root, name);
|
||||
File f = s_ui_data_fs->open(p, mode);
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
// A failed WRITE open on the SD-backed history store is the wedge tell (reads
|
||||
// fail legitimately on first boot). Called from the loop task AND the core-0
|
||||
// history worker — sdNoteIoFailure is a volatile stamp, safe from both.
|
||||
@@ -59032,7 +59086,7 @@ static bool uiMsgsWriteResult(bool ok) {
|
||||
s_msgs_write_fail_ms = m ? m : 1;
|
||||
s_msgs_write_fail_epoch = ep;
|
||||
if (s_msgs_write_fails < 0xFFFFu) s_msgs_write_fails = s_msgs_write_fails + 1;
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
if (s_ui_data_fs == &SD) sdNoteIoFailure();
|
||||
#endif
|
||||
}
|
||||
@@ -60736,7 +60790,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
s_tile_fs = &s_tiles_fs;
|
||||
s_tile_root[0] = '\0';
|
||||
}
|
||||
#if defined(HAS_TDECK_GT911)
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_CROWPANEL_35)
|
||||
// Under Launcher there's no "tiles" partition; cache to the SD card instead.
|
||||
// main.cpp already mounted the card for SD data storage (boot runs well before
|
||||
// ui_task.begin()), so PREFER that live mount via SD.cardType(). Calling
|
||||
@@ -61088,6 +61142,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
#else
|
||||
#if defined(HAS_RAK_TAP_V2) || defined(HAS_WIO_TRACKER_L2)
|
||||
const int draw_band_w = 320;
|
||||
#elif defined(HAS_CROWPANEL_35)
|
||||
const int draw_band_w = 480;
|
||||
g_draw_buf_px = draw_band_w * LV_DRAW_BUF_LINES;
|
||||
#else
|
||||
const int draw_band_w = 240;
|
||||
#endif
|
||||
@@ -61304,6 +61361,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
#elif defined(HAS_WIO_TRACKER_L2)
|
||||
g_lv.disp_drv.hor_res = 320;
|
||||
g_lv.disp_drv.ver_res = 240;
|
||||
#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);
|
||||
#else
|
||||
// Landscape rotates the panel in HARDWARE (smooth — no per-pixel software
|
||||
// rotation each flush), so tell LVGL the already-rotated resolution and let
|
||||
@@ -61376,6 +61436,12 @@ 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)
|
||||
else if (!s_remote_mode) {
|
||||
// A file-backed portrait preference may not have been loaded at the boot logo.
|
||||
applyHardwarePanelRotation(LV_DISP_ROT_NONE);
|
||||
}
|
||||
#endif
|
||||
// Swipe-axis transform always matches the visible orientation.
|
||||
heltecV4CapTouchSetRotation(s_ui_rotation);
|
||||
|
||||
@@ -62589,9 +62655,8 @@ static inline void touchScreenBacklight(bool on) {
|
||||
ledcWrite(kM9BlPwmChannel, 255); // inverted: 255 = 0% conduction = off
|
||||
touchPanelSleep(true);
|
||||
}
|
||||
#elif defined(HAS_WIO_TRACKER_L2)
|
||||
// L2: brightness is controlled by the display's I2C light controller, not a
|
||||
// direct TFT_BL GPIO. Restore the saved level on wake and write zero on sleep.
|
||||
#elif defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35)
|
||||
// The display owns its backlight (I2C on Wio, PWM on CrowPanel).
|
||||
if (on) applyBrightness(s_brightness_pct);
|
||||
else display.setBrightness(0);
|
||||
#elif defined(HAS_TDISPLAY_P4)
|
||||
@@ -62653,6 +62718,11 @@ void UITask::wakeScreen() {
|
||||
}
|
||||
|
||||
void UITask::lockScreen() {
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
// No physical unlock button: imported lock preferences must still allow touch wake.
|
||||
sleepScreen();
|
||||
return;
|
||||
#endif
|
||||
// Backlight off + manual lock so touch is ignored (noteUserInput()
|
||||
// early-returns) until a deliberate unlock.
|
||||
#if defined(HAS_TDECK_PRO)
|
||||
@@ -63711,10 +63781,14 @@ static void sdHealthTick() {
|
||||
#if defined(TLORA_PAGER)
|
||||
if (!board.sdCardPresent()) return; // card-detect says the slot is empty
|
||||
#endif
|
||||
#if !defined(HAS_CROWPANEL_35)
|
||||
SPIClass* spi = sdSharedSPI();
|
||||
if (!spi) return;
|
||||
#endif
|
||||
sdMountDiagBegin();
|
||||
#if defined(TLORA_PAGER)
|
||||
#if defined(HAS_CROWPANEL_35)
|
||||
const bool remounted = sdBeginTracked();
|
||||
#elif defined(TLORA_PAGER)
|
||||
// The Pager shares this SPIClass with display + radio: one 4 MHz attempt,
|
||||
// with every other CS parked HIGH. A failed attempt gets one dedicated
|
||||
// SD-rail power cycle before retrying; the display/radio bus stays live.
|
||||
@@ -63739,7 +63813,7 @@ static void sdHealthTick() {
|
||||
#endif
|
||||
markSdIo();
|
||||
if (g_lv.task) g_lv.task->showAlert(TR("SD card remounted"), 1800);
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
// Land the RAM ring on the card promptly, not up to 30+ s later: every
|
||||
// message received while the card was out is only in RAM. Armed as an
|
||||
// OFF-THREAD flush — a synchronous write here froze the UI for >30 s on
|
||||
@@ -63880,7 +63954,7 @@ static void sdHealthTick() {
|
||||
s_sd_data_warn_next_ms = 0;
|
||||
#endif
|
||||
if (g_lv.task) g_lv.task->showAlert(TR("SD card remounted"), 1800);
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
if (!s_ui_data_fs) uiDataFsReady();
|
||||
if (s_ui_data_fs == &SD) {
|
||||
SD.mkdir("/meshcomod"); // fresh replacement card: recreate the data root
|
||||
@@ -63914,16 +63988,20 @@ void UITask::loop() {
|
||||
// because LVGL is never initialised, so nothing ever started the poll task
|
||||
// and the keyboard was never even begun. That is why keys did nothing.
|
||||
//
|
||||
// One call covers both: the background poll task owns the shared I2C bus
|
||||
// and scans the touch panel AND (on the T-Deck) the keyboard, which is
|
||||
// exactly why they must not be polled from two places at once.
|
||||
// The background task scans touch and (on the T-Deck) the keyboard.
|
||||
// CrowPanel 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)
|
||||
heltecV4CapTouchStartBackgroundPoll(8);
|
||||
#endif
|
||||
s_con_input_up = true;
|
||||
}
|
||||
}
|
||||
if (s_con_input_up && !heltecV4CapTouchIsAsyncPolling()) {
|
||||
(void)heltecV4CapTouchCheck();
|
||||
}
|
||||
#endif
|
||||
// The physical-keyboard drain lives further down this function, below this
|
||||
// return, so console mode has to do its own. Same ring, filled by the task
|
||||
@@ -64668,10 +64746,10 @@ void UITask::loop() {
|
||||
}
|
||||
if (g_lv.touch_inited) {
|
||||
pushDiagLine("touch init ok");
|
||||
#if defined(HAS_WIO_TRACKER_L2)
|
||||
#if defined(HAS_WIO_TRACKER_L2) || defined(HAS_CROWPANEL_35)
|
||||
// GT911 (LovyanGFX) and the TCA9535/ADS1115 (Wire) share I2C0 on
|
||||
// GPIO47/48 but use different driver locks. Keep every transaction on
|
||||
// loopTask so the touch poll cannot interrupt an expander/ADC transfer.
|
||||
// Wio's GPIO47/48; the CrowPanel's GT911 shares I2C0 on GPIO15/16.
|
||||
// Keep every transaction on loopTask so polling cannot interrupt Wire.
|
||||
pushDiagLine("touch inline (shared I2C)");
|
||||
#elif defined(HAS_TDECK_PRO)
|
||||
// The e-paper BUSY callback keeps this inline driver polling while the
|
||||
@@ -64779,7 +64857,7 @@ void UITask::loop() {
|
||||
// unmountable card spikes current / churns the bus and can reset the board.
|
||||
if (!sdRuntimeLifecycleBusy() && now >= s_sd_retry_after_ms && fmSdTryMount()) {
|
||||
showAlert(TR("SD card inserted"), 1500);
|
||||
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8)
|
||||
#if CAP_SD || defined(TLORA_PAGER)
|
||||
if (!s_ui_data_fs) uiDataFsReady();
|
||||
if (s_ui_data_fs == &SD) {
|
||||
SD.mkdir("/meshcomod"); // fresh replacement card: recreate the data root
|
||||
|
||||
@@ -25,7 +25,18 @@
|
||||
// =============================================================================
|
||||
|
||||
// ---- Per-board structural capabilities (factored out of the device names) ----
|
||||
#if defined(HAS_WIO_TRACKER_L2) // ===== Seeed Wio Tracker L2 (ESP32-S3) =====
|
||||
#if defined(HAS_CROWPANEL_35) // ===== Elecrow CrowPanel Advance 3.5 (ESP32-S3) =====
|
||||
#define CAP_TOUCH 1 // GT911 capacitive touch
|
||||
#define CAP_ROTATABLE 1 // ILI9488 native portrait, upright landscape = rotation 3
|
||||
#define CAP_LARGE_SCREEN 0 // native 480x320, no upscaling
|
||||
#define CAP_SD 1 // microSD over software SPI, SD-compatible filesystem API
|
||||
#define CAP_FILESYSTEM 1 // internal SPIFFS + tiles LittleFS
|
||||
#define CAP_GPS 1 // optional external UART1 GPS
|
||||
#define CAP_OTA 1
|
||||
#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_WIO_TRACKER_L2) // ===== Seeed Wio Tracker L2 (ESP32-S3) =====
|
||||
#define CAP_TOUCH 1
|
||||
#define CAP_ROTATABLE 0
|
||||
#define CAP_LARGE_SCREEN 0 // fixed 320x240 landscape
|
||||
@@ -170,9 +181,14 @@
|
||||
#define CAP_LOCK_SCREEN 0
|
||||
#endif
|
||||
|
||||
// Boards without a battery ADC must not advertise battery history/calibration.
|
||||
#ifndef CAP_BATTERY
|
||||
#define CAP_BATTERY 1
|
||||
#endif
|
||||
|
||||
// Physical removable microSD slot, independent of the filesystem API used to
|
||||
// drive it. CAP_SD specifically means Arduino SD over SPI; these three boards
|
||||
// use SD_MMC instead but still need the same user-facing card diagnostics.
|
||||
// drive it. CAP_SD means the SD-compatible SPI API (hardware or software);
|
||||
// SD_MMC boards still need the same user-facing card diagnostics.
|
||||
#if CAP_SD || defined(HAS_WIO_TRACKER_L2) || defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4)
|
||||
#define CAP_MICROSD 1
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include "CrowPanel35SdCodec.h"
|
||||
|
||||
int main() {
|
||||
using namespace CrowPanel35SdCodec;
|
||||
const uint8_t cmd0[] = {0x40, 0, 0, 0, 0};
|
||||
const uint8_t cmd8[] = {0x48, 0, 0, 0x01, 0xAA};
|
||||
assert(commandCrc(cmd0, sizeof cmd0) == 0x95);
|
||||
assert(commandCrc(cmd8, sizeof cmd8) == 0x87);
|
||||
uint16_t crc = 0;
|
||||
for (const char* byte = "123456789"; *byte; ++byte) crc = dataCrc(crc, (uint8_t)*byte);
|
||||
assert(crc == 0x31C3);
|
||||
|
||||
uint8_t csd[16] = {};
|
||||
csd[0] = 0x40;
|
||||
csd[8] = 0xFF;
|
||||
csd[9] = 0xFF;
|
||||
assert(sectorsFromCsd(csd) == 67108864); // 32 GiB, block-addressed.
|
||||
csd[7] = 0x3F;
|
||||
assert(sectorsFromCsd(csd) == 0); // 2 TiB overflows 32-bit sector count.
|
||||
csd[0] = 0x80;
|
||||
assert(sectorsFromCsd(csd) == 0); // Unsupported CSD structure.
|
||||
|
||||
memset(csd, 0, sizeof csd);
|
||||
csd[5] = 9;
|
||||
csd[6] = 3;
|
||||
csd[7] = 0xFF;
|
||||
csd[8] = 0xC0;
|
||||
csd[9] = 3;
|
||||
csd[10] = 0x80;
|
||||
assert(sectorsFromCsd(csd) == 2097152); // 1 GiB, byte-addressed.
|
||||
assert(validRange(100, 0, 100));
|
||||
assert(validRange(100, 99, 1));
|
||||
assert(!validRange(100, 100, 1));
|
||||
assert(!validRange(100, 0, 0));
|
||||
assert(!validRange(100, 99, 2));
|
||||
assert(!validRange(UINT32_MAX, UINT32_MAX - 1, UINT32_MAX));
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import os
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "merge-bin.py"
|
||||
|
||||
|
||||
class MergeBinTests(unittest.TestCase):
|
||||
def load_action(self, output=None):
|
||||
self.build_dir = "/project with spaces/board's build"
|
||||
self.app_path = self.build_dir + "/firmware.bin"
|
||||
self.images = [
|
||||
("0x0", self.build_dir + "/bootloader.bin"),
|
||||
("0x8000", self.build_dir + "/partitions.bin"),
|
||||
("0xe000", "/framework with spaces/boot_app0.bin"),
|
||||
]
|
||||
self.process_env = {"PATH": "/tools with spaces"}
|
||||
env = MagicMock()
|
||||
env.BoardConfig.return_value = {
|
||||
"build.mcu": "esp32s3",
|
||||
"build.flash_mode": "qio",
|
||||
"upload.flash_size": "16MB",
|
||||
}
|
||||
env.get.return_value = self.images
|
||||
env.Flatten.side_effect = lambda images: [value for pair in images for value in pair]
|
||||
substitutions = {
|
||||
"$PYTHONEXE": "/python with spaces/python",
|
||||
"$OBJCOPY": "/tools with spaces/esptool.py",
|
||||
"${BUILD_DIR}/${PROGNAME}-merged.bin": self.build_dir + "/firmware-merged.bin",
|
||||
"${__get_board_f_flash(__env__)}": "80m",
|
||||
"$ESP32_APP_OFFSET": "0x10000",
|
||||
}
|
||||
env.subst.side_effect = lambda value: substitutions.get(value, value)
|
||||
env.__getitem__.return_value = self.process_env
|
||||
environ = {} if output is None else {"MERGED_BIN_PATH": output}
|
||||
with patch.dict(os.environ, environ, clear=True):
|
||||
script = runpy.run_path(str(SCRIPT), init_globals={
|
||||
"Import": lambda *args: None,
|
||||
"env": env,
|
||||
"projenv": env,
|
||||
})
|
||||
source = MagicMock()
|
||||
source.get_abspath.return_value = self.app_path
|
||||
return script["merge_bin_action"], [source], env
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_paths_and_flash_offsets_are_preserved(self, run):
|
||||
action, source, env = self.load_action()
|
||||
run.return_value.returncode = 0
|
||||
self.assertEqual(action(source, [], env), 0)
|
||||
run.assert_called_once_with([
|
||||
"/python with spaces/python",
|
||||
"/tools with spaces/esptool.py",
|
||||
"--chip", "esp32s3",
|
||||
"merge_bin",
|
||||
"-o", self.build_dir + "/firmware-merged.bin",
|
||||
"--flash_mode", "qio",
|
||||
"--flash_freq", "80m",
|
||||
"--flash_size", "16MB",
|
||||
*[value for pair in self.images for value in pair],
|
||||
"0x10000", self.app_path,
|
||||
], env=self.process_env)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_output_override_is_preserved(self, run):
|
||||
output = "/custom output/owner's merged image.bin"
|
||||
action, source, env = self.load_action(output)
|
||||
run.return_value.returncode = 0
|
||||
self.assertEqual(action(source, [], env), 0)
|
||||
args = run.call_args.args[0]
|
||||
self.assertEqual(args[args.index("-o") + 1], output)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_merge_failure_is_returned(self, run):
|
||||
action, source, env = self.load_action()
|
||||
run.return_value.returncode = 2
|
||||
self.assertEqual(action(source, [], env), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <helpers/ESP32Board.h>
|
||||
|
||||
class CrowPanel35Board : public ESP32Board {
|
||||
public:
|
||||
void begin() {
|
||||
digitalWrite(PIN_SD_CS, HIGH);
|
||||
pinMode(PIN_SD_CS, OUTPUT);
|
||||
digitalWrite(45, LOW); // LOW routes the expansion slot to LoRa, not the microphone.
|
||||
pinMode(45, OUTPUT);
|
||||
ESP32Board::begin();
|
||||
}
|
||||
|
||||
const char* getManufacturerName() const override { return "Elecrow CrowPanel Advance 3.5"; }
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include "CrowPanel35Display.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
CrowPanel35Display::CrowPanel35Display() : DisplayDriver(480, 320) {
|
||||
{
|
||||
auto cfg = _bus.config();
|
||||
cfg.spi_host = SPI3_HOST;
|
||||
cfg.spi_mode = 0;
|
||||
cfg.freq_write = 40000000;
|
||||
cfg.freq_read = 16000000;
|
||||
cfg.spi_3wire = false;
|
||||
cfg.use_lock = true;
|
||||
cfg.dma_channel = SPI_DMA_CH_AUTO;
|
||||
cfg.pin_sclk = 42;
|
||||
cfg.pin_mosi = 39;
|
||||
cfg.pin_miso = -1;
|
||||
cfg.pin_dc = 41;
|
||||
_bus.config(cfg);
|
||||
_panel.setBus(&_bus);
|
||||
}
|
||||
{
|
||||
auto cfg = _panel.config();
|
||||
cfg.pin_cs = 40;
|
||||
cfg.pin_rst = -1;
|
||||
cfg.pin_busy = -1;
|
||||
cfg.panel_width = 320;
|
||||
cfg.panel_height = 480;
|
||||
cfg.memory_width = 320;
|
||||
cfg.memory_height = 480;
|
||||
cfg.offset_x = 0;
|
||||
cfg.offset_y = 0;
|
||||
cfg.readable = false;
|
||||
cfg.invert = true;
|
||||
cfg.rgb_order = false;
|
||||
cfg.dlen_16bit = false;
|
||||
cfg.bus_shared = false;
|
||||
_panel.config(cfg);
|
||||
}
|
||||
{
|
||||
auto cfg = _light.config();
|
||||
cfg.pin_bl = 38;
|
||||
cfg.invert = false;
|
||||
cfg.freq = 44000;
|
||||
cfg.pwm_channel = 7;
|
||||
_light.config(cfg);
|
||||
_panel.setLight(&_light);
|
||||
}
|
||||
{
|
||||
auto cfg = _touch.config();
|
||||
cfg.i2c_port = 0;
|
||||
cfg.i2c_addr = 0x5D;
|
||||
cfg.pin_sda = 15;
|
||||
cfg.pin_scl = 16;
|
||||
cfg.pin_int = 47;
|
||||
cfg.pin_rst = 48;
|
||||
cfg.bus_shared = false;
|
||||
cfg.freq = 400000;
|
||||
cfg.x_min = 0;
|
||||
cfg.x_max = 319;
|
||||
cfg.y_min = 0;
|
||||
cfg.y_max = 479;
|
||||
cfg.offset_rotation = 0;
|
||||
_touch.config(cfg);
|
||||
_panel.setTouch(&_touch);
|
||||
}
|
||||
_lcd.setPanel(&_panel);
|
||||
}
|
||||
|
||||
bool CrowPanel35Display::begin() {
|
||||
if (_isOn) return true;
|
||||
if (!_lcd.init()) {
|
||||
Serial.println("[crowpanel-35] ILI9488 display init failed");
|
||||
return false;
|
||||
}
|
||||
_lcd.setSwapBytes(true);
|
||||
setDisplayRotation(3);
|
||||
_lcd.setBrightness(160);
|
||||
_lcd.fillScreen(0);
|
||||
Wire.end();
|
||||
if (!Wire.begin(15, 16, 400000)) {
|
||||
Serial.println("[crowpanel-35] GT911 I2C bus init failed");
|
||||
return false;
|
||||
}
|
||||
_isOn = true;
|
||||
Serial.printf("[crowpanel-35] display %dx%d\n", width(), height());
|
||||
return true;
|
||||
}
|
||||
|
||||
void CrowPanel35Display::turnOn() {
|
||||
if (!_isOn) { _lcd.setBrightness(160); _isOn = true; }
|
||||
}
|
||||
void CrowPanel35Display::turnOff() {
|
||||
if (_isOn) { _lcd.setBrightness(0); _isOn = false; }
|
||||
}
|
||||
void CrowPanel35Display::clear() { _lcd.fillScreen(0); }
|
||||
void CrowPanel35Display::setDisplayRotation(uint8_t rotation) {
|
||||
// Keep the saved portrait mode at 0; rotate the panel and touch together by 180 degrees.
|
||||
_lcd.setRotation(rotation == 0 ? 2 : rotation);
|
||||
setLogicalSize(_lcd.width(), _lcd.height());
|
||||
}
|
||||
|
||||
void CrowPanel35Display::startFrame(ColorVal bkg) { _lcd.fillScreen(bkg); }
|
||||
void CrowPanel35Display::setTextSize(int sz) { _lcd.setTextSize(sz); }
|
||||
void CrowPanel35Display::setColor(ColorVal c) { _color = c; _lcd.setTextColor(c); }
|
||||
void CrowPanel35Display::setCursor(int x, int y) { _lcd.setCursor(x, y); }
|
||||
void CrowPanel35Display::print(const char* str) { _lcd.print(str); }
|
||||
void CrowPanel35Display::fillRect(int x, int y, int w, int h) {
|
||||
_lcd.fillRect(x, y, w, h, _color);
|
||||
}
|
||||
void CrowPanel35Display::drawRect(int x, int y, int w, int h) {
|
||||
_lcd.drawRect(x, y, w, h, _color);
|
||||
}
|
||||
void CrowPanel35Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
_lcd.drawXBitmap(x, y, bits, w, h, _color);
|
||||
}
|
||||
uint16_t CrowPanel35Display::getTextWidth(const char* str) { return _lcd.textWidth(str); }
|
||||
void CrowPanel35Display::endFrame() {}
|
||||
void CrowPanel35Display::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<uint16_t*>(pixels), (uint32_t)w * h);
|
||||
_lcd.endWrite();
|
||||
}
|
||||
void CrowPanel35Display::setBrightness(uint8_t value) { _lcd.setBrightness(value); }
|
||||
bool CrowPanel35Display::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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
|
||||
#define LGFX_USE_V1
|
||||
#include <LovyanGFX.hpp>
|
||||
|
||||
class CrowPanel35Display : public DisplayDriver {
|
||||
public:
|
||||
CrowPanel35Display();
|
||||
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);
|
||||
|
||||
private:
|
||||
lgfx::Bus_SPI _bus;
|
||||
lgfx::Panel_ILI9488 _panel;
|
||||
lgfx::Light_PWM _light;
|
||||
lgfx::Touch_GT911 _touch;
|
||||
lgfx::LGFX_Device _lcd;
|
||||
bool _isOn = false;
|
||||
uint16_t _color = 0xffff;
|
||||
};
|
||||
@@ -0,0 +1,478 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (c) 2025-2026 Michael A. Cojocari and contributors
|
||||
// Adapted from Camillia-MT's sd_soft_spi.cpp for WadaMesh's SD filesystem API.
|
||||
#include "CrowPanel35SD.h"
|
||||
#include "CrowPanel35SdCodec.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vfs_api.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "diskio_impl.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "ff.h"
|
||||
#include "soc/gpio_reg.h"
|
||||
#include "rom/ets_sys.h"
|
||||
|
||||
static_assert(PIN_SD_SCLK >= 0 && PIN_SD_SCLK < 32, "SD SCLK must be GPIO0-31");
|
||||
static_assert(PIN_SD_MISO >= 0 && PIN_SD_MISO < 32, "SD MISO must be GPIO0-31");
|
||||
static_assert(PIN_SD_MOSI >= 0 && PIN_SD_MOSI < 32, "SD MOSI must be GPIO0-31");
|
||||
static_assert(PIN_SD_CS >= 0 && PIN_SD_CS < 32, "SD CS must be GPIO0-31");
|
||||
|
||||
namespace {
|
||||
constexpr const char* kMountPoint = "/sd";
|
||||
constexpr uint8_t kMaxOpenFiles = 8;
|
||||
constexpr uint32_t kClockMask = 1UL << PIN_SD_SCLK;
|
||||
constexpr uint32_t kMosiMask = 1UL << PIN_SD_MOSI;
|
||||
constexpr uint32_t kCsMask = 1UL << PIN_SD_CS;
|
||||
constexpr uint8_t kReady = 0x00, kIdle = 0x01, kIllegal = 0x04;
|
||||
constexpr uint8_t kSingle = 0xFE, kMulti = 0xFC, kStop = 0xFD;
|
||||
sdcard_type_t s_type = CARD_NONE;
|
||||
uint32_t s_sectors = 0;
|
||||
BYTE s_drive = FF_DRV_NOT_USED;
|
||||
bool s_mounted = false, s_vfs_registered = false, s_slow = true;
|
||||
SemaphoreHandle_t s_mutex = nullptr;
|
||||
SemaphoreHandle_t s_lifecycle_mutex = nullptr;
|
||||
|
||||
// FatFs serializes filesystem operations, but the health probe also sends SD
|
||||
// commands outside FatFs. Keep each command/data sequence indivisible.
|
||||
class CardLock {
|
||||
SemaphoreHandle_t _mutex;
|
||||
public:
|
||||
explicit CardLock(SemaphoreHandle_t mutex = s_mutex) : _mutex(mutex) {
|
||||
xSemaphoreTakeRecursive(_mutex, portMAX_DELAY);
|
||||
}
|
||||
~CardLock() { xSemaphoreGiveRecursive(_mutex); }
|
||||
CardLock(const CardLock&) = delete;
|
||||
CardLock& operator=(const CardLock&) = delete;
|
||||
};
|
||||
|
||||
uint8_t transfer(uint8_t out) {
|
||||
uint8_t in = 0;
|
||||
for (int bit = 7; bit >= 0; --bit) {
|
||||
REG_WRITE((out >> bit) & 1 ? GPIO_OUT_W1TS_REG : GPIO_OUT_W1TC_REG, kMosiMask);
|
||||
if (s_slow) ets_delay_us(2);
|
||||
REG_WRITE(GPIO_OUT_W1TS_REG, kClockMask);
|
||||
if (s_slow) ets_delay_us(2);
|
||||
in = (uint8_t)((in << 1) | ((REG_READ(GPIO_IN_REG) >> PIN_SD_MISO) & 1));
|
||||
REG_WRITE(GPIO_OUT_W1TC_REG, kClockMask);
|
||||
}
|
||||
return in;
|
||||
}
|
||||
|
||||
uint8_t receive() { return transfer(0xFF); }
|
||||
void select() { REG_WRITE(GPIO_OUT_W1TC_REG, kCsMask); }
|
||||
void release() {
|
||||
REG_WRITE(GPIO_OUT_W1TS_REG, kCsMask);
|
||||
receive(); // Clock MISO free after deselecting.
|
||||
}
|
||||
bool waitReady(uint32_t timeout) {
|
||||
const uint32_t start = millis();
|
||||
do {
|
||||
if (receive() == 0xFF) return true;
|
||||
delay(1);
|
||||
} while ((uint32_t)(millis() - start) < timeout);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t command(uint8_t cmd, uint32_t arg) {
|
||||
select();
|
||||
if (cmd != 0 && cmd != 12 && !waitReady(500)) return 0xFF;
|
||||
const uint8_t frame[] = {
|
||||
(uint8_t)(0x40 | cmd), (uint8_t)(arg >> 24), (uint8_t)(arg >> 16),
|
||||
(uint8_t)(arg >> 8), (uint8_t)arg
|
||||
};
|
||||
for (uint8_t byte : frame) transfer(byte);
|
||||
transfer(CrowPanel35SdCodec::commandCrc(frame, sizeof frame));
|
||||
if (cmd == 12) receive(); // STOP_TRANSMISSION has a leading stuff byte.
|
||||
uint8_t response = 0xFF;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
response = receive();
|
||||
if (!(response & 0x80)) break;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
uint8_t appCommand(uint8_t cmd, uint32_t arg) {
|
||||
const uint8_t response = command(55, 0);
|
||||
return response <= kIdle ? command(cmd, arg) : response;
|
||||
}
|
||||
|
||||
bool readBlock(uint8_t* data, size_t length) {
|
||||
const uint32_t start = millis();
|
||||
uint8_t token;
|
||||
do {
|
||||
token = receive();
|
||||
if (token == 0xFF) delay(1);
|
||||
} while (token == 0xFF && (uint32_t)(millis() - start) < 300);
|
||||
if (token != kSingle) return false;
|
||||
uint16_t crc = 0;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
data[i] = receive();
|
||||
crc = CrowPanel35SdCodec::dataCrc(crc, data[i]);
|
||||
}
|
||||
const uint16_t expected = (uint16_t)receive() << 8;
|
||||
return (expected | receive()) == crc;
|
||||
}
|
||||
|
||||
bool writeBlock(const uint8_t* data, uint8_t token) {
|
||||
if (!waitReady(1000)) return false;
|
||||
transfer(token);
|
||||
uint16_t crc = 0;
|
||||
for (size_t i = 0; i < 512; ++i) {
|
||||
transfer(data[i]);
|
||||
crc = CrowPanel35SdCodec::dataCrc(crc, data[i]);
|
||||
}
|
||||
transfer((uint8_t)(crc >> 8));
|
||||
transfer((uint8_t)crc);
|
||||
const bool accepted = (receive() & 0x1F) == 0x05;
|
||||
const bool ready = waitReady(1000);
|
||||
return accepted && ready;
|
||||
}
|
||||
|
||||
bool cardInit() {
|
||||
digitalWrite(PIN_SD_CS, HIGH);
|
||||
pinMode(PIN_SD_CS, OUTPUT);
|
||||
pinMode(PIN_SD_SCLK, OUTPUT);
|
||||
pinMode(PIN_SD_MOSI, OUTPUT);
|
||||
pinMode(PIN_SD_MISO, INPUT_PULLUP);
|
||||
REG_WRITE(GPIO_OUT_W1TC_REG, kClockMask);
|
||||
REG_WRITE(GPIO_OUT_W1TS_REG, kMosiMask);
|
||||
s_slow = true;
|
||||
s_type = CARD_NONE;
|
||||
s_sectors = 0;
|
||||
for (int i = 0; i < 10; ++i) receive(); // At least 74 power-up clocks, CS high.
|
||||
|
||||
uint8_t response = 0xFF;
|
||||
for (int i = 0; i < 10 && response != kIdle; ++i) {
|
||||
response = command(0, 0);
|
||||
release();
|
||||
if (response != kIdle) delay(10);
|
||||
}
|
||||
if (response != kIdle) {
|
||||
Serial.printf("[sd] soft SPI: CMD0 failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
response = command(59, 1);
|
||||
release();
|
||||
if (response == (kIdle | kIllegal)) {
|
||||
Serial.println("[sd] soft SPI: card does not support write CRC checks");
|
||||
} else if (response != kIdle) {
|
||||
Serial.printf("[sd] soft SPI: CRC enable failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
|
||||
sdcard_type_t type = CARD_NONE;
|
||||
uint32_t start = millis();
|
||||
response = command(8, 0x1AA);
|
||||
if (response == kIdle) {
|
||||
uint8_t echo[4];
|
||||
for (auto& byte : echo) byte = receive();
|
||||
release();
|
||||
if (echo[2] != 0x01 || echo[3] != 0xAA) {
|
||||
Serial.println("[sd] soft SPI: CMD8 voltage/check pattern mismatch");
|
||||
return false;
|
||||
}
|
||||
do {
|
||||
response = appCommand(41, 1UL << 30);
|
||||
release();
|
||||
if (response == kIdle) delay(1);
|
||||
} while (response == kIdle && (uint32_t)(millis() - start) < 1000);
|
||||
if (response != kReady) {
|
||||
Serial.printf("[sd] soft SPI: ACMD41 failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
response = command(58, 0);
|
||||
if (response != kReady) {
|
||||
release();
|
||||
Serial.printf("[sd] soft SPI: CMD58 failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
uint8_t ocr[4];
|
||||
for (auto& byte : ocr) byte = receive();
|
||||
release();
|
||||
if (!(ocr[0] & 0x80) || !(ocr[1] & 0x10)) {
|
||||
Serial.println("[sd] soft SPI: card not ready for 3.3 V operation");
|
||||
return false;
|
||||
}
|
||||
type = (ocr[0] & 0x40) ? CARD_SDHC : CARD_SD;
|
||||
} else {
|
||||
release();
|
||||
if (response != (kIdle | kIllegal)) {
|
||||
Serial.printf("[sd] soft SPI: CMD8 failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
response = appCommand(41, 0);
|
||||
release();
|
||||
if (response <= kIdle) {
|
||||
type = CARD_SD;
|
||||
while (response == kIdle && (uint32_t)(millis() - start) < 1000) {
|
||||
delay(1);
|
||||
response = appCommand(41, 0);
|
||||
release();
|
||||
}
|
||||
} else {
|
||||
type = CARD_MMC;
|
||||
start = millis();
|
||||
do {
|
||||
response = command(1, 0);
|
||||
release();
|
||||
if (response == kIdle) delay(1);
|
||||
} while (response == kIdle && (uint32_t)(millis() - start) < 1000);
|
||||
}
|
||||
if (response != kReady) {
|
||||
Serial.printf("[sd] soft SPI: legacy init failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (type != CARD_SDHC) {
|
||||
response = command(16, 512);
|
||||
release();
|
||||
if (response != kReady) {
|
||||
Serial.printf("[sd] soft SPI: CMD16 failed (R1=0x%02X)\n", response);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
uint8_t csd[16];
|
||||
const bool csd_ok = command(9, 0) == kReady && readBlock(csd, sizeof csd);
|
||||
release();
|
||||
if (!csd_ok) {
|
||||
Serial.println("[sd] soft SPI: CSD read/CRC failed");
|
||||
return false;
|
||||
}
|
||||
s_sectors = CrowPanel35SdCodec::sectorsFromCsd(csd);
|
||||
if (!s_sectors || (type != CARD_SDHC && s_sectors > UINT32_MAX / 512UL + 1)) {
|
||||
Serial.println("[sd] soft SPI: invalid or unsupported capacity");
|
||||
s_sectors = 0;
|
||||
return false;
|
||||
}
|
||||
s_type = type;
|
||||
s_slow = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cardReady() {
|
||||
if (s_type == CARD_NONE) return false;
|
||||
const uint8_t response = command(13, 0);
|
||||
const uint8_t status = response == kReady ? receive() : 0xFF;
|
||||
release();
|
||||
return response == kReady && status == 0;
|
||||
}
|
||||
|
||||
bool readSectors(uint8_t* data, uint32_t sector, unsigned count) {
|
||||
const uint32_t address = s_type == CARD_SDHC ? sector : sector * 512;
|
||||
bool ok;
|
||||
if (count == 1) {
|
||||
ok = command(17, address) == kReady && readBlock(data, 512);
|
||||
} else {
|
||||
ok = command(18, address) == kReady;
|
||||
if (ok) {
|
||||
for (unsigned i = 0; ok && i < count; ++i) ok = readBlock(data + i * 512, 512);
|
||||
const bool stopped = command(12, 0) == kReady;
|
||||
const bool ready = waitReady(500);
|
||||
ok = ok && stopped && ready;
|
||||
}
|
||||
}
|
||||
release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool writeSectors(const uint8_t* data, uint32_t sector, unsigned count) {
|
||||
const uint32_t address = s_type == CARD_SDHC ? sector : sector * 512;
|
||||
bool ok;
|
||||
if (count == 1) {
|
||||
ok = command(24, address) == kReady && writeBlock(data, kSingle);
|
||||
} else {
|
||||
ok = command(25, address) == kReady;
|
||||
if (ok) {
|
||||
for (unsigned i = 0; ok && i < count; ++i) ok = writeBlock(data + i * 512, kMulti);
|
||||
if (waitReady(1000)) {
|
||||
transfer(kStop); // Only an accepted CMD25 owes a stop token.
|
||||
receive();
|
||||
const bool ready = waitReady(1000);
|
||||
ok = ok && ready;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
release();
|
||||
return ok;
|
||||
}
|
||||
|
||||
DSTATUS diskStatus(unsigned char) {
|
||||
CardLock lock;
|
||||
return cardReady() ? 0 : STA_NOINIT;
|
||||
}
|
||||
DSTATUS diskInit(unsigned char drive) { return diskStatus(drive); }
|
||||
|
||||
DRESULT diskRead(unsigned char, unsigned char* data, uint32_t sector, unsigned count) {
|
||||
CardLock lock;
|
||||
if (s_type == CARD_NONE) return RES_NOTRDY;
|
||||
if (!data || !CrowPanel35SdCodec::validRange(s_sectors, sector, count)) {
|
||||
Serial.printf("[sd] soft SPI: invalid read sector=%lu count=%u\n", (unsigned long)sector, count);
|
||||
return RES_PARERR;
|
||||
}
|
||||
if (readSectors(data, sector, count) || readSectors(data, sector, count)) return RES_OK;
|
||||
Serial.printf("[sd] soft SPI: read failed sector=%lu count=%u\n", (unsigned long)sector, count);
|
||||
return RES_ERROR;
|
||||
}
|
||||
DRESULT diskWrite(unsigned char, const unsigned char* data, uint32_t sector, unsigned count) {
|
||||
CardLock lock;
|
||||
if (s_type == CARD_NONE) return RES_NOTRDY;
|
||||
if (!data || !CrowPanel35SdCodec::validRange(s_sectors, sector, count)) {
|
||||
Serial.printf("[sd] soft SPI: invalid write sector=%lu count=%u\n", (unsigned long)sector, count);
|
||||
return RES_PARERR;
|
||||
}
|
||||
if (writeSectors(data, sector, count) || writeSectors(data, sector, count)) return RES_OK;
|
||||
Serial.printf("[sd] soft SPI: write failed sector=%lu count=%u\n", (unsigned long)sector, count);
|
||||
return RES_ERROR;
|
||||
}
|
||||
DRESULT diskIoctl(unsigned char, unsigned char cmd, void* data) {
|
||||
CardLock lock;
|
||||
if (s_type == CARD_NONE) return RES_NOTRDY;
|
||||
if (cmd != CTRL_SYNC && !data) {
|
||||
Serial.printf("[sd] soft SPI: missing ioctl output for %u\n", (unsigned)cmd);
|
||||
return RES_PARERR;
|
||||
}
|
||||
switch (cmd) {
|
||||
case CTRL_SYNC: {
|
||||
select();
|
||||
const bool ready = waitReady(500);
|
||||
release();
|
||||
if (!ready) Serial.println("[sd] soft SPI: sync timed out");
|
||||
return ready ? RES_OK : RES_ERROR;
|
||||
}
|
||||
case GET_SECTOR_COUNT: *(DWORD*)data = s_sectors; return RES_OK;
|
||||
case GET_SECTOR_SIZE: *(WORD*)data = 512; return RES_OK;
|
||||
case GET_BLOCK_SIZE: *(DWORD*)data = 1; return RES_OK;
|
||||
default:
|
||||
Serial.printf("[sd] soft SPI: unsupported ioctl %u\n", (unsigned)cmd);
|
||||
return RES_PARERR;
|
||||
}
|
||||
}
|
||||
const ff_diskio_impl_t kDiskImpl = { diskInit, diskStatus, diskRead, diskWrite, diskIoctl };
|
||||
|
||||
void driveName(char (&name)[8], uint8_t drive) { snprintf(name, sizeof name, "%u:", (unsigned)drive); }
|
||||
|
||||
bool space(uint64_t& total, uint64_t& used) {
|
||||
const uint8_t drive = SD.driveNumber();
|
||||
if (drive == FF_DRV_NOT_USED) return false;
|
||||
char name[8];
|
||||
driveName(name, drive);
|
||||
FATFS* fat = nullptr;
|
||||
DWORD free_clusters;
|
||||
// Do not hold CardLock while acquiring FatFs' volume lock: disk callbacks
|
||||
// acquire CardLock in the opposite order.
|
||||
const FRESULT result = f_getfree(name, &free_clusters, &fat);
|
||||
if (result != FR_OK || !fat || fat->n_fatent < 2 || free_clusters > fat->n_fatent - 2) {
|
||||
Serial.printf("[sd] soft SPI: free-space read failed (%u)\n", (unsigned)result);
|
||||
return false;
|
||||
}
|
||||
total = (uint64_t)(fat->n_fatent - 2) * fat->csize * 512ULL;
|
||||
used = total - (uint64_t)free_clusters * fat->csize * 512ULL;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CrowPanel35SD SD;
|
||||
CrowPanel35SD::CrowPanel35SD() : fs::FS(fs::FSImplPtr(new VFSImpl())) {}
|
||||
|
||||
bool CrowPanel35SD::begin() {
|
||||
if (!s_mutex) s_mutex = xSemaphoreCreateRecursiveMutex();
|
||||
if (!s_lifecycle_mutex) s_lifecycle_mutex = xSemaphoreCreateRecursiveMutex();
|
||||
if (!s_mutex || !s_lifecycle_mutex) {
|
||||
Serial.println("[sd] soft SPI: mutex allocation failed");
|
||||
return false;
|
||||
}
|
||||
CardLock lifecycle(s_lifecycle_mutex);
|
||||
{
|
||||
CardLock lock;
|
||||
if (s_mounted) return true;
|
||||
}
|
||||
{
|
||||
CardLock lock;
|
||||
if (!cardInit()) return false;
|
||||
}
|
||||
const esp_err_t drive_error = ff_diskio_get_drive(&s_drive);
|
||||
if (drive_error != ESP_OK || s_drive == FF_DRV_NOT_USED) {
|
||||
Serial.printf("[sd] soft SPI: no FatFs drive (%s)\n", esp_err_to_name(drive_error));
|
||||
end();
|
||||
return false;
|
||||
}
|
||||
ff_diskio_register(s_drive, &kDiskImpl);
|
||||
char name[8];
|
||||
driveName(name, s_drive);
|
||||
FATFS* fat = nullptr;
|
||||
const esp_err_t vfs_error = esp_vfs_fat_register(kMountPoint, name, kMaxOpenFiles, &fat);
|
||||
if (vfs_error != ESP_OK) {
|
||||
Serial.printf("[sd] soft SPI: VFS registration failed (%s)\n", esp_err_to_name(vfs_error));
|
||||
end();
|
||||
return false;
|
||||
}
|
||||
s_vfs_registered = true;
|
||||
const FRESULT mount_error = f_mount(fat, name, 1);
|
||||
if (mount_error != FR_OK) {
|
||||
Serial.printf("[sd] soft SPI: FAT mount failed (%u); card left unchanged\n", (unsigned)mount_error);
|
||||
end();
|
||||
return false;
|
||||
}
|
||||
_impl->mountpoint(kMountPoint);
|
||||
{
|
||||
CardLock lock;
|
||||
s_mounted = true;
|
||||
}
|
||||
Serial.printf("[sd] soft SPI mounted: %llu MB\n", (unsigned long long)(cardSize() / (1024ULL * 1024)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void CrowPanel35SD::end() {
|
||||
if (!s_lifecycle_mutex) return; // No begin() has created a mount.
|
||||
CardLock lifecycle(s_lifecycle_mutex);
|
||||
_impl->mountpoint(nullptr);
|
||||
if (s_vfs_registered) {
|
||||
char name[8];
|
||||
driveName(name, s_drive);
|
||||
const FRESULT result = f_mount(nullptr, name, 0);
|
||||
if (result != FR_OK) Serial.printf("[sd] soft SPI: FAT unmount failed (%u)\n", (unsigned)result);
|
||||
const esp_err_t vfs_error = esp_vfs_fat_unregister_path(kMountPoint);
|
||||
if (vfs_error != ESP_OK)
|
||||
Serial.printf("[sd] soft SPI: VFS unregister failed (%s)\n", esp_err_to_name(vfs_error));
|
||||
}
|
||||
if (s_drive != FF_DRV_NOT_USED) ff_diskio_unregister(s_drive);
|
||||
if (!s_mutex) return;
|
||||
CardLock lock;
|
||||
s_drive = FF_DRV_NOT_USED;
|
||||
s_type = CARD_NONE;
|
||||
s_sectors = 0;
|
||||
s_mounted = false;
|
||||
s_vfs_registered = false;
|
||||
}
|
||||
|
||||
sdcard_type_t CrowPanel35SD::cardType() const {
|
||||
if (!s_mutex) return CARD_NONE;
|
||||
CardLock lock;
|
||||
return s_mounted ? s_type : CARD_NONE;
|
||||
}
|
||||
uint64_t CrowPanel35SD::cardSize() const {
|
||||
if (!s_mutex) return 0;
|
||||
CardLock lock;
|
||||
return s_mounted ? (uint64_t)s_sectors * 512ULL : 0;
|
||||
}
|
||||
uint8_t CrowPanel35SD::driveNumber() const {
|
||||
if (!s_mutex) return FF_DRV_NOT_USED;
|
||||
CardLock lock;
|
||||
return s_mounted ? s_drive : FF_DRV_NOT_USED;
|
||||
}
|
||||
bool CrowPanel35SD::probeAlive() const {
|
||||
if (!s_mutex) return false;
|
||||
CardLock lock;
|
||||
return s_mounted && cardReady();
|
||||
}
|
||||
uint64_t CrowPanel35SD::totalBytes() const {
|
||||
uint64_t total = 0, used = 0;
|
||||
return space(total, used) ? total : 0;
|
||||
}
|
||||
uint64_t CrowPanel35SD::usedBytes() const {
|
||||
uint64_t total = 0, used = 0;
|
||||
return space(total, used) ? used : 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (c) 2025-2026 Michael A. Cojocari and contributors
|
||||
#pragma once
|
||||
|
||||
#include <FS.h>
|
||||
#include <sd_defines.h>
|
||||
|
||||
// SD-compatible filesystem facade for Camillia-MT's software-SPI card backend.
|
||||
// Neither the display's SPI3 host nor the radio's SPI2 host is reconfigured.
|
||||
class CrowPanel35SD : public fs::FS {
|
||||
public:
|
||||
CrowPanel35SD();
|
||||
bool begin();
|
||||
void end();
|
||||
sdcard_type_t cardType() const;
|
||||
uint64_t cardSize() const;
|
||||
uint64_t totalBytes() const;
|
||||
uint64_t usedBytes() const;
|
||||
uint8_t driveNumber() const;
|
||||
bool probeAlive() const;
|
||||
};
|
||||
|
||||
extern CrowPanel35SD SD;
|
||||
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (c) 2025-2026 Michael A. Cojocari and contributors
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace CrowPanel35SdCodec {
|
||||
|
||||
inline uint8_t commandCrc(const uint8_t* frame, size_t length) {
|
||||
uint8_t crc = 0;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
uint8_t byte = frame[i];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
crc = (uint8_t)(crc << 1);
|
||||
if ((byte ^ crc) & 0x80) crc ^= 0x09;
|
||||
byte = (uint8_t)(byte << 1);
|
||||
}
|
||||
}
|
||||
return (uint8_t)((crc << 1) | 1);
|
||||
}
|
||||
|
||||
inline uint16_t dataCrc(uint16_t crc, uint8_t byte) {
|
||||
crc ^= (uint16_t)byte << 8;
|
||||
for (int bit = 0; bit < 8; ++bit)
|
||||
crc = (uint16_t)((crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1);
|
||||
return crc;
|
||||
}
|
||||
|
||||
inline uint32_t sectorsFromCsd(const uint8_t (&csd)[16]) {
|
||||
uint64_t sectors;
|
||||
const uint8_t version = csd[0] >> 6;
|
||||
if (version == 1) {
|
||||
const uint32_t size = ((uint32_t)(csd[7] & 0x3F) << 16)
|
||||
| ((uint32_t)csd[8] << 8) | csd[9];
|
||||
sectors = ((uint64_t)size + 1) * 1024;
|
||||
} else if (version == 0) {
|
||||
const uint32_t blockLength = csd[5] & 0x0F;
|
||||
const uint32_t size = ((uint32_t)(csd[6] & 0x03) << 10)
|
||||
| ((uint32_t)csd[7] << 2) | (csd[8] >> 6);
|
||||
const uint32_t multiplier = ((csd[9] & 0x03) << 1) | (csd[10] >> 7);
|
||||
sectors = (((uint64_t)size + 1) << (multiplier + 2 + blockLength)) / 512;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
return sectors <= UINT32_MAX ? (uint32_t)sectors : 0;
|
||||
}
|
||||
|
||||
inline bool validRange(uint32_t sectors, uint32_t sector, unsigned count) {
|
||||
return count && sector < sectors && count <= sectors - sector;
|
||||
}
|
||||
|
||||
} // namespace CrowPanel35SdCodec
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
|
||||
CrowPanel35Board 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);
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
MicroNMEALocationProvider gps(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors(gps);
|
||||
#else
|
||||
EnvironmentSensorManager sensors;
|
||||
#endif
|
||||
|
||||
CrowPanel35Display display;
|
||||
|
||||
bool radio_init() {
|
||||
fallback_clock.begin();
|
||||
rtc_clock.begin(Wire);
|
||||
if (!radio.std_init(&radioSpi)) {
|
||||
Serial.println("[crowpanel-35] SX1262 not found on the LoRa expansion slot");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include "../../src/helpers/ClockFloorRTC.h"
|
||||
#include "helpers/sensors/EnvironmentSensorManager.h"
|
||||
#include "helpers/sensors/MicroNMEALocationProvider.h"
|
||||
#include "CrowPanel35Board.h"
|
||||
#include "CrowPanel35Display.h"
|
||||
|
||||
extern CrowPanel35Board board;
|
||||
extern RADIO_CLASS radio;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern ClockFloorRTC rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
extern CrowPanel35Display display;
|
||||
|
||||
bool radio_init();
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
Reference in New Issue
Block a user