Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions boards/heltec-lora32-expansion-v2/connections.md
Original file line number Diff line number Diff line change
@@ -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).
126 changes: 126 additions & 0 deletions boards/heltec-lora32-expansion-v2/interface.cpp
Original file line number Diff line number Diff line change
@@ -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 <SD.h>
#include <Wire.h>
#include <driver/rtc_io.h>
#include <esp_sleep.h>
#include <interface.h>

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<gpio_num_t>(TFT_CS), HIGH);
output(static_cast<gpio_num_t>(SDCARD_CS), HIGH);
output(vext, LOW);
gpio_hold_dis(static_cast<gpio_num_t>(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<gpio_num_t>(pin);
gpio_set_direction(gpio, GPIO_MODE_INPUT);
gpio_set_pull_mode(gpio, GPIO_FLOATING);
}
output(vext, HIGH);
esp_deep_sleep_start();
}
46 changes: 46 additions & 0 deletions boards/heltec-lora32-expansion-v2/platformio.ini
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/hal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 →
Expand Down
5 changes: 3 additions & 2 deletions src/hal/inputs/buttons.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion src/hal/inputs/buttons.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down