Implemented LilyGO T-Lora Pager (#295)

This commit is contained in:
flip 2025-07-23 20:45:57 +00:00 committed by GitHub
parent ab4cf79a47
commit 00b62a2831
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 2013 additions and 9 deletions

View File

@ -37,6 +37,8 @@ menu "Tactility App"
bool "Elecrow CrowPanel Basic 5.0" bool "Elecrow CrowPanel Basic 5.0"
config TT_BOARD_LILYGO_TDECK config TT_BOARD_LILYGO_TDECK
bool "LilyGo T-Deck" bool "LilyGo T-Deck"
config TT_BOARD_LILYGO_TLORA_PAGER
bool "LilyGo T-Lora Pager"
config TT_BOARD_M5STACK_CORE2 config TT_BOARD_M5STACK_CORE2
bool "M5Stack Core2" bool "M5Stack Core2"
config TT_BOARD_M5STACK_CORES3 config TT_BOARD_M5STACK_CORES3

View File

@ -8,6 +8,9 @@
#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) #if defined(CONFIG_TT_BOARD_LILYGO_TDECK)
#include "LilygoTdeck.h" #include "LilygoTdeck.h"
#define TT_BOARD_HARDWARE &lilygo_tdeck #define TT_BOARD_HARDWARE &lilygo_tdeck
#elif defined(CONFIG_TT_BOARD_LILYGO_TLORA_PAGER)
#include "LilygoTloraPager.h"
#define TT_BOARD_HARDWARE &lilygo_tlora_pager
#elif defined(CONFIG_TT_BOARD_CYD_2432S024C) #elif defined(CONFIG_TT_BOARD_CYD_2432S024C)
#include "CYD2432S024C.h" #include "CYD2432S024C.h"
#define TT_BOARD_HARDWARE &cyd_2432s024c_config #define TT_BOARD_HARDWARE &cyd_2432s024c_config
@ -68,4 +71,4 @@
extern tt::hal::Configuration hardware; extern tt::hal::Configuration hardware;
#define TT_BOARD_HARDWARE &hardware #define TT_BOARD_HARDWARE &hardware
#endif // ESP_PLATFORM #endif // ESP_PLATFORM

View File

@ -12,6 +12,8 @@ dependencies:
version: "1.1.1" version: "1.1.1"
rules: rules:
- if: "target in [esp32s3, esp32p4]" - if: "target in [esp32s3, esp32p4]"
espressif/esp_lcd_st7796:
version: "1.3.2"
espressif/esp_lcd_panel_io_additions: "1.0.1" espressif/esp_lcd_panel_io_additions: "1.0.1"
espressif/esp_tinyusb: espressif/esp_tinyusb:
version: "1.5.0" version: "1.5.0"

View File

@ -0,0 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd ST7796 BQ27220 TCA8418 PwmBacklight driver esp_adc
)

View File

@ -0,0 +1,55 @@
#include "PwmBacklight.h"
#include "Tactility/kernel/SystemEvents.h"
#include "Tactility/service/gps/GpsService.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/hal/gps/GpsConfiguration.h>
#include <driver/gpio.h>
#include <Bq27220.h>
#include <Tca8418.h>
#define TAG "tpager"
// Power on
#define TDECK_POWERON_GPIO GPIO_NUM_10
std::shared_ptr<Bq27220> bq27220;
std::shared_ptr<Tca8418> tca8418;
bool tpagerInit() {
ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START);
/* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness
* when moving the brightness slider rapidly from a lower setting to 100%.
* This is not a slider bug (data was debug-traced) */
if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) {
TT_LOG_E(TAG, "Backlight init failed");
return false;
}
bq27220 = std::make_shared<Bq27220>(I2C_NUM_0);
tt::hal::registerDevice(bq27220);
tca8418 = std::make_shared<Tca8418>(I2C_NUM_0);
tt::hal::registerDevice(tca8418);
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
bq27220->configureCapacity(1500, 1500);
auto gps_service = tt::service::gps::findGpsService();
if (gps_service != nullptr) {
std::vector<tt::hal::gps::GpsConfiguration> gps_configurations;
gps_service->getGpsConfigurations(gps_configurations);
if (gps_configurations.empty()) {
if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "Grove", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) {
TT_LOG_I(TAG, "Configured internal GPS");
} else {
TT_LOG_E(TAG, "Failed to configure internal GPS");
}
}
}
});
return true;
}

View File

@ -0,0 +1,83 @@
#include "Tactility/lvgl/LvglSync.h"
#include "hal/TpagerDisplay.h"
#include "hal/TpagerDisplayConstants.h"
#include "hal/TpagerKeyboard.h"
#include "hal/TpagerPower.h"
#include "hal/TpagerSdCard.h"
#include <Tactility/hal/Configuration.h>
#define TPAGER_SPI_TRANSFER_SIZE_LIMIT (TPAGER_LCD_HORIZONTAL_RESOLUTION * TPAGER_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8))
bool tpagerInit();
using namespace tt::hal;
extern const Configuration lilygo_tlora_pager = {
.initBoot = tpagerInit,
.createDisplay = createDisplay,
.createKeyboard = createKeyboard,
.sdcard = createTpagerSdCard(),
.power = tpager_get_power,
.i2c = {
i2c::Configuration {
.name = "Shared",
.port = I2C_NUM_0,
.initMode = i2c::InitMode::ByTactility,
.isMutable = true,
.config = (i2c_config_t) {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_3,
.scl_io_num = GPIO_NUM_2,
.sda_pullup_en = false,
.scl_pullup_en = false,
.master = {
.clk_speed = 100'000
},
.clk_flags = 0
}
}
},
.spi {spi::Configuration {
.device = SPI2_HOST,
.dma = SPI_DMA_CH_AUTO,
.config = {.mosi_io_num = GPIO_NUM_34, .miso_io_num = GPIO_NUM_33, .sclk_io_num = GPIO_NUM_35,
.quadwp_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported
.quadhd_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported
.data4_io_num = GPIO_NUM_NC,
.data5_io_num = GPIO_NUM_NC,
.data6_io_num = GPIO_NUM_NC,
.data7_io_num = GPIO_NUM_NC,
.data_io_default_level = false,
.max_transfer_sz = TPAGER_SPI_TRANSFER_SIZE_LIMIT,
.flags = 0,
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
.intr_flags = 0},
.initMode = spi::InitMode::ByTactility,
.isMutable = false,
.lock = tt::lvgl::getSyncLock() // esp_lvgl_port owns the lock for the display
}},
.uart {uart::Configuration {
.name = "Grove",
.port = UART_NUM_1,
.rxPin = GPIO_NUM_4,
.txPin = GPIO_NUM_12,
.rtsPin = GPIO_NUM_NC,
.ctsPin = GPIO_NUM_NC,
.rxBufferSize = 1024,
.txBufferSize = 1024,
.config = {
.baud_rate = 38400,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.rx_flow_ctrl_thresh = 0,
.source_clk = UART_SCLK_DEFAULT,
.flags = {
.allow_pd = 0,
.backup_before_sleep = 0,
}
}
}}
};

View File

@ -0,0 +1,5 @@
#pragma once
#include <Tactility/hal/Configuration.h>
extern const tt::hal::Configuration lilygo_tlora_pager;

View File

@ -0,0 +1,30 @@
#include "TpagerDisplay.h"
#include "TpagerDisplayConstants.h"
#include <PwmBacklight.h>
#include <St7796Display.h>
#include <driver/spi_master.h>
#define TAG "TPAGER_display"
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto configuration = std::make_unique<St7796Display::Configuration>(
TPAGER_LCD_SPI_HOST,
TPAGER_LCD_PIN_CS,
TPAGER_LCD_PIN_DC,
480, // w
222, // h
nullptr,
true, //swapXY
true, //mirrorX
true, //mirrorY
true, //invertColor
0, //gapX
49 //gapY
);
configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty;
return std::make_shared<St7796Display>(std::move(configuration));
}

View File

@ -0,0 +1,40 @@
#pragma once
#include "Tactility/hal/display/DisplayDevice.h"
#include <esp_lcd_types.h>
#include <lvgl.h>
class TpagerDisplay : public tt::hal::display::DisplayDevice {
private:
esp_lcd_panel_io_handle_t ioHandle = nullptr;
esp_lcd_panel_handle_t panelHandle = nullptr;
lv_display_t* displayHandle = nullptr;
bool poweredOn = false;
public:
std::string getName() const final { return "ST7796"; }
std::string getDescription() const final { return "SPI display"; }
bool start() override;
bool stop() override;
void setPowerOn(bool turnOn) override;
bool isPoweredOn() const override { return poweredOn; };
bool supportsPowerControl() const override { return true; }
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable createTouch() override;
void setBacklightDuty(uint8_t backlightDuty) override;
bool supportsBacklightDuty() const override { return true; }
void setGammaCurve(uint8_t index) override;
uint8_t getGammaCurveCount() const override { return 4; };
lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; }
};
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -0,0 +1,8 @@
#pragma once
#define TPAGER_LCD_SPI_HOST SPI2_HOST
#define TPAGER_LCD_PIN_CS GPIO_NUM_38
#define TPAGER_LCD_PIN_DC GPIO_NUM_37 // RS
#define TPAGER_LCD_HORIZONTAL_RESOLUTION 222
#define TPAGER_LCD_VERTICAL_RESOLUTION 480
#define TPAGER_LCD_SPI_TRANSFER_HEIGHT (TPAGER_LCD_VERTICAL_RESOLUTION / 10)

View File

@ -0,0 +1,359 @@
#include "TpagerKeyboard.h"
#include <Tactility/hal/i2c/I2c.h>
#include <driver/i2c.h>
#include "driver/gpio.h"
#include "freertos/queue.h"
#include <Tactility/Log.h>
#define TAG "tpager_keyboard"
#define ENCODER_A GPIO_NUM_40
#define ENCODER_B GPIO_NUM_41
#define ENCODER_ENTER GPIO_NUM_7
#define BACKLIGHT GPIO_NUM_46
#define KB_ROWS 4
#define KB_COLS 11
// Lowercase Keymap
static constexpr char keymap_lc[KB_ROWS][KB_COLS] = {
{'\0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'},
{'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\n', '\0'},
{'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', LV_KEY_BACKSPACE, ' ', '\0'},
{'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}
};
// Uppercase Keymap
static constexpr char keymap_uc[KB_ROWS][KB_COLS] = {
{'\0', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'},
{'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '\n', '\0'},
{'Z', 'X', 'C', 'V', 'B', 'N', 'M', '\0', LV_KEY_BACKSPACE, ' ', '\0'},
{'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}
};
// Symbol Keymap
static constexpr char keymap_sy[KB_ROWS][KB_COLS] = {
{'\0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
{'.', '/', '+', '-', '=', ':', '\'', '"', '@', '\t', '\0'},
{'_', '$', ';', '?', '!', ',', '.', '\0', LV_KEY_BACKSPACE, ' ', '\0'},
{'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}
};
static QueueHandle_t keyboardMsg;
static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev);
static bool enter_prev = false;
char keypress = 0;
// Defaults
data->key = 0;
data->state = LV_INDEV_STATE_RELEASED;
if (xQueueReceive(keyboardMsg, &keypress, pdMS_TO_TICKS(50)) == pdPASS) {
data->key = keypress;
data->state = LV_INDEV_STATE_PRESSED;
}
}
static void encoder_read_callback(lv_indev_t* indev, lv_indev_data_t* data) {
TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev);
const int enter_filter_threshold = 2;
static int enter_filter = 0;
const int pulses_click = 4;
static int pulses_prev = 0;
bool anyinput = false;
// Defaults
data->enc_diff = 0;
data->state = LV_INDEV_STATE_RELEASED;
int pulses = kb->getEncoderPulses();
int pulse_diff = (pulses - pulses_prev);
if ((pulse_diff > pulses_click) || (pulse_diff < -pulses_click)) {
data->enc_diff = pulse_diff / pulses_click;
pulses_prev = pulses;
anyinput = true;
}
bool enter = !gpio_get_level(ENCODER_ENTER);
if (enter && (enter_filter < enter_filter_threshold)) {
enter_filter++;
}
if (!enter && (enter_filter > 0)) {
enter_filter--;
}
if (enter_filter == enter_filter_threshold) {
data->state = LV_INDEV_STATE_PRESSED;
anyinput = true;
}
if (anyinput) {
kb->makeBacklightImpulse();
}
}
void TpagerKeyboard::processKeyboard() {
static bool shift_pressed = false;
static bool sym_pressed = false;
static bool cap_toggle = false;
static bool cap_toggle_armed = true;
bool anykey_pressed = false;
if (keypad->update()) {
anykey_pressed = (keypad->pressed_key_count > 0);
for (int i = 0; i < keypad->pressed_key_count; i++) {
auto row = keypad->pressed_list[i].row;
auto col = keypad->pressed_list[i].col;
auto hold = keypad->pressed_list[i].hold_time;
if ((row == 1) && (col == 10)) {
sym_pressed = true;
}
if ((row == 2) && (col == 7)) {
shift_pressed = true;
}
}
if ((sym_pressed && shift_pressed) && cap_toggle_armed) {
cap_toggle = !cap_toggle;
cap_toggle_armed = false;
}
for (int i = 0; i < keypad->pressed_key_count; i++) {
auto row = keypad->pressed_list[i].row;
auto col = keypad->pressed_list[i].col;
auto hold = keypad->pressed_list[i].hold_time;
char chr = '\0';
if (sym_pressed) {
chr = keymap_sy[row][col];
} else if (shift_pressed || cap_toggle) {
chr = keymap_uc[row][col];
} else {
chr = keymap_lc[row][col];
}
if (chr != '\0') xQueueSend(keyboardMsg, (void*)&chr, portMAX_DELAY);
}
for (int i = 0; i < keypad->released_key_count; i++) {
auto row = keypad->released_list[i].row;
auto col = keypad->released_list[i].col;
if ((row == 1) && (col == 10)) {
sym_pressed = false;
}
if ((row == 2) && (col == 7)) {
shift_pressed = false;
}
}
if ((!sym_pressed && !shift_pressed) && !cap_toggle_armed) {
cap_toggle_armed = true;
}
if (anykey_pressed) {
makeBacklightImpulse();
}
}
}
bool TpagerKeyboard::start(lv_display_t* display) {
backlightOkay = initBacklight(BACKLIGHT, 30000, LEDC_TIMER_0, LEDC_CHANNEL_1);
initEncoder();
keypad->init(KB_ROWS, KB_COLS);
gpio_input_enable(ENCODER_ENTER);
assert(inputTimer == nullptr);
inputTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, [this] {
processKeyboard();
});
assert(backlightImpulseTimer == nullptr);
backlightImpulseTimer = std::make_unique<tt::Timer>(tt::Timer::Type::Periodic, [this] {
processBacklightImpuse();
});
kbHandle = lv_indev_create();
lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(kbHandle, &keyboard_read_callback);
lv_indev_set_display(kbHandle, display);
lv_indev_set_user_data(kbHandle, this);
encHandle = lv_indev_create();
lv_indev_set_type(encHandle, LV_INDEV_TYPE_ENCODER);
lv_indev_set_read_cb(encHandle, &encoder_read_callback);
lv_indev_set_display(encHandle, display);
lv_indev_set_user_data(encHandle, this);
inputTimer->start(20 / portTICK_PERIOD_MS);
backlightImpulseTimer->start(50 / portTICK_PERIOD_MS);
return true;
}
bool TpagerKeyboard::stop() {
assert(inputTimer);
inputTimer->stop();
inputTimer = nullptr;
assert(backlightImpulseTimer);
backlightImpulseTimer->stop();
backlightImpulseTimer = nullptr;
lv_indev_delete(kbHandle);
kbHandle = nullptr;
lv_indev_delete(encHandle);
encHandle = nullptr;
return true;
}
bool TpagerKeyboard::isAttached() const {
return tt::hal::i2c::masterHasDeviceAtAddress(keypad->getPort(), keypad->getAddress(), 100);
}
void TpagerKeyboard::initEncoder(void) {
const int low_limit = -127;
const int high_limit = 126;
// Accum. count makes it that over- and underflows are automatically compensated.
// Prerequisite: watchpoints at low and high limit
pcnt_unit_config_t unit_config = {
.low_limit = low_limit,
.high_limit = high_limit,
.flags = {.accum_count = 1},
};
if (pcnt_new_unit(&unit_config, &encPcntUnit) != ESP_OK) {
TT_LOG_E(TAG, "Pulsecounter intialization failed");
}
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = 5000,
};
if (pcnt_unit_set_glitch_filter(encPcntUnit, &filter_config) != ESP_OK) {
TT_LOG_E(TAG, "Pulsecounter glitch filter config failed");
}
pcnt_chan_config_t chan_1_config = {
.edge_gpio_num = ENCODER_A,
.level_gpio_num = ENCODER_B,
};
pcnt_chan_config_t chan_2_config = {
.edge_gpio_num = ENCODER_B,
.level_gpio_num = ENCODER_A,
};
pcnt_channel_handle_t pcnt_chan_1 = NULL;
pcnt_channel_handle_t pcnt_chan_2 = NULL;
if ((pcnt_new_channel(encPcntUnit, &chan_1_config, &pcnt_chan_1) != ESP_OK) ||
(pcnt_new_channel(encPcntUnit, &chan_2_config, &pcnt_chan_2) != ESP_OK)) {
TT_LOG_E(TAG, "Pulsecounter channel config failed");
}
// second argument is rising edge, third argument is falling edge
if ((pcnt_channel_set_edge_action(pcnt_chan_1, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE) != ESP_OK) ||
(pcnt_channel_set_edge_action(pcnt_chan_2, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE) != ESP_OK)) {
TT_LOG_E(TAG, "Pulsecounter edge action config failed");
}
// second argument is low level, third argument is high level
if ((pcnt_channel_set_level_action(pcnt_chan_1, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK) ||
(pcnt_channel_set_level_action(pcnt_chan_2, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE) != ESP_OK)) {
TT_LOG_E(TAG, "Pulsecounter level action config failed");
}
if ((pcnt_unit_add_watch_point(encPcntUnit, low_limit) != ESP_OK) ||
(pcnt_unit_add_watch_point(encPcntUnit, high_limit) != ESP_OK)) {
TT_LOG_E(TAG, "Pulsecounter watch point config failed");
}
if (pcnt_unit_enable(encPcntUnit) != ESP_OK) {
TT_LOG_E(TAG, "Pulsecounter could not be enabled");
}
if (pcnt_unit_clear_count(encPcntUnit) != ESP_OK) {
TT_LOG_E(TAG, "Pulsecounter could not be cleared");
}
if (pcnt_unit_start(encPcntUnit) != ESP_OK) {
TT_LOG_E(TAG, "Pulsecounter could not be started");
}
}
int TpagerKeyboard::getEncoderPulses() {
int pulses = 0;
pcnt_unit_get_count(encPcntUnit, &pulses);
return pulses;
}
bool TpagerKeyboard::initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel) {
backlightPin = pin;
backlightTimer = timer;
backlightChannel = channel;
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = backlightTimer,
.freq_hz = frequencyHz,
.clk_cfg = LEDC_AUTO_CLK,
.deconfigure = false
};
if (ledc_timer_config(&ledc_timer) != ESP_OK) {
TT_LOG_E(TAG, "Backlight timer config failed");
return false;
}
ledc_channel_config_t ledc_channel = {
.gpio_num = backlightPin,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = backlightChannel,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = backlightTimer,
.duty = 0,
.hpoint = 0,
.sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD,
.flags = {
.output_invert = 0
}
};
if (ledc_channel_config(&ledc_channel) != ESP_OK) {
TT_LOG_E(TAG, "Backlight channel config failed");
}
return true;
}
bool TpagerKeyboard::setBacklightDuty(uint8_t duty) {
if (!backlightOkay) {
TT_LOG_E(TAG, "Backlight not ready");
return false;
}
return (ledc_set_duty(LEDC_LOW_SPEED_MODE, backlightChannel, duty) == ESP_OK) &&
(ledc_update_duty(LEDC_LOW_SPEED_MODE, backlightChannel) == ESP_OK);
}
void TpagerKeyboard::makeBacklightImpulse() {
backlightImpulseDuty = 255;
setBacklightDuty(backlightImpulseDuty);
}
void TpagerKeyboard::processBacklightImpuse() {
if (backlightImpulseDuty > 64) {
backlightImpulseDuty--;
setBacklightDuty(backlightImpulseDuty);
}
}
extern std::shared_ptr<Tca8418> tca8418;
std::shared_ptr<tt::hal::keyboard::KeyboardDevice> createKeyboard() {
keyboardMsg = xQueueCreate(20, sizeof(char));
return std::make_shared<TpagerKeyboard>(tca8418);
}

View File

@ -0,0 +1,54 @@
#pragma once
#include <Tactility/TactilityCore.h>
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include <Tca8418.h>
#include <driver/gpio.h>
#include <driver/ledc.h>
#include <driver/pulse_cnt.h>
#include <Tactility/Timer.h>
class TpagerKeyboard : public tt::hal::keyboard::KeyboardDevice {
private:
lv_indev_t* _Nullable kbHandle = nullptr;
lv_indev_t* _Nullable encHandle = nullptr;
pcnt_unit_handle_t encPcntUnit = nullptr;
gpio_num_t backlightPin = GPIO_NUM_NC;
ledc_timer_t backlightTimer;
ledc_channel_t backlightChannel;
bool backlightOkay = false;
int backlightImpulseDuty = 0;
std::shared_ptr<Tca8418> keypad;
std::unique_ptr<tt::Timer> inputTimer;
std::unique_ptr<tt::Timer> backlightImpulseTimer;
void initEncoder(void);
bool initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel);
void processKeyboard();
void processBacklightImpuse();
public:
TpagerKeyboard(std::shared_ptr<Tca8418> tca) : keypad(std::move(tca)) {}
~TpagerKeyboard() {}
std::string getName() const final { return "T-Lora Pager Keyboard"; }
std::string getDescription() const final { return "I2C keyboard with encoder"; }
bool start(lv_display_t* display) override;
bool stop() override;
bool isAttached() const override;
lv_indev_t* _Nullable getLvglIndev() override { return kbHandle; }
int getEncoderPulses();
bool setBacklightDuty(uint8_t duty);
void makeBacklightImpulse();
};
std::shared_ptr<tt::hal::keyboard::KeyboardDevice> createKeyboard();

View File

@ -0,0 +1,90 @@
#include "TpagerPower.h"
#include <Tactility/Log.h>
#define TAG "power"
#define TPAGER_GAUGE_I2C_BUS_HANDLE I2C_NUM_0
/*
TpagerPower::TpagerPower() : gauge(TPAGER_GAUGE_I2C_BUS_HANDLE) {
gauge->configureCapacity(1500, 1500);
}*/
TpagerPower::~TpagerPower() {}
bool TpagerPower::supportsMetric(MetricType type) const {
switch (type) {
using enum MetricType;
case IsCharging:
case Current:
case BatteryVoltage:
case ChargeLevel:
return true;
default:
return false;
}
return false; // Safety guard for when new enum values are introduced
}
bool TpagerPower::getMetric(MetricType type, MetricData& data) {
/* IsCharging, // bool
Current, // int32_t, mAh - battery current: either during charging (positive value) or discharging (negative value)
BatteryVoltage, // uint32_t, mV
ChargeLevel, // uint8_t [0, 100]
*/
uint16_t u16 = 0;
int16_t s16 = 0;
switch (type) {
using enum MetricType;
case IsCharging:
Bq27220::BatteryStatus status;
if (gauge->getBatteryStatus(status)) {
data.valueAsBool = !status.reg.DSG;
return true;
}
return false;
break;
case Current:
if (gauge->getCurrent(s16)) {
data.valueAsInt32 = s16;
return true;
} else {
return false;
}
break;
case BatteryVoltage:
if (gauge->getVoltage(u16)) {
data.valueAsUint32 = u16;
return true;
} else {
return false;
}
break;
case ChargeLevel:
if (gauge->getStateOfCharge(u16)) {
data.valueAsUint8 = u16;
return true;
} else {
return false;
}
break;
default:
return false;
break;
}
return false; // Safety guard for when new enum values are introduced
}
static std::shared_ptr<PowerDevice> power;
extern std::shared_ptr<Bq27220> bq27220;
std::shared_ptr<PowerDevice> tpager_get_power() {
if (power == nullptr) {
power = std::make_shared<TpagerPower>(bq27220);
}
return power;
}

View File

@ -0,0 +1,26 @@
#pragma once
#include "Tactility/hal/power/PowerDevice.h"
#include <Bq27220.h>
#include <memory>
using tt::hal::power::PowerDevice;
class TpagerPower : public PowerDevice {
std::shared_ptr<Bq27220> gauge;
public:
TpagerPower(std::shared_ptr<Bq27220> bq) : gauge(std::move(bq)) {}
~TpagerPower();
std::string getName() const final { return "T-LoRa Pager Power measument"; }
std::string getDescription() const final { return "Power measurement interface via I2C fuel gauge"; }
bool supportsMetric(MetricType type) const override;
bool getMetric(MetricType type, MetricData& data) override;
private:
};
std::shared_ptr<PowerDevice> tpager_get_power();

View File

@ -0,0 +1,31 @@
#include "TpagerSdCard.h"
#include <Tactility/hal/sdcard/SpiSdCardDevice.h>
#include <Tactility/lvgl/LvglSync.h>
#include <esp_vfs_fat.h>
using tt::hal::sdcard::SpiSdCardDevice;
#define TPAGER_SDCARD_PIN_CS GPIO_NUM_21
#define TPAGER_LCD_PIN_CS GPIO_NUM_38
#define TPAGER_RADIO_PIN_CS GPIO_NUM_36
std::shared_ptr<SdCardDevice> createTpagerSdCard() {
auto* configuration = new SpiSdCardDevice::Config(
TPAGER_SDCARD_PIN_CS,
GPIO_NUM_NC,
GPIO_NUM_NC,
GPIO_NUM_NC,
SdCardDevice::MountBehaviour::AtBoot,
tt::lvgl::getSyncLock(),
{TPAGER_RADIO_PIN_CS,
TPAGER_LCD_PIN_CS}
);
auto* sdcard = (SdCardDevice*)new SpiSdCardDevice(
std::unique_ptr<SpiSdCardDevice::Config>(configuration)
);
return std::shared_ptr<SdCardDevice>(sdcard);
}

View File

@ -0,0 +1,7 @@
#pragma once
#include "Tactility/hal/sdcard/SdCardDevice.h"
using tt::hal::sdcard::SdCardDevice;
std::shared_ptr<SdCardDevice> createTpagerSdCard();

View File

@ -47,6 +47,8 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50) set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50)
elseif (board_id STREQUAL "lilygo-tdeck") elseif (board_id STREQUAL "lilygo-tdeck")
set(TACTILITY_BOARD_PROJECT LilygoTdeck) set(TACTILITY_BOARD_PROJECT LilygoTdeck)
elseif (board_id STREQUAL "lilygo-tlora-pager")
set(TACTILITY_BOARD_PROJECT LilygoTLoraPager)
elseif (board_id STREQUAL "m5stack-core2") elseif (board_id STREQUAL "m5stack-core2")
set(TACTILITY_BOARD_PROJECT M5stackCore2) set(TACTILITY_BOARD_PROJECT M5stackCore2)
elseif (board_id STREQUAL "m5stack-cores3") elseif (board_id STREQUAL "m5stack-cores3")

View File

@ -53,6 +53,12 @@ Website: https://github.com/meshtastic/firmware
License: [GPL v3.0](https://github.com/meshtastic/firmware/blob/master/LICENSE) License: [GPL v3.0](https://github.com/meshtastic/firmware/blob/master/LICENSE)
### BQ27220 Driver
Website: https://github.com/Xinyuan-LilyGO/T-Echo/blob/main/LICENSE
License: [MIT](https://github.com/Xinyuan-LilyGO/T-Echo/blob/main/LICENSE)
### Other Components ### Other Components
See `/components` for the respective projects and their licenses. See `/components` for the respective projects and their licenses.

View File

@ -8,10 +8,10 @@
* https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads * https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads
*/ */
namespace registers { namespace registers {
static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl
static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control
static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status
static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status
} // namespace registers } // namespace registers
bool Bq24295::readChargeTermination(uint8_t& out) const { bool Bq24295::readChargeTermination(uint8_t& out) const {

View File

@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility
)

View File

@ -0,0 +1,6 @@
# BQ27220
Power management: Single-Cell CEDV Fuel Gauge
[Datasheet](https://www.ti.com/lit/gpn/bq27220)
[User Guide](https://www.ti.com/lit/pdf/sluubd4)

View File

@ -0,0 +1,370 @@
#include "Bq27220.h"
#include <Tactility/Log.h>
#include "esp_sleep.h"
#define TAG "bq27220"
#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a)))
static uint8_t highByte(const uint16_t word) { return (word >> 8) & 0xFF; }
static uint8_t lowByte(const uint16_t word) { return word & 0xFF; }
static constexpr void swapEndianess(uint16_t &word) { word = (lowByte(word) << 8) | highByte(word); }
namespace registers {
static const uint16_t SUBCMD_CTRL_STATUS = 0x0000U;
static const uint16_t SUBCMD_DEVICE_NUMBER = 0x0001U;
static const uint16_t SUBCMD_FW_VERSION = 0x0002U;
static const uint16_t SUBCMD_HW_VERSION = 0x0003U;
static const uint16_t SUBCMD_BOARD_OFFSET = 0x0009U;
static const uint16_t SUBCMD_CC_OFFSET = 0x000AU;
static const uint16_t SUBCMD_CC_OFFSET_SAVE = 0x000BU;
static const uint16_t SUBCMD_OCV_CMD = 0x000CU;
static const uint16_t SUBCMD_BAT_INSERT = 0x000DU;
static const uint16_t SUBCMD_BAT_REMOVE = 0x000EU;
static const uint16_t SUBCMD_SET_SNOOZE = 0x0013U;
static const uint16_t SUBCMD_CLEAR_SNOOZE = 0x0014U;
static const uint16_t SUBCMD_SET_PROFILE_1 = 0x0015U;
static const uint16_t SUBCMD_SET_PROFILE_2 = 0x0016U;
static const uint16_t SUBCMD_SET_PROFILE_3 = 0x0017U;
static const uint16_t SUBCMD_SET_PROFILE_4 = 0x0018U;
static const uint16_t SUBCMD_SET_PROFILE_5 = 0x0019U;
static const uint16_t SUBCMD_SET_PROFILE_6 = 0x001AU;
static const uint16_t SUBCMD_CAL_TOGGLE = 0x002DU;
static const uint16_t SUBCMD_SEALED = 0x0030U;
static const uint16_t SUBCMD_RESET = 0x0041U;
static const uint16_t SUBCMD_EXIT_CAL = 0x0080U;
static const uint16_t SUBCMD_ENTER_CAL = 0x0081U;
static const uint16_t SUBCMD_ENTER_CFG_UPDATE = 0x0090U;
static const uint16_t SUBCMD_EXIT_CFG_UPDATE_REINIT = 0x0091U;
static const uint16_t SUBCMD_EXIT_CFG_UPDATE = 0x0092U;
static const uint16_t SUBCMD_RETURN_TO_ROM = 0x0F00U;
static const uint8_t CMD_CONTROL = 0x00U;
static const uint8_t CMD_AT_RATE = 0x02U;
static const uint8_t CMD_AT_RATE_TIME_TO_EMPTY = 0x04U;
static const uint8_t CMD_TEMPERATURE = 0x06U;
static const uint8_t CMD_VOLTAGE = 0x08U;
static const uint8_t CMD_BATTERY_STATUS = 0x0AU;
static const uint8_t CMD_CURRENT = 0x0CU;
static const uint8_t CMD_REMAINING_CAPACITY = 0x10U;
static const uint8_t CMD_FULL_CHARGE_CAPACITY = 0x12U;
static const uint8_t CMD_AVG_CURRENT = 0x14U;
static const uint8_t CMD_TIME_TO_EMPTY = 0x16U;
static const uint8_t CMD_TIME_TO_FULL = 0x18U;
static const uint8_t CMD_STANDBY_CURRENT = 0x1AU;
static const uint8_t CMD_STANDBY_TIME_TO_EMPTY = 0x1CU;
static const uint8_t CMD_MAX_LOAD_CURRENT = 0x1EU;
static const uint8_t CMD_MAX_LOAD_TIME_TO_EMPTY = 0x20U;
static const uint8_t CMD_RAW_COULOMB_COUNT = 0x22U;
static const uint8_t CMD_AVG_POWER = 0x24U;
static const uint8_t CMD_INTERNAL_TEMPERATURE = 0x28U;
static const uint8_t CMD_CYCLE_COUNT = 0x2AU;
static const uint8_t CMD_STATE_OF_CHARGE = 0x2CU;
static const uint8_t CMD_STATE_OF_HEALTH = 0x2EU;
static const uint8_t CMD_CHARGE_VOLTAGE = 0x30U;
static const uint8_t CMD_CHARGE_CURRENT = 0x32U;
static const uint8_t CMD_BTP_DISCHARGE_SET = 0x34U;
static const uint8_t CMD_BTP_CHARGE_SET = 0x36U;
static const uint8_t CMD_OPERATION_STATUS = 0x3AU;
static const uint8_t CMD_DESIGN_CAPACITY = 0x3CU;
static const uint8_t CMD_SELECT_SUBCLASS = 0x3EU;
static const uint8_t CMD_MAC_DATA = 0x40U;
static const uint8_t CMD_MAC_DATA_SUM = 0x60U;
static const uint8_t CMD_MAC_DATA_LEN = 0x61U;
static const uint8_t CMD_ANALOG_COUNT = 0x79U;
static const uint8_t CMD_RAW_CURRENT = 0x7AU;
static const uint8_t CMD_RAW_VOLTAGE = 0x7CU;
static const uint8_t CMD_RAW_INTERNAL_TEMPERATURE = 0x7EU;
static const uint8_t MAC_BUFFER_START = 0x40U;
static const uint8_t MAC_BUFFER_END = 0x5FU;
static const uint8_t MAC_DATA_SUM = 0x60U;
static const uint8_t MAC_DATA_LEN = 0x61U;
static const uint8_t ROM_START = 0x3EU;
static const uint16_t ROM_FULL_CHARGE_CAPACITY = 0x929DU;
static const uint16_t ROM_DESIGN_CAPACITY = 0x929FU;
static const uint16_t ROM_OPERATION_CONFIG_A = 0x9206U;
static const uint16_t ROM_OPERATION_CONFIG_B = 0x9208U;
} // namespace registers
bool Bq27220::configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity) {
return performConfigUpdate([this, designCapacity, fullChargeCapacity]() {
// Set the design capacity
if (!writeConfig16(registers::ROM_DESIGN_CAPACITY, designCapacity)) {
TT_LOG_E(TAG, "Failed to set design capacity!");
return false;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
// Set full charge capacity
if (!writeConfig16(registers::ROM_FULL_CHARGE_CAPACITY, fullChargeCapacity)) {
TT_LOG_E(TAG, "Failed to set full charge capacity!");
return false;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
return true;
});
}
bool Bq27220::getVoltage(uint16_t &value) {
if (readRegister16(registers::CMD_VOLTAGE, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getCurrent(int16_t &value) {
uint16_t u16 = 0;
if (readRegister16(registers::CMD_CURRENT, u16)) {
swapEndianess(u16);
value = (int16_t)u16;
return true;
}
return false;
}
bool Bq27220::getBatteryStatus(Bq27220::BatteryStatus &batt_sta) {
if (readRegister16(registers::CMD_BATTERY_STATUS, batt_sta.full)) {
swapEndianess(batt_sta.full);
return true;
}
return false;
}
bool Bq27220::getOperationStatus(OperationStatus &oper_sta) {
if (readRegister16(registers::CMD_OPERATION_STATUS, oper_sta.full)) {
swapEndianess(oper_sta.full);
return true;
}
return false;
}
bool Bq27220::getTemperature(uint16_t &value) {
if (readRegister16(registers::CMD_TEMPERATURE, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getFullChargeCapacity(uint16_t &value) {
if (readRegister16(registers::CMD_FULL_CHARGE_CAPACITY, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getDesignCapacity(uint16_t &value) {
if (readRegister16(registers::CMD_DESIGN_CAPACITY, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getRemainingCapacity(uint16_t &value) {
if (readRegister16(registers::CMD_REMAINING_CAPACITY, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getStateOfCharge(uint16_t &value) {
if (readRegister16(registers::CMD_STATE_OF_CHARGE, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getStateOfHealth(uint16_t &value) {
if (readRegister16(registers::CMD_STATE_OF_HEALTH, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::getChargeVoltageMax(uint16_t &value) {
if (readRegister16(registers::CMD_CHARGE_VOLTAGE, value)) {
swapEndianess(value);
return true;
}
return false;
}
bool Bq27220::unsealDevice() {
uint8_t cmd1[] = {0x00, 0x14, 0x04};
if (!write(cmd1, ARRAYSIZE(cmd1))) {
return false;
}
vTaskDelay(50 / portTICK_PERIOD_MS);
uint8_t cmd2[] = {0x00, 0x72, 0x36};
if (!write(cmd2, ARRAYSIZE(cmd2))) {
return false;
}
vTaskDelay(50 / portTICK_PERIOD_MS);
return true;
}
bool Bq27220::unsealFullAccess()
{
uint8_t buffer[3];
buffer[0] = 0x00;
buffer[1] = lowByte((accessKey >> 24));
buffer[2] = lowByte((accessKey >> 16));
if (!write(buffer, ARRAYSIZE(buffer))) {
return false;
}
vTaskDelay(50 / portTICK_PERIOD_MS);
buffer[1] = lowByte((accessKey >> 8));
buffer[2] = lowByte((accessKey));
if (!write(buffer, ARRAYSIZE(buffer))) {
return false;
}
vTaskDelay(50 / portTICK_PERIOD_MS);
return true;
}
bool Bq27220::exitSealMode() {
return sendSubCommand(registers::SUBCMD_SEALED);
}
bool Bq27220::sendSubCommand(uint16_t subCmd, bool waitConfirm)
{
uint8_t buffer[3];
buffer[0] = 0x00;
buffer[1] = lowByte(subCmd);
buffer[2] = highByte(subCmd);
if (!write(buffer, ARRAYSIZE(buffer))) {
return false;
}
if (!waitConfirm) {
vTaskDelay(10 / portTICK_PERIOD_MS);
return true;
}
constexpr uint8_t statusReg = 0x00;
int waitCount = 20;
vTaskDelay(10 / portTICK_PERIOD_MS);
while (waitCount--) {
writeRead(&statusReg, 1, buffer, 2);
uint16_t *value = reinterpret_cast<uint16_t *>(buffer);
if (*value == 0xFFA5) {
return true;
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
TT_LOG_E(TAG, "Subcommand x%X failed!", subCmd);
return false;
}
bool Bq27220::writeConfig16(uint16_t address, uint16_t value) {
constexpr uint8_t fixedDataLength = 0x06;
const uint8_t msbAccessValue = highByte(address);
const uint8_t lsbAccessValue = lowByte(address);
// Write to access the MSB of Capacity
writeRegister8(registers::ROM_START, msbAccessValue);
// Write to access the LSB of Capacity
writeRegister8(registers::ROM_START + 1, lsbAccessValue);
// Write two Capacity bytes starting from 0x40
uint8_t valueMsb = highByte(value);
uint8_t valueLsb = lowByte(value);
uint8_t configRaw[] = {valueMsb, valueLsb};
writeRegister(registers::MAC_BUFFER_START, configRaw, 2);
// Calculate new checksum
uint8_t checksum = 0xFF - ((msbAccessValue + lsbAccessValue + valueMsb + valueLsb) & 0xFF);
// Write new checksum (0x60)
writeRegister8(registers::MAC_DATA_SUM, checksum);
// Write the block length
writeRegister8(registers::MAC_DATA_LEN, fixedDataLength);
return true;
}
bool Bq27220::configPreamble(bool &isSealed) {
int timeout = 0;
OperationStatus status;
// Check access settings
if(!getOperationStatus(status)) {
TT_LOG_E(TAG, "Cannot read initial operation status!");
return false;
}
if (status.reg.SEC == OperationStatusSecSealed) {
isSealed = true;
if (!unsealDevice()) {
TT_LOG_E(TAG, "Unsealing device failure!");
return false;
}
}
if (status.reg.SEC != OperationStatusSecFull) {
if (!unsealFullAccess()) {
TT_LOG_E(TAG, "Unsealing full access failure!");
return false;
}
}
// Send ENTER_CFG_UPDATE command (0x0090)
if (!sendSubCommand(registers::SUBCMD_ENTER_CFG_UPDATE)) {
TT_LOG_E(TAG, "Config Update Subcommand failure!");
}
// Confirm CFUPDATE mode by polling the OperationStatus() register until Bit 2 is set.
bool isConfigUpdate = false;
for (timeout = 30; timeout; --timeout) {
getOperationStatus(status);
if (status.reg.CFGUPDATE) {
isConfigUpdate = true;
break;
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
if (!isConfigUpdate) {
TT_LOG_E(TAG, "Update Mode timeout, maybe the access key for full permissions is invalid!");
return false;
}
return true;
}
bool Bq27220::configEpilouge(const bool isSealed) {
int timeout = 0;
OperationStatus status;
// Exit CFUPDATE mode by sending the EXIT_CFG_UPDATE_REINIT (0x0091) or EXIT_CFG_UPDATE (0x0092) command
sendSubCommand(registers::SUBCMD_EXIT_CFG_UPDATE_REINIT);
vTaskDelay(10 / portTICK_PERIOD_MS);
// Confirm that CFUPDATE mode has been exited by polling the OperationStatus() register until bit 2 is cleared
for (timeout = 60; timeout; --timeout) {
getOperationStatus(status);
if (!status.reg.CFGUPDATE) {
break;
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
if (timeout == 0) {
TT_LOG_E(TAG, "Timed out waiting to exit update mode.");
return false;
}
// If the device was previously in SEALED state, return to SEALED mode by sending the Control(0x0030) subcommand
if (isSealed) {
TT_LOG_D(TAG, "Restore Safe Mode!");
exitSealMode();
}
return true;
}

View File

@ -0,0 +1,107 @@
#pragma once
#include <Tactility/hal/i2c/I2cDevice.h>
#define BQ27220_ADDRESS 0x55
class Bq27220 final : public tt::hal::i2c::I2cDevice {
private:
uint32_t accessKey;
bool unsealDevice();
bool unsealFullAccess();
bool exitSealMode();
bool sendSubCommand(uint16_t subCmd, bool waitConfirm = false);
bool writeConfig16(uint16_t address, uint16_t value);
bool configPreamble(bool &isSealed);
bool configEpilouge(const bool isSealed);
template<typename T>
bool performConfigUpdate(T configUpdateFunc)
{
bool isSealed = false;
if (!configPreamble(isSealed)) {
return false;
}
bool result = configUpdateFunc();
configEpilouge(isSealed);
return result;
}
public:
// Register structures lifted from
// https://github.com/Xinyuan-LilyGO/T-Deck-Pro/blob/master/lib/BQ27220/bq27220.h
// Copyright (c) 2025 Liygo / Shenzhen Xinyuan Electronic Technology Co., Ltd
union BatteryStatus {
struct
{
// Low byte, Low bit first
uint16_t DSG : 1; /**< The device is in DISCHARGE */
uint16_t SYSDWN : 1; /**< System down bit indicating the system should shut down */
uint16_t TDA : 1; /**< Terminate Discharge Alarm */
uint16_t BATTPRES : 1; /**< Battery Present detected */
uint16_t AUTH_GD : 1; /**< Detect inserted battery */
uint16_t OCVGD : 1; /**< Good OCV measurement taken */
uint16_t TCA : 1; /**< Terminate Charge Alarm */
uint16_t RSVD : 1; /**< Reserved */
// High byte, Low bit first
uint16_t CHGING : 1; /**< Charge inhibit */
uint16_t FC : 1; /**< Full-charged is detected */
uint16_t OTD : 1; /**< Overtemperature in discharge condition is detected */
uint16_t OTC : 1; /**< Overtemperature in charge condition is detected */
uint16_t SLEEP : 1; /**< Device is operating in SLEEP mode when set */
uint16_t OCVFALL : 1; /**< Status bit indicating that the OCV reading failed due to current */
uint16_t OCVCOMP : 1; /**< An OCV measurement update is complete */
uint16_t FD : 1; /**< Full-discharge is detected */
} reg;
uint16_t full;
};
enum OperationStatusSec {
OperationStatusSecSealed = 0b11,
OperationStatusSecUnsealed = 0b10,
OperationStatusSecFull = 0b01,
};
union OperationStatus {
struct
{
// Low byte, Low bit first
bool CALMD : 1; /**< Calibration mode enabled */
uint8_t SEC : 2; /**< Current security access */
bool EDV2 : 1; /**< EDV2 threshold exceeded */
bool VDQ : 1; /**< Indicates if Current discharge cycle is NOT qualified or qualified for an FCC updated */
bool INITCOMP : 1; /**< gauge initialization is complete */
bool SMTH : 1; /**< RemainingCapacity is scaled by smooth engine */
bool BTPINT : 1; /**< BTP threshold has been crossed */
// High byte, Low bit first
uint8_t RSVD1 : 2; /**< Reserved */
bool CFGUPDATE : 1; /**< Gauge is in CONFIG UPDATE mode */
uint8_t RSVD0 : 5; /**< Reserved */
} reg;
uint16_t full;
};
std::string getName() const final { return "BQ27220"; }
std::string getDescription() const final { return "I2C-controlled CEDV battery fuel gauge"; }
explicit Bq27220(i2c_port_t port) : I2cDevice(port, BQ27220_ADDRESS), accessKey(0xFFFFFFFF) {}
bool configureCapacity(uint16_t designCapacity, uint16_t fullChargeCapacity);
bool getVoltage(uint16_t &value);
bool getCurrent(int16_t &value);
bool getBatteryStatus(BatteryStatus &batt_sta);
bool getOperationStatus(OperationStatus &oper_sta);
bool getTemperature(uint16_t &value);
bool getFullChargeCapacity(uint16_t &value);
bool getDesignCapacity(uint16_t &value);
bool getRemainingCapacity(uint16_t &value);
bool getStateOfCharge(uint16_t &value);
bool getStateOfHealth(uint16_t &value);
bool getChargeVoltageMax(uint16_t &value);
};

View File

@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd_st7796 driver
)

3
Drivers/ST7796/README.md Normal file
View File

@ -0,0 +1,3 @@
# ST7796
A basic ESP32 LVGL driver for ST7796 displays.

View File

@ -0,0 +1,210 @@
#include "St7796Display.h"
#include <Tactility/Log.h>
#include <esp_lcd_panel_commands.h>
#include <esp_lcd_panel_dev.h>
#include <esp_lcd_st7796.h>
#include <esp_lvgl_port.h>
#define TAG "st7796"
bool St7796Display::start() {
TT_LOG_I(TAG, "Starting");
const esp_lcd_panel_io_spi_config_t panel_io_config = {
.cs_gpio_num = configuration->csPin,
.dc_gpio_num = configuration->dcPin,
.spi_mode = 0,
.pclk_hz = configuration->pixelClockFrequency,
.trans_queue_depth = configuration->transactionQueueDepth,
.on_color_trans_done = nullptr,
.user_ctx = nullptr,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.flags = {
.dc_high_on_cmd = 0,
.dc_low_on_data = 0,
.dc_low_on_param = 0,
.octal_mode = 0,
.quad_mode = 0,
.sio_mode = 0,
.lsb_first = 0,
.cs_high_active = 0
}
};
if (esp_lcd_new_panel_io_spi(configuration->spiBusHandle, &panel_io_config, &ioHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to create panel");
return false;
}
static const st7796_lcd_init_cmd_t lcd_init_cmds[] = {
{0x01, (uint8_t[]) {0x00}, 0, 120},
{0x11, (uint8_t[]) {0x00}, 0, 120},
{0xF0, (uint8_t[]) {0xC3}, 1, 0},
{0xF0, (uint8_t[]) {0xC3}, 1, 0},
{0xF0, (uint8_t[]) {0x96}, 1, 0},
{0x36, (uint8_t[]) {0x58}, 1, 0},
{0x3A, (uint8_t[]) {0x55}, 1, 0},
{0xB4, (uint8_t[]) {0x01}, 1, 0},
{0xB6, (uint8_t[]) {0x80, 0x02, 0x3B}, 3, 0},
{0xE8, (uint8_t[]) {0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0},
{0xC1, (uint8_t[]) {0x06}, 1, 0},
{0xC2, (uint8_t[]) {0xA7}, 1, 0},
{0xC5, (uint8_t[]) {0x18}, 1, 0},
{0xE0, (uint8_t[]) {0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B}, 15, 0},
{0xE1, (uint8_t[]) {0xE0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B}, 15, 120},
{0xF0, (uint8_t[]) {0x3C}, 1, 0},
{0xF0, (uint8_t[]) {0x69}, 1, 0},
{0x21, (uint8_t[]) {0x00}, 1, 0},
{0x29, (uint8_t[]) {0x00}, 1, 0},
};
st7796_vendor_config_t vendor_config = {
// Uncomment these lines if use custom initialization commands
.init_cmds = lcd_init_cmds,
.init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t),
};
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = configuration->resetPin, // Set to -1 if not use
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
.color_space = ESP_LCD_COLOR_SPACE_RGB,
#else
.color_space = LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
#endif
.bits_per_pixel = 16,
.vendor_config = &vendor_config
};
/*
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = configuration->resetPin,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = 16,
.flags = {
.reset_active_high = false
},
.vendor_config = nullptr
};
*/
if (esp_lcd_new_panel_st7796(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to create panel");
return false;
}
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to reset panel");
return false;
}
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
TT_LOG_E(TAG, "Failed to init panel");
return false;
}
if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set panel to invert");
return false;
}
if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) {
TT_LOG_E(TAG, "Failed to swap XY ");
return false;
}
if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set panel to mirror");
return false;
}
if (esp_lcd_panel_set_gap(panelHandle, configuration->gapX, configuration->gapY) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set panel gap");
return false;
}
if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) {
TT_LOG_E(TAG, "Failed to turn display on");
return false;
}
uint32_t buffer_size;
if (configuration->bufferSize == 0) {
buffer_size = configuration->horizontalResolution * configuration->verticalResolution / 10;
} else {
buffer_size = configuration->bufferSize;
}
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = ioHandle,
.panel_handle = panelHandle,
.control_handle = nullptr,
.buffer_size = buffer_size,
.double_buffer = false,
.trans_size = 0,
.hres = configuration->horizontalResolution,
.vres = configuration->verticalResolution,
.monochrome = false,
.rotation = {
.swap_xy = configuration->swapXY,
.mirror_x = configuration->mirrorX,
.mirror_y = configuration->mirrorY,
},
.color_format = LV_COLOR_FORMAT_NATIVE,
.flags = {.buff_dma = true, .buff_spiram = false, .sw_rotate = false, .swap_bytes = true, .full_refresh = false, .direct_mode = false}
};
displayHandle = lvgl_port_add_disp(&disp_cfg);
TT_LOG_I(TAG, "Finished");
return displayHandle != nullptr;
}
bool St7796Display::stop() {
assert(displayHandle != nullptr);
lvgl_port_remove_disp(displayHandle);
if (esp_lcd_panel_del(panelHandle) != ESP_OK) {
return false;
}
if (esp_lcd_panel_io_del(ioHandle) != ESP_OK) {
return false;
}
displayHandle = nullptr;
return true;
}
void St7796Display::setGammaCurve(uint8_t index) {
uint8_t gamma_curve;
switch (index) {
case 0:
gamma_curve = 0x01;
break;
case 1:
gamma_curve = 0x04;
break;
case 2:
gamma_curve = 0x02;
break;
case 3:
gamma_curve = 0x08;
break;
default:
return;
}
const uint8_t param[] = {
gamma_curve
};
/*if (esp_lcd_panel_io_tx_param(ioHandle , LCD_CMD_GAMSET, param, 1) != ESP_OK) {
TT_LOG_E(TAG, "Failed to set gamma");
}*/
}

View File

@ -0,0 +1,103 @@
#pragma once
#include "Tactility/hal/display/DisplayDevice.h"
#include <driver/gpio.h>
#include <driver/spi_common.h>
#include <esp_lcd_panel_io.h>
#include <esp_lcd_types.h>
#include <functional>
#include <lvgl.h>
class St7796Display final : public tt::hal::display::DisplayDevice {
public:
class Configuration {
public:
Configuration(
esp_lcd_spi_bus_handle_t spi_bus_handle,
gpio_num_t csPin,
gpio_num_t dcPin,
unsigned int horizontalResolution,
unsigned int verticalResolution,
std::shared_ptr<tt::hal::touch::TouchDevice> touch,
bool swapXY = false,
bool mirrorX = false,
bool mirrorY = false,
bool invertColor = false,
unsigned int gapX = 0,
unsigned int gapY = 0,
uint32_t bufferSize = 0 // Size in pixel count. 0 means default, which is 1/10 of the screen size
) : spiBusHandle(spi_bus_handle),
csPin(csPin),
dcPin(dcPin),
horizontalResolution(horizontalResolution),
verticalResolution(verticalResolution),
swapXY(swapXY),
mirrorX(mirrorX),
mirrorY(mirrorY),
invertColor(invertColor),
gapX(gapX),
gapY(gapY),
bufferSize(bufferSize),
touch(std::move(touch)) {}
esp_lcd_spi_bus_handle_t spiBusHandle;
gpio_num_t csPin;
gpio_num_t dcPin;
gpio_num_t resetPin = GPIO_NUM_NC;
unsigned int pixelClockFrequency = 80'000'000; // Hertz
size_t transactionQueueDepth = 2;
unsigned int horizontalResolution;
unsigned int verticalResolution;
bool swapXY = false;
bool mirrorX = false;
bool mirrorY = false;
bool invertColor = false;
unsigned int gapX = 0;
unsigned int gapY = 0;
uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
std::function<void(uint8_t)> _Nullable backlightDutyFunction = nullptr;
};
private:
std::unique_ptr<Configuration> configuration;
esp_lcd_panel_io_handle_t ioHandle = nullptr;
esp_lcd_panel_handle_t panelHandle = nullptr;
lv_display_t* displayHandle = nullptr;
public:
explicit St7796Display(std::unique_ptr<Configuration> inConfiguration) : configuration(std::move(inConfiguration)) {
assert(configuration != nullptr);
}
std::string getName() const final { return "ST7796"; }
std::string getDescription() const final { return "ST7796 display"; }
bool start() final;
bool stop() final;
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable createTouch() final { return configuration->touch; }
void setBacklightDuty(uint8_t backlightDuty) final {
if (configuration->backlightDutyFunction != nullptr) {
configuration->backlightDutyFunction(backlightDuty);
}
}
void setGammaCurve(uint8_t index) final;
uint8_t getGammaCurveCount() const final { return 4; };
bool supportsBacklightDuty() const final { return configuration->backlightDutyFunction != nullptr; }
lv_display_t* _Nullable getLvglDisplay() const final { return displayHandle; }
};
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "Source"
INCLUDE_DIRS "Source"
REQUIRES Tactility
)

View File

@ -0,0 +1,18 @@
Copyright 2023 Anthony DiGirolamo
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,4 @@
# TCA8418 I2C Controlled Keypad Scan IC With Integrated ESD Protection
[Datasheet](https://www.ti.com/lit/ds/symlink/tca8418.pdf?ts=1751500237439)
[Original implementation](https://github.com/AnthonyDiGirolamo/i2c-thumb-keyboard/tree/master) by Anthony DiGirolamo

View File

@ -0,0 +1,202 @@
#include "Tca8418.h"
#include <Tactility/Log.h>
#define TAG "tca8418"
namespace registers {
static const uint8_t CFG = 0x01U;
static const uint8_t KP_GPIO1 = 0x1DU;
static const uint8_t KP_GPIO2 = 0x1EU;
static const uint8_t KP_GPIO3 = 0x1FU;
static const uint8_t KEY_EVENT_A = 0x04U;
static const uint8_t KEY_EVENT_B = 0x05U;
static const uint8_t KEY_EVENT_C = 0x06U;
static const uint8_t KEY_EVENT_D = 0x07U;
static const uint8_t KEY_EVENT_E = 0x08U;
static const uint8_t KEY_EVENT_F = 0x09U;
static const uint8_t KEY_EVENT_G = 0x0AU;
static const uint8_t KEY_EVENT_H = 0x0BU;
static const uint8_t KEY_EVENT_I = 0x0CU;
static const uint8_t KEY_EVENT_J = 0x0DU;
} // namespace registers
void Tca8418::init(uint8_t numrows, uint8_t numcols) {
/*
* | ADDRESS | REGISTER NAME | REGISTER DESCRIPTION | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 |
* |---------+---------------+----------------------+------+------+------+------+------+------+------+------|
* | 0x1D | KP_GPIO1 | Keypad/GPIO Select 1 | ROW7 | ROW6 | ROW5 | ROW4 | ROW3 | ROW2 | ROW1 | ROW0 |
* | 0x1E | KP_GPIO2 | Keypad/GPIO Select 2 | COL7 | COL6 | COL5 | COL4 | COL3 | COL2 | COL1 | COL0 |
* | 0x1F | KP_GPIO3 | Keypad/GPIO Select 3 | N/A | N/A | N/A | N/A | N/A | N/A | COL9 | COL8 |
*/
num_rows = numrows;
num_cols = numcols;
// everything enabled in key scan mode
uint8_t enabled_rows = 0x3F;
uint16_t enabled_cols = 0x3FF;
writeRegister8(registers::KP_GPIO1, enabled_rows);
writeRegister8(registers::KP_GPIO2, (uint8_t)(0xFF & enabled_cols));
writeRegister8(registers::KP_GPIO3, (uint8_t)(0x03 & (enabled_cols >> 8)));
/*
* BIT: NAME
*
* 7: AI
* Auto-increment for read and write operations; See below table for more information
* 0 = disabled
* 1 = enabled
*
* 6: GPI_E_CFG
* GPI event mode configuration
* 0 = GPI events are tracked when keypad is locked
* 1 = GPI events are not tracked when keypad is locked
*
* 5: OVR_FLOW_M
* Overflow mode
* 0 = disabled; Overflow data is lost
* 1 = enabled; Overflow data shifts with last event pushing first event out
*
* 4: INT_CFG
* Interrupt configuration
* 0 = processor interrupt remains asserted (or low) if host tries to clear interrupt while there is
* still a pending key press, key release or GPI interrupt
* 1 = processor interrupt is deasserted for 50 μs and reassert with pending interrupts
*
* 3: OVR_FLOW_IEN
* Overflow interrupt enable
* 0 = disabled; INT is not asserted if the FIFO overflows
* 1 = enabled; INT becomes asserted if the FIFO overflows
*
* 2: K_LCK_IEN
* Keypad lock interrupt enable
* 0 = disabled; INT is not asserted after a correct unlock key sequence
* 1 = enabled; INT becomes asserted after a correct unlock key sequence
*
* 1: GPI_IEN
* GPI interrupt enable to host processor
* 0 = disabled; INT is not asserted for a change on a GPI
* 1 = enabled; INT becomes asserted for a change on a GPI
*
* 0: KE_IEN
* Key events interrupt enable to host processor
* 0 = disabled; INT is not asserted when a key event occurs
* 1 = enabled; INT becomes asserted when a key event occurs
*/
// 10111001 xB9 -- fifo overflow enabled
// 10011001 x99 -- fifo overflow disabled
writeRegister8(registers::CFG, 0x99);
clear_released_list();
clear_pressed_list();
}
bool Tca8418::update() {
last_update_micros = this_update_micros;
uint8_t key_code, key_down, key_event, key_row, key_col;
key_event = get_key_event();
// TODO: read gpio R7/R6 status? 0x14 bits 7&6
// read(0x14, &new_keycode)
// TODO: use tick function to get an update delta time
this_update_micros = 0;
delta_micros = this_update_micros - last_update_micros;
if (key_event > 0) {
key_code = key_event & 0x7F;
key_down = (key_event & 0x80) >> 7;
key_row = key_code / num_cols;
key_col = key_code % num_cols;
// always clear the released list
clear_released_list();
if (key_down) {
add_pressed_key(key_row, key_col);
// TODO reject ghosts (assume multiple key presses with the same hold time are ghosts.)
} else {
add_released_key(key_row, key_col);
remove_pressed_key(key_row, key_col);
}
return true;
}
// Increment hold times for pressed keys
for (int i = 0; i < pressed_key_count; i++) {
pressed_list[i].hold_time += delta_micros;
}
return false;
}
void Tca8418::add_pressed_key(uint8_t row, uint8_t col) {
if (pressed_key_count >= KEY_EVENT_LIST_SIZE)
return;
pressed_list[pressed_key_count].row = row;
pressed_list[pressed_key_count].col = col;
pressed_list[pressed_key_count].hold_time = 0;
pressed_key_count++;
}
void Tca8418::add_released_key(uint8_t row, uint8_t col) {
if (released_key_count >= KEY_EVENT_LIST_SIZE)
return;
released_key_count++;
released_list[0].row = row;
released_list[0].col = col;
}
void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) {
if (pressed_key_count == 0)
return;
// delete the pressed key
for (int i = 0; i < pressed_key_count; i++) {
if (pressed_list[i].row == row &&
pressed_list[i].col == col) {
// shift remaining keys left one index
for (int j = i; i < pressed_key_count; j++) {
if (j == KEY_EVENT_LIST_SIZE - 1)
break;
pressed_list[j].row = pressed_list[j + 1].row;
pressed_list[j].col = pressed_list[j + 1].col;
pressed_list[j].hold_time = pressed_list[j + 1].hold_time;
}
break;
}
}
pressed_key_count--;
}
void Tca8418::clear_pressed_list() {
for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) {
pressed_list[i].row = 255;
pressed_list[i].col = 255;
}
pressed_key_count = 0;
}
void Tca8418::clear_released_list() {
for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) {
released_list[i].row = 255;
released_list[i].col = 255;
}
released_key_count = 0;
}
uint8_t Tca8418::get_key_event() {
uint8_t new_keycode = 0;
readRegister8(registers::KEY_EVENT_A, new_keycode);
return new_keycode;
}

View File

@ -0,0 +1,69 @@
#pragma once
#include <array>
#include <Tactility/hal/i2c/I2cDevice.h>
#define TCA8418_ADDRESS 0x34U
#define KEY_EVENT_LIST_SIZE 10
class Tca8418 final : public tt::hal::i2c::I2cDevice {
private:
uint8_t tca8418_address;
uint32_t last_update_micros;
uint32_t this_update_micros;
uint8_t new_pressed_keys_count;
void clear_released_list();
void clear_pressed_list();
void add_pressed_key(uint8_t row, uint8_t col);
void add_released_key(uint8_t row, uint8_t col);
void remove_pressed_key(uint8_t row, uint8_t col);
void write(uint8_t register_address, uint8_t data);
bool read(uint8_t register_address, uint8_t* data);
public:
struct PressedKey {
uint8_t row;
uint8_t col;
uint32_t hold_time;
};
struct ReleasedKey {
uint8_t row;
uint8_t col;
};
std::string getName() const final { return "TCA8418"; }
std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; }
explicit Tca8418(i2c_port_t port) : I2cDevice(port, TCA8418_ADDRESS) {
delta_micros = 0;
last_update_micros = 0;
this_update_micros = 0;
}
~Tca8418() {}
uint8_t num_rows;
uint8_t num_cols;
uint32_t delta_micros;
std::array<PressedKey, KEY_EVENT_LIST_SIZE> pressed_list;
std::array<ReleasedKey, KEY_EVENT_LIST_SIZE> released_list;
uint8_t pressed_key_count;
uint8_t released_key_count;
void init(uint8_t numrows, uint8_t numcols);
bool update();
uint8_t get_key_event();
bool button_pressed(uint8_t row, uint8_t button_bit_position);
bool button_released(uint8_t row, uint8_t button_bit_position);
bool button_held(uint8_t row, uint8_t button_bit_position);
};

View File

@ -1,7 +1,7 @@
#pragma once #pragma once
#include "I2c.h"
#include "../Device.h" #include "../Device.h"
#include "I2c.h"
namespace tt::hal::i2c { namespace tt::hal::i2c {
@ -20,7 +20,11 @@ protected:
static constexpr TickType_t DEFAULT_TIMEOUT = 1000 / portTICK_PERIOD_MS; static constexpr TickType_t DEFAULT_TIMEOUT = 1000 / portTICK_PERIOD_MS;
bool read(uint8_t* data, size_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT);
bool write(const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT);
bool writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout = DEFAULT_TIMEOUT);
bool readRegister8(uint8_t reg, uint8_t& result) const; bool readRegister8(uint8_t reg, uint8_t& result) const;
bool writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT);
bool writeRegister8(uint8_t reg, uint8_t value) const; bool writeRegister8(uint8_t reg, uint8_t value) const;
bool readRegister12(uint8_t reg, float& out) const; bool readRegister12(uint8_t reg, float& out) const;
bool readRegister14(uint8_t reg, float& out) const; bool readRegister14(uint8_t reg, float& out) const;
@ -41,4 +45,4 @@ public:
uint8_t getAddress() const { return address; } uint8_t getAddress() const { return address; }
}; };
} } // namespace tt::hal::i2c

View File

@ -4,6 +4,22 @@
namespace tt::hal::i2c { namespace tt::hal::i2c {
bool I2cDevice::read(uint8_t* data, size_t dataSize, TickType_t timeout) {
return tt::hal::i2c::masterRead(port, address, data, dataSize, timeout);
}
bool I2cDevice::write(const uint8_t* data, uint16_t dataSize, TickType_t timeout) {
return tt::hal::i2c::masterWrite(port, address, data, dataSize, timeout);
}
bool I2cDevice::writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout) {
return masterWriteRead(port, address, writeData, writeDataSize, readData, readDataSize, timeout);
}
bool I2cDevice::writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) {
return masterWriteRegister(port, address, reg, data, dataSize, timeout);
}
bool I2cDevice::readRegister12(uint8_t reg, float& out) const { bool I2cDevice::readRegister12(uint8_t reg, float& out) const {
std::uint8_t data[2] = {0}; std::uint8_t data[2] = {0};
if (tt::hal::i2c::masterReadRegister(port, address, reg, data, 2, DEFAULT_TIMEOUT)) { if (tt::hal::i2c::masterReadRegister(port, address, reg, data, 2, DEFAULT_TIMEOUT)) {
@ -62,4 +78,4 @@ bool I2cDevice::bitOff(uint8_t reg, uint8_t bitmask) const {
} }
} }
} // namespace } // namespace tt::hal::i2c

View File

@ -39,6 +39,17 @@ void redraw(Gui* gui) {
if (gui->appToRender != nullptr) { if (gui->appToRender != nullptr) {
// Create a default group which adds all objects automatically,
// and assign all indevs to it.
// This enables navigation with limited input, such as encoder wheels.
lv_group_t* group = lv_group_create();
auto* indev = lv_indev_get_next(nullptr);
while (indev) {
lv_indev_set_group(indev, group);
indev = lv_indev_get_next(indev);
}
lv_group_set_default(group);
app::Flags flags = std::static_pointer_cast<app::AppInstance>(gui->appToRender)->getFlags(); app::Flags flags = std::static_pointer_cast<app::AppInstance>(gui->appToRender)->getFlags();
if (flags.showStatusbar) { if (flags.showStatusbar) {
lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN);
@ -61,4 +72,4 @@ void redraw(Gui* gui) {
unlock(); unlock();
} }
} // namespace } // namespace tt::service::gui

View File

@ -0,0 +1,56 @@
# Software defaults
# Increase stack size for WiFi (fixes crash after scan)
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_FONT_MONTSERRAT_18=y
CONFIG_LV_USE_USER_DATA=y
CONFIG_LV_USE_FS_STDIO=y
CONFIG_LV_FS_STDIO_LETTER=65
CONFIG_LV_FS_STDIO_PATH=""
CONFIG_LV_FS_STDIO_CACHE_SIZE=4096
CONFIG_LV_USE_LODEPNG=y
CONFIG_LV_USE_BUILTIN_MALLOC=n
CONFIG_LV_USE_CLIB_MALLOC=y
CONFIG_LV_USE_MSGBOX=n
CONFIG_LV_USE_SPINNER=n
CONFIG_LV_USE_WIN=n
CONFIG_LV_USE_SNAPSHOT=y
CONFIG_FREERTOS_HZ=1000
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2
CONFIG_FREERTOS_SMP=n
CONFIG_FREERTOS_UNICORE=n
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_VOLUME_COUNT=3
# Hardware: Main
CONFIG_TT_BOARD_LILYGO_TLORA_PAGER=y
CONFIG_TT_BOARD_NAME="LilyGo T-Lora Pager"
CONFIG_TT_BOARD_ID="lilygo-tlora-pager"
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_FLASHMODE_DIO=y
# Hardware: SPI RAM
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
#CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_TYPE_AUTO=y
CONFIG_SPIRAM_SPEED_120M=y
#CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved)
CONFIG_ESPTOOLPY_FLASHFREQ_40M=y
# LVGL
CONFIG_LV_DPI_DEF=90
CONFIG_LV_DISP_DEF_REFR_PERIOD=10
CONFIG_LV_THEME_DEFAULT_DARK=y
# USB
CONFIG_TINYUSB_MSC_ENABLED=y
CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard"