Files
Nic Boet 7df116b4f1 Fix case-sensitive filesystem build failures (Linux) for ESP32 LilyGo variants
The repo has apparently only ever been built on case-insensitive
filesystems (macOS/Windows): every #include in the codebase uses
intended PascalCase/CamelCase header names (e.g. "SettingsScreen.h",
"WiFiMQTT.h"), but 28 of the actual files on disk were saved with
inconsistent casing (e.g. "Settingsscreen.h", "wifimqtt.h"). On a
case-sensitive filesystem (Linux) this is a hard compile failure, not
a cosmetic mismatch -- confirmed by running `pio run -e meck_audio_ble`
on Gentoo Linux, which failed immediately on "target.h: No such file
or directory" and a cascade of similar errors as each fix exposed the
next one.

Root causes, two flavors of the same underlying bug:

1. Header filename casing (29 files renamed via `git mv` to preserve
   history): examples/companion_radio/ui-new/*, examples/simple_repeater/*,
   and two variant-local headers (PCF85063Clock.h, TCA8418Keyboard.h x2).
   Verified safe before renaming: every file has exactly one consistent
   intended casing across all the places that #include it (checked via
   a repo-wide scan comparing every #include against on-disk filenames,
   zero conflicts found), so each rename is a pure no-op for behavior.

2. PlatformIO config paths using the wrong case for variant directories
   that are actually lowercase on disk (variants/lilygo_tdeck_pro,
   variants/lilygo_t5s3_epaper_pro):
   - `-I variants/LilyGo_TDeck_Pro` / `-I variants/LilyGo_T5S3_EPaper_Pro`
     in build_flags (3 occurrences, including lilygo_tdeck_max's
     reference to TDeck Pro's shared headers) -- broke header resolution
     for target.h and friends.
   - `+<../variants/LilyGo_TDeck_Pro>` / `+<../variants/LilyGo_T5S3_EPaper_Pro>`
     in build_src_filter (2 occurrences) -- silently excluded the board-init
     .cpp files (TDeckBoard.cpp etc.) from compilation entirely, which
     didn't fail until the *link* stage ("undefined reference to
     radio_init()", `TDeckBoard::begin()`, etc.) since PlatformIO's glob
     just matched nothing rather than erroring.

Verified fix: `pio run -e meck_audio_ble` now compiles, links, and
produces a firmware image cleanly (RAM 53.1%, Flash 49.6%).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 14:44:46 -05:00

164 lines
5.7 KiB
C++

#pragma once
// =============================================================================
// TouchInput - Minimal CST328/CST3530 touch driver for T-Deck Pro
//
// Uses raw I2C reads on the shared Wire bus. No external library needed.
// Protocol confirmed via raw serial capture from actual hardware:
//
// Register 0xD000, 7 bytes:
// buf[0]: event flags (0xAB = idle/no touch, other = active touch)
// buf[1]: X coordinate high data
// buf[2]: Y coordinate high data
// buf[3]: X low nibble (bits 7:4) | Y low nibble (bits 3:0)
// buf[4]: pressure
// buf[5]: touch count (& 0x7F), typically 0x01 for single touch
// buf[6]: 0xAB always (check byte, ignore)
//
// Coordinate formula:
// x = (buf[1] << 4) | ((buf[3] >> 4) & 0x0F) → 0..239
// y = (buf[2] << 4) | (buf[3] & 0x0F) → 0..319
//
// Hardware: CST328 at 0x1A, INT=GPIO12, RST=GPIO38 (V1.1)
//
// Guard: HAS_TOUCHSCREEN
// =============================================================================
#ifdef HAS_TOUCHSCREEN
#ifndef TOUCH_INPUT_H
#define TOUCH_INPUT_H
#include <Arduino.h>
#include <Wire.h>
// The CST328 pulses its INT line low (falling edge) when a new touch report is
// ready. We attach an edge interrupt that sets this flag, and only read/ack the
// controller when it has fired -- mirroring the Hynitron driver, which reads
// only on the INT edge instead of blind-polling. Blind polling at 50 Hz was the
// source of the i2cRead -1/263 errors (reading when no report was pending).
namespace {
volatile bool _touchIrqFired = false;
void IRAM_ATTR _touchIsr() { _touchIrqFired = true; }
}
class TouchInput {
public:
static const uint8_t TOUCH_ADDR = 0x1A;
TouchInput(TwoWire* wire = &Wire)
: _wire(wire), _intPin(-1), _initialized(false), _debugCount(0) {}
bool begin(int intPin) {
_intPin = intPin;
pinMode(_intPin, INPUT);
// On ESP32 every GPIO is interrupt-capable and digitalPinToInterrupt is an
// identity macro (pin == interrupt number), but it is not always visible in
// this header's include context -- pass the GPIO number directly.
attachInterrupt(_intPin, _touchIsr, FALLING);
// Verify the touch controller is present on the bus
_wire->beginTransmission(TOUCH_ADDR);
uint8_t err = _wire->endTransmission();
if (err != 0) {
Serial.printf("[Touch] CST328 not found at 0x%02X (err=%d)\n", TOUCH_ADDR, err);
return false;
}
Serial.printf("[Touch] CST328 found at 0x%02X, INT=GPIO%d\n", TOUCH_ADDR, _intPin);
// Enter normal report mode: write command register 0xD109. The Hynitron
// driver does this once after reset (cst3xx_init -> set_workmode NOMAL_MODE).
_wire->beginTransmission(TOUCH_ADDR);
_wire->write(0xD1);
_wire->write(0x09);
_wire->endTransmission(true);
_initialized = true;
return true;
}
bool isReady() const { return _initialized; }
// Returns true if a finger is down, fills x and y (physical display space:
// 0-239 X, 0-319 Y). Reads only when the INT edge interrupt has fired.
bool getPoint(int16_t &x, int16_t &y) {
if (!_initialized) return false;
// Only touch the bus when the INT line has signalled a new report. With no
// pending report there is nothing to read, and reading anyway is what
// produced the i2cRead -1/263 errors.
if (!_touchIrqFired) return false;
_touchIrqFired = false;
uint8_t buf[7];
memset(buf, 0, sizeof(buf));
// Write register address 0xD000.
// Use a STOP here (true), not a repeated start (false): the repeated-start
// combined read (i2cWriteReadNonStop) is what was throwing the -1/263 errors
// on this bus, while the keyboard's stop-then-read pattern never errors.
_wire->beginTransmission(TOUCH_ADDR);
_wire->write(0xD0);
_wire->write(0x00);
if (_wire->endTransmission(true) != 0) return false;
// Read 7 bytes of touch data
uint8_t received = _wire->requestFrom(TOUCH_ADDR, (uint8_t)7);
if (received < 7) return false;
for (int i = 0; i < 7; i++) buf[i] = _wire->read();
// Acknowledge the report: write 0xAB to register 0xD000 so the controller
// releases its buffer for the next frame. Required after EVERY read of
// 0xD000 -- without it the CST328 re-serves stale frames (phantom touches)
// and eventually NAKs the read. Matches the Hynitron driver tail-end write.
_wire->beginTransmission(TOUCH_ADDR);
_wire->write(0xD0);
_wire->write(0x00);
_wire->write(0xAB);
_wire->endTransmission(true);
// Check byte: a valid frame always has buf[6] == 0xAB. Reject anything else.
if (buf[6] != 0xAB) return false;
// buf[0] == 0xAB means idle (no touch active)
if (buf[0] == 0xAB) return false;
// buf[0] == 0x00 can appear on finger-up transition — ignore
if (buf[0] == 0x00) return false;
// Touch count from buf[5]
uint8_t count = buf[5] & 0x7F;
if (count == 0 || count > 5) return false;
// Parse coordinates (CST226/CST328 format confirmed by hardware capture)
// x = (buf[1] << 4) | high nibble of buf[3]
// y = (buf[2] << 4) | low nibble of buf[3]
int16_t tx = ((int16_t)buf[1] << 4) | ((buf[3] >> 4) & 0x0F);
int16_t ty = ((int16_t)buf[2] << 4) | (buf[3] & 0x0F);
// Sanity check (panel is 240x320)
if (tx < 0 || tx > 260 || ty < 0 || ty > 340) return false;
// Debug: log first 20 touch events with parsed coordinates
if (_debugCount < 50) {
Serial.printf("[Touch] Raw: %02X %02X %02X %02X %02X %02X %02X → x=%d y=%d\n",
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6],
tx, ty);
_debugCount++;
}
x = tx;
y = ty;
return true;
}
private:
TwoWire* _wire;
int _intPin;
bool _initialized;
int _debugCount;
};
#endif // TOUCH_INPUT_H
#endif // HAS_TOUCHSCREEN