feat(boards): add M5Stack Cardputer ADV and LilyGO T-Echo Lite KeyShield

New M5Stack Cardputer ADV variant (ESP32-S3, ST7789 TFT, built-in TCA8418
QWERTY keyboard, PI4IOE5V6408 LoRa-cap IO-expander autodetect), and a
KeyShield accessory variant for the existing LilyGO T-Echo Lite (external
TCA8418 T9 keypad + AW21009 backlight driver). Both keyboards share one
ENV_USE_TCA8418 polling block in UITask.cpp::loop(), coexisting with the
unrelated CardKB support (different chip/address/flag).

Fixes carried in from the contributed T-Echo Lite code: swapped GPS RX/TX
pins, TX-LED hooks, TCXO voltage, missing GxEPD2_122_T61 panel include.
Fixed during integration: I2C bus was probed for an RTC before Wire.begin()
configured its pins on Cardputer ADV (silent RTC autodetect failure).

Added dedicated *_solo_dual release envs for both boards (auto-picked up by
the solo-firmware release workflow). Gave the T-Echo Lite KeyShield solo
build -Os/-Ofast-unflag like every other nRF52 solo build (was missing,
cut flash usage from 90.7% to 61.4%).

Ported the shared misc-fixed 6x9 font (full Latin/Greek/Cyrillic, opt-in via
OLED_MISC_FIXED_FONT) to ST7789Display for the Cardputer's on-screen
keyboard. ST7789Spi isn't Adafruit_GFX-based like the other single-font
drivers, and this panel's logical->physical scale is non-integer, so glyphs
are re-packed to XBM and blitted through the existing drawXbm(), which
already does correct fractional-scale boundary math, rather than
duplicating that logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-08-08 01:06:24 +02:00
co-authored by Claude Opus 5
parent 6fd1c4bae2
commit 05e57357d3
15 changed files with 1061 additions and 5 deletions
@@ -0,0 +1,66 @@
// TechoKeyBacklight.cpp
// Self-contained AW21009(QNR) keyboard-backlight driver for the T-Echo Lite
// KeyShield. Provides a single free function consumed by the UITask Home-key hook:
//
// void techo_keyshield_backlight_toggle(); // on <-> off
//
// Raw-Wire register driver. The register sequence mirrors LilyGo's
// cpp_bus_driver Aw21009 implementation (reset / global-control / global-current
// / per-channel scaling / latch, then 12-bit per-channel brightness), so no
// dependency on that library is pulled in.
#ifdef LILYGO_TECHO_LITE_KEYSHIELD
#include <Arduino.h>
#include <Wire.h>
// ---- AW21009 (KeyShield backlight) -----------------------------------------
// I2C 0x20 on the shared Wire bus. Wire.begin() (TechoBoard::begin) and the
// shield 3V3 rail are already up before the UI loop runs, so this lazy-inits on
// first toggle.
#define AW21009_ADDR 0x20
#define AW21009_REG_GCR 0x20 // global control (auto-save / PWM / chip enable)
#define AW21009_REG_BRIGHT 0x21 // brightness start; per ch: LSB=0x21+ch*2, MSB+1
#define AW21009_REG_UPDATE 0x45 // write-to-latch register
#define AW21009_REG_SCALE 0x46 // per-channel current scaling start
#define AW21009_REG_GCCR 0x58 // global current control
#define AW21009_REG_RESET 0x70 // software reset
#define AW21009_CHANNELS 6
#define AW21009_ON_LEVEL 4095 // 0..4095; lower this to dim the backlight
static void aw_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission(AW21009_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
static void aw_init() {
aw_write(AW21009_REG_RESET, 0x00); // software reset
delay(10);
aw_write(AW21009_REG_GCR, 0x87); // auto power save + 12-bit PWM + chip enable
aw_write(AW21009_REG_GCCR, 0xFF); // global current = max
for (uint8_t i = 0; i < AW21009_CHANNELS; i++)
aw_write(AW21009_REG_SCALE + i, 0xFF); // per-channel current = max
aw_write(AW21009_REG_UPDATE, 0x00); // latch
}
static void aw_set_brightness(uint16_t value) {
if (value > 4095) value = 4095;
for (uint8_t i = 0; i < AW21009_CHANNELS; i++) {
aw_write(AW21009_REG_BRIGHT + i * 2, (uint8_t)(value & 0xFF)); // LSB
aw_write(AW21009_REG_BRIGHT + i * 2 + 1, (uint8_t)((value >> 8) & 0x0F)); // MSB (12-bit)
}
aw_write(AW21009_REG_UPDATE, 0x00); // latch
}
void techo_keyshield_backlight_toggle() {
static bool inited = false;
static bool on = false;
if (!inited) { aw_init(); inited = true; }
on = !on;
aw_set_brightness(on ? AW21009_ON_LEVEL : 0);
}
#endif
@@ -0,0 +1,240 @@
#ifdef LILYGO_TECHO_LITE_KEYSHIELD
#include <Arduino.h>
#include <Wire.h>
#include <helpers/ui/UIScreen.h> // KEY_PREV / KEY_NEXT / KEY_ENTER / KEY_CANCEL
// ---- TCA8418 (KeyShield keypad) --------------------------------------------
#define TCA8418_ADDR 0x34
#define TCA8418_REG_CFG 0x01
#define TCA8418_REG_INT_STAT 0x02
#define TCA8418_REG_KEY_LCK_EC 0x03
#define TCA8418_REG_KEY_EVENT_A 0x04
#define TCA8418_REG_KP_GPIO1 0x1D
#define TCA8418_REG_KP_GPIO2 0x1E
#define TCA8418_REG_KP_GPIO3 0x1F
#define TCA8418_REG_GPI_EM1 0x20
#define TCA8418_REG_GPI_EM2 0x21
#define TCA8418_REG_GPI_EM3 0x22
#define TCA8418_REG_DEBOUNCE 0x29
#define MULTI_TAP_THRESHOLD 300 // multitap threshold csúszó ablak ideje
#define LONG_PRESS_THRESHOLD 600 // longpress threshold idő
// ---- Multi-tap / long-press állapotgép -------------------------------------
// true : folyamatos mód minden koppintáskor azonnal küld backspace-t + új karaktert
// (gyors, sima kijelzőknek jó, "élőben" látszik a T9 ciklus)
// false : halasztott mód csak akkor küldi a végleges karaktert, ha lejárt a
// MULTI_TAP_THRESHOLD (e-ink-hez ajánlott)
static bool sendBackSpace = false;
static void tca_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission(TCA8418_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
static uint8_t tca_read(uint8_t reg) {
Wire.beginTransmission(TCA8418_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom((uint8_t)TCA8418_ADDR, (uint8_t)1);
return Wire.available() ? Wire.read() : 0;
}
// Num chars per key (T9 ciklus hossz)
static uint8_t KeyTapMod[5*4] = {
1, 5, 1, 5,
1, 9, 7, 9,
1, 7, 7, 7,
1, 1, 7, 7,
1, 1, 1, 1
};
static const unsigned char keymap[5*4][9] = {
{' '},
{'.', ',', '?', '!', '*'},
{'0'},
{':', ';', '-', '+', '#'},
{'\b'},
{'p', 'q', 'r', 's', 'P', 'Q', 'R', 'S', '7'},
{'t', 'u', 'v', 'T', 'U', 'V', '8'},
{'w', 'x', 'y', 'z', 'W', 'X', 'Y', 'Z', '9'},
{KEY_DOWN},
{'g', 'h', 'i', 'G', 'H', 'I', '4'},
{'j', 'k', 'l', 'J', 'K', 'L', '5'},
{'m', 'n', 'o', 'M', 'N', 'O', '6'},
{KEY_ENTER},
{'1'},
{'a', 'b', 'c', 'A', 'B', 'C', '2'},
{'d', 'e', 'f', 'D', 'E', 'F', '3'},
{KEY_UP},
{KEY_CANCEL},
{KEY_HOME},
{KEY_CONTEXT_MENU}
};
// long press
static const unsigned char keymapLongPress[5*4] = {
0, 0, 0, 0,
0, 0, 0, 0,
KEY_LEFT, 0, 0, 0,
KEY_KB_ENTER, 0, 0, 0,
KEY_RIGHT, KEY_CANCEL, KEY_HOME, KEY_CONTEXT_MENU
};
void tca8418_keypad_set_backspace_mode(bool continuous) {
sendBackSpace = continuous;
}
// kimeneti kör-FIFO, mert egy koppintás akár 2 karaktert is eredményezhet
// (backspace + új kar) folyamatos módban
#define KEY_OUT_QUEUE_LEN 4
static char s_outQueue[KEY_OUT_QUEUE_LEN];
static uint8_t s_outHead = 0, s_outTail = 0;
static void queue_push(char c) {
uint8_t next = (s_outTail + 1) % KEY_OUT_QUEUE_LEN;
if (next != s_outHead) { s_outQueue[s_outTail] = c; s_outTail = next; }
}
static bool queue_pop(char &c) {
if (s_outHead == s_outTail) return false;
c = s_outQueue[s_outHead];
s_outHead = (s_outHead + 1) % KEY_OUT_QUEUE_LEN;
return true;
}
// aktuálisan lenyomva tartott (fizikai) billentyű, a long-press méréshez
static uint8_t s_downKeyIndex = 0xFF;
static unsigned long s_downTime = 0;
// függőben lévő (még nem lezárt) multi-tap ciklus
static uint8_t s_pendingIndex = 0xFF; // 0xFF = nincs függő ciklus
static uint8_t s_pendingTap = 0; // aktuális tap-index (0-alapú)
static unsigned long s_pendingTapTime = 0;
static void keypad_init() {
tca_write(TCA8418_REG_CFG, 0x00);
for (int i = 0; i < 16; i++) {
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
tca_read(TCA8418_REG_KEY_EVENT_A);
}
tca_write(TCA8418_REG_INT_STAT, 0x1F);
tca_write(TCA8418_REG_GPI_EM1, 0x00);
tca_write(TCA8418_REG_GPI_EM2, 0x00);
tca_write(TCA8418_REG_GPI_EM3, 0x00);
tca_write(TCA8418_REG_KP_GPIO1, 0x1F); // ROW0-ROW4 in keypad matrix 0b00011111
tca_write(TCA8418_REG_KP_GPIO2, 0x0F); // COL0-COL3 in keypad matrix 0b00001111
tca_write(TCA8418_REG_KP_GPIO3, 0x00); // COL8, COL9
tca_write(TCA8418_REG_DEBOUNCE, 0x03);
tca_write(TCA8418_REG_INT_STAT, 0x1F);
tca_write(TCA8418_REG_CFG, 0x11);
delay(5);
for (int i = 0; i < 16; i++) {
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
tca_read(TCA8418_REG_KEY_EVENT_A);
}
tca_write(TCA8418_REG_INT_STAT, 0x1F);
}
// lezár egy függő multi-tap ciklust: halasztott módban ilyenkor kell
// kiküldeni a "végleges" kart, mert eddig semmi nem ment ki érte
static void commit_pending_if_needed() {
if (s_pendingIndex == 0xFF) return;
if (!sendBackSpace) {
uint8_t idx = s_pendingTap % KeyTapMod[s_pendingIndex];
queue_push(keymap[s_pendingIndex][idx]);
}
s_pendingIndex = 0xFF;
}
char tca8418_keypad_read() {
static bool inited = false;
if (!inited) { keypad_init(); inited = true; }
unsigned long now = millis();
// 1) lejárt-e a függő multi-tap ciklus? (nem kap I2C eseményt, ezért
// minden híváskor ellenőrizni kell)
if (s_pendingIndex != 0xFF && (now - s_pendingTapTime) >= MULTI_TAP_THRESHOLD) {
commit_pending_if_needed();
}
// 2) van-e még kiküldendő karakter a pufferben?
char qc;
if (queue_pop(qc)) return qc;
// 3) hardver FIFO
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) return 0;
uint8_t ev = tca_read(TCA8418_REG_KEY_EVENT_A);
tca_write(TCA8418_REG_INT_STAT, 0x1F);
bool pressed = (ev & 0x80) != 0;
uint8_t code = ev & 0x7F;
uint8_t row = (code - 1) / 10;
uint8_t col = (code - 1) % 10;
uint8_t key_index = row * 4 + col;
if (key_index >= 5*4) return 0;
if (pressed) {
// csak eltároljuk, a döntés (rövid/hosszú) a release-nél történik
s_downKeyIndex = key_index;
s_downTime = now;
return 0;
}
// --- release ---
if (s_downKeyIndex != key_index) return 0; // eltévedt/pár nélküli release
unsigned long duration = now - s_downTime;
s_downKeyIndex = 0xFF;
bool longEligible = keymapLongPress[key_index] != 0;
bool isLong = longEligible && duration >= LONG_PRESS_THRESHOLD;
if (isLong) {
if (sendBackSpace && s_pendingIndex == key_index) {
queue_push('\b'); // az előzőleg megjelenített preview karakter törlése
} else {
commit_pending_if_needed(); // más gombra vonatkozó függő ciklus lezárása
}
s_pendingIndex = 0xFF;
queue_push(keymapLongPress[key_index]);
}
else if (KeyTapMod[key_index] <= 1) {
// nem ciklikus gomb -> mindig azonnali, önálló leütés
commit_pending_if_needed();
s_pendingIndex = 0xFF;
queue_push(keymap[key_index][0]);
}
else {
// Serial.printf("gap: %lu ms\n", now - s_pendingTapTime); // gap idő debug
bool isContinuation = (s_pendingIndex == key_index) &&
((now - s_pendingTapTime) < MULTI_TAP_THRESHOLD);
if (!isContinuation) {
commit_pending_if_needed(); // esetleges más gombos függő ciklus lezárása
s_pendingIndex = key_index;
s_pendingTap = 0;
} else {
s_pendingTap = (s_pendingTap + 1) % KeyTapMod[key_index];
}
s_pendingTapTime = now;
if (sendBackSpace) {
if (isContinuation) queue_push('\b');
queue_push(keymap[key_index][s_pendingTap]);
}
// halasztott módban itt nem küldünk semmit, a threshold lejártakor
// (1-es lépés) vagy a következő eltérő gombnál (commit_pending_if_needed)
// fog kimenni a végleges karakter
}
if (queue_pop(qc)) return qc;
return 0;
}
#endif
+10
View File
@@ -21,6 +21,16 @@ public:
return "LilyGo T-Echo Lite";
}
#if defined(P_LORA_TX_LED)
void onBeforeTransmit() override {
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on
}
void onAfterTransmit() override {
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off
}
#endif
void powerOff() override {
digitalWrite(PIN_VBAT_MEAS_EN, LOW);
#ifdef LED_RED
+44 -2
View File
@@ -12,7 +12,7 @@ build_flags = ${nrf52_base.build_flags}
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_POWER_EN=30
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_DIO3_TCXO_VOLTAGE=3.0 ;https://github.com/Xinyuan-LilyGO/T-Echo-Lite/issues/14
-D SX126X_CURRENT_LIMIT=140
-D SX126X_RX_BOOSTED_GAIN=1
-D P_LORA_TX_LED=LED_GREEN
@@ -28,10 +28,11 @@ build_flags = ${nrf52_base.build_flags}
-D EINK_Y_OFFSET=10
-D DISPLAY_ROTATION=4
-D AUTO_OFF_MILLIS=0
-D DUAL_SERIAL=1
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/*.cpp>
+<TechoBoard.cpp>
+<helpers/sensors/EnvironmentSensorManager.cpp>
+<helpers/sensors>
+<helpers/ui/GxEPDDisplay.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../variants/lilygo_techo_lite>
@@ -44,6 +45,45 @@ lib_deps =
debug_tool = jlink
upload_protocol = nrfutil
[env:LilyGo_T-Echo-Lite_keyshield_companion_solo_dual]
extends = LilyGo_T-Echo-Lite
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
; Optimise for size, not speed: the nRF52 Arduino core defaults to -Ofast
; (≈ -O3 + -ffast-math), which bloats flash by ~200 KB on this feature-rich
; solo build. -Os keeps every feature, fits comfortably, and is actually safer
; for float (no fast-math reassociation). Speed is irrelevant for the UI/mesh.
build_unflags = -Ofast
build_flags =
${LilyGo_T-Echo-Lite.build_flags}
-I src/helpers/ui
-I examples/companion_radio/ui-new
-D LILYGO_TECHO_LITE_KEYSHIELD
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; -D QSPIFLASH=1
-D BLE_PIN_CODE=123456
; -D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
-D UI_RECENT_LIST_SIZE=9
-D UI_SENSORS_PAGE=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
-D ENV_USE_TCA8418=1
-D FIRMWARE_SOLO_BUILD=1
-D MESHCORE_VERSION='"1.16"'
-D ENABLE_SCREENSHOT
-Os
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${LilyGo_T-Echo-Lite.lib_deps}
densaugeo/base64 @ ~1.4.0
;https://github.com/lewisxhe/SensorLib.git
[env:LilyGo_T-Echo-Lite_repeater]
extends = LilyGo_T-Echo-Lite
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
@@ -90,6 +130,8 @@ build_flags =
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
-D FIRMWARE_SOLO_BUILD=1
-D MESHCORE_VERSION='"1.16"'
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<../examples/companion_radio/*.cpp>
+5 -2
View File
@@ -105,6 +105,7 @@
#define LORA_CS _PINNUM(0, 11)
#define SX126X_POWER_EN _PINNUM(0, 30)
#define SX126X_DIO1 _PINNUM(1, 8)
#define SX1262_DIO2 _PINNUM(0, 5)
#define SX126X_BUSY _PINNUM(0, 14)
#define SX126X_RESET _PINNUM(0, 7)
#define SX126X_RXEN _PINNUM(1, 1)
@@ -152,8 +153,10 @@ extern const int SCK;
// GPS — per LilyGo t_echo_lite_config.h
// PIN_GPS_TX/RX named from GPS module's perspective
#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX
#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX
//#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX
//#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX
#define PIN_GPS_RX _PINNUM(0, 29) // GPS UART TX → MCU RX
#define PIN_GPS_TX _PINNUM(1, 10) // GPS UART RX ← MCU TX
#define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable
#define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up
#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS
@@ -0,0 +1,31 @@
#include <Arduino.h>
#include "CardputerADVBoard.h"
uint32_t deviceOnline = 0x00;
void CardputerADVBoard::begin() {
ESP32Board::begin();
// Configure user button
pinMode(PIN_USER_BTN, INPUT);
// Configure LoRa Pins
pinMode(P_LORA_MISO, INPUT_PULLUP);
// pinMode(P_LORA_DIO_1, INPUT_PULLUP);
#ifdef P_LORA_TX_LED
digitalWrite(P_LORA_TX_LED, HIGH); // inverted pin for SX1276 - HIGH for off
#endif
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) {
startup_reason = BD_STARTUP_RX_PACKET; // received a LoRa packet (while in deep sleep)
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
}
@@ -0,0 +1,58 @@
#pragma once
#include <Wire.h>
#include <Arduino.h>
#include "helpers/ESP32Board.h"
#include <driver/rtc_io.h>
#define PIN_VBAT_READ 10
#define BATTERY_SAMPLES 8
#define ADC_MULTIPLIER (2.0f * 3.3f * 1000)
class CardputerADVBoard : public ESP32Board {
public:
void begin();
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
uint16_t getBattMilliVolts() {
#if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER)
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < BATTERY_SAMPLES; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / BATTERY_SAMPLES;
return (ADC_MULTIPLIER * raw) / 4096;
#else
return 0;
#endif
}
const char* getManufacturerName() const{
return "M5Stack Cardputer ADV";
}
};
@@ -0,0 +1,166 @@
#ifdef CARDPUTER_ADV
#include <Arduino.h>
#include <Wire.h>
#include <helpers/ui/UIScreen.h> // KEY_PREV / KEY_NEXT / KEY_ENTER / KEY_CANCEL
// --- TCA8418 registers ---
#define TCA8418_ADDR 0x34
#define TCA8418_REG_CFG 0x01
#define TCA8418_REG_INT_STAT 0x02
#define TCA8418_REG_KEY_LCK_EC 0x03
#define TCA8418_REG_KEY_EVENT_A 0x04
#define TCA8418_REG_KP_GPIO1 0x1D
#define TCA8418_REG_KP_GPIO2 0x1E
#define TCA8418_REG_KP_GPIO3 0x1F
#define TCA8418_REG_GPI_EM1 0x20
#define TCA8418_REG_GPI_EM2 0x21
#define TCA8418_REG_GPI_EM3 0x22
#define TCA8418_REG_DEBOUNCE 0x29
// --- modifier key event-kódok (nyers TCA8418 "code", key_index előtti érték) ---
#define TCA_CODE_TAB 2 // egyelőre nem használt
#define TCA_CODE_FN 3
#define TCA_CODE_CTRL 4 // egyelőre nincs funkció
#define TCA_CODE_SHIFT 7
#define TCA_CODE_OPT 8 // egyelőre nincs funkció
#define TCA_CODE_ALT 14 // egyelőre nincs funkció
// --- modifier állapot flag-ek ---
static bool mod_fn = false;
static bool mod_ctrl = false;
static bool mod_shift = false;
static bool mod_opt = false;
static bool mod_alt = false;
static void tca_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission(TCA8418_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
static uint8_t tca_read(uint8_t reg) {
Wire.beginTransmission(TCA8418_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom((uint8_t)TCA8418_ADDR, (uint8_t)1);
return Wire.available() ? Wire.read() : 0;
}
// Map a TCA8418 key-event number to a UI value.
// normal
static const unsigned char keymap[7*8] = {
'\'', KEY_CONTEXT_MENU, 0, 0,
'1', 'q', 0, 0,
'2', 'w', 'a', 0,
'3', 'e', 's', 'z',
'4', 'r', 'd', 'x',
'5', 't', 'f', 'c',
'6', 'y', 'g', 'v',
'7', 'u', 'h', 'b',
'8', 'i', 'j', 'n',
'9', 'o', 'k', 'm',
'0', 'p', 'l', ',',
'_', '[', ';', '.',
'=', ']', '\'', '/',
'\b', '\\', KEY_ENTER, ' '
};
// Fn pressed
static const unsigned char keymapFN[7*8] = {
KEY_CANCEL, KEY_CONTEXT_MENU, 0, 0,
'1', 'q', 0, 0,
'2', 'w', 'a', 0,
'3', 'e', 's', 'z',
'4', 'r', 'd', 'x',
'5', 't', 'f', 'c',
'6', 'y', 'g', 'v',
'7', 'u', 'h', 'b',
'8', 'i', 'j', 'n',
'9', 'o', 'k', 'm',
'0', 'p', 'l', KEY_LEFT,
'_', '[', KEY_UP, KEY_DOWN,
'=', ']', '\'', KEY_RIGHT,
'\b', '\\', KEY_KB_ENTER, ' '
};
// Shift pressed
static const unsigned char keymapSH[7*8] = {
'~', 0, 0, 0,
'!', 'Q', 0, 0,
'@', 'W', 'A', 0,
'#', 'E', 'S', 'Z',
'$', 'R', 'D', 'X',
'%', 'T', 'F', 'C',
'^', 'Y', 'G', 'V',
'&', 'U', 'H', 'B',
'*', 'I', 'J', 'N',
'{', 'O', 'K', 'M',
'}', 'P', 'L', '<',
'-', '(', ':', '>',
'+', ')', '"', '?',
'\b', '|', KEY_SELECT, ' '
};
static void keypad_init() {
tca_write(TCA8418_REG_CFG, 0x00); // scanner off
for (int i = 0; i < 16; i++) {
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
tca_read(TCA8418_REG_KEY_EVENT_A);
}
tca_write(TCA8418_REG_INT_STAT, 0x1F); // clear all int flags
tca_write(TCA8418_REG_GPI_EM1, 0x00);
tca_write(TCA8418_REG_GPI_EM2, 0x00);
tca_write(TCA8418_REG_GPI_EM3, 0x00);
tca_write(TCA8418_REG_KP_GPIO1, 0x7F); // ROW0-ROW6 in keypad matrix 0b01111111
tca_write(TCA8418_REG_KP_GPIO2, 0xFF); // COL0-COL7 in keypad matrix 0b11111111
tca_write(TCA8418_REG_KP_GPIO3, 0x00); // COL8, COL9
tca_write(TCA8418_REG_DEBOUNCE, 0x03); //
tca_write(TCA8418_REG_INT_STAT, 0x1F);
tca_write(TCA8418_REG_CFG, 0x11); // KE_IEN + INT_CFG, scanner on
delay(5);
for (int i = 0; i < 16; i++) {
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) break;
tca_read(TCA8418_REG_KEY_EVENT_A);
}
tca_write(TCA8418_REG_INT_STAT, 0x1F);
}
char tca8418_keypad_read() {
static bool inited = false;
if (!inited) { keypad_init(); inited = true; }
if ((tca_read(TCA8418_REG_KEY_LCK_EC) & 0x0F) == 0) return 0; // FIFO empty
uint8_t ev = tca_read(TCA8418_REG_KEY_EVENT_A);
tca_write(TCA8418_REG_INT_STAT, 0x1F); // clear int
bool pressed = (ev & 0x80) != 0;
uint8_t code = ev & 0x7F;
// --- modifier kezelés: press ÉS release is számít, sosem küld karaktert ---
switch (code) {
case TCA_CODE_FN: mod_fn = pressed; return 0;
case TCA_CODE_CTRL: mod_ctrl = pressed; return 0;
case TCA_CODE_SHIFT: mod_shift = pressed; return 0;
case TCA_CODE_OPT: mod_opt = pressed; return 0;
case TCA_CODE_ALT: mod_alt = pressed; return 0;
default: break;
}
if (!pressed) return 0; // sima gomb release -> ignore
uint8_t row = (code - 1) / 10;
uint8_t col = (code - 1) % 10;
uint8_t key_index = row * 8 + col;
if (key_index >= 7*8) return 0; // védelem, ha valamiért kilógna
// --- prioritás: Fn > Shift > normal (ctrl/opt/alt egyelőre nincs saját map) ---
const unsigned char* active_map = keymap;
if (mod_fn) active_map = keymapFN;
else if (mod_shift) active_map = keymapSH;
return active_map[key_index];
}
#endif
@@ -0,0 +1,201 @@
[M5Stack_Cardputer_ADV]
extends = esp32_base
board = m5stack-stamps3
build_flags =
${esp32_base.build_flags}
-I variants/m5stack_cardputer_adv
-D CARDPUTER_ADV
-D ARDUINO_USB_CDC_ON_BOOT=1
;---LoRa
-D P_LORA_DIO_1=4 ;IRQ
-D P_LORA_NSS=5
-D P_LORA_RESET=3
-D P_LORA_BUSY=6
-D P_LORA_SCLK=40
-D P_LORA_MISO=39
-D P_LORA_MOSI=14
-D SX126X_DIO2_AS_RF_SWITCH=true
-D SX126X_DIO3_TCXO_VOLTAGE=1.8
-D SX126X_CURRENT_LIMIT=140
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
-D SX126X_RX_BOOSTED_GAIN=1
;--BATTERY
-D PIN_VBAT_READ=10
-D PIN_USER_BTN=0
;---I2C
-D PIN_BOARD_SDA=8 ;14pin header & TCA8418
-D PIN_BOARD_SCL=9 ;14pin header & TCA8418
-D PIN_BOARD_SDA1=2 ;grove
-D PIN_BOARD_SCL1=1 ;grove
;---GPS
-D ENV_INCLUDE_GPS=1
-D PIN_GPS_RX=13
-D PIN_GPS_TX=15
-D GPS_BAUD_RATE=115200
;---LCD
-D PIN_TFT_SCL=36
-D PIN_TFT_SDA=35
-D PIN_TFT_RST=33
-D PIN_TFT_VDD_CTL=38
-D PIN_TFT_LEDA_CTL=38
-D PIN_TFT_LEDA_CTL_ACTIVE=HIGH
-D PIN_TFT_CS=37
-D PIN_TFT_DC=34 ;RS
-D ST7789
-D DISPLAY_CLASS=ST7789Display
;---TCA8418
-D ENV_USE_TCA8418=1
build_src_filter = ${esp32_base.build_src_filter}
+<../variants/m5stack_cardputer_adv>
+<helpers/sensors/*.cpp>
+<helpers/ui/ST7789Display.cpp>
+<helpers/ui/OLEDDisplay.cpp>
+<helpers/ui/OLEDDisplayFonts.cpp>
lib_deps =
${esp32_base.lib_deps}
${sensor_base.lib_deps}
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
[env:M5Stack_Cardputer_ADV_companion_radio_ble]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D BLE_DEBUG_LOGGING=1
-D OFFLINE_QUEUE_SIZE=256
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:M5Stack_Cardputer_ADV_companion_radio_usb]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1
; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:M5Stack_Cardputer_ADV_companion_solo_dual]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D DUAL_SERIAL=1
-D OFFLINE_QUEUE_SIZE=256
-D FIRMWARE_SOLO_BUILD=1
-D MESHCORE_VERSION='"1.16"'
-D UI_SENSORS_PAGE=1
-D ENABLE_SCREENSHOT
; misc-fixed 6x9 font (full Latin/Greek/Cyrillic), same as the Wio L1/Heltec
; solo builds. Solo only: it costs ~14 KB of flash, and this board's own
; QWERTY keyboard is exactly the case that benefits from typing those
; alphabets and having them render as themselves instead of blocks.
-D OLED_MISC_FIXED_FONT=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:M5Stack_Cardputer_ADV_repeater]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-D ADVERT_NAME='"Cardputer_adv Repeater"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<../examples/simple_repeater>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
${esp32_ota.lib_deps}
[env:M5Stack_Cardputer_ADV_repeater_bridge_espnow]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-D ADVERT_NAME='"ESPNow Bridge"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_ESPNOW_BRIDGE=1
; -D BRIDGE_DEBUG=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<helpers/bridges/ESPNowBridge.cpp>
+<../examples/simple_repeater>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
${esp32_ota.lib_deps}
[env:M5Stack_Cardputer_ADV_terminal_chat]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=1
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<../examples/simple_secure_chat/main.cpp>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
densaugeo/base64 @ ~1.4.0
[env:M5Stack_Cardputer_ADV_room_server]
extends = M5Stack_Cardputer_ADV
build_flags =
${M5Stack_Cardputer_ADV.build_flags}
-D ADVERT_NAME='"Cardputer_adv Room"'
-D ADVERT_LAT=0.0
-D ADVERT_LON=0.0
-D ADMIN_PASSWORD='"password"'
-D ROOM_PASSWORD='"hello"'
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<../examples/simple_room_server>
lib_deps =
${M5Stack_Cardputer_ADV.lib_deps}
${esp32_ota.lib_deps}
[env:M5Stack_Cardputer_ADV_kiss_modem]
extends = M5Stack_Cardputer_ADV
build_src_filter = ${M5Stack_Cardputer_ADV.build_src_filter}
+<../examples/kiss_modem/>
+63
View File
@@ -0,0 +1,63 @@
#include <Arduino.h>
#include "target.h"
CardputerADVBoard board;
static SPIClass spi;
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
WRAPPER_CLASS radio_driver(radio, board);
ESP32RTCClock fallback_clock;
AutoDiscoverRTCClock rtc_clock(fallback_clock);
MicroNMEALocationProvider gps(Serial1, &rtc_clock);
EnvironmentSensorManager sensors(gps);
#ifdef DISPLAY_CLASS
DISPLAY_CLASS display;
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
#endif
static void init_lora_cap_ioe() {
const uint8_t PI4IOE_ADDR = 0x43; // PI4IOE5V6408, ADDR pin -> GND
// Egyszerű "probe": ha nincs ACK, nincs ott az IO expander (régi cap)
Wire.beginTransmission(PI4IOE_ADDR);
if (Wire.endTransmission() != 0) {
return; // régi Cap LoRa868, nincs teendő
}
// Új Cap LoRa-1262: P0 -> output, High-Z letiltva, majd HIGH
Wire.beginTransmission(PI4IOE_ADDR);
Wire.write(0x03); // I/O Direction register
Wire.write(0x01); // bit0 = 1 -> P0 kimenet, többi marad bemenet
Wire.endTransmission();
Wire.beginTransmission(PI4IOE_ADDR);
Wire.write(0x07); // Output High-Impedance register
Wire.write(0x00); // bit0 = 0 -> P0 kijön a High-Z állapotból
Wire.endTransmission();
Wire.beginTransmission(PI4IOE_ADDR);
Wire.write(0x05); // Output Port register
Wire.write(0x01); // bit0 = 1 -> P0 HIGH (LoRa "SW" pin engedélyezve)
Wire.endTransmission();
}
bool radio_init() {
fallback_clock.begin();
Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); // 14pin header & internal
Wire1.begin(PIN_BOARD_SDA1, PIN_BOARD_SCL1); // grove
rtc_clock.begin(Wire); // needs Wire already begun on the right pins to auto-detect an RTC chip
init_lora_cap_ioe();
return radio.std_init(&spi);
}
mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); // create new random identity
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#define RADIOLIB_STATIC_ONLY 1
#include <RadioLib.h>
#include <helpers/radiolib/RadioLibWrappers.h>
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <CardputerADVBoard.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/SensorManager.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/ST7789Display.h>
#include <helpers/ui/MomentaryButton.h>
#endif
#include "helpers/sensors/EnvironmentSensorManager.h"
#include "helpers/sensors/MicroNMEALocationProvider.h"
extern CardputerADVBoard board;
extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors;
#ifdef DISPLAY_CLASS
extern DISPLAY_CLASS display;
extern MomentaryButton user_btn;
#endif
bool radio_init();
mesh::LocalIdentity radio_new_identity();