Compare commits
17 Commits
338e395cb6
...
b37f21f3cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b37f21f3cf | ||
|
|
982fce9207 | ||
|
|
00b62a2831 | ||
|
|
ab4cf79a47 | ||
|
|
d06197a6aa | ||
|
|
efd3dc43ed | ||
|
|
6de0f442fb | ||
|
|
29e4350517 | ||
|
|
f9acf04dcb | ||
|
|
a091923353 | ||
|
|
1593eb80ce | ||
|
|
869a56125f | ||
|
|
6116521556 | ||
|
|
3dfc27e93e | ||
|
|
e4ecec64c9 | ||
|
|
ce96474d84 | ||
|
|
2691dbb014 |
2
.github/actions/build-firmware/action.yml
vendored
@ -18,7 +18,7 @@ runs:
|
||||
shell: bash
|
||||
run: cp sdkconfig.board.${{ inputs.board_id }} sdkconfig
|
||||
- name: 'Build'
|
||||
uses: espressif/esp-idf-ci-action@main
|
||||
uses: espressif/esp-idf-ci-action@v1
|
||||
with:
|
||||
esp_idf_version: v5.4
|
||||
target: ${{ inputs.arch }}
|
||||
|
||||
8
.github/actions/build-sdk/action.yml
vendored
@ -18,14 +18,18 @@ runs:
|
||||
shell: bash
|
||||
run: cp sdkconfig.board.${{ inputs.board_id }} sdkconfig
|
||||
- name: 'Build'
|
||||
uses: espressif/esp-idf-ci-action@main
|
||||
uses: espressif/esp-idf-ci-action@v1
|
||||
with:
|
||||
# NOTE: Update with ESP-IDF!
|
||||
esp_idf_version: v5.4
|
||||
target: ${{ inputs.arch }}
|
||||
path: './'
|
||||
- name: 'Release'
|
||||
shell: bash
|
||||
run: Buildscripts/release-sdk.sh release/TactilitySDK
|
||||
env:
|
||||
# NOTE: Update with ESP-IDF!
|
||||
ESP_IDF_VERSION: '5.4'
|
||||
run: Buildscripts/release-sdk.sh release/TactilitySDK
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
9
.github/workflows/build-firmware.yml
vendored
@ -144,6 +144,15 @@ jobs:
|
||||
with:
|
||||
board_id: lilygo-tdeck-pro
|
||||
arch: esp32s3
|
||||
lilygo-tlora-pager:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Build"
|
||||
uses: ./.github/actions/build-firmware
|
||||
with:
|
||||
board_id: lilygo-tlora-pager
|
||||
arch: esp32s3
|
||||
m5stack-core2:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@ -41,6 +41,8 @@ menu "Tactility App"
|
||||
bool "LilyGo T-Deck Plus"
|
||||
config TT_BOARD_LILYGO_TDECK_PRO
|
||||
bool "LilyGo T-Deck Pro"
|
||||
config TT_BOARD_LILYGO_TLORA_PAGER
|
||||
bool "LilyGo T-Lora Pager"
|
||||
config TT_BOARD_M5STACK_CORE2
|
||||
bool "M5Stack Core2"
|
||||
config TT_BOARD_M5STACK_CORES3
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#pragma once
|
||||
#include <Tactility/hal/Configuration.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
@ -7,6 +8,9 @@
|
||||
#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PLUS) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PRO)
|
||||
#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
|
||||
@ -67,4 +71,4 @@
|
||||
extern tt::hal::Configuration hardware;
|
||||
#define TT_BOARD_HARDWARE &hardware
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
#endif // ESP_PLATFORM
|
||||
|
||||
@ -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"
|
||||
|
||||
7
Boards/LilygoTLoraPager/CMakeLists.txt
Normal 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
|
||||
)
|
||||
55
Boards/LilygoTLoraPager/Source/Init.cpp
Normal 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;
|
||||
}
|
||||
83
Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp
Normal 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,
|
||||
}
|
||||
}
|
||||
}}
|
||||
};
|
||||
5
Boards/LilygoTLoraPager/Source/LilygoTloraPager.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/Configuration.h>
|
||||
|
||||
extern const tt::hal::Configuration lilygo_tlora_pager;
|
||||
30
Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp
Normal 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));
|
||||
}
|
||||
40
Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h
Normal 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();
|
||||
@ -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)
|
||||
359
Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp
Normal 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);
|
||||
}
|
||||
54
Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h
Normal 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();
|
||||
90
Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp
Normal 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;
|
||||
}
|
||||
26
Boards/LilygoTLoraPager/Source/hal/TpagerPower.h
Normal 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();
|
||||
31
Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp
Normal 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);
|
||||
}
|
||||
7
Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h
Normal file
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Tactility/hal/sdcard/SdCardDevice.h"
|
||||
|
||||
using tt::hal::sdcard::SdCardDevice;
|
||||
|
||||
std::shared_ptr<SdCardDevice> createTpagerSdCard();
|
||||
@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Main.h"
|
||||
#include <Tactility/hal/Configuration.h>
|
||||
|
||||
namespace simulator {
|
||||
/** Set the function pointer of the real app_main() */
|
||||
|
||||
@ -19,7 +19,7 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
|
||||
set(id_length 0)
|
||||
math(EXPR id_length "${sdkconfig_board_id_length} - 21")
|
||||
string(SUBSTRING ${sdkconfig_board_id} 20 ${id_length} board_id)
|
||||
message("Building board: ${Cyan}${board_id}${ColorReset}")
|
||||
message("Board name: ${Cyan}${board_id}${ColorReset}")
|
||||
|
||||
if (board_id STREQUAL "cyd-2432s024c")
|
||||
set(TACTILITY_BOARD_PROJECT CYD-2432S024C)
|
||||
@ -51,6 +51,8 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
|
||||
set(TACTILITY_BOARD_PROJECT LilygoTdeck)
|
||||
elseif (board_id STREQUAL "lilygo-tdeck-pro")
|
||||
set(TACTILITY_BOARD_PROJECT LilygoTdeckPro)
|
||||
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")
|
||||
@ -66,7 +68,7 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
|
||||
if (TACTILITY_BOARD_PROJECT STREQUAL "")
|
||||
message(FATAL_ERROR "No subproject mapped to \"${TACTILITY_BOARD_ID}\" in root Buildscripts/board.cmake")
|
||||
else ()
|
||||
message("Board project: ${Cyan}Boards/${TACTILITY_BOARD_PROJECT}${ColorReset}\n")
|
||||
message("Board path: ${Cyan}Boards/${TACTILITY_BOARD_PROJECT}${ColorReset}\n")
|
||||
set_property(GLOBAL PROPERTY TACTILITY_BOARD_PROJECT ${TACTILITY_BOARD_PROJECT})
|
||||
set_property(GLOBAL PROPERTY TACTILITY_BOARD_ID ${board_id})
|
||||
endif ()
|
||||
|
||||
@ -35,6 +35,9 @@ release elecrow-crowpanel-basic-50
|
||||
build lilygo-tdeck
|
||||
release lilygo-tdeck
|
||||
|
||||
build lilygo-tlora-pager
|
||||
release lilygo-tlora-pager
|
||||
|
||||
releaseSdk release/TactilitySDK-esp32s3
|
||||
|
||||
build cyd-2432s024c
|
||||
|
||||
@ -27,12 +27,13 @@ fi
|
||||
|
||||
echoNewPhase "Cleaning build folder"
|
||||
|
||||
rm -rf build
|
||||
#rm -rf build
|
||||
|
||||
echoNewPhase "Building $sdkconfig_file"
|
||||
|
||||
cp $sdkconfig_file sdkconfig
|
||||
if not idf.py build; then
|
||||
idf.py build
|
||||
if [[ $? != 0 ]]; then
|
||||
fatalError "Failed to build esp32s3 SDK"
|
||||
fi
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ file(READ version.txt TACTILITY_VERSION)
|
||||
add_compile_definitions(TT_VERSION="${TACTILITY_VERSION}")
|
||||
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
message("Building with ESP-IDF ${Cyan}v$ENV{ESP_IDF_VERSION}${ColorReset}")
|
||||
message("Using ESP-IDF ${Cyan}v$ENV{ESP_IDF_VERSION}${ColorReset}")
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
|
||||
include("Buildscripts/board.cmake")
|
||||
|
||||
12
COPYRIGHT.md
@ -53,6 +53,18 @@ Website: https://github.com/meshtastic/firmware
|
||||
|
||||
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)
|
||||
|
||||
### esp32s3-gc9a01-lvgl
|
||||
|
||||
Website: https://github.com/UsefulElectronics/esp32s3-gc9a01-lvgl
|
||||
|
||||
License: [Explicitly granted by author](https://github.com/ByteWelder/Tactility/pull/295#discussion_r2226215423)
|
||||
|
||||
### Other Components
|
||||
|
||||
See `/components` for the respective projects and their licenses.
|
||||
|
||||
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 724 B After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.8 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M240-160q-33 0-56.5-23.5T160-240q0-33 23.5-56.5T240-320q33 0 56.5 23.5T320-240q0 33-23.5 56.5T240-160Zm240 0q-33 0-56.5-23.5T400-240q0-33 23.5-56.5T480-320q33 0 56.5 23.5T560-240q0 33-23.5 56.5T480-160Zm240 0q-33 0-56.5-23.5T640-240q0-33 23.5-56.5T720-320q33 0 56.5 23.5T800-240q0 33-23.5 56.5T720-160ZM240-400q-33 0-56.5-23.5T160-480q0-33 23.5-56.5T240-560q33 0 56.5 23.5T320-480q0 33-23.5 56.5T240-400Zm240 0q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm240 0q-33 0-56.5-23.5T640-480q0-33 23.5-56.5T720-560q33 0 56.5 23.5T800-480q0 33-23.5 56.5T720-400ZM240-640q-33 0-56.5-23.5T160-720q0-33 23.5-56.5T240-800q33 0 56.5 23.5T320-720q0 33-23.5 56.5T240-640Zm240 0q-33 0-56.5-23.5T400-720q0-33 23.5-56.5T480-800q33 0 56.5 23.5T560-720q0 33-23.5 56.5T480-640Zm240 0q-33 0-56.5-23.5T640-720q0-33 23.5-56.5T720-800q33 0 56.5 23.5T800-720q0 33-23.5 56.5T720-640Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H447l-80-80H160v480Zm0 0v-480 480Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 301 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 771 B |
42
Data/system_sources/app/Launcher/apps.svg
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
fill="#e8eaed"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="apps.svg"
|
||||
inkscape:export-filename="icon_apps.png"
|
||||
inkscape:export-xdpi="160"
|
||||
inkscape:export-ydpi="160"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="17.088414"
|
||||
inkscape:cx="14.600536"
|
||||
inkscape:cy="18.960215"
|
||||
inkscape:window-width="1503"
|
||||
inkscape:window-height="933"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 122.37644,-3.1685927 q -49.173234,0 -84.190541,-35.0173063 -35.0173063,-35.017307 -35.0173063,-84.190541 0,-49.17324 35.0173063,-84.19055 35.017307,-35.01731 84.190541,-35.01731 49.17324,0 84.19055,35.01731 35.01731,35.01731 35.01731,84.19055 0,49.173234 -35.01731,84.190541 -35.01731,35.0173063 -84.19055,35.0173063 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173063 -35.0173,-35.017307 -35.0173,-84.190541 0,-49.17324 35.0173,-84.19055 35.01731,-35.01731 84.19055,-35.01731 49.17324,0 84.19055,35.01731 35.0173,35.01731 35.0173,84.19055 0,49.173234 -35.0173,84.190541 Q 529.17324,-3.1685927 480,-3.1685927 Z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173063 -35.01731,-35.017307 -35.01731,-84.190541 0,-49.17324 35.01731,-84.19055 35.01731,-35.01731 84.19055,-35.01731 49.17323,0 84.19054,35.01731 35.01731,35.01731 35.01731,84.19055 0,49.173234 -35.01731,84.190541 -35.01731,35.0173063 -84.19054,35.0173063 z M 122.37644,-360.79215 q -49.173234,0 -84.190541,-35.0173 Q 3.1685927,-430.82676 3.1685927,-480 q 0,-49.17324 35.0173063,-84.19055 35.017307,-35.0173 84.190541,-35.0173 49.17324,0 84.19055,35.0173 35.01731,35.01731 35.01731,84.19055 0,49.17324 -35.01731,84.19055 -35.01731,35.0173 -84.19055,35.0173 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173 -35.0173,-35.01731 -35.0173,-84.19055 0,-49.17324 35.0173,-84.19055 35.01731,-35.0173 84.19055,-35.0173 49.17324,0 84.19055,35.0173 35.0173,35.01731 35.0173,84.19055 0,49.17324 -35.0173,84.19055 -35.01731,35.0173 -84.19055,35.0173 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173 Q 718.4157,-430.82676 718.4157,-480 q 0,-49.17324 35.01731,-84.19055 35.01731,-35.0173 84.19055,-35.0173 49.17323,0 84.19054,35.0173 35.01731,35.01731 35.01731,84.19055 0,49.17324 -35.01731,84.19055 -35.01731,35.0173 -84.19054,35.0173 z M 122.37644,-718.4157 q -49.173234,0 -84.190541,-35.01731 -35.0173063,-35.01731 -35.0173063,-84.19055 0,-49.17323 35.0173063,-84.19054 35.017307,-35.01731 84.190541,-35.01731 49.17324,0 84.19055,35.01731 35.01731,35.01731 35.01731,84.19054 0,49.17324 -35.01731,84.19055 -35.01731,35.01731 -84.19055,35.01731 z m 357.62356,0 q -49.17324,0 -84.19055,-35.01731 -35.0173,-35.01731 -35.0173,-84.19055 0,-49.17323 35.0173,-84.19054 35.01731,-35.01731 84.19055,-35.01731 49.17324,0 84.19055,35.01731 35.0173,35.01731 35.0173,84.19054 0,49.17324 -35.0173,84.19055 Q 529.17324,-718.4157 480,-718.4157 Z m 357.62356,0 q -49.17324,0 -84.19055,-35.01731 -35.01731,-35.01731 -35.01731,-84.19055 0,-49.17323 35.01731,-84.19054 35.01731,-35.01731 84.19055,-35.01731 49.17323,0 84.19054,35.01731 35.01731,35.01731 35.01731,84.19054 0,49.17324 -35.01731,84.19055 -35.01731,35.01731 -84.19054,35.01731 z"
|
||||
id="path1"
|
||||
style="stroke-width:1.4901" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
42
Data/system_sources/app/Launcher/folder.svg
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
fill="#e8eaed"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="folder.svg"
|
||||
inkscape:export-filename="icon_files.png"
|
||||
inkscape:export-xdpi="160"
|
||||
inkscape:export-ydpi="160"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="48.333333"
|
||||
inkscape:cx="11.989655"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-width="1503"
|
||||
inkscape:window-height="933"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="m 96.441379,-96.441379 q -39.554482,0 -67.722069,-28.167591 -28.16758586,-28.16758 -28.16758586,-67.72206 v -575.33794 q 0,-39.55448 28.16758586,-67.72206 28.167587,-28.16759 67.722069,-28.16759 H 384.11034 L 480,-767.66897 h 383.55862 q 39.55448,0 67.72207,28.16759 28.16759,28.16759 28.16759,67.72207 v 479.44828 q 0,39.55448 -28.16759,67.72206 -28.16759,28.167591 -67.72207,28.167591 z m 0,-95.889651 H 863.55862 v -479.44828 h -423.1131 l -95.88966,-95.88966 H 96.441379 Z m 0,0 v -575.33794 z"
|
||||
id="path1"
|
||||
style="stroke-width:1.19862" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
42
Data/system_sources/app/Launcher/settings.svg
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
fill="#e8eaed"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="settings.svg"
|
||||
inkscape:export-filename="icon_settings.png"
|
||||
inkscape:export-xdpi="160"
|
||||
inkscape:export-ydpi="160"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="48.333333"
|
||||
inkscape:cx="11.989655"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-width="1503"
|
||||
inkscape:window-height="933"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M 347.80751,0.69994853 328.57952,-153.12403 q -15.62275,-6.00875 -29.44288,-14.421 -13.82012,-8.41225 -27.03937,-18.02625 L 129.08904,-125.48379 -3.1034483,-353.81626 120.67679,-447.55275 q -1.20175,-8.41225 -1.20175,-16.22363 v -32.44724 q 0,-7.81138 1.20175,-16.22363 L -3.1034483,-606.18374 129.08904,-834.51621 l 143.00823,60.08749 q 13.21925,-9.614 27.64025,-18.02625 14.421,-8.41225 28.842,-14.421 l 19.22799,-153.82398 h 264.38498 l 19.22799,153.82398 q 15.62275,6.00875 29.44288,14.421 13.82012,8.41225 27.03937,18.02625 l 143.00823,-60.08749 132.19249,228.33247 -123.78024,93.73649 q 1.20175,8.41225 1.20175,16.22363 v 32.44724 q 0,7.81138 -2.4035,16.22363 l 123.78024,93.73649 -132.19249,228.33247 -141.80648,-60.08749 q -13.21925,9.614 -27.64025,18.02625 -14.421,8.41225 -28.842,14.421 L 612.19249,0.69994853 Z m 84.1225,-96.13998953 h 94.93823 l 16.8245,-127.385489 q 37.25425,-9.614 69.10062,-28.24112 31.84637,-18.62712 58.28487,-45.06562 l 118.97324,49.27175 46.86824,-81.719 -103.35049,-78.11374 q 6.00875,-16.8245 8.41225,-35.45162 2.4035,-18.62712 2.4035,-37.85512 0,-19.228 -2.4035,-37.85512 -2.4035,-18.62712 -8.41225,-35.45162 l 103.35049,-78.11374 -46.86824,-81.719 -118.97324,50.4735 q -26.4385,-27.64025 -58.28487,-46.26737 -31.84637,-18.62712 -69.10062,-28.24112 l -15.62275,-127.38549 h -94.93823 l -16.8245,127.38549 q -37.25425,9.614 -69.10062,28.24112 -31.84637,18.62712 -58.28487,45.06562 l -118.97324,-49.27175 -46.86824,81.719 103.35049,76.91199 q -6.00875,18.02625 -8.41225,36.05249 -2.4035,18.02625 -2.4035,38.456 0,19.228 2.4035,37.25425 2.4035,18.02624 8.41225,36.05249 l -103.35049,78.11374 46.86824,81.719 118.97324,-50.4735 q 26.4385,27.64025 58.28487,46.26737 31.84637,18.62712 69.10062,28.24112 z M 482.4035,-311.75502 q 69.70149,0 118.97324,-49.27174 49.27174,-49.27175 49.27174,-118.97324 0,-69.70149 -49.27174,-118.97324 -49.27175,-49.27174 -118.97324,-49.27174 -70.90324,0 -119.57411,49.27174 -48.67087,49.27175 -48.67087,118.97324 0,69.70149 48.67087,118.97324 48.67087,49.27174 119.57411,49.27174 z M 480,-480 Z"
|
||||
id="path1"
|
||||
style="stroke-width:1.20175" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@ -1,5 +1,6 @@
|
||||
# TODOs
|
||||
|
||||
- Bug: When a Wi-Fi SSID is too long, then it fails to save the credentials
|
||||
- Add a Keyboard setting app to override the behaviour of soft keyboard hiding (e.g. keyboard hardware is present, but user wants soft keyboard)
|
||||
- HAL for display touch calibration
|
||||
- Start using non_null (either via MS GSL, or custom)
|
||||
@ -65,6 +66,5 @@
|
||||
- GPS app
|
||||
- Investigate CSI https://stevenmhernandez.github.io/ESP32-CSI-Tool/
|
||||
- Compile unix tools to ELF apps?
|
||||
- Text editor
|
||||
- Todo list
|
||||
- Calendar
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
BIN
Documentation/pics/screenshot-Launcher.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
28
Documentation/releasing.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Releasing Tactility
|
||||
|
||||
1. Test the latest version on several devices
|
||||
2. Build the SDK locally and test it with `ExternalApps/HelloWorld`
|
||||
3. Test the latest SDK build from GitHub with the CDN:
|
||||
1. Download it from the [main branch](https://github.com/ByteWelder/Tactility/actions/workflows/build-sdk.yml)
|
||||
2. Upload it to the [CDN](https://dash.cloudflare.com)
|
||||
3. Update `sdk.json` from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com)
|
||||
4. Test it with `ExternalApps/HelloWorld` (clear all its cache and update the SDK version)
|
||||
4. Download the latest firmwares [main branch](https://github.com/ByteWelder/Tactility/actions/workflows/build-firmware.yml)
|
||||
5. Prepare a new version of [TactilityWebInstaller](https://github.com/ByteWelder/TactilityWebInstaller) locally:
|
||||
1. Copy the GitHub firmwares into `scripts/` in the `TactilityWebInstaller` project
|
||||
2. Run `python release-all.py`
|
||||
3. Copy the unpacked files to `/rom/(device)/(version)/` and copy in `manifest.json` from existing release
|
||||
1. **WARNING** If the partitions have changed, update the json!
|
||||
4. Update the version in `manifest.json`
|
||||
5. Update `version.json` for the device
|
||||
6. Test the firmwares on all devices with the local web installer
|
||||
7. If all went well: release the web installer
|
||||
8. Test web installer in production (use popular devices)
|
||||
9. Make a new version of the docs available at [TactilityDocs](https://github.com/ByteWelder/TactilityDocs)
|
||||
10. Make a new [GitHub release](https://github.com/ByteWelder/Tactility/releases/new)
|
||||
|
||||
### Post-release
|
||||
|
||||
1. Mention on Discord
|
||||
2. Consider notifying vendors/stakeholders
|
||||
3. Remove dev versions in `sdk.json`from [TactilityTool](https://github.com/ByteWelder/TactilityTool) and upload it to [CDN](https://dash.cloudflare.com)
|
||||
@ -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 {
|
||||
|
||||
5
Drivers/BQ27220/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
6
Drivers/BQ27220/README.md
Normal 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)
|
||||
370
Drivers/BQ27220/Source/Bq27220.cpp
Normal 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;
|
||||
}
|
||||
107
Drivers/BQ27220/Source/Bq27220.h
Normal 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);
|
||||
};
|
||||
5
Drivers/ST7796/CMakeLists.txt
Normal 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
@ -0,0 +1,3 @@
|
||||
# ST7796
|
||||
|
||||
A basic ESP32 LVGL driver for ST7796 displays.
|
||||
210
Drivers/ST7796/Source/St7796Display.cpp
Normal 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");
|
||||
}*/
|
||||
}
|
||||
103
Drivers/ST7796/Source/St7796Display.h
Normal 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();
|
||||
5
Drivers/TCA8418/CMakeLists.txt
Normal file
@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
18
Drivers/TCA8418/COPYRIGHT.md
Normal 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.
|
||||
4
Drivers/TCA8418/README.md
Normal 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
|
||||
202
Drivers/TCA8418/Source/Tca8418.cpp
Normal 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;
|
||||
}
|
||||
69
Drivers/TCA8418/Source/Tca8418.h
Normal 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);
|
||||
};
|
||||
2
ExternalApps/HelloWorld/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build*/
|
||||
.tactility/
|
||||
@ -1,6 +0,0 @@
|
||||
rm sdkconfig
|
||||
cp ../../sdkconfig sdkconfig
|
||||
cat sdkconfig.override >> sdkconfig
|
||||
# First we must run "build" because otherwise "idf.py elf" is not a valid command
|
||||
idf.py build
|
||||
idf.py elf
|
||||
@ -1,2 +0,0 @@
|
||||
CONFIG_PARTITION_TABLE_SINGLE_APP=y
|
||||
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n
|
||||
2
ExternalApps/HelloWorld/tactility.properties
Normal file
@ -0,0 +1,2 @@
|
||||
[sdk]
|
||||
version = 0.4.0
|
||||
410
ExternalApps/HelloWorld/tactility.py
Normal file
@ -0,0 +1,410 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
# Targetable platforms that represent a specific hardware target
|
||||
platform_targets = ["esp32", "esp32s3"]
|
||||
# All valid platform commandline arguments
|
||||
platform_arguments = platform_targets.copy()
|
||||
platform_arguments.append("all")
|
||||
ttbuild_path = ".tactility"
|
||||
ttbuild_version = "1.0.0"
|
||||
ttbuild_properties_file = "tactility.properties"
|
||||
ttbuild_cdn = "https://cdn.tactility.one"
|
||||
ttbuild_sdk_json_validity = 3600 # seconds
|
||||
verbose = False
|
||||
use_local_sdk = False
|
||||
|
||||
spinner_pattern = [
|
||||
"⠋",
|
||||
"⠙",
|
||||
"⠹",
|
||||
"⠸",
|
||||
"⠼",
|
||||
"⠴",
|
||||
"⠦",
|
||||
"⠧",
|
||||
"⠇",
|
||||
"⠏"
|
||||
]
|
||||
|
||||
if sys.platform == "win32":
|
||||
shell_color_red = ""
|
||||
shell_color_orange = ""
|
||||
shell_color_green = ""
|
||||
shell_color_purple = ""
|
||||
shell_color_cyan = ""
|
||||
shell_color_reset = ""
|
||||
else:
|
||||
shell_color_red = "\033[91m"
|
||||
shell_color_orange = "\033[93m"
|
||||
shell_color_green = "\033[32m"
|
||||
shell_color_purple = "\033[35m"
|
||||
shell_color_cyan = "\033[36m"
|
||||
shell_color_reset = "\033[m"
|
||||
|
||||
def print_help():
|
||||
print("Usage: python tactility.py [action] [options]")
|
||||
print("")
|
||||
print("Actions:")
|
||||
print(" build [esp32,esp32s3,all,local] Build the app for the specified platform")
|
||||
print(" esp32: ESP32")
|
||||
print(" esp32s3: ESP32 S3")
|
||||
print(" all: all supported ESP platforms")
|
||||
print(" clean Clean the build folders")
|
||||
print(" clearcache Clear the SDK cache")
|
||||
print(" updateself Update this tool")
|
||||
print("")
|
||||
print("Options:")
|
||||
print(" --help Show this commandline info")
|
||||
print(" --local-sdk Use SDK specifiedc by environment variable TACTILITY_SDK_PATH")
|
||||
print(" --skip-build Run everything except the idf.py/CMake commands")
|
||||
print(" --verbose Show extra console output")
|
||||
|
||||
def download_file(url, filepath):
|
||||
global verbose
|
||||
if verbose:
|
||||
print(f"Downloading from {url} to {filepath}")
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=None,
|
||||
headers={
|
||||
"User-Agent": f"Tactility Build Tool {ttbuild_version}"
|
||||
}
|
||||
)
|
||||
try:
|
||||
response = urllib.request.urlopen(request)
|
||||
file = open(filepath, mode="wb")
|
||||
file.write(response.read())
|
||||
file.close()
|
||||
return True
|
||||
except OSError as error:
|
||||
if verbose:
|
||||
print_error(f"Failed to fetch URL {url}\n{error}")
|
||||
return False
|
||||
|
||||
def print_warning(message):
|
||||
print(f"{shell_color_orange}WARNING: {message}{shell_color_reset}")
|
||||
|
||||
def print_error(message):
|
||||
print(f"{shell_color_red}ERROR: {message}{shell_color_reset}")
|
||||
|
||||
def exit_with_error(message):
|
||||
print_error(message)
|
||||
sys.exit(1)
|
||||
|
||||
def is_valid_platform_name(name):
|
||||
global platform_arguments
|
||||
return name in platform_arguments
|
||||
|
||||
def validate_environment():
|
||||
global ttbuild_properties_file, use_local_sdk
|
||||
if os.environ.get("IDF_PATH") is None:
|
||||
exit_with_error("Cannot find the Espressif IDF SDK. Ensure it is installed and that it is activated via $PATH_TO_IDF_SDK/export.sh")
|
||||
if not os.path.exists(ttbuild_properties_file):
|
||||
exit_with_error(f"{ttbuild_properties_file} file not found")
|
||||
if use_local_sdk == False and os.environ.get("TACTILITY_SDK_PATH") is not None:
|
||||
print_warning("TACTILITY_SDK_PATH is set, but will be ignored by this command.")
|
||||
print_warning("If you want to use it, use the 'build local' parameters.")
|
||||
elif use_local_sdk == True and os.environ.get("TACTILITY_SDK_PATH") is None:
|
||||
exit_with_error("local build was requested, but TACTILITY_SDK_PATH environment variable is not set.")
|
||||
|
||||
def setup_environment():
|
||||
global ttbuild_path
|
||||
os.makedirs(ttbuild_path, exist_ok=True)
|
||||
|
||||
def get_sdk_dir(version, platform):
|
||||
global use_local_sdk
|
||||
if use_local_sdk:
|
||||
return os.environ.get("TACTILITY_SDK_PATH")
|
||||
else:
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}", "TactilitySDK")
|
||||
|
||||
def get_sdk_version():
|
||||
global ttbuild_properties_file
|
||||
parser = configparser.RawConfigParser()
|
||||
parser.read(ttbuild_properties_file)
|
||||
sdk_dict = dict(parser.items("sdk"))
|
||||
if not "version" in sdk_dict:
|
||||
exit_with_error(f"Could not find 'version' in [sdk] section in {ttbuild_properties_file}")
|
||||
return sdk_dict["version"]
|
||||
|
||||
def get_sdk_root_dir(version, platform):
|
||||
global ttbuild_cdn
|
||||
return os.path.join(ttbuild_path, f"{version}-{platform}")
|
||||
|
||||
def get_sdk_url(version, platform):
|
||||
global ttbuild_cdn
|
||||
return f"{ttbuild_cdn}/TactilitySDK-{version}-{platform}.zip"
|
||||
|
||||
def sdk_exists(version, platform):
|
||||
sdk_dir = get_sdk_dir(version, platform)
|
||||
return os.path.isdir(sdk_dir)
|
||||
|
||||
def should_update_sdk_json():
|
||||
global ttbuild_cdn
|
||||
json_filepath = os.path.join(ttbuild_path, "sdk.json")
|
||||
if os.path.exists(json_filepath):
|
||||
json_modification_time = os.path.getmtime(json_filepath)
|
||||
now = time.time()
|
||||
global ttbuild_sdk_json_validity
|
||||
minimum_seconds_difference = ttbuild_sdk_json_validity
|
||||
return (now - json_modification_time) > minimum_seconds_difference
|
||||
else:
|
||||
return True
|
||||
|
||||
def update_sdk_json():
|
||||
global ttbuild_cdn, ttbuild_path
|
||||
json_url = f"{ttbuild_cdn}/sdk.json"
|
||||
json_filepath = os.path.join(ttbuild_path, "sdk.json")
|
||||
return download_file(json_url, json_filepath)
|
||||
|
||||
def should_fetch_sdkconfig_files():
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
if not os.path.exists(os.path.join(ttbuild_path, sdkconfig_filename)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def fetch_sdkconfig_files():
|
||||
for platform in platform_targets:
|
||||
sdkconfig_filename = f"sdkconfig.app.{platform}"
|
||||
target_path = os.path.join(ttbuild_path, sdkconfig_filename)
|
||||
if not download_file(f"{ttbuild_cdn}/{sdkconfig_filename}", target_path):
|
||||
exit_with_error(f"Failed to download sdkconfig file for {platform}")
|
||||
|
||||
|
||||
def validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build):
|
||||
version_map = sdk_json["versions"]
|
||||
if not sdk_version in version_map:
|
||||
exit_with_error(f"Version not found: {sdk_version}")
|
||||
version_data = version_map[sdk_version]
|
||||
available_platforms = version_data["platforms"]
|
||||
for desired_platform in platforms_to_build:
|
||||
if not desired_platform in available_platforms:
|
||||
exit_with_error(f"Platform {desired_platform} is not available. Available ones: {available_platforms}")
|
||||
|
||||
def validate_self(sdk_json):
|
||||
if not "toolVersion" in sdk_json:
|
||||
exit_with_error("Server returned invalid SDK data format (toolVersion not found)")
|
||||
if not "toolCompatibility" in sdk_json:
|
||||
exit_with_error("Server returned invalid SDK data format (toolCompatibility not found)")
|
||||
if not "toolDownloadUrl" in sdk_json:
|
||||
exit_with_error("Server returned invalid SDK data format (toolDownloadUrl not found)")
|
||||
tool_version = sdk_json["toolVersion"]
|
||||
tool_compatibility = sdk_json["toolCompatibility"]
|
||||
if tool_version != ttbuild_version:
|
||||
print_warning(f"New version available: {tool_version} (currently using {ttbuild_version})")
|
||||
print_warning(f"Run 'tactility.py updateself' to update.")
|
||||
if re.search(tool_compatibility, ttbuild_version) is None:
|
||||
print_error("The tool is not compatible anymore.")
|
||||
print_error("Run 'tactility.py updateself' to update.")
|
||||
sys.exit(1)
|
||||
|
||||
def sdk_download(version, platform):
|
||||
sdk_root_dir = get_sdk_root_dir(version, platform)
|
||||
os.makedirs(sdk_root_dir, exist_ok=True)
|
||||
sdk_url = get_sdk_url(version, platform)
|
||||
filepath = os.path.join(sdk_root_dir, f"{version}-{platform}.zip")
|
||||
print(f"Downloading SDK version {version} for {platform}")
|
||||
if download_file(sdk_url, filepath):
|
||||
with zipfile.ZipFile(filepath, "r") as zip_ref:
|
||||
zip_ref.extractall(os.path.join(sdk_root_dir, "TactilitySDK"))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def sdk_download_all(version, platforms):
|
||||
for platform in platforms:
|
||||
if not sdk_exists(version, platform):
|
||||
if not sdk_download(version, platform):
|
||||
return False
|
||||
else:
|
||||
if verbose:
|
||||
print(f"Using cached download for SDK version {version} and platform {platform}")
|
||||
return True
|
||||
|
||||
def find_elf_file(platform):
|
||||
build_dir = f"build-{platform}"
|
||||
if os.path.exists(build_dir):
|
||||
for file in os.listdir(build_dir):
|
||||
if file.endswith(".app.elf"):
|
||||
return os.path.join(build_dir, file)
|
||||
return None
|
||||
|
||||
def build_all(version, platforms, skip_build):
|
||||
for platform in platforms:
|
||||
# First build command must be "idf.py build", otherwise it fails to execute "idf.py elf"
|
||||
# We check if the ELF file exists and run the correct command
|
||||
# This can lead to code caching issues, so sometimes a clean build is required
|
||||
if find_elf_file(platform) is None:
|
||||
if not build_first(version, platform, skip_build):
|
||||
break
|
||||
else:
|
||||
if not build_consecutively(version, platform, skip_build):
|
||||
break
|
||||
|
||||
def wait_for_build(process, platform):
|
||||
buffer = []
|
||||
os.set_blocking(process.stdout.fileno(), False)
|
||||
while process.poll() is None:
|
||||
for i in spinner_pattern:
|
||||
time.sleep(0.1)
|
||||
progress_text = f"Building for {platform} {shell_color_cyan}" + str(i) + shell_color_reset
|
||||
sys.stdout.write(progress_text + "\r")
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
decoded_line = line.decode("UTF-8")
|
||||
if decoded_line != "":
|
||||
buffer.append(decoded_line)
|
||||
else:
|
||||
break
|
||||
return buffer
|
||||
|
||||
# The first build must call "idf.py build" and consecutive builds must call "idf.py elf" as it finishes faster.
|
||||
# The problem is that the "idf.py build" always results in an error, even though the elf file is created.
|
||||
# The solution is to suppress the error if we find that the elf file was created.
|
||||
def build_first(version, platform, skip_build):
|
||||
sdk_dir = get_sdk_dir(version, platform)
|
||||
if verbose:
|
||||
print(f"Using SDK at {sdk_dir}")
|
||||
os.environ["TACTILITY_SDK_PATH"] = sdk_dir
|
||||
sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}")
|
||||
os.system(f"cp {sdkconfig_path} sdkconfig")
|
||||
elf_path = find_elf_file(platform)
|
||||
# Remove previous elf file: re-creation of the file is used to measure if the build succeeded,
|
||||
# as the actual build job will always fail due to technical issues with the elf cmake script
|
||||
if elf_path is not None:
|
||||
os.remove(elf_path)
|
||||
if skip_build:
|
||||
return True
|
||||
print("Building first build")
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "build"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
# The return code is never expected to be 0 due to a bug in the elf cmake script, but we keep it just in case
|
||||
if process.returncode == 0:
|
||||
print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}")
|
||||
return True
|
||||
else:
|
||||
if find_elf_file(platform) is None:
|
||||
for line in build_output:
|
||||
print(line, end="")
|
||||
print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}")
|
||||
return False
|
||||
else:
|
||||
print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}")
|
||||
return True
|
||||
|
||||
def build_consecutively(version, platform, skip_build):
|
||||
sdk_dir = get_sdk_dir(version, platform)
|
||||
if verbose:
|
||||
print(f"Using SDK at {sdk_dir}")
|
||||
os.environ["TACTILITY_SDK_PATH"] = sdk_dir
|
||||
sdkconfig_path = os.path.join(ttbuild_path, f"sdkconfig.app.{platform}")
|
||||
os.system(f"cp {sdkconfig_path} sdkconfig")
|
||||
if skip_build:
|
||||
return True
|
||||
with subprocess.Popen(["idf.py", "-B", f"build-{platform}", "elf"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) as process:
|
||||
build_output = wait_for_build(process, platform)
|
||||
if process.returncode == 0:
|
||||
print(f"{shell_color_green}Building for {platform} ✅{shell_color_reset}")
|
||||
return True
|
||||
else:
|
||||
for line in build_output:
|
||||
print(line, end="")
|
||||
print(f"{shell_color_red}Building for {platform} failed ❌{shell_color_reset}")
|
||||
return False
|
||||
|
||||
def read_sdk_json():
|
||||
json_file_path = os.path.join(ttbuild_path, "sdk.json")
|
||||
json_file = open(json_file_path)
|
||||
return json.load(json_file)
|
||||
|
||||
def build_action(platform_arg):
|
||||
# Environment validation
|
||||
validate_environment()
|
||||
# Environment setup
|
||||
setup_environment()
|
||||
platforms_to_build = platform_targets if platform_arg == "all" else [platform_arg]
|
||||
if not is_valid_platform_name(platform_arg):
|
||||
print_help()
|
||||
exit_with_error("Invalid platform name")
|
||||
if not use_local_sdk:
|
||||
if should_fetch_sdkconfig_files():
|
||||
fetch_sdkconfig_files()
|
||||
# Update SDK cache
|
||||
if should_update_sdk_json() and not update_sdk_json():
|
||||
exit_with_error("Failed to retrieve SDK info")
|
||||
sdk_json = read_sdk_json()
|
||||
validate_self(sdk_json)
|
||||
if not "versions" in sdk_json:
|
||||
exit_with_error("Version data not found in sdk.json")
|
||||
# Build
|
||||
sdk_version = get_sdk_version()
|
||||
if not use_local_sdk:
|
||||
validate_version_and_platforms(sdk_json, sdk_version, platforms_to_build)
|
||||
if not sdk_download_all(sdk_version, platforms_to_build):
|
||||
exit_with_error("Failed to download one or more SDKs")
|
||||
build_all(sdk_version, platforms_to_build, skip_build) # Environment validation
|
||||
|
||||
def clean_action():
|
||||
count = 0
|
||||
for path in os.listdir("."):
|
||||
if path.startswith("build-"):
|
||||
print(f"Removing {path}/")
|
||||
shutil.rmtree(path)
|
||||
count = count + 1
|
||||
if count == 0:
|
||||
print("Nothing to clean")
|
||||
|
||||
def clear_cache_action():
|
||||
if os.path.exists(ttbuild_path):
|
||||
print(f"Removing {ttbuild_path}/")
|
||||
shutil.rmtree(ttbuild_path)
|
||||
else:
|
||||
print("Nothing to clear")
|
||||
|
||||
def update_self_action():
|
||||
sdk_json = read_sdk_json()
|
||||
tool_download_url = sdk_json["toolDownloadUrl"]
|
||||
if download_file(tool_download_url, "tactility.py"):
|
||||
print("Updated")
|
||||
else:
|
||||
exit_with_error("Update failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Tactility Build System v{ttbuild_version}")
|
||||
if "--help" in sys.argv:
|
||||
print_help()
|
||||
sys.exit()
|
||||
# Argument validation
|
||||
if len(sys.argv) == 1:
|
||||
print_help()
|
||||
sys.exit()
|
||||
action_arg = sys.argv[1]
|
||||
verbose = "--verbose" in sys.argv
|
||||
skip_build = "--skip-build" in sys.argv
|
||||
use_local_sdk = "--local-sdk" in sys.argv
|
||||
# Actions
|
||||
if action_arg == "build":
|
||||
if len(sys.argv) < 3:
|
||||
print_help()
|
||||
exit_with_error("Commandline parameter missing")
|
||||
build_action(sys.argv[2])
|
||||
elif action_arg == "clean":
|
||||
clean_action()
|
||||
elif action_arg == "clearcache":
|
||||
clear_cache_action()
|
||||
elif action_arg == "updateself":
|
||||
update_self_action()
|
||||
else:
|
||||
print_help()
|
||||
exit_with_error("Unknown commandline parameter")
|
||||
10
README.md
@ -4,15 +4,11 @@ Tactility is an operating system that focuses on the ESP32 microcontroller famil
|
||||
|
||||
See [https://tactility.one](https://tactility.one) for more information.
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
You can run built-in apps or start them from an SD card:
|
||||
You can run built-in apps or start them from an SD card. It's easy to manage system settings:
|
||||
|
||||
 
|
||||
|
||||
It's easy to manage system settings:
|
||||
|
||||
 
|
||||
 
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
|
||||
list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip)
|
||||
list(APPEND REQUIRES_LIST TactilityCore lvgl driver elf_loader lv_screenshot QRCode esp_lvgl_port minmea esp_wifi nvs_flash spiffs vfs fatfs lwip esp_http_server)
|
||||
if ("${IDF_TARGET}" STREQUAL "esp32s3")
|
||||
list(APPEND REQUIRES_LIST esp_tinyusb)
|
||||
endif ()
|
||||
|
||||
@ -8,9 +8,13 @@ namespace tt {
|
||||
/**
|
||||
* Settings that persist on NVS flash for ESP32.
|
||||
* On simulator, the settings are only in-memory.
|
||||
*
|
||||
* Note that on ESP32, there are limitations:
|
||||
* - namespace name is limited by NVS_NS_NAME_MAX_SIZE (generally 16 characters)
|
||||
* - key is limited by NVS_KEY_NAME_MAX_SIZE (generally 16 characters)
|
||||
*/
|
||||
class Preferences {
|
||||
private:
|
||||
|
||||
const char* namespace_;
|
||||
|
||||
public:
|
||||
|
||||
@ -16,9 +16,9 @@ namespace tt::app {
|
||||
class AppContext;
|
||||
enum class Result;
|
||||
|
||||
class App {
|
||||
typedef unsigned int LaunchId;
|
||||
|
||||
private:
|
||||
class App {
|
||||
|
||||
Mutex mutex;
|
||||
|
||||
@ -44,7 +44,7 @@ public:
|
||||
virtual void onDestroy(AppContext& appContext) {}
|
||||
virtual void onShow(AppContext& appContext, lv_obj_t* parent) {}
|
||||
virtual void onHide(AppContext& appContext) {}
|
||||
virtual void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> _Nullable resultData) {}
|
||||
virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> _Nullable resultData) {}
|
||||
|
||||
Mutex& getMutex() { return mutex; }
|
||||
|
||||
@ -83,15 +83,15 @@ std::shared_ptr<App> create() { return std::shared_ptr<T>(new T); }
|
||||
* @param[in] id application name or id
|
||||
* @param[in] parameters optional parameters to pass onto the application
|
||||
*/
|
||||
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||
|
||||
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
||||
void stop();
|
||||
|
||||
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
|
||||
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext();
|
||||
std::shared_ptr<AppContext> _Nullable getCurrentAppContext();
|
||||
|
||||
/** @return the currently running app (it is only ever null before the splash screen is shown) */
|
||||
std::shared_ptr<app::App> _Nullable getCurrentApp();
|
||||
std::shared_ptr<App> _Nullable getCurrentApp();
|
||||
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ typedef void (*OnCreate)(void* appContext, void* _Nullable data);
|
||||
typedef void (*OnDestroy)(void* appContext, void* _Nullable data);
|
||||
typedef void (*OnShow)(void* appContext, void* _Nullable data, lv_obj_t* parent);
|
||||
typedef void (*OnHide)(void* appContext, void* _Nullable data);
|
||||
typedef void (*OnResult)(void* appContext, void* _Nullable data, Result result, Bundle* resultData);
|
||||
typedef void (*OnResult)(void* appContext, void* _Nullable data, LaunchId launchId, Result result, Bundle* resultData);
|
||||
|
||||
void setElfAppManifest(
|
||||
const char* name,
|
||||
@ -31,10 +31,7 @@ void setElfAppManifest(
|
||||
*/
|
||||
std::string getElfAppId(const std::string& filePath);
|
||||
|
||||
/**
|
||||
* @return true when registration was done, false when app was already registered
|
||||
*/
|
||||
bool registerElfApp(const std::string& filePath);
|
||||
void registerElfApp(const std::string& filePath);
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
void start();
|
||||
|
||||
} // namespace
|
||||
@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "app/files/View.h"
|
||||
#include "app/files/State.h"
|
||||
#include "app/AppManifest.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <dirent.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
void start();
|
||||
|
||||
} // namespace
|
||||
@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
/**
|
||||
* Show a file selection dialog that allows the user to select an existing file.
|
||||
* This app returns the absolute file path as a result.
|
||||
*/
|
||||
LaunchId startForExistingFile();
|
||||
|
||||
/**
|
||||
* Show a file selection dialog that allows the user to select a new or existing file.
|
||||
* This app returns the absolute file path as a result.
|
||||
*/
|
||||
LaunchId startForExistingOrNewFile();
|
||||
|
||||
/**
|
||||
* @param bundle the result bundle of an app
|
||||
* @return the path from the bundle, or empty string if none is present
|
||||
*/
|
||||
std::string getResultPath(const Bundle& bundle);
|
||||
|
||||
} // namespace
|
||||
@ -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
|
||||
|
||||
@ -53,7 +53,7 @@ std::shared_ptr<SdCardDevice> _Nullable find(const std::string& path);
|
||||
* Always calls the function, but doesn't lock if the path is not an SD card path.
|
||||
*/
|
||||
template<typename ReturnType>
|
||||
inline ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
|
||||
ReturnType withSdCardLock(const std::string& path, std::function<ReturnType()> fn) {
|
||||
auto sdcard = find(path);
|
||||
if (sdcard != nullptr) {
|
||||
auto scoped_lockable = sdcard->getLock().asScopedLock();
|
||||
|
||||
9
Tactility/Include/Tactility/lvgl/Color.h
Normal file
@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
lv_color_t lv_color_foreground();
|
||||
|
||||
lv_color_t lv_color_background();
|
||||
|
||||
lv_color_t lv_color_background_darkest();
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
|
||||
5
Tactility/Include/Tactility/lvgl/Lvgl.h
Normal file
@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
#include "./Colors.h"
|
||||
@ -1,4 +1,4 @@
|
||||
#include "lvgl.h"
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "lvgl.h"
|
||||
#include "../app/AppContext.h"
|
||||
#include <lvgl.h>
|
||||
|
||||
namespace tt::lvgl {
|
||||
|
||||
|
||||
29
Tactility/Include/Tactility/network/HttpdReq.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <esp_http_server.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value);
|
||||
|
||||
bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary);
|
||||
|
||||
bool getQueryOrSendError(httpd_req_t* request, std::string& query);
|
||||
|
||||
std::unique_ptr<char[]> receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead);
|
||||
|
||||
std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator);
|
||||
|
||||
std::map<std::string, std::string> parseContentDisposition(const std::vector<std::string>& input);
|
||||
|
||||
bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead);
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
19
Tactility/Include/Tactility/network/Url.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace tt::network {
|
||||
|
||||
/**
|
||||
* Parse a query from a URL
|
||||
* @param[in] query
|
||||
* @return a map with key-values
|
||||
*/
|
||||
std::map<std::string, std::string> parseUrlQuery(std::string query);
|
||||
|
||||
std::string urlEncode(const std::string& input);
|
||||
|
||||
std::string urlDecode(const std::string& input);
|
||||
|
||||
} // namespace
|
||||
@ -4,7 +4,6 @@
|
||||
|
||||
#include <Tactility/Bundle.h>
|
||||
#include <Tactility/PubSub.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
@ -30,7 +29,7 @@ struct LoaderEvent {
|
||||
* @param[in] id application name or id
|
||||
* @param[in] parameters optional parameters to pass onto the application
|
||||
*/
|
||||
void startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||
app::LaunchId startApp(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters = nullptr);
|
||||
|
||||
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
|
||||
void stopApp();
|
||||
|
||||
@ -113,6 +113,11 @@ void setScanRecords(uint16_t records);
|
||||
*/
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* @return the IPv4 address or empty string
|
||||
*/
|
||||
std::string getIp();
|
||||
|
||||
/**
|
||||
* @brief Connect to a network. Disconnects any existing connection.
|
||||
* Returns immediately but runs in the background. Results are through pubsub.
|
||||
|
||||
@ -25,22 +25,21 @@ enum class State {
|
||||
*/
|
||||
class AppInstance : public AppContext {
|
||||
|
||||
private:
|
||||
|
||||
Mutex mutex = Mutex(Mutex::Type::Normal);
|
||||
const std::shared_ptr<AppManifest> manifest;
|
||||
State state = State::Initial;
|
||||
LaunchId launchId;
|
||||
Flags flags = { .showStatusbar = true };
|
||||
/** @brief Optional parameters to start the app with
|
||||
* When these are stored in the app struct, the struct takes ownership.
|
||||
* Do not mutate after app creation.
|
||||
*/
|
||||
std::shared_ptr<const tt::Bundle> _Nullable parameters;
|
||||
std::shared_ptr<const Bundle> _Nullable parameters;
|
||||
|
||||
std::shared_ptr<App> app;
|
||||
|
||||
static std::shared_ptr<app::App> createApp(
|
||||
const std::shared_ptr<app::AppManifest>& manifest
|
||||
static std::shared_ptr<App> createApp(
|
||||
const std::shared_ptr<AppManifest>& manifest
|
||||
) {
|
||||
if (manifest->location.isInternal()) {
|
||||
assert(manifest->createApp != nullptr);
|
||||
@ -50,7 +49,7 @@ private:
|
||||
TT_LOG_W("", "Manifest specifies createApp, but this is not used with external apps");
|
||||
}
|
||||
#ifdef ESP_PLATFORM
|
||||
return app::createElfApp(manifest);
|
||||
return createElfApp(manifest);
|
||||
#else
|
||||
tt_crash("not supported");
|
||||
#endif
|
||||
@ -61,18 +60,23 @@ private:
|
||||
|
||||
public:
|
||||
|
||||
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest) :
|
||||
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId) :
|
||||
manifest(manifest),
|
||||
launchId(launchId),
|
||||
app(createApp(manifest))
|
||||
{}
|
||||
|
||||
AppInstance(const std::shared_ptr<AppManifest>& manifest, std::shared_ptr<const Bundle> parameters) :
|
||||
AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId, std::shared_ptr<const Bundle> parameters) :
|
||||
manifest(manifest),
|
||||
launchId(launchId),
|
||||
parameters(std::move(parameters)),
|
||||
app(createApp(manifest)) {}
|
||||
app(createApp(manifest))
|
||||
{}
|
||||
|
||||
~AppInstance() override = default;
|
||||
|
||||
LaunchId getLaunchId() const { return launchId; }
|
||||
|
||||
void setState(State state);
|
||||
State getState() const;
|
||||
|
||||
|
||||
11
Tactility/Private/Tactility/app/development/Development.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
namespace tt::app::development {
|
||||
|
||||
void start();
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -6,7 +6,7 @@
|
||||
#include <vector>
|
||||
#include <dirent.h>
|
||||
|
||||
namespace tt::app::files {
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
class State {
|
||||
|
||||
11
Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename);
|
||||
bool isSupportedImageFile(const std::string& filename);
|
||||
bool isSupportedTextFile(const std::string& filename);
|
||||
|
||||
} // namespace
|
||||
@ -7,7 +7,7 @@
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::files {
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
class View {
|
||||
std::shared_ptr<State> state;
|
||||
@ -1,66 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <dirent.h>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
|
||||
namespace tt::app::files {
|
||||
|
||||
/** File types for `dirent`'s `d_type`. */
|
||||
enum {
|
||||
TT_DT_UNKNOWN = 0,
|
||||
#define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type
|
||||
TT_DT_FIFO = 1,
|
||||
#define TT_DT_FIFO TT_DT_FIFO // Named pipe or FIFO
|
||||
TT_DT_CHR = 2,
|
||||
#define TT_DT_CHR TT_DT_CHR // Character device
|
||||
TT_DT_DIR = 4,
|
||||
#define TT_DT_DIR TT_DT_DIR // Directory
|
||||
TT_DT_BLK = 6,
|
||||
#define TT_DT_BLK TT_DT_BLK // Block device
|
||||
TT_DT_REG = 8,
|
||||
#define TT_DT_REG TT_DT_REG // Regular file
|
||||
TT_DT_LNK = 10,
|
||||
#define TT_DT_LNK TT_DT_LNK // Symbolic link
|
||||
TT_DT_SOCK = 12,
|
||||
#define TT_DT_SOCK TT_DT_SOCK // Local-domain socket
|
||||
TT_DT_WHT = 14
|
||||
#define TT_DT_WHT TT_DT_WHT // Whiteout inodes
|
||||
};
|
||||
|
||||
|
||||
std::string getChildPath(const std::string& basePath, const std::string& childPath);
|
||||
|
||||
typedef int (*ScandirFilter)(const struct dirent*);
|
||||
|
||||
typedef bool (*ScandirSort)(const struct dirent&, const struct dirent&);
|
||||
|
||||
bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right);
|
||||
|
||||
int dirent_filter_dot_entries(const struct dirent* entry);
|
||||
|
||||
/**
|
||||
* A scandir()-like implementation that works on ESP32.
|
||||
* It does not return "." and ".." items but otherwise functions the same.
|
||||
* It returns an allocated output array with allocated dirent instances.
|
||||
* The caller is responsible for free-ing the memory of these.
|
||||
*
|
||||
* @param[in] path path the scan for files and directories
|
||||
* @param[out] outList a pointer to vector of dirent
|
||||
* @param[in] filter an optional filter to filter out specific items
|
||||
* @param[in] sort an optional sorting function
|
||||
* @return the amount of items that were stored in "output" or -1 when an error occurred
|
||||
*/
|
||||
int scandir(
|
||||
const std::string& path,
|
||||
std::vector<dirent>& outList,
|
||||
ScandirFilter _Nullable filter,
|
||||
ScandirSort _Nullable sort
|
||||
);
|
||||
|
||||
bool isSupportedExecutableFile(const std::string& filename);
|
||||
bool isSupportedImageFile(const std::string& filename);
|
||||
bool isSupportedTextFile(const std::string& filename);
|
||||
|
||||
} // namespace
|
||||
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/Bundle.h>
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
enum class Mode {
|
||||
Existing = 0,
|
||||
ExistingOrNew = 1
|
||||
};
|
||||
|
||||
Mode getMode(const Bundle& bundle);
|
||||
|
||||
}
|
||||
59
Tactility/Private/Tactility/app/fileselection/State.h
Normal file
@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <dirent.h>
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
class State {
|
||||
|
||||
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
||||
std::vector<dirent> dir_entries;
|
||||
std::string current_path;
|
||||
std::string selected_child_entry;
|
||||
|
||||
public:
|
||||
|
||||
State();
|
||||
|
||||
void freeEntries() {
|
||||
dir_entries.clear();
|
||||
}
|
||||
|
||||
~State() {
|
||||
freeEntries();
|
||||
}
|
||||
|
||||
bool setEntriesForChildPath(const std::string& child_path);
|
||||
bool setEntriesForPath(const std::string& path);
|
||||
|
||||
template <std::invocable<const std::vector<dirent> &> Func>
|
||||
void withEntries(Func&& onEntries) const {
|
||||
mutex.withLock([&]() {
|
||||
std::invoke(std::forward<Func>(onEntries), dir_entries);
|
||||
});
|
||||
}
|
||||
|
||||
bool getDirent(uint32_t index, dirent& dirent);
|
||||
|
||||
void setSelectedChildEntry(const std::string& newFile) {
|
||||
selected_child_entry = newFile;
|
||||
}
|
||||
|
||||
std::string getSelectedChildEntry() const { return selected_child_entry; }
|
||||
std::string getCurrentPath() const { return current_path; }
|
||||
std::string getCurrentPathWithTrailingSlash() const {
|
||||
if (current_path.length() > 1) {
|
||||
return current_path + "/";
|
||||
} else {
|
||||
return current_path;
|
||||
}
|
||||
}
|
||||
|
||||
std::string getSelectedChildPath() const;
|
||||
};
|
||||
|
||||
}
|
||||
44
Tactility/Private/Tactility/app/fileselection/View.h
Normal file
@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "./State.h"
|
||||
#include "./FileSelectionPrivate.h"
|
||||
|
||||
#include "Tactility/app/AppManifest.h"
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::fileselection {
|
||||
|
||||
class View {
|
||||
std::shared_ptr<State> state;
|
||||
|
||||
lv_obj_t* dir_entry_list = nullptr;
|
||||
lv_obj_t* navigate_up_button = nullptr;
|
||||
lv_obj_t* path_textarea = nullptr;
|
||||
lv_obj_t* select_button = nullptr;
|
||||
std::function<void(std::string path)> on_file_selected;
|
||||
|
||||
void onTapFile(const std::string&path, const std::string&filename);
|
||||
static void onSelectButtonPressed(lv_event_t* event);
|
||||
static void onPathTextChanged(lv_event_t* event);
|
||||
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
|
||||
|
||||
public:
|
||||
|
||||
explicit View(const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
|
||||
state(state),
|
||||
on_file_selected(std::move(onFileSelected))
|
||||
{}
|
||||
|
||||
void init(lv_obj_t* parent, Mode mode);
|
||||
void update();
|
||||
|
||||
void onNavigateUpPressed();
|
||||
void onDirEntryPressed(uint32_t index);
|
||||
void onFileSelected(const std::string& path) const {
|
||||
on_file_selected(path);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,13 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <TactilityCore.h>
|
||||
#include <Mutex.h>
|
||||
#include <Thread.h>
|
||||
#include "lvgl.h"
|
||||
#include <Tactility/hal/i2c/I2c.h>
|
||||
#include "Timer.h"
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::i2cscanner {
|
||||
|
||||
void start();
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/service/Service.h"
|
||||
|
||||
#include <Tactility/Mutex.h>
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_http_server.h>
|
||||
#include <Tactility/kernel/SystemEvents.h>
|
||||
|
||||
namespace tt::service::development {
|
||||
|
||||
class DevelopmentService final : public Service {
|
||||
|
||||
Mutex mutex = Mutex(Mutex::Type::Recursive);
|
||||
httpd_handle_t server = nullptr;
|
||||
bool enabled = false;
|
||||
kernel::SystemEventSubscription networkConnectEventSubscription = 0;
|
||||
kernel::SystemEventSubscription networkDisconnectEventSubscription = 0;
|
||||
std::string deviceResponse;
|
||||
|
||||
httpd_uri_t handleGetInfoEndpoint = {
|
||||
.uri = "/info",
|
||||
.method = HTTP_GET,
|
||||
.handler = handleGetInfo,
|
||||
.user_ctx = this
|
||||
};
|
||||
|
||||
httpd_uri_t appRunEndpoint = {
|
||||
.uri = "/app/run",
|
||||
.method = HTTP_POST,
|
||||
.handler = handleAppRun,
|
||||
.user_ctx = this
|
||||
};
|
||||
|
||||
httpd_uri_t appInstallEndpoint = {
|
||||
.uri = "/app/install",
|
||||
.method = HTTP_PUT,
|
||||
.handler = handleAppInstall,
|
||||
.user_ctx = this
|
||||
};
|
||||
|
||||
void onNetworkConnected();
|
||||
void onNetworkDisconnected();
|
||||
|
||||
void startServer();
|
||||
void stopServer();
|
||||
|
||||
static esp_err_t handleGetInfo(httpd_req_t* request);
|
||||
static esp_err_t handleAppRun(httpd_req_t* request);
|
||||
static esp_err_t handleAppInstall(httpd_req_t* request);
|
||||
|
||||
public:
|
||||
|
||||
// region Overrides
|
||||
|
||||
void onStart(ServiceContext& service) override;
|
||||
void onStop(ServiceContext& service) override;
|
||||
|
||||
// endregion Overrides
|
||||
|
||||
// region Internal API
|
||||
|
||||
/**
|
||||
* Enabling the service means that the user is willing to start the web server.
|
||||
* @return true when the service is enabled
|
||||
*/
|
||||
bool isEnabled() const;
|
||||
|
||||
/**
|
||||
* Enabling the service means that the user is willing to start the web server.
|
||||
* @param[in] enabled
|
||||
*/
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
/**
|
||||
* @return true if the service will enable itself when it is started (e.g. on boot, or manual start)
|
||||
*/
|
||||
bool isEnabledOnStart() const;
|
||||
|
||||
/**
|
||||
* Set whether the service should auto-enable when it is started.
|
||||
* @param enabled
|
||||
*/
|
||||
void setEnabledOnStart(bool enabled);
|
||||
|
||||
bool isStarted() const;
|
||||
|
||||
// region Internal API
|
||||
};
|
||||
|
||||
std::shared_ptr<DevelopmentService> findService();
|
||||
|
||||
}
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -33,11 +33,13 @@ namespace app {
|
||||
namespace addgps { extern const AppManifest manifest; }
|
||||
namespace alertdialog { extern const AppManifest manifest; }
|
||||
namespace applist { extern const AppManifest manifest; }
|
||||
namespace boot { extern const AppManifest manifest; }
|
||||
namespace calculator { extern const AppManifest manifest; }
|
||||
namespace chat { extern const AppManifest manifest; }
|
||||
namespace boot { extern const AppManifest manifest; }
|
||||
namespace development { extern const AppManifest manifest; }
|
||||
namespace display { extern const AppManifest manifest; }
|
||||
namespace files { extern const AppManifest manifest; }
|
||||
namespace filebrowser { extern const AppManifest manifest; }
|
||||
namespace fileselection { extern const AppManifest manifest; }
|
||||
namespace gpio { extern const AppManifest manifest; }
|
||||
namespace gpssettings { extern const AppManifest manifest; }
|
||||
namespace i2cscanner { extern const AppManifest manifest; }
|
||||
@ -72,13 +74,15 @@ namespace app {
|
||||
|
||||
// endregion
|
||||
|
||||
// List of all apps excluding Boot app (as Boot app calls this function indirectly)
|
||||
static void registerSystemApps() {
|
||||
addApp(app::addgps::manifest);
|
||||
addApp(app::alertdialog::manifest);
|
||||
addApp(app::applist::manifest);
|
||||
addApp(app::calculator::manifest);
|
||||
addApp(app::display::manifest);
|
||||
addApp(app::files::manifest);
|
||||
addApp(app::filebrowser::manifest);
|
||||
addApp(app::fileselection::manifest);
|
||||
addApp(app::gpio::manifest);
|
||||
addApp(app::gpssettings::manifest);
|
||||
addApp(app::i2cscanner::manifest);
|
||||
@ -107,6 +111,7 @@ static void registerSystemApps() {
|
||||
#ifdef ESP_PLATFORM
|
||||
addApp(app::chat::manifest);
|
||||
addApp(app::crashdiagnostics::manifest);
|
||||
addApp(app::development::manifest);
|
||||
#endif
|
||||
|
||||
if (getConfiguration()->hardware->power != nullptr) {
|
||||
|
||||
@ -20,6 +20,7 @@ namespace service::gps { extern const ServiceManifest manifest; }
|
||||
namespace service::wifi { extern const ServiceManifest manifest; }
|
||||
namespace service::sdcard { extern const ServiceManifest manifest; }
|
||||
#ifdef ESP_PLATFORM
|
||||
namespace service::development { extern const ServiceManifest manifest; }
|
||||
namespace service::espnow { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
|
||||
@ -33,6 +34,7 @@ static void registerAndStartSystemServices() {
|
||||
addService(service::sdcard::manifest);
|
||||
addService(service::wifi::manifest);
|
||||
#ifdef ESP_PLATFORM
|
||||
addService(service::development::manifest);
|
||||
addService(service::espnow::manifest);
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -4,19 +4,19 @@
|
||||
|
||||
namespace tt::app {
|
||||
|
||||
void start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
|
||||
service::loader::startApp(id, std::move(parameters));
|
||||
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> _Nullable parameters) {
|
||||
return service::loader::startApp(id, std::move(parameters));
|
||||
}
|
||||
|
||||
void stop() {
|
||||
service::loader::stopApp();
|
||||
}
|
||||
|
||||
std::shared_ptr<app::AppContext> _Nullable getCurrentAppContext() {
|
||||
std::shared_ptr<AppContext> _Nullable getCurrentAppContext() {
|
||||
return service::loader::getCurrentAppContext();
|
||||
}
|
||||
|
||||
std::shared_ptr<app::App> _Nullable getCurrentApp() {
|
||||
std::shared_ptr<App> _Nullable getCurrentApp() {
|
||||
return service::loader::getCurrentApp();
|
||||
}
|
||||
|
||||
|
||||
@ -36,8 +36,6 @@ static ElfManifest elfManifest;
|
||||
|
||||
class ElfApp : public App {
|
||||
|
||||
private:
|
||||
|
||||
const std::string filePath;
|
||||
std::unique_ptr<uint8_t[]> elfFileData;
|
||||
esp_elf_t elf;
|
||||
@ -143,9 +141,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> resultBundle) override {
|
||||
void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultBundle) override {
|
||||
if (manifest != nullptr && manifest->onResult != nullptr) {
|
||||
manifest->onResult(&appContext, data, result, resultBundle.get());
|
||||
manifest->onResult(&appContext, data, launchId, result, resultBundle.get());
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -179,7 +177,7 @@ std::string getElfAppId(const std::string& filePath) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
bool registerElfApp(const std::string& filePath) {
|
||||
void registerElfApp(const std::string& filePath) {
|
||||
if (findAppById(filePath) == nullptr) {
|
||||
auto manifest = AppManifest {
|
||||
.id = getElfAppId(filePath),
|
||||
@ -189,7 +187,6 @@ bool registerElfApp(const std::string& filePath) {
|
||||
};
|
||||
addApp(manifest);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest) {
|
||||
|
||||
163
Tactility/Source/app/development/Development.cpp
Normal file
@ -0,0 +1,163 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include "Tactility/app/AppManifest.h"
|
||||
#include "Tactility/lvgl/Style.h"
|
||||
#include "Tactility/lvgl/Toolbar.h"
|
||||
#include "Tactility/service/development/DevelopmentService.h"
|
||||
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
#include <cstring>
|
||||
#include <lvgl.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/service/wifi/Wifi.h>
|
||||
|
||||
namespace tt::app::development {
|
||||
|
||||
constexpr const char* TAG = "Development";
|
||||
|
||||
class DevelopmentApp final : public App {
|
||||
|
||||
lv_obj_t* enableSwitch = nullptr;
|
||||
lv_obj_t* enableOnBootSwitch = nullptr;
|
||||
lv_obj_t* statusLabel = nullptr;
|
||||
std::shared_ptr<service::development::DevelopmentService> service;
|
||||
|
||||
Timer timer = Timer(Timer::Type::Periodic, [this] {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
if (lock.lock(lvgl::defaultLockTime)) {
|
||||
updateViewState();
|
||||
}
|
||||
});
|
||||
|
||||
static void onEnableSwitchChanged(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* widget = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(widget, LV_STATE_CHECKED);
|
||||
auto* app = static_cast<DevelopmentApp*>(lv_event_get_user_data(event));
|
||||
bool is_changed = is_on != app->service->isEnabled();
|
||||
if (is_changed) {
|
||||
app->service->setEnabled(is_on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void onEnableOnBootSwitchChanged(lv_event_t* event) {
|
||||
lv_event_code_t code = lv_event_get_code(event);
|
||||
auto* widget = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
if (code == LV_EVENT_VALUE_CHANGED) {
|
||||
bool is_on = lv_obj_has_state(widget, LV_STATE_CHECKED);
|
||||
auto* app = static_cast<DevelopmentApp*>(lv_event_get_user_data(event));
|
||||
bool is_changed = is_on != app->service->isEnabledOnStart();
|
||||
if (is_changed) {
|
||||
app->service->setEnabledOnStart(is_on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateViewState() {
|
||||
if (!service->isEnabled()) {
|
||||
lv_label_set_text(statusLabel, "Service disabled");
|
||||
} else if (!service->isStarted()) {
|
||||
lv_label_set_text(statusLabel, "Waiting for connection...");
|
||||
} else { // enabled and started
|
||||
auto ip = service::wifi::getIp();
|
||||
if (ip.empty()) {
|
||||
lv_label_set_text(statusLabel, "Waiting for IP...");
|
||||
} else {
|
||||
std::string status = std::string("Available at ") + ip;
|
||||
lv_label_set_text(statusLabel, status.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onCreate(AppContext& appContext) override {
|
||||
service = service::development::findService();
|
||||
if (service == nullptr) {
|
||||
TT_LOG_E(TAG, "Service not found");
|
||||
service::loader::stopApp();
|
||||
}
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
|
||||
// Toolbar
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_t* toolbar = lvgl::toolbar_create(parent, app);
|
||||
|
||||
enableSwitch = lvgl::toolbar_add_switch_action(toolbar);
|
||||
lv_obj_add_event_cb(enableSwitch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
|
||||
if (service->isEnabled()) {
|
||||
lv_obj_add_state(enableSwitch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(enableSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
// Wrappers
|
||||
|
||||
lv_obj_t* secondary_flex = lv_obj_create(parent);
|
||||
lv_obj_set_width(secondary_flex, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(secondary_flex, 1);
|
||||
lv_obj_set_flex_flow(secondary_flex, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(secondary_flex, 0, 0);
|
||||
lv_obj_set_style_pad_all(secondary_flex, 0, 0);
|
||||
lv_obj_set_style_pad_gap(secondary_flex, 0, 0);
|
||||
lvgl::obj_set_style_bg_invisible(secondary_flex);
|
||||
|
||||
// align() methods don't work on flex, so we need this extra wrapper
|
||||
lv_obj_t* wrapper = lv_obj_create(secondary_flex);
|
||||
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
|
||||
// Enable on boot
|
||||
|
||||
lv_obj_t* enable_label = lv_label_create(wrapper);
|
||||
lv_label_set_text(enable_label, "Enable on boot");
|
||||
lv_obj_align(enable_label, LV_ALIGN_TOP_LEFT, 0, 6);
|
||||
|
||||
enableOnBootSwitch = lv_switch_create(wrapper);
|
||||
lv_obj_add_event_cb(enableOnBootSwitch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_obj_align(enableOnBootSwitch, LV_ALIGN_TOP_RIGHT, 0, 0);
|
||||
if (service->isEnabledOnStart()) {
|
||||
lv_obj_add_state(enableOnBootSwitch, LV_STATE_CHECKED);
|
||||
} else {
|
||||
lv_obj_remove_state(enableOnBootSwitch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
statusLabel = lv_label_create(wrapper);
|
||||
lv_obj_align(statusLabel, LV_ALIGN_TOP_LEFT, 0, 50);
|
||||
|
||||
updateViewState();
|
||||
|
||||
timer.start(1000);
|
||||
}
|
||||
|
||||
void onHide(AppContext& appContext) override {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
// Ensure that the update isn't already happening
|
||||
lock.lock();
|
||||
timer.stop();
|
||||
}
|
||||
};
|
||||
|
||||
extern const AppManifest manifest = {
|
||||
.id = "Development",
|
||||
.name = "Development",
|
||||
.type = Type::Settings,
|
||||
.createApp = create<DevelopmentApp>
|
||||
};
|
||||
|
||||
void start() {
|
||||
app::start(manifest.id);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#endif // ESP_PLATFORM
|
||||
@ -1,5 +1,5 @@
|
||||
#include "Tactility/app/files/View.h"
|
||||
#include "Tactility/app/files/State.h"
|
||||
#include "Tactility/app/filebrowser/View.h"
|
||||
#include "Tactility/app/filebrowser/State.h"
|
||||
#include "Tactility/app/AppContext.h"
|
||||
|
||||
#include <Tactility/Assets.h>
|
||||
@ -7,18 +7,18 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace tt::app::files {
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
#define TAG "files_app"
|
||||
#define TAG "filebrowser_app"
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
class FilesApp : public App {
|
||||
class FileBrowser : public App {
|
||||
std::unique_ptr<View> view;
|
||||
std::shared_ptr<State> state;
|
||||
|
||||
public:
|
||||
FilesApp() {
|
||||
FileBrowser() {
|
||||
state = std::make_shared<State>();
|
||||
view = std::make_unique<View>(state);
|
||||
}
|
||||
@ -27,7 +27,7 @@ public:
|
||||
view->init(parent);
|
||||
}
|
||||
|
||||
void onResult(AppContext& appContext, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle) override {
|
||||
view->onResult(result, std::move(bundle));
|
||||
}
|
||||
};
|
||||
@ -37,7 +37,7 @@ extern const AppManifest manifest = {
|
||||
.name = "Files",
|
||||
.icon = TT_ASSETS_APP_ICON_FILES,
|
||||
.type = Type::Hidden,
|
||||
.createApp = create<FilesApp>
|
||||
.createApp = create<FileBrowser>
|
||||
};
|
||||
|
||||
void start() {
|
||||
@ -1,6 +1,6 @@
|
||||
#include "Tactility/app/files/State.h"
|
||||
#include "Tactility/app/files/FileUtils.h"
|
||||
#include "Tactility/app/filebrowser/State.h"
|
||||
|
||||
#include <Tactility/file/File.h>
|
||||
#include "Tactility/hal/sdcard/SdCardDevice.h"
|
||||
#include <Tactility/Log.h>
|
||||
#include <Tactility/Partitions.h>
|
||||
@ -11,9 +11,9 @@
|
||||
#include <vector>
|
||||
#include <dirent.h>
|
||||
|
||||
#define TAG "files_app"
|
||||
#define TAG "filebrowser_app"
|
||||
|
||||
namespace tt::app::files {
|
||||
namespace tt::app::filebrowser {
|
||||
|
||||
State::State() {
|
||||
if (kernel::getPlatform() == kernel::PlatformSimulator) {
|
||||
@ -30,7 +30,7 @@ State::State() {
|
||||
}
|
||||
|
||||
std::string State::getSelectedChildPath() const {
|
||||
return getChildPath(current_path, selected_child_entry);
|
||||
return file::getChildPath(current_path, selected_child_entry);
|
||||
}
|
||||
|
||||
bool State::setEntriesForPath(const std::string& path) {
|
||||
@ -52,12 +52,12 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
dir_entries.clear();
|
||||
dir_entries.push_back(dirent{
|
||||
.d_ino = 0,
|
||||
.d_type = TT_DT_DIR,
|
||||
.d_type = file::TT_DT_DIR,
|
||||
.d_name = SYSTEM_PARTITION_NAME
|
||||
});
|
||||
dir_entries.push_back(dirent{
|
||||
.d_ino = 1,
|
||||
.d_type = TT_DT_DIR,
|
||||
.d_type = file::TT_DT_DIR,
|
||||
.d_name = DATA_PARTITION_NAME
|
||||
});
|
||||
|
||||
@ -68,7 +68,7 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
auto mount_name = sdcard->getMountPath().substr(1);
|
||||
auto dir_entry = dirent {
|
||||
.d_ino = 2,
|
||||
.d_type = TT_DT_DIR,
|
||||
.d_type = file::TT_DT_DIR,
|
||||
.d_name = { 0 }
|
||||
};
|
||||
assert(mount_name.length() < sizeof(dirent::d_name));
|
||||
@ -83,7 +83,7 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
return true;
|
||||
} else {
|
||||
dir_entries.clear();
|
||||
int count = tt::app::files::scandir(path, dir_entries, &dirent_filter_dot_entries, dirent_sort_alpha_and_type);
|
||||
int count = file::scandir(path, dir_entries, &file::direntFilterDotEntries, file::direntSortAlphaAndType);
|
||||
if (count >= 0) {
|
||||
TT_LOG_I(TAG, "%s has %u entries", path.c_str(), count);
|
||||
current_path = path;
|
||||
@ -97,8 +97,8 @@ bool State::setEntriesForPath(const std::string& path) {
|
||||
}
|
||||
}
|
||||
|
||||
bool State::setEntriesForChildPath(const std::string& child_path) {
|
||||
auto path = getChildPath(current_path, child_path);
|
||||
bool State::setEntriesForChildPath(const std::string& childPath) {
|
||||
auto path = file::getChildPath(current_path, childPath);
|
||||
TT_LOG_I(TAG, "Navigating from %s to %s", current_path.c_str(), path.c_str());
|
||||
return setEntriesForPath(path);
|
||||
}
|
||||