Skip to content
Open
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
7 changes: 7 additions & 0 deletions Devices/lilygo-tdeck-max/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)

idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility GDEQ031T10 TCA8418 BQ27220 driver
)
109 changes: 109 additions & 0 deletions Devices/lilygo-tdeck-max/Source/Configuration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#include "devices/Display.h"
#include "devices/TdeckmaxKeyboard.h"
#include "devices/TdeckmaxPower.h"

#include <Bq27220.h>
#include <driver/gpio.h>

#include <Tactility/hal/Configuration.h>
#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/gpio_controller.h>

using namespace tt::hal;

// LoRa and SD card share the EPD's SPI bus but aren't wired up yet, so their
// chip-select lines are left floating. Deassert them before the EPD driver
// touches the bus, matching the vendor reference driver's own setup(), so a
// floating CS can't make either chip latch EPD command bytes meant for it.
constexpr auto LORA_PIN_CS = GPIO_NUM_3;
constexpr auto SD_PIN_CS = GPIO_NUM_48;

// Reset lines routed through the XL9555 IO expander (P-numbers from the vendor
// lib/TDeckMaxBoard/src/TDeckMaxBoard.h). Both are active low.
constexpr auto XL9555_PIN_TOUCH_RST = 7; // P07
constexpr auto XL9555_PIN_KEY_RST = 9; // P11

// Release the touch and keyboard reset lines held by the XL9555. Without this
// the touch controller may stay in a half-powered state and the keyboard's
// TCA8418 is held in reset, so neither responds. Mirrors the vendor factory
// firmware's XL9555 init (examples/factory/factory.ino).
static void initIoExpander() {
auto* xl9555 = device_find_by_name("xl9555");
if (xl9555 == nullptr) {
// Boot anyway; display works without the expander, but touch/keyboard won't.
return;
}

auto* touch_rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_TOUCH_RST, GPIO_OWNER_GPIO);
auto* key_rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_KEY_RST, GPIO_OWNER_GPIO);

if (touch_rst != nullptr) {
gpio_descriptor_set_flags(touch_rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(touch_rst, false);
}
if (key_rst != nullptr) {
gpio_descriptor_set_flags(key_rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(key_rst, false);
}

delay_millis(20);

// Release both resets and give the controllers time to boot before they're probed.
if (touch_rst != nullptr) {
gpio_descriptor_set_level(touch_rst, true);
gpio_descriptor_release(touch_rst);
}
if (key_rst != nullptr) {
gpio_descriptor_set_level(key_rst, true);
gpio_descriptor_release(key_rst);
}

delay_millis(60);
}

static bool initBoot() {
gpio_config_t config = {
.pin_bit_mask = (1ULL << LORA_PIN_CS) | (1ULL << SD_PIN_CS),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&config);
gpio_set_level(LORA_PIN_CS, 1);
gpio_set_level(SD_PIN_CS, 1);

initIoExpander();

return true;
}

static DeviceVector createDevices() {
auto* i2c = device_find_by_name("i2c0");
check(i2c != nullptr);

// SD card is not created here: it's an espressif,esp32-sdspi node in the
// devicetree (sdcard@1 under spi0), started by Hal::init's mountAll().
DeviceVector devices = {
createDisplay()
};

auto keypad = std::make_shared<Tca8418>(i2c);
devices.push_back(keypad);
devices.push_back(std::make_shared<TdeckmaxKeyboard>(keypad));

// Battery metrics from the BQ27220 fuel gauge (0x55); power-off via the
// SY6970 charger (0x6A). Enables the launcher's power-off button.
auto gauge = std::make_shared<Bq27220>(i2c);
devices.push_back(gauge);
devices.push_back(std::make_shared<TdeckmaxPower>(gauge, i2c));

return devices;
}

extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices
};
225 changes: 225 additions & 0 deletions Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
#include "Cst66xxTouch.h"

#include <tactility/log.h>
#include <Tactility/app/App.h>
#include <tactility/drivers/gpio_controller.h>
#include <tactility/drivers/i2c_controller.h>

#include <freertos/FreeRTOS.h>

// Touch reset is wired to XL9555 P07 (active low).
static constexpr uint32_t XL9555_PIN_TOUCH_RST = 7;

constexpr auto* TAG = "CST66xx";

static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(20);

// The protocol uses 4-byte register addresses, taken from the vendor's
// lib/HynTouch/src/hyn_cst66xx.c.

// Normal-mode setup sequence (cst66xx_set_workmode, NOMAL_MODE).
static constexpr uint8_t CMD_DISABLE_LP_I2C[4] = {0xD0, 0x00, 0x04, 0x00};
static constexpr uint8_t CMD_NORMAL_A[4] = {0xD0, 0x00, 0x00, 0x00};
static constexpr uint8_t CMD_NORMAL_B[4] = {0xD0, 0x00, 0x0C, 0x00};
static constexpr uint8_t CMD_NORMAL_C[4] = {0xD0, 0x00, 0x01, 0x00};
// Read-point register: write these 4 bytes, then read the touch frame.
static constexpr uint8_t REG_READ_POINT[4] = {0xD0, 0x07, 0x00, 0x00};
// Acknowledge/clear after each read.
static constexpr uint8_t CMD_ACK[4] = {0xD0, 0x00, 0x02, 0xAB};
// Identity/config register (cst66xx_updata_tpinfo): read 50 bytes; buf[2]==0xCA
// && buf[3]==0xCA confirms a CST66xx.
static constexpr uint8_t REG_INFO[4] = {0xD0, 0x03, 0x00, 0x00};

static void setNormalMode(::Device* i2c, uint8_t address) {
i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT);
vTaskDelay(pdMS_TO_TICKS(1));
i2c_controller_write(i2c, address, CMD_DISABLE_LP_I2C, sizeof(CMD_DISABLE_LP_I2C), I2C_TIMEOUT);
i2c_controller_write(i2c, address, CMD_NORMAL_A, sizeof(CMD_NORMAL_A), I2C_TIMEOUT);
i2c_controller_write(i2c, address, CMD_NORMAL_B, sizeof(CMD_NORMAL_B), I2C_TIMEOUT);
i2c_controller_write(i2c, address, CMD_NORMAL_C, sizeof(CMD_NORMAL_C), I2C_TIMEOUT);
}

bool Cst66xxTouch::start() {
// Reset the controller (XL9555 P07, active low). The chip only re-initialises
// into normal reporting mode after a clean reset pulse.
auto* xl9555 = device_find_by_name("xl9555");
if (xl9555 != nullptr) {
auto* rst = gpio_descriptor_acquire(xl9555, XL9555_PIN_TOUCH_RST, GPIO_OWNER_GPIO);
if (rst != nullptr) {
gpio_descriptor_set_flags(rst, GPIO_FLAG_DIRECTION_OUTPUT);
gpio_descriptor_set_level(rst, false);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_descriptor_set_level(rst, true);
gpio_descriptor_release(rst);
}
}
// The reset pulse exits boot mode; give the controller time to re-init.
vTaskDelay(pdMS_TO_TICKS(80));

// Put the controller into normal reporting mode and verify the identity. The
// first read after reset can miss, so retry like the vendor's
// cst66xx_updata_tpinfo (read 0xD0030000, expect buf[2]==buf[3]==0xCA).
auto* i2c = configuration.i2cController;
const uint8_t address = configuration.address;
bool confirmed = false;
for (int attempt = 0; attempt < 5 && !confirmed; ++attempt) {
setNormalMode(i2c, address);
uint8_t info[50] = {};
if (i2c_controller_write(i2c, address, REG_INFO, sizeof(REG_INFO), I2C_TIMEOUT) == ERROR_NONE &&
i2c_controller_read(i2c, address, info, sizeof(info), I2C_TIMEOUT) == ERROR_NONE &&
info[2] == 0xCA && info[3] == 0xCA) {
confirmed = true;
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
if (confirmed) {
LOG_I(TAG, "CST66xx initialised");
} else {
LOG_W(TAG, "CST66xx identity not confirmed (continuing)");
}
return true;
}

bool Cst66xxTouch::stop() {
return true;
}

bool Cst66xxTouch::readPoint(int16_t& x, int16_t& y) {
// CST66xx frame (cst66xx_report): write the read-point register, then read 9
// bytes. buf[2]=report type (0xFF=position/key), buf[3] low nibble = finger
// count, high nibble = key count. The first 5-byte slot (buf[4..8]) is a key
// slot when key count > 0, otherwise the first finger; further fingers, if
// any, follow at buf[index+...].
uint8_t buf[9] = {};
error_t err = i2c_controller_write(configuration.i2cController, configuration.address, REG_READ_POINT, sizeof(REG_READ_POINT), I2C_TIMEOUT);
if (err == ERROR_NONE) {
err = i2c_controller_read(configuration.i2cController, configuration.address, buf, sizeof(buf), I2C_TIMEOUT);
}
if (err != ERROR_NONE) {
return false;
}

// Acknowledge/clear the frame after every read.
i2c_controller_write(configuration.i2cController, configuration.address, CMD_ACK, sizeof(CMD_ACK), I2C_TIMEOUT);

const uint8_t reportType = buf[2];
const uint8_t fingerCount = buf[3] & 0x0F;
const uint8_t keyCount = (buf[3] & 0xF0) >> 4;

// Bezel touch-keys are reported in the first slot (buf[8]): low nibble = key
// id, high nibble = state (non-zero = pressed). The controller reports keys
// mutually exclusively with finger coordinates, so a key frame never carries
// a touch point.
if (reportType == 0xFF && keyCount > 0) {
const uint8_t keyByte = buf[8];
handleBezelKey(keyByte & 0x0F, (keyByte >> 4) != 0);
return false;
}
// No key in this frame: clear the latch so the next press fires once.
bezelKeyDown = false;

if (reportType != 0xFF || fingerCount < 1) {
return false;
}

// First finger's slot follows any key slots.
const uint8_t index = keyCount * 5;
const uint8_t event = buf[index + 8] >> 4; // 0 = up
if (event == 0) {
return false;
}

int16_t rawX = static_cast<int16_t>(buf[index + 4] | (static_cast<uint16_t>(buf[index + 7] & 0x0F) << 8));
int16_t rawY = static_cast<int16_t>(buf[index + 5] | (static_cast<uint16_t>(buf[index + 7] & 0xF0) << 4));

if (configuration.swapXy) {
std::swap(rawX, rawY);
}
if (configuration.mirrorX) {
rawX = static_cast<int16_t>(configuration.width - 1 - rawX);
}
if (configuration.mirrorY) {
rawY = static_cast<int16_t>(configuration.height - 1 - rawY);
}

x = rawX;
y = rawY;
return true;
}

// Bezel touch-key ids as reported by the CST66xx, left to right (confirmed on
// hardware). To re-map a button, change the action in the switch below — the
// mapping is also documented in NOTES.md.
static constexpr uint8_t BEZEL_KEY_HEART = 0; // left
static constexpr uint8_t BEZEL_KEY_SPEECH = 1; // centre
static constexpr uint8_t BEZEL_KEY_AIRPLANE = 2; // right

void Cst66xxTouch::handleBezelKey(uint8_t keyId, bool pressed) {
if (!pressed) {
bezelKeyDown = false;
return;
}
if (bezelKeyDown) {
return; // still held; the press edge was already handled
}
bezelKeyDown = true;

LOG_D(TAG, "bezel key %u", keyId);

// Bezel button -> navigation action. Edit these cases to customise:
switch (keyId) {
case BEZEL_KEY_HEART: // Back: stop the current app (no-op at the launcher root)
tt::app::stop();
break;
// Home (BEZEL_KEY_SPEECH) and Recents (BEZEL_KEY_AIRPLANE) are intentionally
// unbound: tt::app::start() always pushes a new instance onto the app
// stack, so repeated presses would keep growing it (Launcher/AppList
// instances never get cleaned up) and leak memory over time. The app
// stack has no API yet to collapse back to an existing instance instead
// of pushing a new one; wire these up once that exists.
default:
break;
}
}

void Cst66xxTouch::readCallback(lv_indev_t* indev, lv_indev_data_t* data) {
auto* self = static_cast<Cst66xxTouch*>(lv_indev_get_user_data(indev));

int16_t x;
int16_t y;
if (self->readPoint(x, y)) {
self->lastX = x;
self->lastY = y;
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
// Report the release at the last known position.
data->point.x = self->lastX;
data->point.y = self->lastY;
data->state = LV_INDEV_STATE_RELEASED;
}
}

bool Cst66xxTouch::startLvgl(lv_display_t* display) {
if (indev != nullptr) {
return true;
}

indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
lv_indev_set_read_cb(indev, &readCallback);
lv_indev_set_display(indev, display);
lv_indev_set_user_data(indev, this);
return true;
}

bool Cst66xxTouch::stopLvgl() {
if (indev == nullptr) {
return true;
}
lv_indev_delete(indev);
indev = nullptr;
return true;
}
54 changes: 54 additions & 0 deletions Devices/lilygo-tdeck-max/Source/devices/Cst66xxTouch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once

#include <Tactility/hal/touch/TouchDevice.h>
#include <tactility/device.h>

// Capacitive touch for the LilyGO T-Deck Max's Hynitron CST66xx controller at
// I2C 0x1A. (The vendor docs label it "CST328", but the vendor HynTouch driver
// probes cst66xx first and that is what answers here.) This is a minimal,
// single-touch port of the vendor's hyn_cst66xx.c protocol for LVGL pointer input.
class Cst66xxTouch final : public tt::hal::touch::TouchDevice {

public:

struct Configuration {
::Device* i2cController;
uint8_t address;
uint16_t width;
uint16_t height;
bool swapXy;
bool mirrorX;
bool mirrorY;
};

explicit Cst66xxTouch(const Configuration& configuration) : configuration(configuration) {}

std::string getName() const override { return "CST66xx"; }
std::string getDescription() const override { return "CST66xx I2C capacitive touch"; }

bool start() override;
bool stop() override;

bool supportsLvgl() const override { return true; }
bool startLvgl(lv_display_t* display) override;
bool stopLvgl() override;
lv_indev_t* getLvglIndev() override { return indev; }

bool supportsTouchDriver() override { return false; }
std::shared_ptr<tt::hal::touch::TouchDriver> getTouchDriver() override { return nullptr; }

private:

Configuration configuration;
lv_indev_t* indev = nullptr;
int16_t lastX = 0;
int16_t lastY = 0;
// The CST66xx also reports the three bezel touch-keys (heart / speech-bubble /
// paper-airplane). bezelKeyDown latches a press so we act once on the rising
// edge rather than every poll while the key is held.
bool bezelKeyDown = false;

bool readPoint(int16_t& x, int16_t& y);
void handleBezelKey(uint8_t keyId, bool pressed);
static void readCallback(lv_indev_t* indev, lv_indev_data_t* data);
};
Loading
Loading