diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 741fe15c..2758611a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -73,6 +73,7 @@ - { env: "headless-xiao-esp32s3-8mb" } - { env: "headless-xiao-esp32s3-16mb" } - { env: "headless-xiao-esp32c5-8mb" } + - { env: "heltec-lora32-expansion-v2" } - { env: "CYD-2432S028" } - { env: "CYD-2-USB" } - { env: "CYD-2432W328C_2" } diff --git a/README.md b/README.md index 55d96d95..9ae57793 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Join our [Discord community](https://discord.gg/BE9by2a2FF) * Use M5Burner, or * Download the .bin file from Releases for your device and use https://web.esphome.io/ or esptool.py to flash the file `Launcher-{YourDevice}.bin` to your device. +For the [Heltec WiFi LoRa 32 Expansion Kit V2](boards/heltec-lora32-expansion-v2/connections.md), see the board guide for installation, button controls and recovery. + ## How to Use * Turn your device on * Press Sel (Enter) on the Launcher start screen to get into Launcher diff --git a/boards/heltec-lora32-expansion-v2/connections.md b/boards/heltec-lora32-expansion-v2/connections.md new file mode 100644 index 00000000..d04c8dd4 --- /dev/null +++ b/boards/heltec-lora32-expansion-v2/connections.md @@ -0,0 +1,38 @@ +# Heltec WiFi LoRa 32 Expansion Kit V2 + +This target pairs the V4-R8 (ESP32-S3, 16 MB flash, 8 MB octal PSRAM) with the V2 touchscreen and microSD expansion. Select the full kit name when choosing firmware. The V1 kit and the bare V4 board have different peripherals. + +## Install Launcher + +1. Back up the current firmware before changing the partition table. With esptool 5 and the kit connected by USB, run `esptool --chip esp32s3 --port PORT read-flash 0 0x1000000 before-launcher.bin`. Check the copy with `esptool --chip esp32s3 --port PORT verify-flash 0 before-launcher.bin`. Substitute the kit's actual serial port. Keep this full-chip backup private. +2. From a recursive checkout of Launcher, run `pio run -e heltec-lora32-expansion-v2`. The merged installer appears at the root of the checkout. +3. Run `esptool --chip esp32s3 --port PORT write-flash 0 Launcher-heltec-lora32-expansion-v2.bin`. This replaces the bootloader, partition table and current application. If automatic download mode fails, hold USER, tap reset, then release USER. Leave IO released during USB flashing. +4. In Launcher, open **SD**, choose an application binary built for this exact kit and select **Install**. The serial installer in `tools/serial_flasher.py` also accepts an application image. The online installer uses the separate LauncherHub category `heltec_lora32_expansion_v2`. Firmware appears there when the catalog operator provisions that category and lists compatible builds. + +To return the device to a verified backup, write the full-chip backup at address zero using the same port and `write-flash --flash-mode keep --flash-size keep --flash-freq keep`. This restores the internal flash contents as of the backup. + +## Controls + +| Control | Action | +| --- | --- | +| Touchscreen | Tap a menu item or the navigation strip. Tap the center of the startup screen to enter Launcher. | +| USER (GPIO0) | Click for Next. Hold for 600 ms or double-click for Select. | +| IO (GPIO46) | Click for Previous. Hold for 600 ms or double-click for Back. | +| OFF | Enter deep sleep. Press USER to wake. The physical power switch controls the battery supply. | + +If enabling **GPIO on boot starts Launcher** in Settings, choose **IO** and set **GPIO Level** to **1**. The board pulls this input low at rest. Battery percentage is an estimate from the cell voltage. + +## Expansion connections + +| Function | ESP32-S3 GPIO | +| --- | --- | +| TFT and microSD SPI | SCLK 16, MOSI 15, MISO 45. TFT CS 47, SD CS 3. | +| ST7789P3 TFT | D/C 48, reset 21, backlight PWM 44. Native 240 x 320. | +| CHSC6xxx touch | SDA 17, SCL 18, address 0x2E. The TFT resets both controllers through GPIO21. Touch is polled. | +| Buttons | USER 0 active low. IO 46 active high. | +| Expansion supply | GPIO40 active low, powers display, SD and touch pull-ups. | +| Battery divider | GPIO1, 390 kOhm over 100 kOhm. | +| GNSS | GPIO42 active low. Launcher powers down the main GNSS supply. | +| LoRa and buzzer | Radio CS 8 high, RF enable 2 and RF supply 7 low, buzzer 4 low while in Launcher. Installed apps initialize these peripherals themselves. | + +See the [kit specifications](https://heltec.org/project/v4-r8-ex/), [V4-R8 mainboard schematic](https://resource.heltec.cn/download/WiFi_LoRa_32_V4-R8/Schematic/HTIT-WBR8H_V4.3.2.pdf) and [V2.03 expansion schematic](https://resource.heltec.cn/download/WiFi_LoRa_32_Expansion_Kit-V2/schematic/Expansion_board_V2.03.pdf). diff --git a/boards/heltec-lora32-expansion-v2/interface.cpp b/boards/heltec-lora32-expansion-v2/interface.cpp new file mode 100644 index 00000000..14cbf2c8 --- /dev/null +++ b/boards/heltec-lora32-expansion-v2/interface.cpp @@ -0,0 +1,126 @@ +#include "hal/bright/bright.h" +#include "hal/inputs/buttons.h" +#include "hal/inputs/touch.h" +#include "idf/launcher_platform.h" +#include "mykeyboard.h" +#include +#include +#include +#include +#include + +namespace { +constexpr gpio_num_t userButton = GPIO_NUM_0; +constexpr gpio_num_t ioButton = GPIO_NUM_46; +constexpr gpio_num_t vext = GPIO_NUM_40; // Active low: LCD, SD and touch pull-ups +constexpr gpio_num_t gnssPower = GPIO_NUM_42; // Active low +constexpr uint8_t touchAddress = 0x2e; +bool touchReady = false; + +void output(gpio_num_t pin, int level) { + gpio_set_level(pin, level); + gpio_set_direction(pin, GPIO_MODE_OUTPUT); + gpio_hold_dis(pin); +} + +// CHSC6xxx normal mapped report. The controller returns native portrait coordinates. +bool readTouch(LTouchPoint &point) { + Wire.beginTransmission(touchAddress); + Wire.write(uint8_t{0}); + if (Wire.endTransmission() != 0) return false; + uint8_t data[7]; + if (Wire.requestFrom(touchAddress, sizeof(data)) != sizeof(data)) return false; + for (auto &byte : data) byte = Wire.read(); + const int count = data[2] & 7; + const int event = data[3] >> 6; + if (count == 0 || count > 2 || event == 1 || event == 3) return false; + const int x = ((data[3] & 15) << 8) | data[4]; + const int y = ((data[5] & 15) << 8) | data[6]; + if (x >= TFT_WIDTH || y >= TFT_HEIGHT) return false; + + switch (rotation & 3) { + case 0: + point.x = x; + point.y = y; + break; + case 1: + point.x = y; + point.y = TFT_WIDTH - 1 - x; + break; + case 2: + point.x = TFT_WIDTH - 1 - x; + point.y = TFT_HEIGHT - 1 - y; + break; + case 3: + point.x = TFT_HEIGHT - 1 - y; + point.y = x; + break; + } + point.pressed = true; + return true; +} +} // namespace + +void _setup_gpio() { + gpio_deep_sleep_hold_dis(); + rtc_gpio_deinit(userButton); + output(GPIO_NUM_2, LOW); // RF front-end enable + output(GPIO_NUM_7, LOW); // RF front-end supply + output(GPIO_NUM_8, HIGH); // SX1262 CS + output(GPIO_NUM_4, LOW); // buzzer + output(gnssPower, HIGH); + output(static_cast(TFT_CS), HIGH); + output(static_cast(SDCARD_CS), HIGH); + output(vext, LOW); + gpio_hold_dis(static_cast(TFT_BL)); + hal_bright_attach(TFT_BL); + hal_buttons_init_2(DeviceButtons{userButton, ioButton}, 600, true); + launcherDelayMs(100); +} + +void _post_setup_gpio() { + // The display init has already pulsed GPIO21, shared by the LCD and touch. + if (Wire.begin(17, 18, 100000)) { + Wire.setTimeOut(10); + Wire.beginTransmission(touchAddress); + touchReady = Wire.endTransmission() == 0; + } + launcherConsolePrintf("Heltec V2 touch: %s\n", touchReady ? "ready" : "unavailable"); +} + +void _setBrightness(uint8_t value) { hal_bright_set(TFT_BL, value); } + +void InputHandler() { + hal_buttons_poll_2(); + if (!touchReady) return; + static uint32_t lastPoll = 0; + static bool held = false; + const uint32_t now = launcherMillis(); + if (now - lastPoll < 20) return; + lastPoll = now; + LTouchPoint point; + bool down = readTouch(point); + if (down && (!held || LongPress)) hal_touch_apply(point); + held = down; +} + +void powerOff() { + const auto start = launcherMillis(); + while (!gpio_get_level(userButton)) { + if (launcherMillis() - start >= 1500) return; + launcherDelayMs(10); + } + if (esp_sleep_enable_ext0_wakeup(userButton, LOW) != ESP_OK) return; + launcherInputLock(); // Park touch and button polling before removing power. + _setBrightness(0); + if (sdcardMounted) SDM.end(); + Wire.end(); + // Leave the shared bus high impedance before switching its supply off. + for (int pin : {TFT_CS, SDCARD_CS, TFT_SCLK, TFT_MOSI, TFT_MISO, TFT_DC, TFT_RST, 17, 18}) { + const auto gpio = static_cast(pin); + gpio_set_direction(gpio, GPIO_MODE_INPUT); + gpio_set_pull_mode(gpio, GPIO_FLOATING); + } + output(vext, HIGH); + esp_deep_sleep_start(); +} diff --git a/boards/heltec-lora32-expansion-v2/platformio.ini b/boards/heltec-lora32-expansion-v2/platformio.ini new file mode 100644 index 00000000..2e000fc0 --- /dev/null +++ b/boards/heltec-lora32-expansion-v2/platformio.ini @@ -0,0 +1,46 @@ +[env:heltec-lora32-expansion-v2] +board = esp32s3 +board_build.partitions = support_files/custom_16Mb.csv +board_upload.speed = 460800 +build_src_filter = ${env.build_src_filter} +<../boards/heltec-lora32-expansion-v2> +build_flags = + ${env.build_flags} + -DBOARD_HAS_PSRAM=1 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + -D DEVICE_NAME='"Heltec LoRa 32 Expansion V2"' + -D OTA_TAG='"heltec_lora32_expansion_v2"' + -D OTA_EXTRA='"esp=S3"' + -D LAUNCHER_GPIO_KEYS='{{"IO", 46}}' + -D HAS_2_BUTTONS=1 + -D BUTTONS_IDF_COMPONENT=1 + -D HAS_TOUCH=1 + -D ROTATION=1 + -D ANALOG_BAT_PIN=1 + -D ANALOG_BAT_MULTIPLIER=4.9f + + -D TFT_DATABUS_N=0 + -D TFT_DISPLAY_DRIVER_N=1 + -D TFT_WIDTH=240 + -D TFT_HEIGHT=320 + -D TFT_CS=47 + -D TFT_DC=48 + -D TFT_RST=21 + -D TFT_SCLK=16 + -D TFT_MOSI=15 + -D TFT_MISO=45 + -D TFT_BL=44 + -D TFT_IPS=1 + -D TFT_COL_OFS1=0 + -D TFT_COL_OFS2=0 + -D TFT_ROW_OFS1=0 + -D TFT_ROW_OFS2=0 + -D TFT_PREF_SPEED=40000000 + + -D SDCARD_CS=3 + -D SDCARD_SCK=16 + -D SDCARD_MOSI=15 + -D SDCARD_MISO=45 + +lib_deps = + https://github.com/bmorcelli/ESP32_Button diff --git a/platformio.ini b/platformio.ini index e82e6487..b3163407 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,6 +46,7 @@ default_envs = ; headless-xiao-esp32s3-8mb ; headless-xiao-esp32s3-16mb ; headless-xiao-esp32c5-8mb + ; heltec-lora32-expansion-v2 ; lilygo-t5-epaper-s3-pro ; lilygo-t-deck ; lilygo-t-deck-plus diff --git a/src/hal/README.md b/src/hal/README.md index aef63c44..ccb12806 100644 --- a/src/hal/README.md +++ b/src/hal/README.md @@ -68,7 +68,7 @@ by the ESP-IDF `button` component instead of raw polling, gated by `_init` once from `_setup_gpio()`, the matching `_poll_N` every `InputHandler()` cycle. `count` must match which `_poll_N` you call. `cfg.pullup = false` for boards without internal/external pull-ups. -- `hal_buttons_init_2(cfg, long_press_ms = 600)` / `hal_buttons_poll_2()` — +- `hal_buttons_init_2(cfg, long_press_ms = 600, btn2ActiveHigh = false)` / `hal_buttons_poll_2()` — the `HAS_2_BUTTONS` pair, needs `lib_deps = https://github.com/bmorcelli/ESP32_Button` and `-DBUTTONS_IDF_COMPONENT=1`. btn1: short click → Next, double-click/hold → Sel. btn2: short click → diff --git a/src/hal/inputs/buttons.cpp b/src/hal/inputs/buttons.cpp index 2bd55748..0f5d6688 100644 --- a/src/hal/inputs/buttons.cpp +++ b/src/hal/inputs/buttons.cpp @@ -47,7 +47,7 @@ void hal_buttons_init(const DeviceButtons &cfg, uint8_t count) { if (count >= 6) configurePin(cfg.btn6, cfg.pullup); } -void hal_buttons_init_2(const DeviceButtons &cfg, uint16_t long_press_ms) { +void hal_buttons_init_2(const DeviceButtons &cfg, uint16_t long_press_ms, bool btn2ActiveHigh) { #if defined(BUTTONS_IDF_COMPONENT) button_config_t btn1Cfg = { .type = BUTTON_TYPE_GPIO, @@ -59,7 +59,7 @@ void hal_buttons_init_2(const DeviceButtons &cfg, uint16_t long_press_ms) { .type = BUTTON_TYPE_GPIO, .long_press_time = long_press_ms, .short_press_time = 120, - .gpio_button_config = {.gpio_num = cfg.btn2, .active_level = 0}, + .gpio_button_config = {.gpio_num = cfg.btn2, .active_level = btn2ActiveHigh}, }; hal2Btn1 = new Button(btn1Cfg); hal2Btn1->attachSingleClickEventCb(&hal2OnBtn1Click, nullptr); @@ -73,6 +73,7 @@ void hal_buttons_init_2(const DeviceButtons &cfg, uint16_t long_press_ms) { #else (void)cfg; (void)long_press_ms; + (void)btn2ActiveHigh; #endif } diff --git a/src/hal/inputs/buttons.h b/src/hal/inputs/buttons.h index 9f570dbb..0799a7e0 100644 --- a/src/hal/inputs/buttons.h +++ b/src/hal/inputs/buttons.h @@ -7,7 +7,7 @@ void hal_buttons_init(const DeviceButtons &cfg, uint8_t count); void hal_buttons_poll_1(const DeviceButtons &cfg); -void hal_buttons_init_2(const DeviceButtons &cfg, uint16_t long_press_ms = 600); +void hal_buttons_init_2(const DeviceButtons &cfg, uint16_t long_press_ms = 600, bool btn2ActiveHigh = false); void hal_buttons_poll_2();