diff --git a/DEVICES.md b/DEVICES.md index a863ef6..3b937cf 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -46,6 +46,9 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases - **RAK WisMesh Tap V2**: newest port, touch-driven. The browser-flash path is fresh; if the flasher cannot open the serial port, put the board in download mode manually and retry, and please report it. +- **square**: 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. ## Requested boards diff --git a/boards/square.json b/boards/square.json new file mode 100644 index 0000000..733da5a --- /dev/null +++ b/boards/square.json @@ -0,0 +1,43 @@ +{ + "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=1", + "-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", + "hwids": [["0x303A", "0x1001"]], + "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": "square (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 8388608, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "", + "vendor": "" +} \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 0d67afa..bca1f26 100644 --- a/platformio.ini +++ b/platformio.ini @@ -678,6 +678,159 @@ lib_deps = densaugeo/base64 @ ~1.4.0 knolleary/PubSubClient @ ^2.8.0 +; ====================================================================== +; square — touch-first ESP32-S3 target with quad-SPI display and SX1262. +; ====================================================================== +[env:square] +platform = platformio/espressif32@6.11.0 +framework = arduino +board = square +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_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__}/Adafruit ADS1X15" + -I"${platformio.libdeps_dir}/${this.__env__}/LovyanGFX/src" + -I variants/square + -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_SQUARE=1 + -D BOARD_HAS_PSRAM=1 + -D ESP32_CPU_FREQ=240 + -D CORE_DEBUG_LEVEL=0 + -D LORA_FREQ=869.618 + -D LORA_BW=62.5 + -D LORA_SF=8 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D P_LORA_NSS=21 + -D P_LORA_RESET=7 + -D P_LORA_DIO_1=9 + -D P_LORA_BUSY=8 + -D P_LORA_SCLK=4 + -D P_LORA_MISO=5 + -D P_LORA_MOSI=6 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -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_SKIP_GPS_DETECT=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 + -D PIN_GPS_RX=18 + -D PIN_GPS_TX=17 + -D GPS_BAUD_RATE=9600 + -D PIN_BOARD_SDA=47 + -D PIN_BOARD_SCL=48 + -D PIN_USER_BTN=0 + -D PIN_TFT_LEDA_CTL=-1 + -D PIN_TFT_RST=-1 + -D PIN_TFT_DC=-1 + -D DISPLAY_CLASS=SquareDisplay + -D DISPLAY_ROTATION=0 + -D DISPLAY_SCALE_X=1.0 + -D DISPLAY_SCALE_Y=1.0 + -D UI_LVGL=1 + -D HAS_TOUCH_UI=1 + -D FIRMWARE_OTA_ENV='"square"' + -D ENABLE_ADVERT_ON_BOOT=0 + -D LV_CONF_PATH=lv_conf.h + -D LV_CONF_INCLUDE_SIMPLE=1 + -D LV_INDEV_DEF_SCROLL_THROW=7 + -D MAX_CONTACTS=2000 + -D MAX_GROUP_CHANNELS=40 + -D ADMIN_PASSWORD='"password"' + -D MULTI_TRANSPORT_COMPANION=1 + -D WADAMESH_FORK_BUILD=1 + -D TCP_PORT=5000 + -D WS_PORT=8765 + -D BLE_PIN_CODE=123456 + -D ENABLE_PRIVATE_KEY_IMPORT=1 + -D ENABLE_PRIVATE_KEY_EXPORT=1 + -D CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=1 + -D CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 + -D CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 + +build_src_filter = + +<*.cpp> + +<*.c> + + + + + + + - + + + + + + + +<../variants/square/*.cpp> + +lib_deps = + https://github.com/ALLFATHER-BV/meshcomod.git#core-v1.17.4 + SPI + Wire + SD_MMC + jgromes/RadioLib @ ^7.6.0 + rweather/Crypto @ ^0.4.0 + adafruit/RTClib @ ^2.1.3 + melopero/Melopero RV3028 @ ^1.1.0 + electroniccats/CayenneLPP @ 1.6.1 + h2zero/NimBLE-Arduino @ 1.4.3 + adafruit/Adafruit ADS1X15 @ 2.6.2 + stevemarple/MicroNMEA @ ^2.0.6 + ESP32Async/ESPAsyncWebServer @ 3.10.3 + file://arch/esp32/AsyncElegantOTA + lvgl/lvgl @ ^8.3.11 + lovyan03/LovyanGFX @ 1.2.27 + densaugeo/base64 @ ~1.4.0 + knolleary/PubSubClient @ ^2.8.0 + [env:tlora_pager_lr1121_companion_radio_touch] platform = platformio/espressif32@6.11.0 framework = arduino diff --git a/scripts/build/patch_lgfx_dmadesc.py b/scripts/build/patch_lgfx_dmadesc.py new file mode 100644 index 0000000..3248280 --- /dev/null +++ b/scripts/build/patch_lgfx_dmadesc.py @@ -0,0 +1,84 @@ +# Guard LovyanGFX's ESP32 SPI DMA descriptor allocation against OOM. +# Each PlatformIO environment owns a separate libdeps copy, so this script only +# changes environments that list it in extra_scripts. +Import("env") +import os + +MARKER = "wadamesh-square-dmadesc-oom-patch" + +OLD_ALLOC = """ void Bus_SPI::_alloc_dmadesc(size_t len) + { + if (_dmadesc) heap_caps_free(_dmadesc); + _dmadesc_size = len; + _dmadesc = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t) * len, MALLOC_CAP_DMA); + }""" + +NEW_ALLOC = """ void Bus_SPI::_alloc_dmadesc(size_t len) + { + // wadamesh-square-dmadesc-oom-patch: retain the working descriptor array + // when a replacement cannot be allocated. + auto next = (lldesc_t*)heap_caps_malloc(sizeof(lldesc_t) * len, MALLOC_CAP_DMA); + if (next == nullptr) { return; } + if (_dmadesc) heap_caps_free(_dmadesc); + _dmadesc = next; + _dmadesc_size = len; + }""" + +OLD_SETUP = """ if (_dmadesc_size * SPI_MAX_DMA_LEN < len) + { + _alloc_dmadesc(len / SPI_MAX_DMA_LEN + 1); + } + lldesc_t *dmadesc = _dmadesc;""" + +NEW_SETUP = """ if (_dmadesc_size * SPI_MAX_DMA_LEN < len) + { + _alloc_dmadesc(len / SPI_MAX_DMA_LEN + 1); + } + // wadamesh-square-dmadesc-oom-patch: skip a frame rather than walking a + // null or undersized descriptor array after allocation failure. + if (_dmadesc == nullptr || _dmadesc_size * SPI_MAX_DMA_LEN < len) { return; } + lldesc_t *dmadesc = _dmadesc;""" + +path = os.path.join( + env.subst("$PROJECT_LIBDEPS_DIR"), + env.subst("$PIOENV"), + "LovyanGFX", "src", "lgfx", "v1", "platforms", "esp32", "Bus_SPI.cpp" +) + +if not os.path.isfile(path): + print("[patch_lgfx_dmadesc] LovyanGFX not fetched yet; patch deferred") +else: + with open(path, encoding="utf-8") as source_file: + source = source_file.read() + marker_count = source.count(MARKER) + if marker_count == 2: + print("[patch_lgfx_dmadesc] already patched") + elif marker_count != 0 or OLD_ALLOC not in source or OLD_SETUP not in source: + raise RuntimeError("[patch_lgfx_dmadesc] LovyanGFX source drift or partial patch") + else: + source = source.replace(OLD_ALLOC, NEW_ALLOC, 1).replace(OLD_SETUP, NEW_SETUP, 1) + with open(path, "w", encoding="utf-8") as source_file: + source_file.write(source) + print("[patch_lgfx_dmadesc] patched square LovyanGFX DMA allocation") + + + def verify_patched(target, source, env): + del target, source, env + if not os.path.isfile(path): + print("[patch_lgfx_dmadesc] ERROR: LovyanGFX still missing at link time") + return 1 + with open(path, encoding="utf-8") as source_file: + current = source_file.read() + if current.count(MARKER) == 2: + return 0 + if MARKER not in current and OLD_ALLOC in current and OLD_SETUP in current: + current = current.replace(OLD_ALLOC, NEW_ALLOC, 1).replace(OLD_SETUP, NEW_SETUP, 1) + with open(path, "w", encoding="utf-8") as source_file: + source_file.write(current) + print("[patch_lgfx_dmadesc] patched after dependency fetch; re-run the build") + else: + print("[patch_lgfx_dmadesc] ERROR: source drift or partial patch at link time") + return 1 + + + env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", verify_patched) \ No newline at end of file diff --git a/src/DataStore.cpp b/src/DataStore.cpp index 4919f10..93ded15 100644 --- a/src/DataStore.cpp +++ b/src/DataStore.cpp @@ -6,7 +6,7 @@ #include #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) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) #include #endif @@ -1132,15 +1132,9 @@ bool DataStore::useSdStorage() { } #endif -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) -// P4 boards: same full-store adoption as useSdStorage(), but on the SD_MMC slot. -// The internal FFat 'locfd' has a broken FAT metadata layer (f_stat/exists lie — -// see the tile-cache notes), and the exists()-gated identity + prefs loads that -// "worked" at -Og read different garbage at -Os and came up empty: fresh node -// identity, default name, profile changes gone every reboot. The card's FAT -// metadata is truthful, so identity/prefs move there with everything else. -// The caller migrates any FFat-resident files first (open()-probed, never -// exists() on FFat). +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) +// Full-store adoption on an SD_MMC slot. P4 callers migrate internal files +// first; square mounts and adopts the card directly at boot. bool DataStore::useSdMmcStorage() { if (!SD_MMC.exists("/meshcomod")) SD_MMC.mkdir("/meshcomod"); if (!SD_MMC.exists("/meshcomod/bl")) SD_MMC.mkdir("/meshcomod/bl"); diff --git a/src/DataStore.h b/src/DataStore.h index 0a547c3..8ca687f 100644 --- a/src/DataStore.h +++ b/src/DataStore.h @@ -43,10 +43,9 @@ public: // if the dir can't be created. No-op on non-ESP32. bool useSdStorage(); #endif -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) - // Full-store adoption of the SD_MMC card (the P4 boards): identity + prefs move - // off the broken-metadata internal FFat (its exists()/f_stat lie, which made the - // gated loads come up empty). Caller migrates FFat-resident files first. +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) + // Full-store adoption of an SD_MMC card. P4 callers migrate internal files + // first; square adopts the card directly at boot. bool useSdMmcStorage(); #endif FILESYSTEM* getPrimaryFS() const { return _fs; } diff --git a/src/main.cpp b/src/main.cpp index e57bec9..ab878a0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,6 +49,10 @@ static uint32_t _atoi(const char* sp) { DataStore store(LittleFS, rtc_clock); #elif defined(ESP32) #include + #if defined(HAS_SQUARE) + #include + #include + #endif #if defined(HAS_TDECK_GT911) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) #include #include "SdFastClock.h" // post-mount operating-clock raise (SD_SPI_FAST_HZ boards) @@ -892,6 +896,25 @@ void setup() { // failure falls back to SPIFFS so the device always boots. bool spiffs_ok = SPIFFS.begin(false); // try first WITHOUT auto-format bool sd_storage = false; +#if defined(HAS_SQUARE) + // This target uses a dedicated one-bit SD_MMC bus rather than the LoRa SPI + // bus. Power the card first, set the non-default pins, and adopt it as the + // complete store when available; SPIFFS remains the graceful fallback. + bool square_sd_begun = false; + if (SquareIo::ready() && SquareIo::setSdPower(true) && + SD_MMC.setPins(2, 3, 1) && + (square_sd_begun = SD_MMC.begin("/sdcard", true, false)) && + SD_MMC.cardType() != CARD_NONE) { + sd_storage = store.useSdMmcStorage(); + g_contacts_on_sd = sd_storage; + g_full_data_on_sd = sd_storage; + Serial.printf("[BOOT] square SD_MMC: %s\n", sd_storage ? "adopted" : "mount only"); + } else { + if (square_sd_begun) SD_MMC.end(); + (void)SquareIo::setSdPower(false); + Serial.println("[BOOT] square SD_MMC unavailable; using SPIFFS"); + } +#endif #if defined(HAS_TDECK_GT911) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) { #if defined(TLORA_PAGER) @@ -1130,7 +1153,10 @@ void setup() { // Route touch settings + Wi-Fi creds to the active filesystem (SD when that's // the data store, else SPIFFS) instead of NVS. Old NVS values still load and // migrate on their next save, so this is a transparent in-place upgrade. - #if defined(HAS_TDECK_GT911) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) + #if defined(HAS_SQUARE) + SdNvsPrefs::useFile(sd_storage ? (fs::FS*)&SD_MMC : (fs::FS*)&SPIFFS, + sd_storage ? "/meshcomod" : "/prefs"); + #elif defined(HAS_TDECK_GT911) || defined(HELTEC_LORA_V4_R8) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) SdNvsPrefs::useFile(sd_storage ? (fs::FS*)&SD : (fs::FS*)&SPIFFS, sd_storage ? "/meshcomod" : "/prefs"); #else diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index dcdfcd9..d4d0a7c 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -99,7 +99,7 @@ // MyMesh.h -> target.h -> TLoraPagerBoard.h above #include // pager ES8311 codec (notification tones + WAV playback) #include "Es8311Codec.h" // PIN_I2S_MCLK/BCK/WS/DOUT/SDIN come from platformio.ini build flags -#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) #include // internal FAT partition (Tanmatsu 'locfd' / P4 'storage') #include // microSD on the P4-class boards' SDMMC slot 0; slot 1 = C6 radio extern bool g_fs_ok; // set in main.cpp once the internal FAT is mounted @@ -149,6 +149,9 @@ #include // badge-bsp-backed DisplayDriver (P4) #elif defined(TLORA_PAGER) #include + #elif defined(HAS_SQUARE) + #include + #include #elif defined(HAS_RAK_TAP_V2) #include // LovyanGFX FSPI on RAK Tap V2 #elif defined(HAS_TDISPLAY_P4) @@ -201,6 +204,8 @@ extern TanmatsuDisplay display; #elif defined(TLORA_PAGER) extern ST7796LCDDisplay display; + #elif defined(HAS_SQUARE) + extern SquareDisplay display; #elif defined(HAS_RAK_TAP_V2) || defined(HELTEC_LORA_V4_R8) extern LGFXDisplay display; #elif defined(HAS_TDISPLAY_P4) @@ -18868,7 +18873,7 @@ static char s_fm_path[160] = {0}; // current dir within s_fm_fs (e.g. // fmRefresh, so they blip the LED too. #if defined(HAS_TDECK_GT911) || 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) -#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) static inline bool fmIsSd(fs::FS* fs) { return fs == &SD_MMC; } // microSD on SDMMC slot 0 #else static inline bool fmIsSd(fs::FS*) { return false; } // Heltec: no Arduino SD global in this FM path @@ -21540,7 +21545,7 @@ static void fmRefresh() { } // Roots screen: list the available storages. -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) // ---- Tanmatsu / T-Display P4 microSD (SDMMC slot 0) ---------------------------------------------- // The card sits on the P4's SDMMC *slot 0* (IOMUX pins CLK43/CMD44/D0-3 39-42, selected by the // BOARD_SDMMC_SLOT=0 build define); slot 1 is the C6 radio (esp-hosted / ESP-AT SDIO), which we never @@ -21553,7 +21558,12 @@ static uint32_t s_tan_sd_retry_after = 0; // backoff so an absent/cold card is static bool tanSdTryMount() { if (s_tan_sd_mounted) return true; if (millis() < s_tan_sd_retry_after) return false; -#if defined(HAS_TDISPLAY_P4) +#if defined(HAS_SQUARE) + if (SD_MMC.cardType() != CARD_NONE || + (SquareIo::ready() && SquareIo::setSdPower(true) && + SD_MMC.setPins(2, 3, 1) && + SD_MMC.begin("/sdcard", true, false) && SD_MMC.cardType() != CARD_NONE)) { +#elif defined(HAS_TDISPLAY_P4) // Hot-insert path (no-op when main.cpp already mounted at boot): 20 MHz like the boot ladder, // not Arduino's 40 MHz HIGHSPEED default. if (SD_MMC.begin("/sdcard", false /*4-bit*/, false, SDMMC_FREQ_DEFAULT) && SD_MMC.cardType() != CARD_NONE) { @@ -21565,6 +21575,9 @@ static bool tanSdTryMount() { return true; } SD_MMC.end(); +#if defined(HAS_SQUARE) + (void)SquareIo::setSdPower(false); +#endif s_tan_sd_retry_after = millis() + 8000; // don't grind a missing card on every roots render return false; } @@ -21636,7 +21649,7 @@ static void fmShowRoots() { lv_obj_add_event_cb(sd, fmSdPagerRetryMountCb, LV_EVENT_CLICKED, nullptr); } #endif -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) // microSD (SDMMC slot 0). Probe outside the mount-backoff window so an absent card doesn't grind // the bus on every render; tapping the row forces a fresh mount attempt (tanSdClickCb clears it). if ((s_tan_sd_mounted || millis() >= s_tan_sd_retry_after) && tanSdTryMount()) { @@ -39671,6 +39684,15 @@ 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_SQUARE) +#define HAS_CC_BRIGHTNESS 1 +static uint8_t s_brightness_pct = 63; +static void applyBrightness(uint8_t pct) { + if (pct < 5) pct = 5; + if (pct > 100) pct = 100; + s_brightness_pct = pct; + display.setBrightness((uint8_t)((uint32_t)pct * 255u / 100u)); +} #elif defined(HAS_THINKNODE_M9) // M9: BL_EN (GPIO17) is a PNP transistor gate. LEDC PWM confirmed working on hardware // (Specter bring-up), but the duty is INVERTED — lower duty on the base = MORE conduction @@ -42109,9 +42131,15 @@ static void statusBarReaderBackCb(lv_event_t* e) { // compression) is the quickest viewable format to emit — rows are a direct copy // of the RGB565 framebuffer (LV_COLOR_16_SWAP is 0). Saved to /screenshots. static void takeScreenshotToSd() { -#if CAP_SD || defined(TLORA_PAGER) +#if CAP_SD || defined(TLORA_PAGER) || defined(HAS_SQUARE) auto toast = [&](const char* m){ if (g_lv.task) g_lv.task->showAlert(m, 1800); }; +#if defined(HAS_SQUARE) + fs::FS& screenshotFs = SD_MMC; + if (SD_MMC.cardType() == CARD_NONE) { toast(TR("Screenshot: no SD card")); return; } +#else + fs::FS& screenshotFs = SD; if (SD.cardType() == CARD_NONE) { toast(TR("Screenshot: no SD card")); return; } +#endif const int W = lv_disp_get_hor_res(nullptr); const int H = lv_disp_get_ver_res(nullptr); lv_color_t* buf = (lv_color_t*)heap_caps_malloc((size_t)W * H * sizeof(lv_color_t), MALLOC_CAP_SPIRAM); @@ -42127,7 +42155,7 @@ static void takeScreenshotToSd() { g_shot_buf = nullptr; markSdIo(); - SD.mkdir("/screenshots"); + screenshotFs.mkdir("/screenshots"); char path[48]; const time_t now = time(nullptr); if (now > 1700000000) { @@ -42136,7 +42164,7 @@ static void takeScreenshotToSd() { } else { snprintf(path, sizeof path, "/screenshots/up_%lu.bmp", (unsigned long)millis()); } - File f = SD.open(path, FILE_WRITE); + File f = screenshotFs.open(path, FILE_WRITE); if (!f) { free(buf); toast(TR("Screenshot: write failed")); return; } const uint32_t row_bytes = (uint32_t)(((W * 16 + 31) / 32) * 4); // 4-byte aligned @@ -47039,6 +47067,9 @@ void UITask::flushHistoryIfDue(unsigned long now) { // the chat to the SD card. static fs::FS* s_ui_data_fs = nullptr; static char s_ui_data_root[16] = ""; +#if defined(HAS_SQUARE) +extern bool g_full_data_on_sd; // boot adopted the square SD_MMC profile +#endif #if defined(TLORA_PAGER) static bool s_ui_data_boot_finalized = false; #endif @@ -47070,7 +47101,17 @@ static bool uiDataFsReady() { return false; } #endif -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +#if defined(HAS_SQUARE) + // Follow the boot-time DataStore decision. Never attach a card later in the + // same boot, because the in-memory ring was not loaded from that profile. + if (g_full_data_on_sd && SD_MMC.cardType() != CARD_NONE) { + SD_MMC.mkdir("/meshcomod"); + s_ui_data_fs = &SD_MMC; + strncpy(s_ui_data_root, "/meshcomod", sizeof s_ui_data_root - 1); + return true; + } + if (SPIFFS.begin(false)) { s_ui_data_fs = &SPIFFS; s_ui_data_root[0] = '\0'; return true; } +#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) // Tanmatsu + T-Display P4: prefer the microSD card. On the Tanmatsu the internal FFat 'locfd' // loses frequently-rewritten data (broken FAT metadata; see the tile-cache notes); on the // T-Display P4 SD-first keeps history on the same medium the DataStore adopts. Mirror the @@ -47902,7 +47943,7 @@ void luaHostTextPrompt(const char* title, const char* initial, void (*cb)(const // message ring (MAX_UI_MESSAGES_SD) since the card has room for the bigger file. static bool uiDataFsIsSdCard() { if (!uiDataFsReady()) return false; -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HAS_SQUARE) return s_ui_data_fs == &SD_MMC; #elif defined(HAS_TDECK_GT911) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8) return s_ui_data_fs == &SD; @@ -50321,7 +50362,15 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no } } #endif -#if !defined(HAS_TDISPLAY_P4) +#if defined(HAS_SQUARE) + else if (SD_MMC.cardType() != CARD_NONE) { + s_tile_fs = &SD_MMC; + s_tile_root[0] = '\0'; + s_tiles_fs_ready = true; + WIRE_DBG("[TILE] square: caching Wi-Fi tiles on SD_MMC /tiles\n"); + } +#endif +#if !defined(HAS_TDISPLAY_P4) && !defined(HAS_SQUARE) else { s_tile_fs = nullptr; // no cache backend at all -> map shows the storage notice } @@ -50571,7 +50620,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no g_draw_buffer = (lv_color_t*)heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!g_draw_buffer) g_draw_buffer = (lv_color_t*)malloc(buf_bytes); #else -#if defined(HAS_RAK_TAP_V2) +#if defined(HAS_RAK_TAP_V2) || defined(HAS_SQUARE) const int draw_band_w = 320; #else const int draw_band_w = 240; @@ -50672,6 +50721,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // must match so LVGL renders the full 320x240 landscape surface. s_ui_rotation = LV_DISP_ROT_270; #endif + #if defined(HAS_SQUARE) + // The panel driver exposes its fixed 320x240 landscape surface directly. + s_ui_rotation = LV_DISP_ROT_NONE; + #endif #if defined(ATTAKY_MESH_SERIES) // Display and touch share this landscape transform. s_ui_rotation = LV_DISP_ROT_90; @@ -50769,6 +50822,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // display for the browser without changing the physical placeholder panel. g_lv.disp_drv.hor_res = (s_remote_mode && !s_remote_landscape) ? 222 : 480; g_lv.disp_drv.ver_res = (s_remote_mode && !s_remote_landscape) ? 480 : 222; +#elif defined(HAS_SQUARE) + g_lv.disp_drv.hor_res = 320; + g_lv.disp_drv.ver_res = 240; #else // Landscape rotates the panel in HARDWARE (smooth — no per-pixel software // rotation each flush), so tell LVGL the already-rotated resolution and let @@ -53065,11 +53121,23 @@ void UITask::loop() { { static bool s_user_btn_inited = false; static uint8_t s_user_btn_prev = HIGH; + auto readUserButton = []() { + uint8_t state = digitalRead(PIN_USER_BTN); +#if defined(HAS_SQUARE) + static bool expanderDown = false; + if (digitalRead(45) == LOW || expanderDown) { + bool pressed = false; + if (SquareIo::readWakeButton(pressed)) expanderDown = pressed; + } + if (expanderDown) state = LOW; +#endif + return state; + }; if (!s_user_btn_inited) { - s_user_btn_prev = digitalRead(PIN_USER_BTN); + s_user_btn_prev = readUserButton(); s_user_btn_inited = true; } - uint8_t v = digitalRead(PIN_USER_BTN); + uint8_t v = readUserButton(); #if CAP_TRACKBALL /* On the T-Deck, PIN_USER_BTN (GPIO0) is the trackball centre click — make * it act as a touch at the cursor: a held click = a held press (so taps, @@ -53126,6 +53194,23 @@ void UITask::loop() { s_tb_click_press = tb_pressed && !s_tb_wake_consume; if (s_tb_click_press) { s_tb_last_active_ms = now; noteUserInput(); } } +#elif defined(HAS_SQUARE) + // Either physical button wakes immediately. While awake, a two-second hold + // turns the display off without hard-locking touch input. + static uint32_t s_square_btn_down_ms = 0; + static bool s_square_hold_fired = false; + if (v == LOW && s_user_btn_prev == HIGH) { + if (_screen_off) wakeScreen(); + s_square_btn_down_ms = now; + s_square_hold_fired = false; + } else if (v == LOW && s_user_btn_prev == LOW && !_screen_off && + !s_square_hold_fired && now - s_square_btn_down_ms >= 2000) { + s_square_hold_fired = true; + touchScreenBacklight(false); + setCpuForScreen(false); + _screen_off = true; + _manual_lock = false; + } #elif defined(HAS_RAK_TAP_V2) // RAK Tap V2: single BOOT button (GPIO0), no trackball, no keyboard. // Short press (<1s): toggle screen on/off (does NOT hard-lock -- touch diff --git a/src/ui-touch/device_caps.h b/src/ui-touch/device_caps.h index 879d43f..115064c 100644 --- a/src/ui-touch/device_caps.h +++ b/src/ui-touch/device_caps.h @@ -25,7 +25,17 @@ // ============================================================================= // ---- Per-board structural capabilities (factored out of the device names) ---- -#if defined(HAS_TDECK_GT911) // ===== LilyGo T-Deck (ESP32-S3) ===== +#if defined(HAS_SQUARE) // ===== square (ESP32-S3) ===== + #define CAP_TOUCH 1 + #define CAP_ROTATABLE 0 + #define CAP_LARGE_SCREEN 0 // fixed 320x240 landscape + #define CAP_SD 0 // SD_MMC, not Arduino SD + #define CAP_FILESYSTEM 1 + #define CAP_GPS 1 + #define CAP_OTA 1 + #define CAP_LOCK_SCREEN 1 + +#elif defined(HAS_TDECK_GT911) // ===== LilyGo T-Deck (ESP32-S3) ===== #define CAP_TOUCH 1 // capacitive touchscreen (pointer input) #define CAP_ROTATABLE 0 // panel is fixed landscape #define CAP_LARGE_SCREEN 0 // 320x240 diff --git a/variants/square/SquareBoard.cpp b/variants/square/SquareBoard.cpp new file mode 100644 index 0000000..6177114 --- /dev/null +++ b/variants/square/SquareBoard.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SquareBoard.h" + +#include + +#include "SquareIo.h" + +void SquareBoard::begin() { + ESP32Board::begin(); + Wire.setClock(100000); + if (!SquareIo::begin()) Serial.println("[square] expander unavailable"); +} + +void SquareBoard::powerOff() { + (void)SquareIo::setAudioPaPower(false); + (void)SquareIo::setSdPower(false); + (void)SquareIo::setGnssPower(false); + (void)SquareIo::setLcdPower(false); + enterDeepSleep(0); +} + +bool SquareBoard::ensureBatteryAdc() { + if (_batteryAdcReady) return true; + const uint32_t now = millis(); + if (_nextBatteryProbeMs && (int32_t)(now - _nextBatteryProbeMs) < 0) return false; + if (!SquareIo::ready() || !SquareIo::setBatterySense(true)) return false; + delay(10); + _batteryAdcReady = _batteryAdc.begin(0x48, &Wire); + if (_batteryAdcReady) _batteryAdc.setGain(GAIN_TWO); + (void)SquareIo::setBatterySense(false); + if (!_batteryAdcReady) _nextBatteryProbeMs = now + 15000; + return _batteryAdcReady; +} + +uint16_t SquareBoard::getBattMilliVolts() { + const uint32_t now = millis(); + if (_lastBatteryMv && (uint32_t)(now - _lastBatteryReadMs) < 30000) return _lastBatteryMv; + if (!ensureBatteryAdc() || !SquareIo::setBatterySense(true)) return _lastBatteryMv; + delay(10); + float volts = 0.0f; + uint8_t samples = 0; + for (uint8_t i = 0; i < 3; ++i) { + const int16_t raw = _batteryAdc.readADC_SingleEnded(0); + if (raw > 0) { volts += _batteryAdc.computeVolts(raw); ++samples; } + } + (void)SquareIo::setBatterySense(false); + if (samples) { + _lastBatteryMv = (uint16_t)((volts / samples) * 2000.0f); + _lastBatteryReadMs = now; + } + return _lastBatteryMv; +} + +bool SquareBoard::sdCardPresent() { + bool present = false; + return SquareIo::readSdPresent(present) && present; +} \ No newline at end of file diff --git a/variants/square/SquareBoard.h b/variants/square/SquareBoard.h new file mode 100644 index 0000000..a35cce2 --- /dev/null +++ b/variants/square/SquareBoard.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +class SquareBoard : public ESP32Board { +public: + void begin(); + void powerOff() override; + uint16_t getBattMilliVolts() override; + const char* getManufacturerName() const override { return "square"; } + bool sdCardPresent(); + +private: + bool ensureBatteryAdc(); + + Adafruit_ADS1115 _batteryAdc; + bool _batteryAdcReady = false; + uint16_t _lastBatteryMv = 0; + uint32_t _lastBatteryReadMs = 0; + uint32_t _nextBatteryProbeMs = 0; +}; \ No newline at end of file diff --git a/variants/square/SquareDisplay.cpp b/variants/square/SquareDisplay.cpp new file mode 100644 index 0000000..e15af66 --- /dev/null +++ b/variants/square/SquareDisplay.cpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SquareDisplay.h" + +#include +#include + +namespace { +constexpr uint8_t kLightAddress = 0x2C; +constexpr uint8_t kLightUpdate = 0x0F; +constexpr uint8_t kLightPwm0 = 0x18; +} + +bool SquareLight::writeRegister(uint8_t reg, uint8_t value) { + Wire.beginTransmission(kLightAddress); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; +} + +bool SquareLight::init(uint8_t brightness) { + bool ok = true; + ok = writeRegister(0x00, 0x01) && ok; + ok = writeRegister(0x01, 0x01) && ok; + ok = writeRegister(0x02, 0x00) && ok; + ok = writeRegister(0x04, 0x4E) && ok; + ok = writeRegister(0x05, 0xF0) && ok; + for (uint8_t channel = 0; channel < 4; ++channel) ok = writeRegister(0x14 + channel, 200) && ok; + ok = writeRegister(0x02, 0x0F) && ok; + setBrightness(brightness); + return ok; +} + +void SquareLight::setBrightness(uint8_t brightness) { + for (uint8_t channel = 0; channel < 4; ++channel) (void)writeRegister(kLightPwm0 + channel, brightness); + (void)writeRegister(kLightUpdate, 0x55); + _brightness = brightness; +} + +SquareDisplay::SquareDisplay() : DisplayDriver(320, 240) { + { + auto cfg = _bus.config(); + cfg.spi_host = SPI3_HOST; + cfg.spi_mode = 3; + cfg.freq_write = 75000000; + 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_miso = -1; + cfg.pin_mosi = -1; + cfg.pin_dc = -1; + cfg.pin_io0 = 41; + cfg.pin_io1 = 40; + cfg.pin_io2 = 39; + cfg.pin_io3 = 38; + _bus.config(cfg); + _panel.setBus(&_bus); + } + { + auto cfg = _panel.config(); + cfg.pin_cs = 46; + cfg.pin_rst = -1; + cfg.pin_busy = -1; + cfg.panel_width = 240; + cfg.panel_height = 320; + cfg.memory_width = 240; + cfg.memory_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 1; + cfg.readable = false; + cfg.invert = true; + cfg.rgb_order = true; + cfg.dlen_16bit = false; + cfg.bus_shared = false; + _panel.config(cfg); + } + _panel.setLight(&_light); + { + auto cfg = _touch.config(); + cfg.x_min = 0; cfg.x_max = 239; + cfg.y_min = 0; cfg.y_max = 319; + cfg.pin_int = -1; + cfg.bus_shared = false; + cfg.offset_rotation = 2; + cfg.i2c_port = 0; + cfg.i2c_addr = 0x5D; + cfg.pin_sda = 47; + cfg.pin_scl = 48; + cfg.freq = 100000; + _touch.config(cfg); + _panel.setTouch(&_touch); + } + _lcd.setPanel(&_panel); +} + +bool SquareDisplay::begin() { + if (_isOn) return true; + (void)_light.init(160); + _lcd.init(); + Wire.end(); + Wire.begin(47, 48, 100000); + _lcd.setSwapBytes(true); + _lcd.setRotation(0); + _lcd.setBrightness(160); + _lcd.fillScreen(0); + setLogicalSize(_lcd.width(), _lcd.height()); + _isOn = true; + Serial.printf("[square] display %dx%d\n", width(), height()); + return true; +} + +void SquareDisplay::turnOn() { if (!_isOn) { _lcd.setBrightness(160); _isOn = true; } } +void SquareDisplay::turnOff() { if (_isOn) { _lcd.setBrightness(0); _isOn = false; } } +void SquareDisplay::clear() { _lcd.fillScreen(0); } +void SquareDisplay::startFrame(ColorVal bkg) { _lcd.fillScreen(bkg); } +void SquareDisplay::setTextSize(int sz) { _lcd.setTextSize(sz); } +void SquareDisplay::setColor(ColorVal c) { _color = c; _lcd.setTextColor(c); } +void SquareDisplay::setCursor(int x, int y) { _lcd.setCursor(x, y); } +void SquareDisplay::print(const char* str) { _lcd.print(str); } +void SquareDisplay::fillRect(int x, int y, int w, int h) { _lcd.fillRect(x, y, w, h, _color); } +void SquareDisplay::drawRect(int x, int y, int w, int h) { _lcd.drawRect(x, y, w, h, _color); } +void SquareDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { _lcd.drawXBitmap(x, y, bits, w, h, _color); } +uint16_t SquareDisplay::getTextWidth(const char* str) { return _lcd.textWidth(str); } +void SquareDisplay::endFrame() {} + +void SquareDisplay::writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels) { + if (!_isOn || !pixels || w <= 0 || h <= 0) return; + _lcd.startWrite(); + _lcd.setAddrWindow(x, y, w, h); + _lcd.writePixels(const_cast(pixels), (uint32_t)w * h); + _lcd.endWrite(); +} + +void SquareDisplay::setDisplayRotation(uint8_t) { + _lcd.setRotation(0); + setLogicalSize(_lcd.width(), _lcd.height()); +} + +void SquareDisplay::setBrightness(uint8_t brightness) { + _lcd.setBrightness(brightness); +} + +bool SquareDisplay::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; +} \ No newline at end of file diff --git a/variants/square/SquareDisplay.h b/variants/square/SquareDisplay.h new file mode 100644 index 0000000..d9ac560 --- /dev/null +++ b/variants/square/SquareDisplay.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#define LGFX_USE_V1 +#include +#include + +class SquareLight : public lgfx::v1::ILight { +public: + bool init(uint8_t brightness) override; + void setBrightness(uint8_t brightness) override; + uint8_t getBrightness() const { return _brightness; } + +private: + bool writeRegister(uint8_t reg, uint8_t value); + uint8_t _brightness = 160; +}; + +class SquareDisplay : public DisplayDriver { +public: + SquareDisplay(); + bool begin(); + + bool isOn() override { return _isOn; } + void turnOn() override; + void turnOff() override; + void clear() override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; + void setTextSize(int sz) override; + void setColor(ColorVal c) override; + void setCursor(int x, int y) override; + void print(const char* str) override; + void fillRect(int x, int y, int w, int h) override; + void drawRect(int x, int y, int w, int h) override; + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; + uint16_t getTextWidth(const char* str) override; + void endFrame() override; + + void writePixelsRGB565(int x, int y, int w, int h, const uint16_t* pixels); + void setDisplayRotation(uint8_t rotation); + void setBrightness(uint8_t brightness); + bool getTouchPoint(uint16_t& x, uint16_t& y); + +private: + lgfx::Panel_NV3031B _panel; + lgfx::Bus_SPI _bus; + SquareLight _light; + lgfx::Touch_GT911 _touch; + lgfx::LGFX_Device _lcd; + bool _isOn = false; + uint16_t _color = 0xFFFF; +}; \ No newline at end of file diff --git a/variants/square/SquareIo.cpp b/variants/square/SquareIo.cpp new file mode 100644 index 0000000..8c6bac7 --- /dev/null +++ b/variants/square/SquareIo.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "SquareIo.h" + +#include +#include + +namespace { + +constexpr uint8_t kAddress = 0x21; +constexpr uint8_t kRegInput0 = 0x00; +constexpr uint8_t kRegOutput0 = 0x02; +constexpr uint8_t kRegConfig0 = 0x06; + +constexpr uint8_t kWakeButton = 0; +constexpr uint8_t kI2cInterrupt = 1; +constexpr uint8_t kSdDetect = 2; +constexpr uint8_t kTouchInterrupt = 3; +constexpr uint8_t kLcdControl = 4; +constexpr uint8_t kLcdPower = 5; +constexpr uint8_t kLcdReset = 6; +constexpr uint8_t kGrovePower = 7; +constexpr uint8_t kTouchReset = 8; +constexpr uint8_t kGnssReset = 9; +constexpr uint8_t kUserLed = 10; +constexpr uint8_t kUsbOtg = 11; +constexpr uint8_t kAudioPa = 12; +constexpr uint8_t kGnssPower = 13; +constexpr uint8_t kSdPower = 14; +constexpr uint8_t kBatterySense = 15; + +uint8_t s_output[2] = {0xFF, 0xFF}; +uint8_t s_config[2] = {0xFF, 0xFF}; +bool s_ready = false; + +bool readRegisters(uint8_t reg, uint8_t* data, size_t length) { + Wire.beginTransmission(kAddress); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return false; + if (Wire.requestFrom((int)kAddress, (int)length) != length) return false; + for (size_t i = 0; i < length; ++i) data[i] = (uint8_t)Wire.read(); + return true; +} + +bool writeRegisters(uint8_t reg, const uint8_t* data, size_t length) { + Wire.beginTransmission(kAddress); + Wire.write(reg); + for (size_t i = 0; i < length; ++i) Wire.write(data[i]); + return Wire.endTransmission() == 0; +} + +void stageInput(uint8_t bit) { + s_config[bit >> 3] |= (uint8_t)(1U << (bit & 7)); +} + +void stageOutput(uint8_t bit, bool high) { + const uint8_t mask = (uint8_t)(1U << (bit & 7)); + uint8_t& output = s_output[bit >> 3]; + uint8_t& config = s_config[bit >> 3]; + if (high) output |= mask; + else output &= (uint8_t)~mask; + config &= (uint8_t)~mask; +} + +bool writeState() { + // Latch output levels before exposing pins as outputs. + return writeRegisters(kRegOutput0, s_output, sizeof(s_output)) && + writeRegisters(kRegConfig0, s_config, sizeof(s_config)); +} + +bool setOutput(uint8_t bit, bool high) { + if (!s_ready) return false; + const uint8_t output0 = s_output[0], output1 = s_output[1]; + const uint8_t config0 = s_config[0], config1 = s_config[1]; + stageOutput(bit, high); + if (writeState()) return true; + s_output[0] = output0; s_output[1] = output1; + s_config[0] = config0; s_config[1] = config1; + return false; +} + +bool readInputBit(uint8_t bit, bool& high) { + uint8_t inputs[2] = {0xFF, 0xFF}; + if (!s_ready || !readRegisters(kRegInput0, inputs, sizeof(inputs))) return false; + high = (inputs[bit >> 3] & (uint8_t)(1U << (bit & 7))) != 0; + return true; +} + +} // namespace + +namespace SquareIo { + +bool begin() { + s_ready = false; + Wire.setClock(100000); + Wire.setTimeOut(30); + pinMode(45, INPUT_PULLUP); + + if (!readRegisters(kRegOutput0, s_output, sizeof(s_output)) || + !readRegisters(kRegConfig0, s_config, sizeof(s_config))) { + Serial.println("[square] expander probe failed"); + return false; + } + + stageInput(kWakeButton); + stageInput(kI2cInterrupt); + stageInput(kSdDetect); + stageInput(kLcdControl); + stageOutput(kTouchInterrupt, false); + stageOutput(kLcdPower, true); + stageOutput(kLcdReset, true); + stageOutput(kGrovePower, false); + stageOutput(kTouchReset, false); + stageOutput(kGnssReset, true); + stageOutput(kUserLed, false); + stageOutput(kUsbOtg, false); + stageOutput(kAudioPa, false); + stageOutput(kGnssPower, true); + stageOutput(kSdPower, false); + stageOutput(kBatterySense, false); + if (!writeState()) return false; + + delay(10); + stageOutput(kGnssReset, false); + if (!writeState()) return false; + delay(40); + stageOutput(kLcdReset, false); + if (!writeState()) return false; + delay(10); + stageOutput(kLcdReset, true); + if (!writeState()) return false; + delay(500); + stageOutput(kLcdControl, true); + if (!writeState()) return false; + delay(10); + stageOutput(kTouchReset, true); + if (!writeState()) return false; + delay(60); + + s_ready = true; + Serial.printf("[square] expander ready out=%02X/%02X cfg=%02X/%02X\n", + s_output[0], s_output[1], s_config[0], s_config[1]); + return true; +} + +bool ready() { return s_ready; } + +bool readWakeButton(bool& pressed) { + bool high = true; + if (!readInputBit(kWakeButton, high)) return false; + pressed = !high; + return true; +} + +bool readSdPresent(bool& present) { + bool high = true; + if (!readInputBit(kSdDetect, high)) return false; + present = !high; + return true; +} + +bool setLcdPower(bool enabled) { return setOutput(kLcdPower, enabled); } +bool setLcdResetReleased(bool released) { return setOutput(kLcdReset, released); } +bool setTouchResetReleased(bool released) { return setOutput(kTouchReset, released); } +bool setGnssPower(bool enabled) { return setOutput(kGnssPower, enabled); } +bool setGnssResetReleased(bool released) { return setOutput(kGnssReset, !released); } +bool setSdPower(bool enabled) { return setOutput(kSdPower, enabled); } +bool setBatterySense(bool enabled) { return setOutput(kBatterySense, enabled); } +bool setAudioPaPower(bool enabled) { return setOutput(kAudioPa, enabled); } + +} // namespace SquareIo \ No newline at end of file diff --git a/variants/square/SquareIo.h b/variants/square/SquareIo.h new file mode 100644 index 0000000..b82f5e2 --- /dev/null +++ b/variants/square/SquareIo.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +namespace SquareIo { + +bool begin(); +bool ready(); +bool readWakeButton(bool& pressed); +bool readSdPresent(bool& present); + +bool setLcdPower(bool enabled); +bool setLcdResetReleased(bool released); +bool setTouchResetReleased(bool released); +bool setGnssPower(bool enabled); +bool setGnssResetReleased(bool released); +bool setSdPower(bool enabled); +bool setBatterySense(bool enabled); +bool setAudioPaPower(bool enabled); + +} // namespace SquareIo \ No newline at end of file diff --git a/variants/square/SquareTouch.cpp b/variants/square/SquareTouch.cpp new file mode 100644 index 0000000..a115130 --- /dev/null +++ b/variants/square/SquareTouch.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#if defined(HAS_SQUARE) && defined(ESP32) + +#include +#include +#include +#include +#include + +#include "SquareDisplay.h" + +extern SquareDisplay display; + +namespace { +bool s_ready = false; +bool s_down = false; +bool s_live = false; +bool s_tapPending = false; +bool s_swipePending = false; +bool s_swiping = false; +uint16_t s_x = 0, s_y = 0, s_startX = 0, s_startY = 0, s_tapX = 0, s_tapY = 0; +int8_t s_swipeX = 0, s_swipeY = 0; +uint32_t s_downAt = 0; +TaskHandle_t s_task = nullptr; +bool s_async = false; +uint32_t s_periodMs = 8; + +void poll() { + uint16_t x = 0, y = 0; + const bool touched = display.getTouchPoint(x, y); + if (touched) { + s_x = x; s_y = y; s_live = true; + if (!s_down) { s_down = true; s_startX = x; s_startY = y; s_downAt = millis(); s_swiping = false; } + const int dx = (int)x - s_startX, dy = (int)y - s_startY; + const int adx = abs(dx), ady = abs(dy); + if (!s_swiping && adx >= 40 && adx > ady) s_swiping = true; + return; + } + s_live = false; + if (!s_down) return; + s_down = false; + const int dx = (int)s_x - s_startX, dy = (int)s_y - s_startY; + const int adx = abs(dx), ady = abs(dy); + s_swiping = false; + if (adx >= 40 && adx > ady + 8) { + s_swipeX = dx < 0 ? -1 : 1; s_swipeY = 0; s_swipePending = true; + } else if (ady >= 40 && ady > adx + 8) { + s_swipeX = 0; s_swipeY = dy < 0 ? -1 : 1; s_swipePending = true; + } else if ((uint32_t)(millis() - s_downAt) >= 12 && adx <= 16 && ady <= 16) { + s_tapX = s_x; s_tapY = s_y; s_tapPending = true; + } +} + +void pollTask(void*) { + for (;;) { heltecV4CapTouchCheck(); vTaskDelay(pdMS_TO_TICKS(s_periodMs)); } +} +} + +bool heltecV4CapTouchBegin() { 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; + if (x) *x = s_tapX; if (y) *y = s_tapY; return true; +} +bool heltecV4CapTouchGetLive(uint16_t* x, uint16_t* y) { + if (!s_live) return false; if (x) *x = s_x; if (y) *y = s_y; return true; +} +bool heltecV4CapTouchPopSwipe(int8_t* x, int8_t* y) { + if (!s_swipePending) return false; s_swipePending = false; + if (x) *x = s_swipeX; if (y) *y = s_swipeY; return true; +} +bool heltecV4CapTouchStartBackgroundPoll(uint32_t periodMs) { + if (s_async || !s_ready) return false; + s_periodMs = periodMs < 4 ? 4 : (periodMs > 100 ? 100 : periodMs); + if (xTaskCreatePinnedToCore(pollTask, "square_touch", 3072, nullptr, 2, &s_task, 0) != pdPASS) return false; + s_async = true; return true; +} +bool heltecV4CapTouchIsAsyncPolling() { return s_async; } +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 "square GT911"; } +void heltecV4CapTouchGetRaw(uint16_t* x, uint16_t* y) { if (x) *x = s_x; if (y) *y = s_y; } + +#endif \ No newline at end of file diff --git a/variants/square/target.cpp b/variants/square/target.cpp new file mode 100644 index 0000000..c83cb9c --- /dev/null +++ b/variants/square/target.cpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include + +#include "target.h" + +SquareBoard 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 + +SquareDisplay display; +MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + return radio.std_init(&radioSpi); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} \ No newline at end of file diff --git a/variants/square/target.h b/variants/square/target.h new file mode 100644 index 0000000..82a3f15 --- /dev/null +++ b/variants/square/target.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include "../../src/helpers/ClockFloorRTC.h" +#include "helpers/sensors/EnvironmentSensorManager.h" +#include "helpers/sensors/MicroNMEALocationProvider.h" + +#include "SquareBoard.h" +#include "SquareDisplay.h" + +extern SquareBoard board; +extern RADIO_CLASS radio; +extern WRAPPER_CLASS radio_driver; +extern ClockFloorRTC rtc_clock; +extern EnvironmentSensorManager sensors; +extern SquareDisplay display; +extern MomentaryButton user_btn; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); \ No newline at end of file