From baa4ed96df3295951c514953a7ac45d23176b13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Mon, 30 Jun 2025 20:49:36 +0200 Subject: [PATCH] Basic implementation of Lilygo T-Lora Pager board support --- App/Kconfig | 2 + App/Source/Boards.h | 5 +- App/idf_component.yml | 2 + Boards/LilygoTLoraPager/CMakeLists.txt | 7 + Boards/LilygoTLoraPager/Source/Init.cpp | 80 ++++ .../Source/LilygoTloraPager.cpp | 64 +++ .../Source/LilygoTloraPager.h | 5 + .../Source/hal/TpagerDisplay.cpp | 28 ++ .../Source/hal/TpagerDisplay.h | 40 ++ .../Source/hal/TpagerDisplayConstants.h | 8 + .../Source/hal/TpagerKeyboard.cpp | 336 ++++++++++++++++ .../Source/hal/TpagerKeyboard.h | 54 +++ .../Source/hal/TpagerPower.cpp | 90 +++++ .../LilygoTLoraPager/Source/hal/TpagerPower.h | 26 ++ .../Source/hal/TpagerSdCard.cpp | 31 ++ .../Source/hal/TpagerSdCard.h | 7 + Buildscripts/board.cmake | 2 + Drivers/BQ24295/Source/Bq24295.cpp | 8 +- Drivers/BQ27220/CMakeLists.txt | 5 + Drivers/BQ27220/README.md | 5 + Drivers/BQ27220/Source/Bq27220.cpp | 372 ++++++++++++++++++ Drivers/BQ27220/Source/Bq27220.h | 103 +++++ Drivers/ST7796/CMakeLists.txt | 5 + Drivers/ST7796/README.md | 3 + Drivers/ST7796/Source/St7796Display.cpp | 210 ++++++++++ Drivers/ST7796/Source/St7796Display.h | 97 +++++ Drivers/TCA8418/CMakeLists.txt | 5 + Drivers/TCA8418/README.md | 5 + Drivers/TCA8418/Source/Tca8418.cpp | 202 ++++++++++ Drivers/TCA8418/Source/Tca8418.h | 69 ++++ .../Include/Tactility/hal/i2c/I2cDevice.h | 8 +- Tactility/Source/hal/i2c/I2cDevice.cpp | 18 +- Tactility/Source/service/gui/GuiDraw.cpp | 13 +- sdkconfig.board.lilygo-tlora-pager | 56 +++ 34 files changed, 1962 insertions(+), 9 deletions(-) create mode 100644 Boards/LilygoTLoraPager/CMakeLists.txt create mode 100644 Boards/LilygoTLoraPager/Source/Init.cpp create mode 100644 Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp create mode 100644 Boards/LilygoTLoraPager/Source/LilygoTloraPager.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerPower.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h create mode 100644 Drivers/BQ27220/CMakeLists.txt create mode 100644 Drivers/BQ27220/README.md create mode 100644 Drivers/BQ27220/Source/Bq27220.cpp create mode 100644 Drivers/BQ27220/Source/Bq27220.h create mode 100644 Drivers/ST7796/CMakeLists.txt create mode 100644 Drivers/ST7796/README.md create mode 100644 Drivers/ST7796/Source/St7796Display.cpp create mode 100644 Drivers/ST7796/Source/St7796Display.h create mode 100644 Drivers/TCA8418/CMakeLists.txt create mode 100644 Drivers/TCA8418/README.md create mode 100644 Drivers/TCA8418/Source/Tca8418.cpp create mode 100644 Drivers/TCA8418/Source/Tca8418.h create mode 100644 sdkconfig.board.lilygo-tlora-pager diff --git a/App/Kconfig b/App/Kconfig index 302912a1..f6582cd1 100644 --- a/App/Kconfig +++ b/App/Kconfig @@ -37,6 +37,8 @@ menu "Tactility App" bool "Elecrow CrowPanel Basic 5.0" config TT_BOARD_LILYGO_TDECK bool "LilyGo T-Deck" + config TT_BOARD_LILYGO_TLORA_PAGER + bool "LilyGo T-Lora Pager" config TT_BOARD_M5STACK_CORE2 bool "M5Stack Core2" config TT_BOARD_M5STACK_CORES3 diff --git a/App/Source/Boards.h b/App/Source/Boards.h index 982b0cee..650da360 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -8,6 +8,9 @@ #if defined(CONFIG_TT_BOARD_LILYGO_TDECK) #include "LilygoTdeck.h" #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) #include "CYD2432S024C.h" #define TT_BOARD_HARDWARE &cyd_2432s024c_config @@ -68,4 +71,4 @@ extern tt::hal::Configuration hardware; #define TT_BOARD_HARDWARE &hardware -#endif // ESP_PLATFORM \ No newline at end of file +#endif // ESP_PLATFORM diff --git a/App/idf_component.yml b/App/idf_component.yml index a9f6aa56..4dbbcb52 100644 --- a/App/idf_component.yml +++ b/App/idf_component.yml @@ -12,6 +12,8 @@ dependencies: version: "1.1.1" rules: - if: "target in [esp32s3, esp32p4]" + espressif/esp_lcd_st7796: + version: "1.3.2" espressif/esp_lcd_panel_io_additions: "1.0.1" espressif/esp_tinyusb: version: "1.5.0" diff --git a/Boards/LilygoTLoraPager/CMakeLists.txt b/Boards/LilygoTLoraPager/CMakeLists.txt new file mode 100644 index 00000000..3e3a7c3c --- /dev/null +++ b/Boards/LilygoTLoraPager/CMakeLists.txt @@ -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 +) diff --git a/Boards/LilygoTLoraPager/Source/Init.cpp b/Boards/LilygoTLoraPager/Source/Init.cpp new file mode 100644 index 00000000..9c478f6a --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/Init.cpp @@ -0,0 +1,80 @@ +#include "PwmBacklight.h" +#include "Tactility/kernel/SystemEvents.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +#include + +#include +#include + +#define TAG "tpager" + +// Power on +#define TDECK_POWERON_GPIO GPIO_NUM_10 + +std::shared_ptr bq27220; +std::shared_ptr tca8418; + +static bool powerOn() { + /* + gpio_config_t device_power_signal_config = { + .pin_bit_mask = BIT64(TDECK_POWERON_GPIO), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + if (gpio_config(&device_power_signal_config) != ESP_OK) { + return false; + } + + if (gpio_set_level(TDECK_POWERON_GPIO, 1) != ESP_OK) { + return false; + } +*/ + return true; +} + +bool tdeckInit() { + ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); + if (!powerOn()) { + TT_LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED); + return false; + } + + /* 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(I2C_NUM_0); + tt::hal::registerDevice(bq27220); + + tca8418 = std::make_shared(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 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; +} diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp new file mode 100644 index 00000000..42add446 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp @@ -0,0 +1,64 @@ +#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 + +#define TDECK_SPI_TRANSFER_SIZE_LIMIT (TDECK_LCD_HORIZONTAL_RESOLUTION * TDECK_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8)) + +bool tdeckInit(); + +using namespace tt::hal; + +extern const Configuration lilygo_tlora_pager = { + .initBoot = tdeckInit, + .createDisplay = createDisplay, + .createKeyboard = createKeyboard, + .sdcard = createTpagerSdCard(), + .power = tpager_get_power, + .i2c = { + i2c::Configuration { + .name = "Internal", + .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 = TDECK_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, + }}}} +}; diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h new file mode 100644 index 00000000..b3e010fe --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern const tt::hal::Configuration lilygo_tlora_pager; diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp new file mode 100644 index 00000000..4736f022 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp @@ -0,0 +1,28 @@ +#include "TpagerDisplay.h" +#include "TpagerDisplayConstants.h" + +#include +#include + +#include + +#define TAG "tdeck_display" + +std::shared_ptr createDisplay() { + auto configuration = std::make_unique( + TDECK_LCD_SPI_HOST, + TDECK_LCD_PIN_CS, + TDECK_LCD_PIN_DC, + 480, // w + 222, // h + nullptr, + true, //swapXY + true, //mirrorX + true, //mirrorY + true //invertColor + ); + + configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; + + return std::make_shared(std::move(configuration)); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h new file mode 100644 index 00000000..df09ebc9 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" +#include +#include + +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 _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 createDisplay(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h new file mode 100644 index 00000000..c2d379a8 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h @@ -0,0 +1,8 @@ +#pragma once + +#define TDECK_LCD_SPI_HOST SPI2_HOST +#define TDECK_LCD_PIN_CS GPIO_NUM_38 +#define TDECK_LCD_PIN_DC GPIO_NUM_37 // RS +#define TDECK_LCD_HORIZONTAL_RESOLUTION 222 +#define TDECK_LCD_VERTICAL_RESOLUTION 480 +#define TDECK_LCD_SPI_TRANSFER_HEIGHT (TDECK_LCD_VERTICAL_RESOLUTION / 10) diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp new file mode 100644 index 00000000..993a8b05 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp @@ -0,0 +1,336 @@ +#include "TpagerKeyboard.h" +#include +#include + +#include "driver/gpio.h" +#include "freertos/queue.h" + +#include + +#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::Type::Periodic, [this] { + processKeyboard(); + }); + + assert(backlightImpulseTimer == nullptr); + backlightImpulseTimer = std::make_unique(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}, + }; + + ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &encPcntUnit)); + + pcnt_glitch_filter_config_t filter_config = { + .max_glitch_ns = 1000, + }; + ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(encPcntUnit, &filter_config)); + + pcnt_chan_config_t chan_a_config = { + .edge_gpio_num = ENCODER_A, + .level_gpio_num = ENCODER_B, + }; + pcnt_channel_handle_t pcnt_chan_a = NULL; + ESP_ERROR_CHECK(pcnt_new_channel(encPcntUnit, &chan_a_config, &pcnt_chan_a)); + pcnt_chan_config_t chan_b_config = { + .edge_gpio_num = ENCODER_B, + .level_gpio_num = ENCODER_A, + }; + pcnt_channel_handle_t pcnt_chan_b = NULL; + ESP_ERROR_CHECK(pcnt_new_channel(encPcntUnit, &chan_b_config, &pcnt_chan_b)); + + ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE)); + ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE)); + ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE)); + ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE)); + + ESP_ERROR_CHECK(pcnt_unit_add_watch_point(encPcntUnit, low_limit)); + ESP_ERROR_CHECK(pcnt_unit_add_watch_point(encPcntUnit, high_limit)); + + ESP_ERROR_CHECK(pcnt_unit_enable(encPcntUnit)); + ESP_ERROR_CHECK(pcnt_unit_clear_count(encPcntUnit)); + ESP_ERROR_CHECK(pcnt_unit_start(encPcntUnit)); +} + +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; +std::shared_ptr createKeyboard() { + keyboardMsg = xQueueCreate(20, sizeof(char)); + + return std::make_shared(tca8418); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h new file mode 100644 index 00000000..1aba8c08 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + + +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 keypad; + std::unique_ptr inputTimer; + std::unique_ptr 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 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 createKeyboard(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp new file mode 100644 index 00000000..d39e3216 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp @@ -0,0 +1,90 @@ +#include "TpagerPower.h" + +#include + +#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 power; +extern std::shared_ptr bq27220; + +std::shared_ptr tpager_get_power() { + if (power == nullptr) { + power = std::make_shared(bq27220); + } + return power; +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h new file mode 100644 index 00000000..a215d3e7 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Tactility/hal/power/PowerDevice.h" +#include +#include + +using tt::hal::power::PowerDevice; + +class TpagerPower : public PowerDevice { + std::shared_ptr gauge; + +public: + + TpagerPower(std::shared_ptr 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 tpager_get_power(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp new file mode 100644 index 00000000..54593414 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp @@ -0,0 +1,31 @@ +#include "TpagerSdCard.h" + +#include +#include + +#include + +using tt::hal::sdcard::SpiSdCardDevice; + +#define TDECK_SDCARD_PIN_CS GPIO_NUM_21 +#define TDECK_LCD_PIN_CS GPIO_NUM_38 +#define TDECK_RADIO_PIN_CS GPIO_NUM_36 + +std::shared_ptr createTpagerSdCard() { + auto* configuration = new SpiSdCardDevice::Config( + TDECK_SDCARD_PIN_CS, + GPIO_NUM_NC, + GPIO_NUM_NC, + GPIO_NUM_NC, + SdCardDevice::MountBehaviour::AtBoot, + tt::lvgl::getSyncLock(), + {TDECK_RADIO_PIN_CS, + TDECK_LCD_PIN_CS} + ); + + auto* sdcard = (SdCardDevice*)new SpiSdCardDevice( + std::unique_ptr(configuration) + ); + + return std::shared_ptr(sdcard); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h new file mode 100644 index 00000000..95da81a6 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Tactility/hal/sdcard/SdCardDevice.h" + +using tt::hal::sdcard::SdCardDevice; + +std::shared_ptr createTpagerSdCard(); diff --git a/Buildscripts/board.cmake b/Buildscripts/board.cmake index 29afb107..77018252 100644 --- a/Buildscripts/board.cmake +++ b/Buildscripts/board.cmake @@ -47,6 +47,8 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE) set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50) elseif (board_id STREQUAL "lilygo-tdeck") set(TACTILITY_BOARD_PROJECT LilygoTdeck) + elseif (board_id STREQUAL "lilygo-tlora-pager") + set(TACTILITY_BOARD_PROJECT LilygoTLoraPager) elseif (board_id STREQUAL "m5stack-core2") set(TACTILITY_BOARD_PROJECT M5stackCore2) elseif (board_id STREQUAL "m5stack-cores3") diff --git a/Drivers/BQ24295/Source/Bq24295.cpp b/Drivers/BQ24295/Source/Bq24295.cpp index 22e94b13..4dfae7bf 100644 --- a/Drivers/BQ24295/Source/Bq24295.cpp +++ b/Drivers/BQ24295/Source/Bq24295.cpp @@ -8,10 +8,10 @@ * https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads */ namespace registers { - 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 STATUS = 0x08U; // Datasheet page 38: System status - static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status +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 STATUS = 0x08U; // Datasheet page 38: System status +static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status } // namespace registers bool Bq24295::readChargeTermination(uint8_t& out) const { diff --git a/Drivers/BQ27220/CMakeLists.txt b/Drivers/BQ27220/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/BQ27220/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/BQ27220/README.md b/Drivers/BQ27220/README.md new file mode 100644 index 00000000..5ecc1181 --- /dev/null +++ b/Drivers/BQ27220/README.md @@ -0,0 +1,5 @@ +# BQ24295 + +Power management: I2C-controlled 3A single cell USB charger with narrow VDC 4.5-5.5V adjustable voltage at 1.5A synchronous boost operation. + +[Datasheet](https://www.ti.com/lit/ds/symlink/bq24295.pdf) diff --git a/Drivers/BQ27220/Source/Bq27220.cpp b/Drivers/BQ27220/Source/Bq27220.cpp new file mode 100644 index 00000000..9a67ff51 --- /dev/null +++ b/Drivers/BQ27220/Source/Bq27220.cpp @@ -0,0 +1,372 @@ +#include "Bq27220.h" +#include + +#include "esp_sleep.h" + +#define TAG "bq27220" + +#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a))) + +uint8_t highByte(const uint16_t word) { return (word >> 8) & 0xFF; } +uint8_t lowByte(const uint16_t word) { return word & 0xFF; } +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(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; + } else { + TT_LOG_I(TAG, "Full access theoretically."); + } + } + + // 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; +} diff --git a/Drivers/BQ27220/Source/Bq27220.h b/Drivers/BQ27220/Source/Bq27220.h new file mode 100644 index 00000000..abf5b86d --- /dev/null +++ b/Drivers/BQ27220/Source/Bq27220.h @@ -0,0 +1,103 @@ +#pragma once + +#include + +#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 + bool performConfigUpdate(T configUpdateFunc) + { + bool isSealed = false; + + if (!configPreamble(isSealed)) { + return false; + } + bool result = configUpdateFunc(); + configEpilouge(isSealed); + + return result; + } + +public: + 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); +}; diff --git a/Drivers/ST7796/CMakeLists.txt b/Drivers/ST7796/CMakeLists.txt new file mode 100644 index 00000000..983cc248 --- /dev/null +++ b/Drivers/ST7796/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd_st7796 driver +) diff --git a/Drivers/ST7796/README.md b/Drivers/ST7796/README.md new file mode 100644 index 00000000..2f6c6fb7 --- /dev/null +++ b/Drivers/ST7796/README.md @@ -0,0 +1,3 @@ +# ST7789 + +A basic ESP32 LVGL driver for ST7789 displays. diff --git a/Drivers/ST7796/Source/St7796Display.cpp b/Drivers/ST7796/Source/St7796Display.cpp new file mode 100644 index 00000000..58aad133 --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.cpp @@ -0,0 +1,210 @@ +#include "St7796Display.h" + +#include + +#include +#include +#include +#include + +#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, 0, 49) != 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"); + }*/ +} diff --git a/Drivers/ST7796/Source/St7796Display.h b/Drivers/ST7796/Source/St7796Display.h new file mode 100644 index 00000000..edbea517 --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.h @@ -0,0 +1,97 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" + +#include +#include +#include +#include +#include +#include + +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 touch, + bool swapXY = false, + bool mirrorX = false, + bool mirrorY = false, + bool invertColor = false, + 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), + 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; + uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size + std::shared_ptr touch; + std::function _Nullable backlightDutyFunction = nullptr; + }; + +private: + + std::unique_ptr 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 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 _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 createDisplay(); diff --git a/Drivers/TCA8418/CMakeLists.txt b/Drivers/TCA8418/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/TCA8418/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/TCA8418/README.md b/Drivers/TCA8418/README.md new file mode 100644 index 00000000..5ecc1181 --- /dev/null +++ b/Drivers/TCA8418/README.md @@ -0,0 +1,5 @@ +# BQ24295 + +Power management: I2C-controlled 3A single cell USB charger with narrow VDC 4.5-5.5V adjustable voltage at 1.5A synchronous boost operation. + +[Datasheet](https://www.ti.com/lit/ds/symlink/bq24295.pdf) diff --git a/Drivers/TCA8418/Source/Tca8418.cpp b/Drivers/TCA8418/Source/Tca8418.cpp new file mode 100644 index 00000000..f4bac8b8 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.cpp @@ -0,0 +1,202 @@ +#include "Tca8418.h" +#include + +#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; +} diff --git a/Drivers/TCA8418/Source/Tca8418.h b/Drivers/TCA8418/Source/Tca8418.h new file mode 100644 index 00000000..d48d14a3 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include + +#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 pressed_list; + std::array 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); +}; diff --git a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h index c7754999..0d4c2e7e 100644 --- a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h +++ b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h @@ -1,7 +1,7 @@ #pragma once -#include "I2c.h" #include "../Device.h" +#include "I2c.h" namespace tt::hal::i2c { @@ -20,7 +20,11 @@ protected: 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 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 readRegister12(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; } }; -} +} // namespace tt::hal::i2c diff --git a/Tactility/Source/hal/i2c/I2cDevice.cpp b/Tactility/Source/hal/i2c/I2cDevice.cpp index 4891e471..1e96f8d5 100644 --- a/Tactility/Source/hal/i2c/I2cDevice.cpp +++ b/Tactility/Source/hal/i2c/I2cDevice.cpp @@ -4,6 +4,22 @@ 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 { std::uint8_t data[2] = {0}; 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 diff --git a/Tactility/Source/service/gui/GuiDraw.cpp b/Tactility/Source/service/gui/GuiDraw.cpp index ed52c19e..a3ba2333 100644 --- a/Tactility/Source/service/gui/GuiDraw.cpp +++ b/Tactility/Source/service/gui/GuiDraw.cpp @@ -39,6 +39,17 @@ void redraw(Gui* gui) { 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(gui->appToRender)->getFlags(); if (flags.showStatusbar) { lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); @@ -61,4 +72,4 @@ void redraw(Gui* gui) { unlock(); } -} // namespace +} // namespace tt::service::gui diff --git a/sdkconfig.board.lilygo-tlora-pager b/sdkconfig.board.lilygo-tlora-pager new file mode 100644 index 00000000..16fb1e30 --- /dev/null +++ b/sdkconfig.board.lilygo-tlora-pager @@ -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"