diff --git a/.github/actions/build-firmware/action.yml b/.github/actions/build-firmware/action.yml index 4c44b347..fbf826c6 100644 --- a/.github/actions/build-firmware/action.yml +++ b/.github/actions/build-firmware/action.yml @@ -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 }} diff --git a/.github/actions/build-sdk/action.yml b/.github/actions/build-sdk/action.yml index 4348680a..96de2367 100644 --- a/.github/actions/build-sdk/action.yml +++ b/.github/actions/build-sdk/action.yml @@ -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: diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index 47d58ab5..702b7799 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -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: diff --git a/App/Kconfig b/App/Kconfig index 73f961c2..c845a76c 100644 --- a/App/Kconfig +++ b/App/Kconfig @@ -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 diff --git a/App/Source/Boards.h b/App/Source/Boards.h index 3f9697af..bb4a67d7 100644 --- a/App/Source/Boards.h +++ b/App/Source/Boards.h @@ -1,4 +1,5 @@ #pragma once +#include #ifdef ESP_PLATFORM #include @@ -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 \ No newline at end of file +#endif // ESP_PLATFORM diff --git a/App/idf_component.yml b/App/idf_component.yml index a9f6aa56..4dbbcb52 100644 --- a/App/idf_component.yml +++ b/App/idf_component.yml @@ -12,6 +12,8 @@ dependencies: version: "1.1.1" rules: - if: "target in [esp32s3, esp32p4]" + espressif/esp_lcd_st7796: + version: "1.3.2" espressif/esp_lcd_panel_io_additions: "1.0.1" espressif/esp_tinyusb: version: "1.5.0" diff --git a/Boards/LilygoTLoraPager/CMakeLists.txt b/Boards/LilygoTLoraPager/CMakeLists.txt new file mode 100644 index 00000000..3e3a7c3c --- /dev/null +++ b/Boards/LilygoTLoraPager/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd ST7796 BQ27220 TCA8418 PwmBacklight driver esp_adc +) diff --git a/Boards/LilygoTLoraPager/Source/Init.cpp b/Boards/LilygoTLoraPager/Source/Init.cpp new file mode 100644 index 00000000..72a70096 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/Init.cpp @@ -0,0 +1,55 @@ +#include "PwmBacklight.h" +#include "Tactility/kernel/SystemEvents.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +#include + +#include +#include + +#define TAG "tpager" + +// Power on +#define TDECK_POWERON_GPIO GPIO_NUM_10 + +std::shared_ptr bq27220; +std::shared_ptr tca8418; + +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(I2C_NUM_0); + tt::hal::registerDevice(bq27220); + + tca8418 = std::make_shared(I2C_NUM_0); + tt::hal::registerDevice(tca8418); + + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { + bq27220->configureCapacity(1500, 1500); + + auto gps_service = tt::service::gps::findGpsService(); + if (gps_service != nullptr) { + std::vector gps_configurations; + gps_service->getGpsConfigurations(gps_configurations); + if (gps_configurations.empty()) { + if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "Grove", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) { + TT_LOG_I(TAG, "Configured internal GPS"); + } else { + TT_LOG_E(TAG, "Failed to configure internal GPS"); + } + } + } + }); + return true; +} diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp new file mode 100644 index 00000000..f37e054c --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp @@ -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 + +#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, + } + } + }} +}; diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h new file mode 100644 index 00000000..b3e010fe --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern const tt::hal::Configuration lilygo_tlora_pager; diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp new file mode 100644 index 00000000..30dceb9a --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp @@ -0,0 +1,30 @@ +#include "TpagerDisplay.h" +#include "TpagerDisplayConstants.h" + +#include +#include + +#include + +#define TAG "TPAGER_display" + +std::shared_ptr createDisplay() { + auto configuration = std::make_unique( + 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(std::move(configuration)); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h new file mode 100644 index 00000000..df09ebc9 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" +#include +#include + +class TpagerDisplay : public tt::hal::display::DisplayDevice { + +private: + + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + bool poweredOn = false; + +public: + + std::string getName() const final { return "ST7796"; } + std::string getDescription() const final { return "SPI display"; } + + bool start() override; + + bool stop() override; + + void setPowerOn(bool turnOn) override; + bool isPoweredOn() const override { return poweredOn; }; + bool supportsPowerControl() const override { return true; } + + std::shared_ptr _Nullable createTouch() override; + + void setBacklightDuty(uint8_t backlightDuty) override; + bool supportsBacklightDuty() const override { return true; } + + void setGammaCurve(uint8_t index) override; + uint8_t getGammaCurveCount() const override { return 4; }; + + lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h new file mode 100644 index 00000000..98847e46 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h @@ -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) diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp new file mode 100644 index 00000000..93b31d4f --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp @@ -0,0 +1,359 @@ +#include "TpagerKeyboard.h" +#include +#include + +#include "driver/gpio.h" +#include "freertos/queue.h" + +#include + +#define TAG "tpager_keyboard" + +#define ENCODER_A GPIO_NUM_40 +#define ENCODER_B GPIO_NUM_41 +#define ENCODER_ENTER GPIO_NUM_7 +#define BACKLIGHT GPIO_NUM_46 + +#define KB_ROWS 4 +#define KB_COLS 11 + +// Lowercase Keymap +static constexpr char keymap_lc[KB_ROWS][KB_COLS] = { + {'\0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\n', '\0'}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +// Uppercase Keymap +static constexpr char keymap_uc[KB_ROWS][KB_COLS] = { + {'\0', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'}, + {'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '\n', '\0'}, + {'Z', 'X', 'C', 'V', 'B', 'N', 'M', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +// Symbol Keymap +static constexpr char keymap_sy[KB_ROWS][KB_COLS] = { + {'\0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'.', '/', '+', '-', '=', ':', '\'', '"', '@', '\t', '\0'}, + {'_', '$', ';', '?', '!', ',', '.', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +static QueueHandle_t keyboardMsg; + +static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev); + static bool enter_prev = false; + char keypress = 0; + + // Defaults + data->key = 0; + data->state = LV_INDEV_STATE_RELEASED; + + if (xQueueReceive(keyboardMsg, &keypress, pdMS_TO_TICKS(50)) == pdPASS) { + data->key = keypress; + data->state = LV_INDEV_STATE_PRESSED; + } +} + +static void encoder_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev); + const int enter_filter_threshold = 2; + static int enter_filter = 0; + const int pulses_click = 4; + static int pulses_prev = 0; + bool anyinput = false; + + // Defaults + data->enc_diff = 0; + data->state = LV_INDEV_STATE_RELEASED; + + int pulses = kb->getEncoderPulses(); + int pulse_diff = (pulses - pulses_prev); + if ((pulse_diff > pulses_click) || (pulse_diff < -pulses_click)) { + data->enc_diff = pulse_diff / pulses_click; + pulses_prev = pulses; + anyinput = true; + } + + bool enter = !gpio_get_level(ENCODER_ENTER); + if (enter && (enter_filter < enter_filter_threshold)) { + enter_filter++; + } + if (!enter && (enter_filter > 0)) { + enter_filter--; + } + + if (enter_filter == enter_filter_threshold) { + data->state = LV_INDEV_STATE_PRESSED; + anyinput = true; + } + + if (anyinput) { + kb->makeBacklightImpulse(); + } +} + +void TpagerKeyboard::processKeyboard() { + static bool shift_pressed = false; + static bool sym_pressed = false; + static bool cap_toggle = false; + static bool cap_toggle_armed = true; + bool anykey_pressed = false; + + if (keypad->update()) { + anykey_pressed = (keypad->pressed_key_count > 0); + for (int i = 0; i < keypad->pressed_key_count; i++) { + auto row = keypad->pressed_list[i].row; + auto col = keypad->pressed_list[i].col; + auto hold = keypad->pressed_list[i].hold_time; + + if ((row == 1) && (col == 10)) { + sym_pressed = true; + } + if ((row == 2) && (col == 7)) { + shift_pressed = true; + } + } + + if ((sym_pressed && shift_pressed) && cap_toggle_armed) { + cap_toggle = !cap_toggle; + cap_toggle_armed = false; + } + + for (int i = 0; i < keypad->pressed_key_count; i++) { + auto row = keypad->pressed_list[i].row; + auto col = keypad->pressed_list[i].col; + auto hold = keypad->pressed_list[i].hold_time; + char chr = '\0'; + if (sym_pressed) { + chr = keymap_sy[row][col]; + } else if (shift_pressed || cap_toggle) { + chr = keymap_uc[row][col]; + } else { + chr = keymap_lc[row][col]; + } + + if (chr != '\0') xQueueSend(keyboardMsg, (void*)&chr, portMAX_DELAY); + } + + for (int i = 0; i < keypad->released_key_count; i++) { + auto row = keypad->released_list[i].row; + auto col = keypad->released_list[i].col; + + if ((row == 1) && (col == 10)) { + sym_pressed = false; + } + if ((row == 2) && (col == 7)) { + shift_pressed = false; + } + } + + if ((!sym_pressed && !shift_pressed) && !cap_toggle_armed) { + cap_toggle_armed = true; + } + + if (anykey_pressed) { + makeBacklightImpulse(); + } + } +} + +bool TpagerKeyboard::start(lv_display_t* display) { + backlightOkay = initBacklight(BACKLIGHT, 30000, LEDC_TIMER_0, LEDC_CHANNEL_1); + initEncoder(); + keypad->init(KB_ROWS, KB_COLS); + gpio_input_enable(ENCODER_ENTER); + + assert(inputTimer == nullptr); + inputTimer = std::make_unique(tt::Timer::Type::Periodic, [this] { + processKeyboard(); + }); + + assert(backlightImpulseTimer == nullptr); + backlightImpulseTimer = std::make_unique(tt::Timer::Type::Periodic, [this] { + processBacklightImpuse(); + }); + + kbHandle = lv_indev_create(); + lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(kbHandle, &keyboard_read_callback); + lv_indev_set_display(kbHandle, display); + lv_indev_set_user_data(kbHandle, this); + + encHandle = lv_indev_create(); + lv_indev_set_type(encHandle, LV_INDEV_TYPE_ENCODER); + lv_indev_set_read_cb(encHandle, &encoder_read_callback); + lv_indev_set_display(encHandle, display); + lv_indev_set_user_data(encHandle, this); + + inputTimer->start(20 / portTICK_PERIOD_MS); + backlightImpulseTimer->start(50 / portTICK_PERIOD_MS); + + return true; +} + +bool TpagerKeyboard::stop() { + assert(inputTimer); + inputTimer->stop(); + inputTimer = nullptr; + + assert(backlightImpulseTimer); + backlightImpulseTimer->stop(); + backlightImpulseTimer = nullptr; + + lv_indev_delete(kbHandle); + kbHandle = nullptr; + lv_indev_delete(encHandle); + encHandle = nullptr; + return true; +} + +bool TpagerKeyboard::isAttached() const { + return tt::hal::i2c::masterHasDeviceAtAddress(keypad->getPort(), keypad->getAddress(), 100); +} + +void TpagerKeyboard::initEncoder(void) { + const int low_limit = -127; + const int high_limit = 126; + + // Accum. count makes it that over- and underflows are automatically compensated. + // Prerequisite: watchpoints at low and high limit + pcnt_unit_config_t unit_config = { + .low_limit = low_limit, + .high_limit = high_limit, + .flags = {.accum_count = 1}, + }; + + 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; +std::shared_ptr createKeyboard() { + keyboardMsg = xQueueCreate(20, sizeof(char)); + + return std::make_shared(tca8418); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h new file mode 100644 index 00000000..1aba8c08 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include + + +class TpagerKeyboard : public tt::hal::keyboard::KeyboardDevice { + +private: + + lv_indev_t* _Nullable kbHandle = nullptr; + lv_indev_t* _Nullable encHandle = nullptr; + pcnt_unit_handle_t encPcntUnit = nullptr; + gpio_num_t backlightPin = GPIO_NUM_NC; + ledc_timer_t backlightTimer; + ledc_channel_t backlightChannel; + bool backlightOkay = false; + int backlightImpulseDuty = 0; + + std::shared_ptr keypad; + std::unique_ptr inputTimer; + std::unique_ptr backlightImpulseTimer; + + void initEncoder(void); + bool initBacklight(gpio_num_t pin, uint32_t frequencyHz, ledc_timer_t timer, ledc_channel_t channel); + void processKeyboard(); + void processBacklightImpuse(); + +public: + + TpagerKeyboard(std::shared_ptr tca) : keypad(std::move(tca)) {} + ~TpagerKeyboard() {} + + std::string getName() const final { return "T-Lora Pager Keyboard"; } + std::string getDescription() const final { return "I2C keyboard with encoder"; } + + bool start(lv_display_t* display) override; + bool stop() override; + bool isAttached() const override; + lv_indev_t* _Nullable getLvglIndev() override { return kbHandle; } + + int getEncoderPulses(); + bool setBacklightDuty(uint8_t duty); + void makeBacklightImpulse(); +}; + +std::shared_ptr createKeyboard(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp new file mode 100644 index 00000000..d39e3216 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp @@ -0,0 +1,90 @@ +#include "TpagerPower.h" + +#include + +#define TAG "power" + +#define TPAGER_GAUGE_I2C_BUS_HANDLE I2C_NUM_0 + +/* +TpagerPower::TpagerPower() : gauge(TPAGER_GAUGE_I2C_BUS_HANDLE) { + gauge->configureCapacity(1500, 1500); +}*/ + +TpagerPower::~TpagerPower() {} + +bool TpagerPower::supportsMetric(MetricType type) const { + switch (type) { + using enum MetricType; + case IsCharging: + case Current: + case BatteryVoltage: + case ChargeLevel: + return true; + default: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TpagerPower::getMetric(MetricType type, MetricData& data) { + /* IsCharging, // bool + Current, // int32_t, mAh - battery current: either during charging (positive value) or discharging (negative value) + BatteryVoltage, // uint32_t, mV + ChargeLevel, // uint8_t [0, 100] +*/ + + uint16_t u16 = 0; + int16_t s16 = 0; + switch (type) { + using enum MetricType; + case IsCharging: + Bq27220::BatteryStatus status; + if (gauge->getBatteryStatus(status)) { + data.valueAsBool = !status.reg.DSG; + return true; + } + return false; + break; + case Current: + if (gauge->getCurrent(s16)) { + data.valueAsInt32 = s16; + return true; + } else { + return false; + } + break; + case BatteryVoltage: + if (gauge->getVoltage(u16)) { + data.valueAsUint32 = u16; + return true; + } else { + return false; + } + break; + case ChargeLevel: + if (gauge->getStateOfCharge(u16)) { + data.valueAsUint8 = u16; + return true; + } else { + return false; + } + break; + default: + return false; + break; + } + + return false; // Safety guard for when new enum values are introduced +} + +static std::shared_ptr power; +extern std::shared_ptr bq27220; + +std::shared_ptr tpager_get_power() { + if (power == nullptr) { + power = std::make_shared(bq27220); + } + return power; +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h new file mode 100644 index 00000000..a215d3e7 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h @@ -0,0 +1,26 @@ +#pragma once + +#include "Tactility/hal/power/PowerDevice.h" +#include +#include + +using tt::hal::power::PowerDevice; + +class TpagerPower : public PowerDevice { + std::shared_ptr gauge; + +public: + + TpagerPower(std::shared_ptr bq) : gauge(std::move(bq)) {} + ~TpagerPower(); + + std::string getName() const final { return "T-LoRa Pager Power measument"; } + std::string getDescription() const final { return "Power measurement interface via I2C fuel gauge"; } + + bool supportsMetric(MetricType type) const override; + bool getMetric(MetricType type, MetricData& data) override; + +private: +}; + +std::shared_ptr tpager_get_power(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp new file mode 100644 index 00000000..38340ec2 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp @@ -0,0 +1,31 @@ +#include "TpagerSdCard.h" + +#include +#include + +#include + +using tt::hal::sdcard::SpiSdCardDevice; + +#define 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 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(configuration) + ); + + return std::shared_ptr(sdcard); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h new file mode 100644 index 00000000..95da81a6 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Tactility/hal/sdcard/SdCardDevice.h" + +using tt::hal::sdcard::SdCardDevice; + +std::shared_ptr createTpagerSdCard(); diff --git a/Boards/Simulator/Source/Simulator.h b/Boards/Simulator/Source/Simulator.h index 340e6c2e..34da7869 100644 --- a/Boards/Simulator/Source/Simulator.h +++ b/Boards/Simulator/Source/Simulator.h @@ -1,7 +1,6 @@ #pragma once #include "Main.h" -#include namespace simulator { /** Set the function pointer of the real app_main() */ diff --git a/Buildscripts/board.cmake b/Buildscripts/board.cmake index 25f21742..8b615b86 100644 --- a/Buildscripts/board.cmake +++ b/Buildscripts/board.cmake @@ -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 () diff --git a/Buildscripts/build-and-release-all.sh b/Buildscripts/build-and-release-all.sh index db6229d3..8472761c 100755 --- a/Buildscripts/build-and-release-all.sh +++ b/Buildscripts/build-and-release-all.sh @@ -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 diff --git a/Buildscripts/build.sh b/Buildscripts/build.sh index 9f7b4af3..b4abca05 100755 --- a/Buildscripts/build.sh +++ b/Buildscripts/build.sh @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index d7907658..6ff5f4c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/COPYRIGHT.md b/COPYRIGHT.md index 671b0ceb..943b3a4e 100644 --- a/COPYRIGHT.md +++ b/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. diff --git a/Data/system/app/Launcher/icon_apps.png b/Data/system/app/Launcher/icon_apps.png index 9381aaa4..d7500276 100644 Binary files a/Data/system/app/Launcher/icon_apps.png and b/Data/system/app/Launcher/icon_apps.png differ diff --git a/Data/system/app/Launcher/icon_files.png b/Data/system/app/Launcher/icon_files.png index 8e6c6055..e0919072 100644 Binary files a/Data/system/app/Launcher/icon_files.png and b/Data/system/app/Launcher/icon_files.png differ diff --git a/Data/system/app/Launcher/icon_settings.png b/Data/system/app/Launcher/icon_settings.png index cdb52d7b..ba33da9f 100644 Binary files a/Data/system/app/Launcher/icon_settings.png and b/Data/system/app/Launcher/icon_settings.png differ diff --git a/Data/system_sources/app/Desktop/apps.svg b/Data/system_sources/app/Desktop/apps.svg deleted file mode 100644 index bb14bc03..00000000 --- a/Data/system_sources/app/Desktop/apps.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Data/system_sources/app/Desktop/folder.svg b/Data/system_sources/app/Desktop/folder.svg deleted file mode 100644 index 021db149..00000000 --- a/Data/system_sources/app/Desktop/folder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Data/system_sources/app/Desktop/settings.svg b/Data/system_sources/app/Desktop/settings.svg deleted file mode 100644 index 63cebb1c..00000000 --- a/Data/system_sources/app/Desktop/settings.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Data/system_sources/app/Launcher/apps.svg b/Data/system_sources/app/Launcher/apps.svg new file mode 100644 index 00000000..3d4efc50 --- /dev/null +++ b/Data/system_sources/app/Launcher/apps.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/Data/system_sources/app/Launcher/folder.svg b/Data/system_sources/app/Launcher/folder.svg new file mode 100644 index 00000000..298363ee --- /dev/null +++ b/Data/system_sources/app/Launcher/folder.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/Data/system_sources/app/Launcher/settings.svg b/Data/system_sources/app/Launcher/settings.svg new file mode 100644 index 00000000..45925e90 --- /dev/null +++ b/Data/system_sources/app/Launcher/settings.svg @@ -0,0 +1,42 @@ + + + + + + diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 15188b10..34db7bb5 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -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 diff --git a/Documentation/pics/screenshot-AppList.png b/Documentation/pics/screenshot-AppList.png index 0dac67e4..c5e8d194 100644 Binary files a/Documentation/pics/screenshot-AppList.png and b/Documentation/pics/screenshot-AppList.png differ diff --git a/Documentation/pics/screenshot-Desktop.png b/Documentation/pics/screenshot-Desktop.png deleted file mode 100644 index c565ac8e..00000000 Binary files a/Documentation/pics/screenshot-Desktop.png and /dev/null differ diff --git a/Documentation/pics/screenshot-Files.png b/Documentation/pics/screenshot-Files.png deleted file mode 100644 index 5d7d3e9b..00000000 Binary files a/Documentation/pics/screenshot-Files.png and /dev/null differ diff --git a/Documentation/pics/screenshot-HelloWorld.png b/Documentation/pics/screenshot-HelloWorld.png deleted file mode 100644 index 6acd44fa..00000000 Binary files a/Documentation/pics/screenshot-HelloWorld.png and /dev/null differ diff --git a/Documentation/pics/screenshot-Launcher.png b/Documentation/pics/screenshot-Launcher.png new file mode 100644 index 00000000..b5e4bd97 Binary files /dev/null and b/Documentation/pics/screenshot-Launcher.png differ diff --git a/Documentation/pics/screenshot-Settings.png b/Documentation/pics/screenshot-Settings.png index 917749a2..3809d80f 100644 Binary files a/Documentation/pics/screenshot-Settings.png and b/Documentation/pics/screenshot-Settings.png differ diff --git a/Documentation/pics/screenshot-WifiManage.png b/Documentation/pics/screenshot-WifiManage.png deleted file mode 100644 index 0c1478fc..00000000 Binary files a/Documentation/pics/screenshot-WifiManage.png and /dev/null differ diff --git a/Documentation/releasing.md b/Documentation/releasing.md new file mode 100644 index 00000000..be83f7a4 --- /dev/null +++ b/Documentation/releasing.md @@ -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) \ No newline at end of file diff --git a/Drivers/BQ24295/Source/Bq24295.cpp b/Drivers/BQ24295/Source/Bq24295.cpp index 22e94b13..4dfae7bf 100644 --- a/Drivers/BQ24295/Source/Bq24295.cpp +++ b/Drivers/BQ24295/Source/Bq24295.cpp @@ -8,10 +8,10 @@ * https://gitlab.com/hamishcunningham/unphonelibrary/-/blob/main/unPhone.h?ref_type=heads */ namespace registers { - static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl - static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control - static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status - static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status +static const uint8_t CHARGE_TERMINATION = 0x05U; // Datasheet page 35: Charge end/timer cntrl +static const uint8_t OPERATION_CONTROL = 0x07U; // Datasheet page 37: Misc operation control +static const uint8_t STATUS = 0x08U; // Datasheet page 38: System status +static const uint8_t VERSION = 0x0AU; // Datasheet page 38: Vendor/part/revision status } // namespace registers bool Bq24295::readChargeTermination(uint8_t& out) const { diff --git a/Drivers/BQ27220/CMakeLists.txt b/Drivers/BQ27220/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/BQ27220/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/BQ27220/README.md b/Drivers/BQ27220/README.md new file mode 100644 index 00000000..6a9240a5 --- /dev/null +++ b/Drivers/BQ27220/README.md @@ -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) diff --git a/Drivers/BQ27220/Source/Bq27220.cpp b/Drivers/BQ27220/Source/Bq27220.cpp new file mode 100644 index 00000000..cbf31ebb --- /dev/null +++ b/Drivers/BQ27220/Source/Bq27220.cpp @@ -0,0 +1,370 @@ +#include "Bq27220.h" +#include + +#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(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; +} diff --git a/Drivers/BQ27220/Source/Bq27220.h b/Drivers/BQ27220/Source/Bq27220.h new file mode 100644 index 00000000..be96f551 --- /dev/null +++ b/Drivers/BQ27220/Source/Bq27220.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +#define BQ27220_ADDRESS 0x55 + +class Bq27220 final : public tt::hal::i2c::I2cDevice { + +private: + uint32_t accessKey; + + bool unsealDevice(); + bool unsealFullAccess(); + bool exitSealMode(); + bool sendSubCommand(uint16_t subCmd, bool waitConfirm = false); + bool writeConfig16(uint16_t address, uint16_t value); + bool configPreamble(bool &isSealed); + bool configEpilouge(const bool isSealed); + + template + bool performConfigUpdate(T configUpdateFunc) + { + bool isSealed = false; + + if (!configPreamble(isSealed)) { + return false; + } + bool result = configUpdateFunc(); + configEpilouge(isSealed); + + return result; + } + +public: + // 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); +}; diff --git a/Drivers/ST7796/CMakeLists.txt b/Drivers/ST7796/CMakeLists.txt new file mode 100644 index 00000000..983cc248 --- /dev/null +++ b/Drivers/ST7796/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd_st7796 driver +) diff --git a/Drivers/ST7796/README.md b/Drivers/ST7796/README.md new file mode 100644 index 00000000..eae8f783 --- /dev/null +++ b/Drivers/ST7796/README.md @@ -0,0 +1,3 @@ +# ST7796 + +A basic ESP32 LVGL driver for ST7796 displays. diff --git a/Drivers/ST7796/Source/St7796Display.cpp b/Drivers/ST7796/Source/St7796Display.cpp new file mode 100644 index 00000000..22ae268f --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.cpp @@ -0,0 +1,210 @@ +#include "St7796Display.h" + +#include + +#include +#include +#include +#include + +#define TAG "st7796" + +bool St7796Display::start() { + TT_LOG_I(TAG, "Starting"); + + const esp_lcd_panel_io_spi_config_t panel_io_config = { + .cs_gpio_num = configuration->csPin, + .dc_gpio_num = configuration->dcPin, + .spi_mode = 0, + .pclk_hz = configuration->pixelClockFrequency, + .trans_queue_depth = configuration->transactionQueueDepth, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .flags = { + .dc_high_on_cmd = 0, + .dc_low_on_data = 0, + .dc_low_on_param = 0, + .octal_mode = 0, + .quad_mode = 0, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0 + } + }; + + if (esp_lcd_new_panel_io_spi(configuration->spiBusHandle, &panel_io_config, &ioHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + static const st7796_lcd_init_cmd_t lcd_init_cmds[] = { + {0x01, (uint8_t[]) {0x00}, 0, 120}, + {0x11, (uint8_t[]) {0x00}, 0, 120}, + {0xF0, (uint8_t[]) {0xC3}, 1, 0}, + {0xF0, (uint8_t[]) {0xC3}, 1, 0}, + {0xF0, (uint8_t[]) {0x96}, 1, 0}, + {0x36, (uint8_t[]) {0x58}, 1, 0}, + {0x3A, (uint8_t[]) {0x55}, 1, 0}, + {0xB4, (uint8_t[]) {0x01}, 1, 0}, + {0xB6, (uint8_t[]) {0x80, 0x02, 0x3B}, 3, 0}, + {0xE8, (uint8_t[]) {0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0}, + {0xC1, (uint8_t[]) {0x06}, 1, 0}, + {0xC2, (uint8_t[]) {0xA7}, 1, 0}, + {0xC5, (uint8_t[]) {0x18}, 1, 0}, + {0xE0, (uint8_t[]) {0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B}, 15, 0}, + {0xE1, (uint8_t[]) {0xE0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B}, 15, 120}, + {0xF0, (uint8_t[]) {0x3C}, 1, 0}, + {0xF0, (uint8_t[]) {0x69}, 1, 0}, + {0x21, (uint8_t[]) {0x00}, 1, 0}, + {0x29, (uint8_t[]) {0x00}, 1, 0}, + }; + + st7796_vendor_config_t vendor_config = { + // Uncomment these lines if use custom initialization commands + .init_cmds = lcd_init_cmds, + .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), + }; + + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration->resetPin, // Set to -1 if not use +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + .color_space = ESP_LCD_COLOR_SPACE_RGB, +#else + .color_space = LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, +#endif + .bits_per_pixel = 16, + .vendor_config = &vendor_config + }; + /* + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration->resetPin, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = 16, + .flags = { + .reset_active_high = false + }, + .vendor_config = nullptr + }; +*/ + if (esp_lcd_new_panel_st7796(ioHandle, &panel_config, &panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + if (esp_lcd_panel_reset(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to reset panel"); + return false; + } + + if (esp_lcd_panel_init(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to init panel"); + return false; + } + + if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to invert"); + return false; + } + + if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to swap XY "); + return false; + } + + if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to mirror"); + return false; + } + + if (esp_lcd_panel_set_gap(panelHandle, 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"); + }*/ +} diff --git a/Drivers/ST7796/Source/St7796Display.h b/Drivers/ST7796/Source/St7796Display.h new file mode 100644 index 00000000..3b7bf0a8 --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.h @@ -0,0 +1,103 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" + +#include +#include +#include +#include +#include +#include + +class St7796Display final : public tt::hal::display::DisplayDevice { + +public: + + class Configuration { + + public: + + Configuration( + esp_lcd_spi_bus_handle_t spi_bus_handle, + gpio_num_t csPin, + gpio_num_t dcPin, + unsigned int horizontalResolution, + unsigned int verticalResolution, + std::shared_ptr touch, + bool swapXY = false, + bool mirrorX = false, + bool mirrorY = false, + bool invertColor = false, + 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 touch; + std::function _Nullable backlightDutyFunction = nullptr; + }; + +private: + + std::unique_ptr configuration; + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + +public: + + explicit St7796Display(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { + assert(configuration != nullptr); + } + + std::string getName() const final { return "ST7796"; } + std::string getDescription() const final { return "ST7796 display"; } + + bool start() final; + + bool stop() final; + + std::shared_ptr _Nullable createTouch() final { return configuration->touch; } + + void setBacklightDuty(uint8_t backlightDuty) final { + if (configuration->backlightDutyFunction != nullptr) { + configuration->backlightDutyFunction(backlightDuty); + } + } + + void setGammaCurve(uint8_t index) final; + uint8_t getGammaCurveCount() const final { return 4; }; + + bool supportsBacklightDuty() const final { return configuration->backlightDutyFunction != nullptr; } + + lv_display_t* _Nullable getLvglDisplay() const final { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Drivers/TCA8418/CMakeLists.txt b/Drivers/TCA8418/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/TCA8418/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/TCA8418/COPYRIGHT.md b/Drivers/TCA8418/COPYRIGHT.md new file mode 100644 index 00000000..dff105a9 --- /dev/null +++ b/Drivers/TCA8418/COPYRIGHT.md @@ -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. diff --git a/Drivers/TCA8418/README.md b/Drivers/TCA8418/README.md new file mode 100644 index 00000000..e3ef16a1 --- /dev/null +++ b/Drivers/TCA8418/README.md @@ -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 diff --git a/Drivers/TCA8418/Source/Tca8418.cpp b/Drivers/TCA8418/Source/Tca8418.cpp new file mode 100644 index 00000000..f4bac8b8 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.cpp @@ -0,0 +1,202 @@ +#include "Tca8418.h" +#include + +#define TAG "tca8418" + +namespace registers { +static const uint8_t CFG = 0x01U; +static const uint8_t KP_GPIO1 = 0x1DU; +static const uint8_t KP_GPIO2 = 0x1EU; +static const uint8_t KP_GPIO3 = 0x1FU; + +static const uint8_t KEY_EVENT_A = 0x04U; +static const uint8_t KEY_EVENT_B = 0x05U; +static const uint8_t KEY_EVENT_C = 0x06U; +static const uint8_t KEY_EVENT_D = 0x07U; +static const uint8_t KEY_EVENT_E = 0x08U; +static const uint8_t KEY_EVENT_F = 0x09U; +static const uint8_t KEY_EVENT_G = 0x0AU; +static const uint8_t KEY_EVENT_H = 0x0BU; +static const uint8_t KEY_EVENT_I = 0x0CU; +static const uint8_t KEY_EVENT_J = 0x0DU; +} // namespace registers + + +void Tca8418::init(uint8_t numrows, uint8_t numcols) { + /* + * | ADDRESS | REGISTER NAME | REGISTER DESCRIPTION | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | + * |---------+---------------+----------------------+------+------+------+------+------+------+------+------| + * | 0x1D | KP_GPIO1 | Keypad/GPIO Select 1 | ROW7 | ROW6 | ROW5 | ROW4 | ROW3 | ROW2 | ROW1 | ROW0 | + * | 0x1E | KP_GPIO2 | Keypad/GPIO Select 2 | COL7 | COL6 | COL5 | COL4 | COL3 | COL2 | COL1 | COL0 | + * | 0x1F | KP_GPIO3 | Keypad/GPIO Select 3 | N/A | N/A | N/A | N/A | N/A | N/A | COL9 | COL8 | + */ + + num_rows = numrows; + num_cols = numcols; + + // everything enabled in key scan mode + uint8_t enabled_rows = 0x3F; + uint16_t enabled_cols = 0x3FF; + + writeRegister8(registers::KP_GPIO1, enabled_rows); + writeRegister8(registers::KP_GPIO2, (uint8_t)(0xFF & enabled_cols)); + writeRegister8(registers::KP_GPIO3, (uint8_t)(0x03 & (enabled_cols >> 8))); + + /* + * BIT: NAME + * + * 7: AI + * Auto-increment for read and write operations; See below table for more information + * 0 = disabled + * 1 = enabled + * + * 6: GPI_E_CFG + * GPI event mode configuration + * 0 = GPI events are tracked when keypad is locked + * 1 = GPI events are not tracked when keypad is locked + * + * 5: OVR_FLOW_M + * Overflow mode + * 0 = disabled; Overflow data is lost + * 1 = enabled; Overflow data shifts with last event pushing first event out + * + * 4: INT_CFG + * Interrupt configuration + * 0 = processor interrupt remains asserted (or low) if host tries to clear interrupt while there is + * still a pending key press, key release or GPI interrupt + * 1 = processor interrupt is deasserted for 50 μs and reassert with pending interrupts + * + * 3: OVR_FLOW_IEN + * Overflow interrupt enable + * 0 = disabled; INT is not asserted if the FIFO overflows + * 1 = enabled; INT becomes asserted if the FIFO overflows + * + * 2: K_LCK_IEN + * Keypad lock interrupt enable + * 0 = disabled; INT is not asserted after a correct unlock key sequence + * 1 = enabled; INT becomes asserted after a correct unlock key sequence + * + * 1: GPI_IEN + * GPI interrupt enable to host processor + * 0 = disabled; INT is not asserted for a change on a GPI + * 1 = enabled; INT becomes asserted for a change on a GPI + * + * 0: KE_IEN + * Key events interrupt enable to host processor + * 0 = disabled; INT is not asserted when a key event occurs + * 1 = enabled; INT becomes asserted when a key event occurs + */ + + // 10111001 xB9 -- fifo overflow enabled + // 10011001 x99 -- fifo overflow disabled + writeRegister8(registers::CFG, 0x99); + + clear_released_list(); + clear_pressed_list(); +} + +bool Tca8418::update() { + last_update_micros = this_update_micros; + uint8_t key_code, key_down, key_event, key_row, key_col; + + key_event = get_key_event(); + // TODO: read gpio R7/R6 status? 0x14 bits 7&6 + // read(0x14, &new_keycode) + + // TODO: use tick function to get an update delta time + this_update_micros = 0; + delta_micros = this_update_micros - last_update_micros; + + if (key_event > 0) { + key_code = key_event & 0x7F; + key_down = (key_event & 0x80) >> 7; + key_row = key_code / num_cols; + key_col = key_code % num_cols; + + // always clear the released list + clear_released_list(); + + if (key_down) { + add_pressed_key(key_row, key_col); + // TODO reject ghosts (assume multiple key presses with the same hold time are ghosts.) + + } else { + add_released_key(key_row, key_col); + remove_pressed_key(key_row, key_col); + } + + return true; + } + + // Increment hold times for pressed keys + for (int i = 0; i < pressed_key_count; i++) { + pressed_list[i].hold_time += delta_micros; + } + + return false; +} + + +void Tca8418::add_pressed_key(uint8_t row, uint8_t col) { + if (pressed_key_count >= KEY_EVENT_LIST_SIZE) + return; + + pressed_list[pressed_key_count].row = row; + pressed_list[pressed_key_count].col = col; + pressed_list[pressed_key_count].hold_time = 0; + pressed_key_count++; +} + +void Tca8418::add_released_key(uint8_t row, uint8_t col) { + if (released_key_count >= KEY_EVENT_LIST_SIZE) + return; + + released_key_count++; + released_list[0].row = row; + released_list[0].col = col; +} + +void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) { + if (pressed_key_count == 0) + return; + + // delete the pressed key + for (int i = 0; i < pressed_key_count; i++) { + if (pressed_list[i].row == row && + pressed_list[i].col == col) { + // shift remaining keys left one index + for (int j = i; i < pressed_key_count; j++) { + if (j == KEY_EVENT_LIST_SIZE - 1) + break; + pressed_list[j].row = pressed_list[j + 1].row; + pressed_list[j].col = pressed_list[j + 1].col; + pressed_list[j].hold_time = pressed_list[j + 1].hold_time; + } + break; + } + } + pressed_key_count--; +} + +void Tca8418::clear_pressed_list() { + for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) { + pressed_list[i].row = 255; + pressed_list[i].col = 255; + } + pressed_key_count = 0; +} + +void Tca8418::clear_released_list() { + for (int i = 0; i < KEY_EVENT_LIST_SIZE; i++) { + released_list[i].row = 255; + released_list[i].col = 255; + } + released_key_count = 0; +} + +uint8_t Tca8418::get_key_event() { + uint8_t new_keycode = 0; + + readRegister8(registers::KEY_EVENT_A, new_keycode); + return new_keycode; +} diff --git a/Drivers/TCA8418/Source/Tca8418.h b/Drivers/TCA8418/Source/Tca8418.h new file mode 100644 index 00000000..d48d14a3 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include + +#define TCA8418_ADDRESS 0x34U +#define KEY_EVENT_LIST_SIZE 10 + +class Tca8418 final : public tt::hal::i2c::I2cDevice { + +private: + + uint8_t tca8418_address; + uint32_t last_update_micros; + uint32_t this_update_micros; + + uint8_t new_pressed_keys_count; + + void clear_released_list(); + void clear_pressed_list(); + void add_pressed_key(uint8_t row, uint8_t col); + void add_released_key(uint8_t row, uint8_t col); + void remove_pressed_key(uint8_t row, uint8_t col); + void write(uint8_t register_address, uint8_t data); + bool read(uint8_t register_address, uint8_t* data); + +public: + + struct PressedKey { + uint8_t row; + uint8_t col; + uint32_t hold_time; + }; + + struct ReleasedKey { + uint8_t row; + uint8_t col; + }; + + std::string getName() const final { return "TCA8418"; } + + std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; } + + explicit Tca8418(i2c_port_t port) : I2cDevice(port, TCA8418_ADDRESS) { + delta_micros = 0; + last_update_micros = 0; + this_update_micros = 0; + } + + ~Tca8418() {} + + uint8_t num_rows; + uint8_t num_cols; + + uint32_t delta_micros; + + std::array pressed_list; + std::array released_list; + uint8_t pressed_key_count; + uint8_t released_key_count; + + void init(uint8_t numrows, uint8_t numcols); + bool update(); + uint8_t get_key_event(); + bool button_pressed(uint8_t row, uint8_t button_bit_position); + bool button_released(uint8_t row, uint8_t button_bit_position); + bool button_held(uint8_t row, uint8_t button_bit_position); +}; diff --git a/ExternalApps/HelloWorld/.gitignore b/ExternalApps/HelloWorld/.gitignore new file mode 100644 index 00000000..89baa26e --- /dev/null +++ b/ExternalApps/HelloWorld/.gitignore @@ -0,0 +1,2 @@ +build*/ +.tactility/ diff --git a/ExternalApps/HelloWorld/build.sh b/ExternalApps/HelloWorld/build.sh deleted file mode 100755 index a2f0b9ff..00000000 --- a/ExternalApps/HelloWorld/build.sh +++ /dev/null @@ -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 diff --git a/ExternalApps/HelloWorld/sdkconfig.override b/ExternalApps/HelloWorld/sdkconfig.override deleted file mode 100644 index b02eb18b..00000000 --- a/ExternalApps/HelloWorld/sdkconfig.override +++ /dev/null @@ -1,2 +0,0 @@ -CONFIG_PARTITION_TABLE_SINGLE_APP=y -CONFIG_ESP_SYSTEM_MEMPROT_FEATURE_LOCK=n diff --git a/ExternalApps/HelloWorld/tactility.properties b/ExternalApps/HelloWorld/tactility.properties new file mode 100644 index 00000000..aa5aa4a2 --- /dev/null +++ b/ExternalApps/HelloWorld/tactility.properties @@ -0,0 +1,2 @@ +[sdk] +version = 0.4.0 diff --git a/ExternalApps/HelloWorld/tactility.py b/ExternalApps/HelloWorld/tactility.py new file mode 100644 index 00000000..310de9f3 --- /dev/null +++ b/ExternalApps/HelloWorld/tactility.py @@ -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") diff --git a/README.md b/README.md index b2025e7a..7b40fd5b 100644 --- a/README.md +++ b/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. -![photo of devices running Tactility](Documentation/pics/tactility-devices.webp)  ![screenshot of desktop app](Documentation/pics/screenshot-Desktop.png) +![photo of devices running Tactility](Documentation/pics/tactility-devices.webp)  ![screenshot of launcher app](Documentation/pics/screenshot-Launcher.png) -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: -![screenshot off app list app](Documentation/pics/screenshot-AppList.png)  ![screenshot of files app](Documentation/pics/screenshot-Files.png) - -It's easy to manage system settings: - -![screenshot of settings app](Documentation/pics/screenshot-Settings.png)  ![screenshot of wifi management app](Documentation/pics/screenshot-WifiManage.png) +![screenshot off app list app](Documentation/pics/screenshot-AppList.png)  ![screenshot of settings app](Documentation/pics/screenshot-Settings.png) ## License diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 56ee17ec..db26fe3e 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -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 () diff --git a/Tactility/Include/Tactility/Preferences.h b/Tactility/Include/Tactility/Preferences.h index d2a34812..b1f778ac 100644 --- a/Tactility/Include/Tactility/Preferences.h +++ b/Tactility/Include/Tactility/Preferences.h @@ -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: diff --git a/Tactility/Include/Tactility/app/App.h b/Tactility/Include/Tactility/app/App.h index 5135b485..e9808323 100644 --- a/Tactility/Include/Tactility/app/App.h +++ b/Tactility/Include/Tactility/app/App.h @@ -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 _Nullable resultData) {} + virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr _Nullable resultData) {} Mutex& getMutex() { return mutex; } @@ -83,15 +83,15 @@ std::shared_ptr create() { return std::shared_ptr(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 _Nullable parameters = nullptr); +LaunchId start(const std::string& id, std::shared_ptr _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 _Nullable getCurrentAppContext(); +std::shared_ptr _Nullable getCurrentAppContext(); /** @return the currently running app (it is only ever null before the splash screen is shown) */ -std::shared_ptr _Nullable getCurrentApp(); +std::shared_ptr _Nullable getCurrentApp(); } diff --git a/Tactility/Include/Tactility/app/ElfApp.h b/Tactility/Include/Tactility/app/ElfApp.h index 9821f4eb..d9a592d8 100644 --- a/Tactility/Include/Tactility/app/ElfApp.h +++ b/Tactility/Include/Tactility/app/ElfApp.h @@ -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 createElfApp(const std::shared_ptr& manifest); diff --git a/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h b/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h new file mode 100644 index 00000000..86fcf424 --- /dev/null +++ b/Tactility/Include/Tactility/app/filebrowser/FileBrowser.h @@ -0,0 +1,7 @@ +#pragma once + +namespace tt::app::filebrowser { + +void start(); + +} // namespace diff --git a/Tactility/Include/Tactility/app/files/Files.h b/Tactility/Include/Tactility/app/files/Files.h deleted file mode 100644 index 1d77e44a..00000000 --- a/Tactility/Include/Tactility/app/files/Files.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "app/files/View.h" -#include "app/files/State.h" -#include "app/AppManifest.h" - -#include -#include -#include - -namespace tt::app::files { - -void start(); - -} // namespace diff --git a/Tactility/Include/Tactility/app/fileselection/FileSelection.h b/Tactility/Include/Tactility/app/fileselection/FileSelection.h new file mode 100644 index 00000000..f2c08473 --- /dev/null +++ b/Tactility/Include/Tactility/app/fileselection/FileSelection.h @@ -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 diff --git a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h index c7754999..0d4c2e7e 100644 --- a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h +++ b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h @@ -1,7 +1,7 @@ #pragma once -#include "I2c.h" #include "../Device.h" +#include "I2c.h" namespace tt::hal::i2c { @@ -20,7 +20,11 @@ protected: static constexpr TickType_t DEFAULT_TIMEOUT = 1000 / portTICK_PERIOD_MS; + bool read(uint8_t* data, size_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); + bool write(const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); + bool writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout = DEFAULT_TIMEOUT); bool readRegister8(uint8_t reg, uint8_t& result) const; + bool writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); bool writeRegister8(uint8_t reg, uint8_t value) const; bool readRegister12(uint8_t reg, float& out) const; bool readRegister14(uint8_t reg, float& out) const; @@ -41,4 +45,4 @@ public: uint8_t getAddress() const { return address; } }; -} +} // namespace tt::hal::i2c diff --git a/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h b/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h index 7ca7bf10..8e753b22 100644 --- a/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h +++ b/Tactility/Include/Tactility/hal/sdcard/SdCardDevice.h @@ -53,7 +53,7 @@ std::shared_ptr _Nullable find(const std::string& path); * Always calls the function, but doesn't lock if the path is not an SD card path. */ template -inline ReturnType withSdCardLock(const std::string& path, std::function fn) { + ReturnType withSdCardLock(const std::string& path, std::function fn) { auto sdcard = find(path); if (sdcard != nullptr) { auto scoped_lockable = sdcard->getLock().asScopedLock(); diff --git a/Tactility/Include/Tactility/lvgl/Color.h b/Tactility/Include/Tactility/lvgl/Color.h new file mode 100644 index 00000000..67a58e98 --- /dev/null +++ b/Tactility/Include/Tactility/lvgl/Color.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +lv_color_t lv_color_foreground(); + +lv_color_t lv_color_background(); + +lv_color_t lv_color_background_darkest(); diff --git a/Tactility/Include/Tactility/lvgl/LabelUtils.h b/Tactility/Include/Tactility/lvgl/LabelUtils.h index 04cc892f..2e508e42 100644 --- a/Tactility/Include/Tactility/lvgl/LabelUtils.h +++ b/Tactility/Include/Tactility/lvgl/LabelUtils.h @@ -1,6 +1,6 @@ #pragma once -#include "lvgl.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/lvgl/Lvgl.h b/Tactility/Include/Tactility/lvgl/Lvgl.h new file mode 100644 index 00000000..53117f26 --- /dev/null +++ b/Tactility/Include/Tactility/lvgl/Lvgl.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +#include "./Colors.h" diff --git a/Tactility/Include/Tactility/lvgl/Spinner.h b/Tactility/Include/Tactility/lvgl/Spinner.h index 71bd173c..39b20008 100644 --- a/Tactility/Include/Tactility/lvgl/Spinner.h +++ b/Tactility/Include/Tactility/lvgl/Spinner.h @@ -1,4 +1,4 @@ -#include "lvgl.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/lvgl/Style.h b/Tactility/Include/Tactility/lvgl/Style.h index dd89dd07..cb97c811 100644 --- a/Tactility/Include/Tactility/lvgl/Style.h +++ b/Tactility/Include/Tactility/lvgl/Style.h @@ -1,6 +1,6 @@ #pragma once -#include "lvgl.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/lvgl/Toolbar.h b/Tactility/Include/Tactility/lvgl/Toolbar.h index eac3d747..733f1474 100644 --- a/Tactility/Include/Tactility/lvgl/Toolbar.h +++ b/Tactility/Include/Tactility/lvgl/Toolbar.h @@ -1,7 +1,7 @@ #pragma once -#include "lvgl.h" #include "../app/AppContext.h" +#include namespace tt::lvgl { diff --git a/Tactility/Include/Tactility/network/HttpdReq.h b/Tactility/Include/Tactility/network/HttpdReq.h new file mode 100644 index 00000000..0fbf9cac --- /dev/null +++ b/Tactility/Include/Tactility/network/HttpdReq.h @@ -0,0 +1,29 @@ +#pragma once + +#ifdef ESP_PLATFORM + +#include +#include +#include +#include +#include + +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 receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead); + +std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator); + +std::map parseContentDisposition(const std::vector& input); + +bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead); + +} + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Include/Tactility/network/Url.h b/Tactility/Include/Tactility/network/Url.h new file mode 100644 index 00000000..6fb6c236 --- /dev/null +++ b/Tactility/Include/Tactility/network/Url.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace tt::network { + +/** + * Parse a query from a URL + * @param[in] query + * @return a map with key-values + */ +std::map parseUrlQuery(std::string query); + +std::string urlEncode(const std::string& input); + +std::string urlDecode(const std::string& input); + +} // namespace \ No newline at end of file diff --git a/Tactility/Include/Tactility/service/loader/Loader.h b/Tactility/Include/Tactility/service/loader/Loader.h index edd02d57..d5d3b651 100644 --- a/Tactility/Include/Tactility/service/loader/Loader.h +++ b/Tactility/Include/Tactility/service/loader/Loader.h @@ -4,7 +4,6 @@ #include #include -#include #include @@ -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 _Nullable parameters = nullptr); +app::LaunchId startApp(const std::string& id, std::shared_ptr _Nullable parameters = nullptr); /** @brief Stop the currently showing app. Show the previous app if any app was still running. */ void stopApp(); diff --git a/Tactility/Include/Tactility/service/wifi/Wifi.h b/Tactility/Include/Tactility/service/wifi/Wifi.h index a43e7799..b41ad0a8 100644 --- a/Tactility/Include/Tactility/service/wifi/Wifi.h +++ b/Tactility/Include/Tactility/service/wifi/Wifi.h @@ -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. diff --git a/Tactility/Private/Tactility/app/AppInstance.h b/Tactility/Private/Tactility/app/AppInstance.h index 3bd76558..f38cfb2b 100644 --- a/Tactility/Private/Tactility/app/AppInstance.h +++ b/Tactility/Private/Tactility/app/AppInstance.h @@ -25,22 +25,21 @@ enum class State { */ class AppInstance : public AppContext { -private: - Mutex mutex = Mutex(Mutex::Type::Normal); const std::shared_ptr 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 _Nullable parameters; + std::shared_ptr _Nullable parameters; std::shared_ptr app; - static std::shared_ptr createApp( - const std::shared_ptr& manifest + static std::shared_ptr createApp( + const std::shared_ptr& 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& manifest) : + explicit AppInstance(const std::shared_ptr& manifest, LaunchId launchId) : manifest(manifest), + launchId(launchId), app(createApp(manifest)) {} - AppInstance(const std::shared_ptr& manifest, std::shared_ptr parameters) : + AppInstance(const std::shared_ptr& manifest, LaunchId launchId, std::shared_ptr 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; diff --git a/Tactility/Private/Tactility/app/development/Development.h b/Tactility/Private/Tactility/app/development/Development.h new file mode 100644 index 00000000..ca93c52d --- /dev/null +++ b/Tactility/Private/Tactility/app/development/Development.h @@ -0,0 +1,11 @@ +#pragma once + +#ifdef ESP_PLATFORM + +namespace tt::app::development { + +void start(); + +} + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Private/Tactility/app/files/State.h b/Tactility/Private/Tactility/app/filebrowser/State.h similarity index 97% rename from Tactility/Private/Tactility/app/files/State.h rename to Tactility/Private/Tactility/app/filebrowser/State.h index c29e1521..04308e3d 100644 --- a/Tactility/Private/Tactility/app/files/State.h +++ b/Tactility/Private/Tactility/app/filebrowser/State.h @@ -6,7 +6,7 @@ #include #include -namespace tt::app::files { +namespace tt::app::filebrowser { class State { diff --git a/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h new file mode 100644 index 00000000..0ae68fbb --- /dev/null +++ b/Tactility/Private/Tactility/app/filebrowser/SupportedFiles.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace tt::app::filebrowser { + +bool isSupportedExecutableFile(const std::string& filename); +bool isSupportedImageFile(const std::string& filename); +bool isSupportedTextFile(const std::string& filename); + +} // namespace diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/filebrowser/View.h similarity index 96% rename from Tactility/Private/Tactility/app/files/View.h rename to Tactility/Private/Tactility/app/filebrowser/View.h index 0c616c7c..c6f24026 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/filebrowser/View.h @@ -7,7 +7,7 @@ #include #include -namespace tt::app::files { +namespace tt::app::filebrowser { class View { std::shared_ptr state; diff --git a/Tactility/Private/Tactility/app/files/FileUtils.h b/Tactility/Private/Tactility/app/files/FileUtils.h deleted file mode 100644 index d05cba3c..00000000 --- a/Tactility/Private/Tactility/app/files/FileUtils.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -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& 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 diff --git a/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h b/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h new file mode 100644 index 00000000..479edd12 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/FileSelectionPrivate.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace tt::app::fileselection { + +enum class Mode { + Existing = 0, + ExistingOrNew = 1 +}; + +Mode getMode(const Bundle& bundle); + +} diff --git a/Tactility/Private/Tactility/app/fileselection/State.h b/Tactility/Private/Tactility/app/fileselection/State.h new file mode 100644 index 00000000..912f8466 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/State.h @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include + +namespace tt::app::fileselection { + +class State { + + Mutex mutex = Mutex(Mutex::Type::Recursive); + std::vector 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 &> Func> + void withEntries(Func&& onEntries) const { + mutex.withLock([&]() { + std::invoke(std::forward(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; +}; + +} diff --git a/Tactility/Private/Tactility/app/fileselection/View.h b/Tactility/Private/Tactility/app/fileselection/View.h new file mode 100644 index 00000000..4f8d2085 --- /dev/null +++ b/Tactility/Private/Tactility/app/fileselection/View.h @@ -0,0 +1,44 @@ +#pragma once + +#include "./State.h" +#include "./FileSelectionPrivate.h" + +#include "Tactility/app/AppManifest.h" + +#include +#include + +namespace tt::app::fileselection { + +class View { + std::shared_ptr 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 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, std::function 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); + } +}; + +} diff --git a/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h b/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h index e83fbd13..0baa9eb3 100644 --- a/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h +++ b/Tactility/Private/Tactility/app/i2cscanner/I2cScanner.h @@ -1,13 +1,5 @@ #pragma once -#include -#include -#include -#include "lvgl.h" -#include -#include "Timer.h" -#include - namespace tt::app::i2cscanner { void start(); diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h new file mode 100644 index 00000000..7fb0d9d4 --- /dev/null +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -0,0 +1,97 @@ +#pragma once +#ifdef ESP_PLATFORM + +#include "Tactility/service/Service.h" + +#include + +#include +#include +#include + +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 findService(); + +} + +#endif // ESP_PLATFORM diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 6aa5d1d7..5cf7e9cf 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -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) { diff --git a/Tactility/Source/TactilityHeadless.cpp b/Tactility/Source/TactilityHeadless.cpp index 7b968f6e..237b110c 100644 --- a/Tactility/Source/TactilityHeadless.cpp +++ b/Tactility/Source/TactilityHeadless.cpp @@ -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 } diff --git a/Tactility/Source/app/App.cpp b/Tactility/Source/app/App.cpp index 2c74ceb8..91ea12e3 100644 --- a/Tactility/Source/app/App.cpp +++ b/Tactility/Source/app/App.cpp @@ -4,19 +4,19 @@ namespace tt::app { -void start(const std::string& id, std::shared_ptr _Nullable parameters) { - service::loader::startApp(id, std::move(parameters)); +LaunchId start(const std::string& id, std::shared_ptr _Nullable parameters) { + return service::loader::startApp(id, std::move(parameters)); } void stop() { service::loader::stopApp(); } -std::shared_ptr _Nullable getCurrentAppContext() { +std::shared_ptr _Nullable getCurrentAppContext() { return service::loader::getCurrentAppContext(); } -std::shared_ptr _Nullable getCurrentApp() { +std::shared_ptr _Nullable getCurrentApp() { return service::loader::getCurrentApp(); } diff --git a/Tactility/Source/app/ElfApp.cpp b/Tactility/Source/app/ElfApp.cpp index 7f75e709..a868e6cd 100644 --- a/Tactility/Source/app/ElfApp.cpp +++ b/Tactility/Source/app/ElfApp.cpp @@ -36,8 +36,6 @@ static ElfManifest elfManifest; class ElfApp : public App { -private: - const std::string filePath; std::unique_ptr elfFileData; esp_elf_t elf; @@ -143,9 +141,9 @@ public: } } - void onResult(AppContext& appContext, Result result, std::unique_ptr resultBundle) override { + void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr 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 createElfApp(const std::shared_ptr& manifest) { diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp new file mode 100644 index 00000000..4ea05b93 --- /dev/null +++ b/Tactility/Source/app/development/Development.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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; + + 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_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(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_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(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 +}; + +void start() { + app::start(manifest.id); +} + +} // namespace + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Source/app/files/Files.cpp b/Tactility/Source/app/filebrowser/FileBrowser.cpp similarity index 66% rename from Tactility/Source/app/files/Files.cpp rename to Tactility/Source/app/filebrowser/FileBrowser.cpp index 37b1ceb4..c39edcc2 100644 --- a/Tactility/Source/app/files/Files.cpp +++ b/Tactility/Source/app/filebrowser/FileBrowser.cpp @@ -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 @@ -7,18 +7,18 @@ #include -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; std::shared_ptr state; public: - FilesApp() { + FileBrowser() { state = std::make_shared(); view = std::make_unique(state); } @@ -27,7 +27,7 @@ public: view->init(parent); } - void onResult(AppContext& appContext, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& appContext, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr 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 + .createApp = create }; void start() { diff --git a/Tactility/Source/app/files/State.cpp b/Tactility/Source/app/filebrowser/State.cpp similarity index 84% rename from Tactility/Source/app/files/State.cpp rename to Tactility/Source/app/filebrowser/State.cpp index fae8c301..e2dbe18d 100644 --- a/Tactility/Source/app/files/State.cpp +++ b/Tactility/Source/app/filebrowser/State.cpp @@ -1,6 +1,6 @@ -#include "Tactility/app/files/State.h" -#include "Tactility/app/files/FileUtils.h" +#include "Tactility/app/filebrowser/State.h" +#include #include "Tactility/hal/sdcard/SdCardDevice.h" #include #include @@ -11,9 +11,9 @@ #include #include -#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); } diff --git a/Tactility/Source/app/filebrowser/SupportedFiles.cpp b/Tactility/Source/app/filebrowser/SupportedFiles.cpp new file mode 100644 index 00000000..3069ad08 --- /dev/null +++ b/Tactility/Source/app/filebrowser/SupportedFiles.cpp @@ -0,0 +1,34 @@ +#include +#include + +namespace tt::app::filebrowser { + +#define TAG "filebrowser_app" + +bool isSupportedExecutableFile(const std::string& filename) { +#ifdef ESP_PLATFORM + // Currently only the PNG library is built into Tactility + return filename.ends_with(".elf"); +#else + return false; +#endif +} + +bool isSupportedImageFile(const std::string& filename) { + // Currently only the PNG library is built into Tactility + return string::lowercase(filename).ends_with(".png"); +} + +bool isSupportedTextFile(const std::string& filename) { + std::string filename_lower = string::lowercase(filename); + return filename_lower.ends_with(".txt") || + filename_lower.ends_with(".ini") || + filename_lower.ends_with(".json") || + filename_lower.ends_with(".yaml") || + filename_lower.ends_with(".yml") || + filename_lower.ends_with(".lua") || + filename_lower.ends_with(".js") || + filename_lower.ends_with(".properties"); +} + +} // namespace tt::app::filebrowser diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/filebrowser/View.cpp similarity index 87% rename from Tactility/Source/app/files/View.cpp rename to Tactility/Source/app/filebrowser/View.cpp index f927bd4c..5da95b85 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/filebrowser/View.cpp @@ -1,5 +1,5 @@ -#include "Tactility/app/files/FileUtils.h" -#include "Tactility/app/files/View.h" +#include "Tactility/app/filebrowser/View.h" +#include "Tactility/app/filebrowser/SupportedFiles.h" #include "Tactility/app/alertdialog/AlertDialog.h" #include "Tactility/app/imageviewer/ImageViewer.h" @@ -10,6 +10,7 @@ #include "Tactility/lvgl/LvglSync.h" #include +#include "Tactility/file/File.h" #include #include @@ -19,43 +20,43 @@ #include "Tactility/service/loader/Loader.h" #endif -#define TAG "files_app" +#define TAG "filebrowser_app" -namespace tt::app::files { +namespace tt::app::filebrowser { // region Callbacks static void dirEntryListScrollBeginCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onDirEntryListScrollBegin(); } static void onDirEntryPressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); auto* button = lv_event_get_target_obj(event); auto index = lv_obj_get_index(button); view->onDirEntryPressed(index); } static void onDirEntryLongPressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); auto* button = lv_event_get_target_obj(event); auto index = lv_obj_get_index(button); view->onDirEntryLongPressed(index); } static void onRenamePressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onRenamePressed(); } static void onDeletePressedCallback(lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onDeletePressed(); } static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { - auto* view = (View*)lv_event_get_user_data(event); + auto* view = static_cast(lv_event_get_user_data(event)); view->onNavigateUpPressed(); } @@ -86,18 +87,18 @@ void View::viewFile(const std::string& path, const std::string& filename) { if (isSupportedExecutableFile(filename)) { #ifdef ESP_PLATFORM - app::registerElfApp(processed_filepath); - auto app_id = app::getElfAppId(processed_filepath); + registerElfApp(processed_filepath); + auto app_id = getElfAppId(processed_filepath); service::loader::startApp(app_id); #endif } else if (isSupportedImageFile(filename)) { - app::imageviewer::start(processed_filepath); + imageviewer::start(processed_filepath); } else if (isSupportedTextFile(filename)) { if (kernel::getPlatform() == kernel::PlatformEsp) { - app::textviewer::start(processed_filepath); + textviewer::start(processed_filepath); } else { // Remove forward slash, because we need a relative path - app::textviewer::start(processed_filepath.substr(1)); + textviewer::start(processed_filepath.substr(1)); } } else { TT_LOG_W(TAG, "opening files of this type is not supported"); @@ -111,6 +112,7 @@ void View::onDirEntryPressed(uint32_t index) { if (state->getDirent(index, dir_entry)) { TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + using namespace tt::file; switch (dir_entry.d_type) { case TT_DT_DIR: case TT_DT_CHR: @@ -140,6 +142,7 @@ void View::onDirEntryLongPressed(int32_t index) { if (state->getDirent(index, dir_entry)) { TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); state->setSelectedChildEntry(dir_entry.d_name); + using namespace file; switch (dir_entry.d_type) { case TT_DT_DIR: case TT_DT_CHR: @@ -161,15 +164,14 @@ void View::onDirEntryLongPressed(int32_t index) { } -void View::createDirEntryWidget(lv_obj_t* parent, struct dirent& dir_entry) { - tt_check(parent); - auto* list = (lv_obj_t*)parent; +void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { + tt_check(list); const char* symbol; - if (dir_entry.d_type == TT_DT_DIR || dir_entry.d_type == TT_DT_CHR) { + if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) { symbol = LV_SYMBOL_DIRECTORY; } else if (isSupportedImageFile(dir_entry.d_name)) { symbol = LV_SYMBOL_IMAGE; - } else if (dir_entry.d_type == TT_DT_LNK) { + } else if (dir_entry.d_type == file::TT_DT_LNK) { symbol = LV_SYMBOL_LOOP; } else { symbol = LV_SYMBOL_FILE; @@ -195,7 +197,7 @@ void View::onRenamePressed() { std::string entry_name = state->getSelectedChildEntry(); TT_LOG_I(TAG, "Pending rename %s", entry_name.c_str()); state->setPendingAction(State::ActionRename); - app::inputdialog::start("Rename", "", entry_name); + inputdialog::start("Rename", "", entry_name); } void View::onDeletePressed() { @@ -204,7 +206,7 @@ void View::onDeletePressed() { state->setPendingAction(State::ActionDelete); std::string message = "Do you want to delete this?\n" + file_path; const std::vector choices = { "Yes", "No" }; - app::alertdialog::start("Are you sure?", message, choices); + alertdialog::start("Are you sure?", message, choices); } void View::showActionsForDirectory() { @@ -301,7 +303,7 @@ void View::onResult(Result result, std::unique_ptr bundle) { switch (state->getPendingAction()) { case State::ActionDelete: { if (alertdialog::getResultIndex(*bundle) == 0) { - int delete_count = (int)remove(filepath.c_str()); + int delete_count = remove(filepath.c_str()); if (delete_count > 0) { TT_LOG_I(TAG, "Deleted %d items", delete_count); } else { @@ -313,9 +315,9 @@ void View::onResult(Result result, std::unique_ptr bundle) { break; } case State::ActionRename: { - auto new_name = app::inputdialog::getResult(*bundle); + auto new_name = inputdialog::getResult(*bundle); if (!new_name.empty() && new_name != state->getSelectedChildEntry()) { - std::string rename_to = getChildPath(state->getCurrentPath(), new_name); + std::string rename_to = file::getChildPath(state->getCurrentPath(), new_name); if (rename(filepath.c_str(), rename_to.c_str())) { TT_LOG_I(TAG, "Renamed \"%s\" to \"%s\"", filepath.c_str(), rename_to.c_str()); } else { diff --git a/Tactility/Source/app/files/FileUtils.cpp b/Tactility/Source/app/files/FileUtils.cpp deleted file mode 100644 index a649ea91..00000000 --- a/Tactility/Source/app/files/FileUtils.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "Tactility/app/files/FileUtils.h" - -#include -#include - -#include - -namespace tt::app::files { - -#define TAG "file_utils" - -std::string getChildPath(const std::string& basePath, const std::string& childPath) { - // Postfix with "/" when the current path isn't "/" - if (basePath.length() != 1) { - return basePath + "/" + childPath; - } else { - return "/" + childPath; - } -} - -int dirent_filter_dot_entries(const struct dirent* entry) { - return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; -} - -bool dirent_sort_alpha_and_type(const struct dirent& left, const struct dirent& right) { - bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR; - bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR; - if (left_is_dir == right_is_dir) { - return strcmp(left.d_name, right.d_name) < 0; - } else { - return left_is_dir > right_is_dir; - } -} - -int scandir( - const std::string& path, - std::vector& outList, - ScandirFilter _Nullable filterMethod, - ScandirSort _Nullable sortMethod -) { - TT_LOG_I(TAG, "scandir start"); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); - return -1; - } - - struct dirent* current_entry; - while ((current_entry = readdir(dir)) != nullptr) { - if (filterMethod(current_entry) == 0) { - outList.push_back(*current_entry); - } - } - - closedir(dir); - - if (sortMethod != nullptr) { - sort(outList.begin(), outList.end(), sortMethod); - } - - TT_LOG_I(TAG, "scandir finish"); - return (int)outList.size(); -}; - -bool isSupportedExecutableFile(const std::string& filename) { -#ifdef ESP_PLATFORM - // Currently only the PNG library is built into Tactility - return filename.ends_with(".elf"); -#else - return false; -#endif -} - -bool isSupportedImageFile(const std::string& filename) { - // Currently only the PNG library is built into Tactility - return string::lowercase(filename).ends_with(".png"); -} - -bool isSupportedTextFile(const std::string& filename) { - std::string filename_lower = string::lowercase(filename); - return filename_lower.ends_with(".txt") || - filename_lower.ends_with(".ini") || - filename_lower.ends_with(".json") || - filename_lower.ends_with(".yaml") || - filename_lower.ends_with(".yml") || - filename_lower.ends_with(".lua") || - filename_lower.ends_with(".js") || - filename_lower.ends_with(".properties"); -} - -} // namespace tt::app::files diff --git a/Tactility/Source/app/fileselection/FileSelection.cpp b/Tactility/Source/app/fileselection/FileSelection.cpp new file mode 100644 index 00000000..0784d001 --- /dev/null +++ b/Tactility/Source/app/fileselection/FileSelection.cpp @@ -0,0 +1,78 @@ +#include "Tactility/app/fileselection/FileSelectionPrivate.h" +#include "Tactility/app/fileselection/View.h" +#include "Tactility/app/fileselection/State.h" +#include "Tactility/app/AppContext.h" + +#include +#include + +#include + +namespace tt::app::fileselection { + +#define TAG "fileselection_app" + +extern const AppManifest manifest; + +std::string getResultPath(const Bundle& bundle) { + std::string result; + if (bundle.optString("path", result)) { + return result; + } else { + return ""; + } +} + +Mode getMode(const Bundle& bundle) { + int32_t mode = static_cast(Mode::ExistingOrNew); + bundle.optInt32("mode", mode); + return static_cast(mode); +} + +void setMode(Bundle& bundle, Mode mode) { + auto mode_int = static_cast(mode); + bundle.putInt32("mode", mode_int); +} + +class FileSelection : public App { + std::unique_ptr view; + std::shared_ptr state; + +public: + FileSelection() { + state = std::make_shared(); + view = std::make_unique(state, [this](const std::string& path) { + auto bundle = std::make_unique(); + bundle->putString("path", path); + setResult(Result::Ok, std::move(bundle)); + service::loader::stopApp(); + }); + } + + void onShow(AppContext& appContext, lv_obj_t* parent) override { + auto mode = getMode(*appContext.getParameters()); + view->init(parent, mode); + } +}; + +extern const AppManifest manifest = { + .id = "FileSelection", + .name = "File Selection", + .icon = TT_ASSETS_APP_ICON_FILES, + .type = Type::Hidden, + .createApp = create +}; + +LaunchId startForExistingFile() { + auto bundle = std::make_shared(); + setMode(*bundle, Mode::Existing); + return service::loader::startApp(manifest.id, bundle); +} + +LaunchId startForExistingOrNewFile() { + auto bundle = std::make_shared(); + setMode(*bundle, Mode::ExistingOrNew); + return service::loader::startApp(manifest.id, bundle); +} + +} // namespace diff --git a/Tactility/Source/app/fileselection/State.cpp b/Tactility/Source/app/fileselection/State.cpp new file mode 100644 index 00000000..c648b6d9 --- /dev/null +++ b/Tactility/Source/app/fileselection/State.cpp @@ -0,0 +1,118 @@ +#include "Tactility/app/fileselection/State.h" + +#include +#include "Tactility/hal/sdcard/SdCardDevice.h" +#include +#include +#include + +#include +#include +#include +#include + +#define TAG "fileselection_app" + +namespace tt::app::fileselection { + +State::State() { + if (kernel::getPlatform() == kernel::PlatformSimulator) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) != nullptr) { + setEntriesForPath(cwd); + } else { + TT_LOG_E(TAG, "Failed to get current work directory files"); + setEntriesForPath("/"); + } + } else { + setEntriesForPath("/"); + } +} + +std::string State::getSelectedChildPath() const { + return file::getChildPath(current_path, selected_child_entry); +} + +bool State::setEntriesForPath(const std::string& path) { + auto lock = mutex.asScopedLock(); + if (!lock.lock(100)) { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "setEntriesForPath"); + return false; + } + + TT_LOG_I(TAG, "Changing path: %s -> %s", current_path.c_str(), path.c_str()); + + /** + * ESP32 does not have a root directory, so we have to create it manually. + * We'll add the NVS Flash partitions and the binding for the sdcard. + */ + bool show_custom_root = (kernel::getPlatform() == kernel::PlatformEsp) && (path == "/"); + if (show_custom_root) { + TT_LOG_I(TAG, "Setting custom root"); + dir_entries.clear(); + dir_entries.push_back(dirent{ + .d_ino = 0, + .d_type = file::TT_DT_DIR, + .d_name = SYSTEM_PARTITION_NAME + }); + dir_entries.push_back(dirent{ + .d_ino = 1, + .d_type = file::TT_DT_DIR, + .d_name = DATA_PARTITION_NAME + }); + + auto sdcards = tt::hal::findDevices(hal::Device::Type::SdCard); + for (auto& sdcard : sdcards) { + auto state = sdcard->getState(); + if (state == hal::sdcard::SdCardDevice::State::Mounted) { + auto mount_name = sdcard->getMountPath().substr(1); + auto dir_entry = dirent { + .d_ino = 2, + .d_type = file::TT_DT_DIR, + .d_name = { 0 } + }; + assert(mount_name.length() < sizeof(dirent::d_name)); + strcpy(dir_entry.d_name, mount_name.c_str()); + dir_entries.push_back(dir_entry); + } + } + + current_path = path; + selected_child_entry = ""; + return true; + } else { + dir_entries.clear(); + 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; + selected_child_entry = ""; + return true; + } else { + TT_LOG_E(TAG, "Failed to fetch entries for %s", path.c_str()); + return false; + } + } +} + +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); +} + +bool State::getDirent(uint32_t index, dirent& dirent) { + auto lock = mutex.asScopedLock(); + if (!lock.lock(50 / portTICK_PERIOD_MS)) { + return false; + } + + if (index < dir_entries.size()) { + dirent = dir_entries[index]; + return true; + } else { + return false; + } +} + +} diff --git a/Tactility/Source/app/fileselection/View.cpp b/Tactility/Source/app/fileselection/View.cpp new file mode 100644 index 00000000..10513923 --- /dev/null +++ b/Tactility/Source/app/fileselection/View.cpp @@ -0,0 +1,215 @@ +#include "Tactility/app/fileselection/View.h" + +#include "Tactility/app/alertdialog/AlertDialog.h" +#include "Tactility/lvgl/Toolbar.h" +#include "Tactility/lvgl/LvglSync.h" + +#include +#include "Tactility/file/File.h" +#include + +#include +#include +#include + +#ifdef ESP_PLATFORM +#include "Tactility/service/loader/Loader.h" +#endif + +#define TAG "fileselection_app" + +namespace tt::app::fileselection { + +// region Callbacks + +static void onDirEntryPressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + auto* button = lv_event_get_target_obj(event); + auto index = lv_obj_get_index(button); + view->onDirEntryPressed(index); +} + +static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onNavigateUpPressed(); +} + +// endregion + +void View::onTapFile(const std::string& path, const std::string& filename) { + std::string file_path = path + "/" + filename; + + // For PC we need to make the path relative to the current work directory, + // because that's how LVGL maps its 'drive letter' to the file system. + std::string processed_filepath; + if (kernel::getPlatform() == kernel::PlatformSimulator) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + TT_LOG_E(TAG, "Failed to get current working directory"); + return; + } + if (!file_path.starts_with(cwd)) { + TT_LOG_E(TAG, "Can only work with files in working directory %s", cwd); + return; + } + processed_filepath = file_path.substr(strlen(cwd)); + } else { + processed_filepath = file_path; + } + + TT_LOG_I(TAG, "Clicked %s", processed_filepath.c_str()); + + lv_textarea_set_text(path_textarea, processed_filepath.c_str()); +} + +void View::onDirEntryPressed(uint32_t index) { + dirent dir_entry; + if (state->getDirent(index, dir_entry)) { + TT_LOG_I(TAG, "Pressed %s %d", dir_entry.d_name, dir_entry.d_type); + state->setSelectedChildEntry(dir_entry.d_name); + using namespace tt::file; + switch (dir_entry.d_type) { + case TT_DT_DIR: + case TT_DT_CHR: + state->setEntriesForChildPath(dir_entry.d_name); + lv_textarea_set_text(path_textarea, state->getCurrentPathWithTrailingSlash().c_str()); + update(); + break; + case TT_DT_LNK: + TT_LOG_W(TAG, "opening links is not supported"); + break; + case TT_DT_REG: + onTapFile(state->getCurrentPath(), dir_entry.d_name); + break; + default: + // Assume it's a file + // TODO: Find a better way to identify a file + onTapFile(state->getCurrentPath(), dir_entry.d_name); + break; + } + } +} + +void View::onSelectButtonPressed(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + const char* path = lv_textarea_get_text(view->path_textarea); + if (path == nullptr || strlen(path) == 0) { + TT_LOG_W(TAG, "Select pressed, but not path found in textarea"); + return; + } + + view->onFileSelected(std::string(path)); +} + +static bool isSelectableFilePath(const char* path) { + if (path == nullptr) { + return false; + } + + auto len = strlen(path); + if (len == 0) { + return false; + } + + return path[len - 1] != '/'; +} + +void View::onPathTextChanged(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + const char* path = lv_textarea_get_text(view->path_textarea); + if (isSelectableFilePath(path)) { + lv_obj_remove_flag(view->select_button, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(view->select_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { + tt_check(list); + const char* symbol; + if (dir_entry.d_type == file::TT_DT_DIR || dir_entry.d_type == file::TT_DT_CHR) { + symbol = LV_SYMBOL_DIRECTORY; + } else { + symbol = LV_SYMBOL_FILE; + } + lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name); + lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this); +} + +void View::onNavigateUpPressed() { + if (state->getCurrentPath() != "/") { + TT_LOG_I(TAG, "Navigating upwards"); + std::string new_absolute_path; + if (string::getPathParent(state->getCurrentPath(), new_absolute_path)) { + state->setEntriesForPath(new_absolute_path); + } + if (new_absolute_path.length() > 1) { + lv_textarea_set_text(path_textarea, (new_absolute_path + "/").c_str()); + } else { + lv_textarea_set_text(path_textarea, new_absolute_path.c_str()); + } + update(); + } +} + +void View::update() { + auto scoped_lockable = lvgl::getSyncLock()->asScopedLock(); + if (scoped_lockable.lock(lvgl::defaultLockTime)) { + lv_obj_clean(dir_entry_list); + + state->withEntries([this](const std::vector& entries) { + for (auto entry : entries) { + TT_LOG_D(TAG, "Entry: %s %d", entry.d_name, entry.d_type); + createDirEntryWidget(dir_entry_list, entry); + } + }); + + if (state->getCurrentPath() == "/") { + lv_obj_add_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_remove_flag(navigate_up_button, LV_OBJ_FLAG_HIDDEN); + } + } else { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "lvgl"); + } +} + +void View::init(lv_obj_t* parent, Mode mode) { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + auto* toolbar = lvgl::toolbar_create(parent, "Select File"); + navigate_up_button = lvgl::toolbar_add_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); + + auto* wrapper = lv_obj_create(parent); + lv_obj_set_width(wrapper, LV_PCT(100)); + lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, 0); + lv_obj_set_flex_grow(wrapper, 1); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + + dir_entry_list = lv_list_create(wrapper); + lv_obj_set_height(dir_entry_list, LV_PCT(100)); + lv_obj_set_flex_grow(dir_entry_list, 1); + + auto* bottom_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(bottom_wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_size(bottom_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(bottom_wrapper, 0, 0); + lv_obj_set_style_pad_all(bottom_wrapper, 0, 0); + + path_textarea = lv_textarea_create(bottom_wrapper); + lv_textarea_set_one_line(path_textarea, true); + lv_obj_set_flex_grow(path_textarea, 1); + service::gui::keyboardAddTextArea(path_textarea); + lv_obj_add_event_cb(path_textarea, onPathTextChanged, LV_EVENT_VALUE_CHANGED, this); + + select_button = lv_button_create(bottom_wrapper); + auto* select_button_label = lv_label_create(select_button); + lv_label_set_text(select_button_label, "Select"); + lv_obj_add_event_cb(select_button, onSelectButtonPressed, LV_EVENT_SHORT_CLICKED, this); + lv_obj_add_flag(select_button, LV_OBJ_FLAG_HIDDEN); + + update(); +} + +} diff --git a/Tactility/Source/app/gpio/Gpio.cpp b/Tactility/Source/app/gpio/Gpio.cpp index 892704ef..9524642f 100644 --- a/Tactility/Source/app/gpio/Gpio.cpp +++ b/Tactility/Source/app/gpio/Gpio.cpp @@ -1,10 +1,10 @@ #include "Tactility/service/loader/Loader.h" -#include "Tactility/lvgl/Toolbar.h" #include #include +#include "Tactility/lvgl/Toolbar.h" #include +#include #include -#include #include namespace tt::app::gpio { @@ -13,10 +13,8 @@ extern const AppManifest manifest; class GpioApp : public App { -private: - - lv_obj_t* lvPins[GPIO_NUM_MAX] = {0 }; - uint8_t pinStates[GPIO_NUM_MAX] = {0 }; + lv_obj_t* lvPins[GPIO_NUM_MAX] = { nullptr }; + uint8_t pinStates[GPIO_NUM_MAX] = { 0 }; std::unique_ptr timer; Mutex mutex; @@ -40,7 +38,7 @@ void GpioApp::updatePinStates() { // Update pin states for (int i = 0; i < GPIO_NUM_MAX; ++i) { #ifdef ESP_PLATFORM - pinStates[i] = gpio_get_level((gpio_num_t)i); + pinStates[i] = gpio_get_level(static_cast(i)); #else pinStates[i] = gpio_get_level(i); #endif @@ -60,9 +58,9 @@ void GpioApp::updatePinWidgets() { if (reinterpret_cast(level) != label_user_data) { lv_obj_set_user_data(label, reinterpret_cast(level)); if (level == 0) { - lv_obj_set_style_text_color(label, lv_color_black(), 0); + lv_obj_set_style_text_color(label, lv_color_background_darkest(), LV_STATE_DEFAULT); } else { - lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), 0); + lv_obj_set_style_text_color(label, lv_color_make(0, 200, 0), LV_STATE_DEFAULT); } } } @@ -71,8 +69,8 @@ void GpioApp::updatePinWidgets() { lv_obj_t* GpioApp::createGpioRowWrapper(lv_obj_t* parent) { lv_obj_t* wrapper = lv_obj_create(parent); - lv_obj_set_style_pad_all(wrapper, 0, 0); - lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_pad_all(wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_size(wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); return wrapper; } @@ -87,7 +85,7 @@ void GpioApp::onTimer() { void GpioApp::startTask() { mutex.lock(); assert(timer == nullptr); - timer = std::make_unique(Timer::Type::Periodic, [this]() { + timer = std::make_unique(Timer::Type::Periodic, [this] { onTimer(); }); timer->start(100 / portTICK_PERIOD_MS); @@ -113,7 +111,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { auto* wrapper = lv_obj_create(parent); lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_flex_grow(wrapper, 1); - lv_obj_set_style_border_width(wrapper, 0, 0); + lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); auto* display = lv_obj_get_display(parent); auto horizontal_px = lv_display_get_horizontal_resolution(display); @@ -122,7 +120,8 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { int32_t x_spacing = 20; uint8_t column = 0; - uint8_t column_limit = is_landscape_display ? 10 : 5; + const uint8_t offset_from_left_label = 4; + const uint8_t column_limit = is_landscape_display ? 10 : 5; auto* row_wrapper = createGpioRowWrapper(wrapper); lv_obj_align(row_wrapper, LV_ALIGN_TOP_MID, 0, 0); @@ -138,8 +137,9 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { // Add a new GPIO status indicator auto* status_label = lv_label_create(row_wrapper); - lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing), 0); + lv_obj_set_pos(status_label, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0); lv_label_set_text_fmt(status_label, "%s", LV_SYMBOL_STOP); + lv_obj_set_style_text_color(status_label, lv_color_background_darkest(), LV_STATE_DEFAULT); lvPins[i] = status_label; column++; @@ -148,7 +148,7 @@ void GpioApp::onShow(AppContext& app, lv_obj_t* parent) { // Add the GPIO number after the last item on a row auto* postfix = lv_label_create(row_wrapper); lv_label_set_text_fmt(postfix, "%02d", i); - lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing), 0); + lv_obj_set_pos(postfix, (int32_t)((column+1) * x_spacing + offset_from_left_label), 0); // Add a new row wrapper underneath the last one auto* new_row_wrapper = createGpioRowWrapper(wrapper); diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index 3c1f3e8d..c80de746 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -24,8 +24,6 @@ extern const AppManifest manifest; class GpsSettingsApp final : public App { -private: - std::unique_ptr timer; std::shared_ptr appReference = std::make_shared(this); lv_obj_t* statusWrapper = nullptr; @@ -96,7 +94,7 @@ private: memcpy(&index, &index_as_voidptr, sizeof(int)); std::vector configurations; - auto gps_service = tt::service::gps::findGpsService(); + auto gps_service = service::gps::findGpsService(); if (gps_service && gps_service->getGpsConfigurations(configurations)) { TT_LOG_I(TAG, "Found service and configs %d %d", index, configurations.size()); if (index <= configurations.size()) { diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 4fd4be01..a61a89ae 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -27,33 +27,37 @@ static lv_obj_t* createAppButton(lv_obj_t* parent, const char* title, const char auto* apps_button = lv_button_create(wrapper); lv_obj_set_style_pad_hor(apps_button, 0, 0); lv_obj_set_style_pad_top(apps_button, 0, 0); - lv_obj_set_style_pad_bottom(apps_button, 16, 0); + lv_obj_set_style_pad_bottom(apps_button, 8, 0); lv_obj_set_style_shadow_width(apps_button, 0, 0); lv_obj_set_style_border_width(apps_button, 0, 0); lv_obj_set_style_bg_opa(apps_button, 0, LV_PART_MAIN); auto* button_image = lv_image_create(apps_button); lv_image_set_src(button_image, imageFile); - lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); - lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), 0); - lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, 0); + lv_obj_set_style_image_recolor(button_image, lv_theme_get_color_primary(parent), LV_STATE_DEFAULT); + lv_obj_set_style_image_recolor_opa(button_image, LV_OPA_COVER, LV_STATE_DEFAULT); // Ensure buttons are still tappable when asset fails to load + // Icon images are 40x40, so we get some extra padding too lv_obj_set_size(button_image, 64, 64); auto* label = lv_label_create(wrapper); lv_label_set_text(label, title); lv_obj_align(label, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_add_event_cb(wrapper, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); + lv_obj_add_event_cb(apps_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); + lv_obj_add_event_cb(label, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)appId); + return wrapper; } class LauncherApp : public App { void onCreate(TT_UNUSED AppContext& app) override { - auto* config = tt::getConfiguration(); + auto* config = getConfiguration(); if (!config->autoStartAppId.empty()) { TT_LOG_I(TAG, "auto-starting %s", config->autoStartAppId.c_str()); - tt::service::loader::startApp(config->autoStartAppId); + service::loader::startApp(config->autoStartAppId); } } diff --git a/Tactility/Source/app/log/Log.cpp b/Tactility/Source/app/log/Log.cpp index fbd858a2..f2877e05 100644 --- a/Tactility/Source/app/log/Log.cpp +++ b/Tactility/Source/app/log/Log.cpp @@ -91,10 +91,9 @@ public: updateLogEntries(); } - void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { - auto resultIndex = selectiondialog::getResultIndex(*bundle); - switch (resultIndex) { + switch (selectiondialog::getResultIndex(*bundle)) { case 0: filterLevel = LogLevel::Verbose; break; diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index 301953cb..b35e3f12 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -5,35 +5,25 @@ #include #include -#include -#include +#include +#include +#include namespace tt::app::notes { constexpr const char* TAG = "Notes"; class NotesApp : public App { - AppContext* appContext = nullptr; lv_obj_t* uiCurrentFileName; lv_obj_t* uiDropDownMenu; - lv_obj_t* uiFileList; - lv_obj_t* uiFileListCloseBtn; lv_obj_t* uiNoteText; - lv_obj_t* uiSaveDialog; - lv_obj_t* uiSaveDialogFileName; - lv_obj_t* uiSaveDialogSaveBtn; - lv_obj_t* uiSaveDialogCancelBtn; - lv_obj_t* uiMessageBox; - lv_obj_t* uiMessageBoxButtonOk; - lv_obj_t* uiMessageBoxButtonNo; - char menuItem[32]; - uint8_t menuIdx = 0; - std::string fileContents; - std::string fileName; - std::string newFileName; std::string filePath; + std::string saveBuffer; + + LaunchId loadFileLaunchId = 0; + LaunchId saveFileLaunchId = 0; #pragma region Main_Events_Functions @@ -41,298 +31,85 @@ class NotesApp : public App { lv_event_code_t code = lv_event_get_code(e); lv_obj_t* obj = lv_event_get_target_obj(e); - if (code == LV_EVENT_CLICKED) { - if (obj == uiFileListCloseBtn) { - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - lv_obj_del(uiFileList); - } else if (obj == uiSaveDialogSaveBtn) { - newFileName = lv_textarea_get_text(uiSaveDialogFileName); - if (newFileName.length() == 0) { - uiMessageBoxShow(menuItem, "Filename is empty.", false); - } else { - std::string noteText = lv_textarea_get_text(uiNoteText); - filePath = appContext->getPaths()->getDataPath(newFileName); - - if (writeFile(filePath, noteText)) { - uiMessageBoxShow(menuItem, "File created successfully!", false); - lv_label_set_text(uiCurrentFileName, newFileName.c_str()); - } else { - uiMessageBoxShow(menuItem, "Something went wrong!\nFile creation failed.", false); - } - } - lv_obj_del(uiSaveDialog); - - } else if (obj == uiMessageBoxButtonOk || obj == uiMessageBoxButtonNo) { - lv_obj_del(uiMessageBox); - } else if (obj == uiSaveDialogCancelBtn) { - lv_obj_del(uiSaveDialog); - } - } - if (code == LV_EVENT_VALUE_CHANGED) { if (obj == uiDropDownMenu) { - lv_dropdown_get_selected_str(obj, menuItem, sizeof(menuItem)); - menuIdx = lv_dropdown_get_selected(obj); - std::string newContents = lv_textarea_get_text(uiNoteText); - if (menuIdx == 1) { //Save - //Normal Save? + switch (lv_dropdown_get_selected(obj)) { + case 0: // New + resetFileContent(); + break; + case 1: // Save + if (!filePath.empty()) { + lvgl::getSyncLock()->lock(); + saveBuffer = lv_textarea_get_text(uiNoteText); + lvgl::getSyncLock()->unlock(); + saveFile(filePath); + } + break; + case 2: // Save as... + lvgl::getSyncLock()->lock(); + saveBuffer = lv_textarea_get_text(uiNoteText); + lvgl::getSyncLock()->unlock(); + saveFileLaunchId = fileselection::startForExistingOrNewFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); + break; + case 3: // Load + loadFileLaunchId = fileselection::startForExistingFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); + break; } - if (menuIdx == 2) { //Save As... - uiSaveFileDialog(); - return; - } - - //Not working...more investigation needed. - //If note contents has changed in currently open file, save it. - - //bool newToSave = newContents != fileContents && newContents.length() != 0; - //if (newToSave) { - //uiMessageBoxShow(menuItem, "Do you want to save it?", true); - //} else { - menuAction(); - //} } else { - lv_obj_t* cont = lv_event_get_current_target_obj(e); + auto* cont = lv_event_get_current_target_obj(e); if (obj == cont) return; if (lv_obj_get_child(cont, 1)) { - uiSaveFileDialog(); + saveFileLaunchId = fileselection::startForExistingOrNewFile(); + TT_LOG_I(TAG, "launched with id %d", loadFileLaunchId); } else { //Reset - lv_textarea_set_text(uiNoteText, ""); - fileName = ""; - lv_label_set_text(uiCurrentFileName, "Untitled"); + resetFileContent(); } - lv_obj_delete(uiMessageBox); } } } - void menuAction() { - switch (menuIdx) { - case 0: //Reset - lv_textarea_set_text(uiNoteText, ""); - fileName = ""; - lv_label_set_text(uiCurrentFileName, "Untitled"); - break; - case 3: - uiOpenFileDialog(); - break; - } + void resetFileContent() { + lv_textarea_set_text(uiNoteText, ""); + filePath = ""; + saveBuffer = ""; + lv_label_set_text(uiCurrentFileName, "Untitled"); } - void uiMessageBoxShow(std::string title, std::string message, bool isSelectable) { - uiMessageBox = lv_obj_create(lv_scr_act()); - lv_obj_set_size(uiMessageBox, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr)); - lv_obj_align(uiMessageBox, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(uiMessageBox, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t* uiMessageBoxTitle = lv_label_create(uiMessageBox); - lv_label_set_text(uiMessageBoxTitle, title.c_str()); - lv_obj_set_size(uiMessageBoxTitle, lv_display_get_horizontal_resolution(nullptr) - 30, 30); - lv_obj_align(uiMessageBoxTitle, LV_ALIGN_TOP_MID, 0, 0); - - lv_obj_t* messageLabel = lv_label_create(uiMessageBox); - lv_obj_align(messageLabel, LV_ALIGN_CENTER, 0, 0); - lv_obj_set_width(messageLabel, LV_PCT(80)); - lv_obj_set_style_text_align(messageLabel, LV_TEXT_ALIGN_CENTER, 0); - lv_label_set_text(messageLabel, message.c_str()); - lv_label_set_long_mode(messageLabel, LV_LABEL_LONG_WRAP); - - lv_obj_t* buttonWrapper = lv_obj_create(uiMessageBox); - lv_obj_set_flex_flow(buttonWrapper, LV_FLEX_FLOW_ROW); - lv_obj_set_size(buttonWrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(buttonWrapper, 0, 0); - lv_obj_set_flex_align(buttonWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_border_width(buttonWrapper, 0, 0); - lv_obj_align(buttonWrapper, LV_ALIGN_BOTTOM_MID, 0, 5); - - if (isSelectable == true) { - lv_obj_t* buttonYes = lv_button_create(buttonWrapper); - lv_obj_t* buttonLabelYes = lv_label_create(buttonYes); - lv_obj_align(buttonLabelYes, LV_ALIGN_BOTTOM_LEFT, 0, 0); - lv_label_set_text(buttonLabelYes, "Yes"); - lv_obj_add_event_cb(buttonYes, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - - uiMessageBoxButtonNo = lv_button_create(buttonWrapper); - lv_obj_t* buttonLabelNo = lv_label_create(uiMessageBoxButtonNo); - lv_obj_align(buttonLabelNo, LV_ALIGN_BOTTOM_RIGHT, 0, 0); - lv_label_set_text(buttonLabelNo, "No"); - lv_obj_add_event_cb(uiMessageBoxButtonNo, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - } else { - uiMessageBoxButtonOk = lv_button_create(buttonWrapper); - lv_obj_t* buttonLabelOk = lv_label_create(uiMessageBoxButtonOk); - lv_obj_align(buttonLabelOk, LV_ALIGN_BOTTOM_MID, 0, 0); - lv_label_set_text(buttonLabelOk, "Ok"); - lv_obj_add_event_cb(uiMessageBoxButtonOk, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - } - } - -#pragma endregion Main_Events_Functions - -#pragma region Save_Events_Functions - - void uiSaveFileDialog() { - uiSaveDialog = lv_obj_create(lv_scr_act()); - if (lv_display_get_horizontal_resolution(nullptr) <= 240 || lv_display_get_vertical_resolution(nullptr) <= 240) { //small screens - lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 80); - } else { //large screens - lv_obj_set_size(uiSaveDialog, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr) - 230); - } - lv_obj_align(uiSaveDialog, LV_ALIGN_TOP_MID, 0, 0); - lv_obj_remove_flag(uiSaveDialog, LV_OBJ_FLAG_SCROLLABLE); - - lv_obj_t* uiSaveDialogTitle = lv_label_create(uiSaveDialog); - lv_label_set_text(uiSaveDialogTitle, menuItem); - lv_obj_set_size(uiSaveDialogTitle, lv_display_get_horizontal_resolution(nullptr) - 30, 30); - lv_obj_align(uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 0); - - uiSaveDialogFileName = lv_textarea_create(uiSaveDialog); - lv_obj_set_size(uiSaveDialogFileName, lv_display_get_horizontal_resolution(nullptr) - 30, 40); - lv_obj_align_to(uiSaveDialogFileName, uiSaveDialogTitle, LV_ALIGN_TOP_MID, 0, 30); - lv_textarea_set_placeholder_text(uiSaveDialogFileName, "Enter file name..."); - lv_textarea_set_one_line(uiSaveDialogFileName, true); - lv_obj_add_state(uiSaveDialogFileName, LV_STATE_FOCUSED); - - //Both hardware and software keyboard not auto attaching here for some reason unless the textarea is touched to focus it... - tt::lvgl::keyboard_add_textarea(uiSaveDialogFileName); - - if (fileName != "" || fileName != "Untitled") { - lv_textarea_set_text(uiSaveDialogFileName, fileName.c_str()); - } else { - lv_textarea_set_placeholder_text(uiSaveDialogFileName, "filename?"); - } - - uiSaveDialogSaveBtn = lv_btn_create(uiSaveDialog); - lv_obj_add_event_cb(uiSaveDialogSaveBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - lv_obj_align(uiSaveDialogSaveBtn, LV_ALIGN_BOTTOM_LEFT, 0, 0); - lv_obj_t* btnLabel = lv_label_create(uiSaveDialogSaveBtn); - lv_label_set_text(btnLabel, "Save"); - lv_obj_center(btnLabel); - - uiSaveDialogCancelBtn = lv_btn_create(uiSaveDialog); - lv_obj_add_event_cb(uiSaveDialogCancelBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - lv_obj_align(uiSaveDialogCancelBtn, LV_ALIGN_BOTTOM_RIGHT, 0, 0); - lv_obj_t* btnLabel2 = lv_label_create(uiSaveDialogCancelBtn); - lv_label_set_text(btnLabel2, "Cancel"); - lv_obj_center(btnLabel2); - } - - bool writeFile(std::string path, std::string message) { - std::ofstream fileStream(path); - - if (!fileStream.is_open()) { - TT_LOG_E(TAG, "Failed to write file"); - return false; - } - if (fileStream.is_open()) { - fileStream << message; - TT_LOG_I(TAG, "File written successfully"); - fileStream.close(); - return true; - } - return true; - } - -#pragma endregion Save_Events_Functions - #pragma region Open_Events_Functions - void openFileEventCb(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - lv_obj_t* obj = lv_event_get_target_obj(e); - - if (code == LV_EVENT_CLICKED) { - std::string selectedFile = lv_list_get_btn_text(uiFileList, obj); - fileName = selectedFile.substr(0, selectedFile.find(" (")); - std::string filePath = appContext->getPaths()->getDataPath(fileName); - fileContents = readFile(filePath.c_str()); - lv_textarea_set_text(uiNoteText, fileContents.c_str()); - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - lv_obj_del(uiFileList); - - lv_label_set_text(uiCurrentFileName, fileName.c_str()); - } + void openFile(const std::string& path) { + // We might be reading from the SD card, which could share a SPI bus with other devices (display) + hal::sdcard::withSdCardLock(path, [this, path]() { + auto data = file::readString(path); + if (data != nullptr) { + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); + lv_textarea_set_text(uiNoteText, reinterpret_cast(data.get())); + lv_label_set_text(uiCurrentFileName, path.c_str()); + filePath = path; + TT_LOG_I(TAG, "Loaded from %s", path.c_str()); + } + }); } - void uiOpenFileDialog() { - uiFileList = lv_list_create(lv_scr_act()); - lv_obj_set_size(uiFileList, lv_display_get_horizontal_resolution(nullptr), lv_display_get_vertical_resolution(nullptr)); - lv_obj_align(uiFileList, LV_ALIGN_TOP_MID, 0, 0); - lv_list_add_text(uiFileList, "Notes"); - - uiFileListCloseBtn = lv_btn_create(uiFileList); - lv_obj_set_size(uiFileListCloseBtn, 36, 36); - lv_obj_add_flag(uiFileListCloseBtn, LV_OBJ_FLAG_FLOATING); - lv_obj_align(uiFileListCloseBtn, LV_ALIGN_TOP_RIGHT, 10, 4); - lv_obj_add_event_cb(uiFileListCloseBtn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_CLICKED, this); - - lv_obj_t* uiFileListCloseLabel = lv_label_create(uiFileListCloseBtn); - lv_label_set_text(uiFileListCloseLabel, LV_SYMBOL_CLOSE); - lv_obj_center(uiFileListCloseLabel); - - lv_obj_add_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - - //TODO: Move this to SD Card? - std::vector noteFileList; - const std::string& path = appContext->getPaths()->getDataDirectory(); - DIR* dir = opendir(path.c_str()); - if (dir == nullptr) { - TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); - return; - } - - struct dirent* current_entry; - while ((current_entry = readdir(dir)) != nullptr) { - noteFileList.push_back(current_entry->d_name); - } - closedir(dir); - - if (noteFileList.size() == 0) return; - - for (std::vector::iterator item = noteFileList.begin(); item != noteFileList.end(); ++item) { - lv_obj_t* btn = lv_list_add_btn(uiFileList, LV_SYMBOL_FILE, (*item).c_str()); - lv_obj_add_event_cb(btn, [](lv_event_t* e) { - auto *self = static_cast(lv_event_get_user_data(e)); - self->openFileEventCb(e); }, LV_EVENT_CLICKED, this); - } - - lv_obj_move_foreground(uiFileListCloseBtn); - lv_obj_remove_flag(uiFileList, LV_OBJ_FLAG_HIDDEN); - } - - std::string readFile(std::string path) { - std::ifstream fileStream(path); - - if (!fileStream.is_open()) { - return ""; - } - - std::string temp = ""; - std::string file_contents; - while (std::getline(fileStream, temp)) { - file_contents += temp; - file_contents.push_back('\n'); - } - fileStream.close(); - return file_contents; + bool saveFile(const std::string& path) { + // We might be writing to SD card, which could share a SPI bus with other devices (display) + return hal::sdcard::withSdCardLock(path, [this, path]() { + if (file::writeString(path, saveBuffer.c_str())) { + TT_LOG_I(TAG, "Saved to %s", path.c_str()); + filePath = path; + return true; + } else { + return false; + } + }); } #pragma endregion Open_Events_Functions void onShow(AppContext& context, lv_obj_t* parent) override { - appContext = &context; - lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); @@ -347,9 +124,14 @@ class NotesApp : public App { lv_obj_set_style_border_color(uiDropDownMenu, lv_color_hex(0xFAFAFA), LV_PART_MAIN); lv_obj_set_style_border_width(uiDropDownMenu, 1, LV_PART_MAIN); lv_obj_align(uiDropDownMenu, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(uiDropDownMenu, [](lv_event_t* e) { + lv_obj_add_event_cb(uiDropDownMenu, + [](lv_event_t* e) { auto *self = static_cast(lv_event_get_user_data(e)); - self->appNotesEventCb(e); }, LV_EVENT_VALUE_CHANGED, this); + self->appNotesEventCb(e); + }, + LV_EVENT_VALUE_CHANGED, + this + ); lv_obj_t* wrapper = lv_obj_create(parent); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); @@ -387,11 +169,31 @@ class NotesApp : public App { lv_obj_align(uiCurrentFileName, LV_ALIGN_CENTER, 0, 0); //TODO: Move this to SD Card? - if (!tt::file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) { + if (!file::findOrCreateDirectory(context.getPaths()->getDataDirectory(), 0777)) { TT_LOG_E(TAG, "Failed to find or create path %s", context.getPaths()->getDataDirectory().c_str()); } - tt::lvgl::keyboard_add_textarea(uiNoteText); + lvgl::keyboard_add_textarea(uiNoteText); + } + + void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr resultData) override { + TT_LOG_I(TAG, "Result for launch id %d", launchId); + if (launchId == loadFileLaunchId) { + loadFileLaunchId = 0; + if (result == Result::Ok && resultData != nullptr) { + auto path = fileselection::getResultPath(*resultData); + openFile(path); + } + } else if (launchId == saveFileLaunchId) { + saveFileLaunchId = 0; + if (result == Result::Ok && resultData != nullptr) { + auto path = fileselection::getResultPath(*resultData); + // Must re-open file, because UI was cleared after opening other app + if (saveFile(path)) { + openFile(path); + } + } + } } }; @@ -401,4 +203,5 @@ extern const AppManifest manifest = { .icon = TT_ASSETS_APP_ICON_NOTES, .createApp = create }; + } // namespace tt::app::notes \ No newline at end of file diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index a56ef480..f9290374 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -88,7 +88,7 @@ public: } } - void onResult(AppContext& app, Result result, std::unique_ptr bundle) override { + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { auto name = timezone::getResultName(*bundle); auto code = timezone::getResultCode(*bundle); diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 8c08f91e..4b18cd01 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -112,7 +112,7 @@ class WifiApSettings : public App { } } - void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED Result result, std::unique_ptr bundle) override { + void onResult(TT_UNUSED AppContext& appContext, TT_UNUSED LaunchId launchId, TT_UNUSED Result result, std::unique_ptr bundle) override { if (result == Result::Ok && bundle != nullptr) { auto index = alertdialog::getResultIndex(*bundle); if (index == 0) { // Yes diff --git a/Tactility/Source/hal/i2c/I2cDevice.cpp b/Tactility/Source/hal/i2c/I2cDevice.cpp index 4891e471..1e96f8d5 100644 --- a/Tactility/Source/hal/i2c/I2cDevice.cpp +++ b/Tactility/Source/hal/i2c/I2cDevice.cpp @@ -4,6 +4,22 @@ namespace tt::hal::i2c { +bool I2cDevice::read(uint8_t* data, size_t dataSize, TickType_t timeout) { + return tt::hal::i2c::masterRead(port, address, data, dataSize, timeout); +} + +bool I2cDevice::write(const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + return tt::hal::i2c::masterWrite(port, address, data, dataSize, timeout); +} + +bool I2cDevice::writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout) { + return masterWriteRead(port, address, writeData, writeDataSize, readData, readDataSize, timeout); +} + +bool I2cDevice::writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + return masterWriteRegister(port, address, reg, data, dataSize, timeout); +} + bool I2cDevice::readRegister12(uint8_t reg, float& out) const { std::uint8_t data[2] = {0}; if (tt::hal::i2c::masterReadRegister(port, address, reg, data, 2, DEFAULT_TIMEOUT)) { @@ -62,4 +78,4 @@ bool I2cDevice::bitOff(uint8_t reg, uint8_t bitmask) const { } } -} // namespace +} // namespace tt::hal::i2c diff --git a/Tactility/Source/lvgl/Color.cpp b/Tactility/Source/lvgl/Color.cpp new file mode 100644 index 00000000..a0fdfb35 --- /dev/null +++ b/Tactility/Source/lvgl/Color.cpp @@ -0,0 +1,13 @@ +#include "Tactility/lvgl/Color.h" + +lv_color_t lv_color_foreground() { + return lv_color_make(0xFF, 0xFF, 0xFF); +} + +lv_color_t lv_color_background() { + return lv_color_make(0x28, 0x2B, 0x30); +} + +lv_color_t lv_color_background_darkest() { + return lv_color_make(0x00, 0x00, 0x00); +} diff --git a/Tactility/Source/lvgl/LabelUtils.cpp b/Tactility/Source/lvgl/LabelUtils.cpp index 78ffbb63..90d6a4e7 100644 --- a/Tactility/Source/lvgl/LabelUtils.cpp +++ b/Tactility/Source/lvgl/LabelUtils.cpp @@ -1,14 +1,18 @@ #include #include +#include namespace tt::lvgl { #define TAG "tt_lv_label" bool label_set_text_file(lv_obj_t* label, const char* filepath) { - auto text = file::readString(filepath); + auto text = hal::sdcard::withSdCardLock>(std::string(filepath), [filepath]() { + return file::readString(filepath); + }); + if (text != nullptr) { - lv_label_set_text(label, (const char*)text.get()); + lv_label_set_text(label, reinterpret_cast(text.get())); return true; } else { return false; diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index b485025f..8bf8d8c2 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -118,7 +118,7 @@ static void statusbar_pubsub_event(TT_UNUSED const void* message, void* obj) { } } -static void onNetworkConnected(TT_UNUSED kernel::SystemEvent event) { +static void onTimeChanged(TT_UNUSED kernel::SystemEvent event) { if (statusbar_data.mutex.lock(100 / portTICK_PERIOD_MS)) { statusbar_data.time_update_timer->stop(); statusbar_data.time_update_timer->start(5); @@ -139,7 +139,7 @@ static void statusbar_constructor(const lv_obj_class_t* class_p, lv_obj_t* obj) statusbar_data.time_update_timer->start(50 / portTICK_PERIOD_MS); statusbar_data.systemEventSubscription = kernel::subscribeSystemEvent( kernel::SystemEvent::Time, - onNetworkConnected + onTimeChanged ); } } diff --git a/Tactility/Source/network/HttpdReq.cpp b/Tactility/Source/network/HttpdReq.cpp new file mode 100644 index 00000000..8e639909 --- /dev/null +++ b/Tactility/Source/network/HttpdReq.cpp @@ -0,0 +1,160 @@ +#include "Tactility/network/HttpdReq.h" + +#include +#include +#include +#include +#include + +#ifdef ESP_PLATFORM + +#define TAG "network" + +namespace tt::network { + +bool getHeaderOrSendError(httpd_req_t* request, const std::string& name, std::string& value) { + size_t header_size = httpd_req_get_hdr_value_len(request, name.c_str()); + if (header_size == 0) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "header missing"); + return false; + } + + auto header_buffer = std::make_unique(header_size + 1); + if (header_buffer == nullptr) { + TT_LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); + httpd_resp_send_500(request); + return false; + } + + if (httpd_req_get_hdr_value_str(request, name.c_str(), header_buffer.get(), header_size + 1) != ESP_OK) { + httpd_resp_send_500(request); + return false; + } + + value = header_buffer.get(); + return true; +} + +bool getMultiPartBoundaryOrSendError(httpd_req_t* request, std::string& boundary) { + std::string content_type_header; + if (!getHeaderOrSendError(request, "Content-Type", content_type_header)) { + return false; + } + + auto boundary_index = content_type_header.find("boundary="); + if (boundary_index == std::string::npos) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "boundary not found in Content-Type"); + return false; + } + + boundary = content_type_header.substr(boundary_index + 9); + return true; +} + +bool getQueryOrSendError(httpd_req_t* request, std::string& query) { + size_t buffer_length = httpd_req_get_url_query_len(request); + if (buffer_length == 0) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); + return false; + } + + auto buffer = std::make_unique(buffer_length + 1); + if (buffer.get() == nullptr || httpd_req_get_url_query_str(request, buffer.get(), buffer_length + 1) != ESP_OK) { + httpd_resp_send_500(request); + return false; + } + + query = buffer.get(); + + return true; +} + +std::unique_ptr receiveByteArray(httpd_req_t* request, size_t length, size_t& bytesRead) { + assert(length > 0); + bytesRead = 0; + + auto result = std::make_unique(length); + + while (bytesRead < length) { + size_t read_size = length - bytesRead; + size_t bytes_received = httpd_req_recv(request, result.get() + bytesRead, read_size); + if (bytes_received <= 0) { + TT_LOG_W(TAG, "Received %zu / %zu", bytesRead + bytes_received, length); + return nullptr; + } + + bytesRead += bytes_received; + } + + return result; +} + +std::string receiveTextUntil(httpd_req_t* request, const std::string& terminator) { + size_t read_index = 0; + std::stringstream result; + while (!result.str().ends_with(terminator)) { + char buffer; + size_t bytes_read = httpd_req_recv(request, &buffer, 1); + if (bytes_read <= 0) { + return ""; + } else { + read_index += bytes_read; + } + + result << buffer; + } + + return result.str(); +} + +std::map parseContentDisposition(const std::vector& input) { + std::map result; + static std::string prefix = "Content-Disposition: "; + + // Find header + auto content_disposition_header = std::ranges::find_if(input, [](const std::string& header) { + return header.starts_with(prefix); + }); + + // Header not found + if (content_disposition_header == input.end()) { + return result; + } + + auto parseable = content_disposition_header->substr(prefix.size()); + auto parts = string::split(parseable, "; "); + for (auto part : parts) { + auto key_value = string::split(part, "="); + if (key_value.size() == 2) { + // Trim trailing newlines + auto value = string::trim(key_value[1], "\r\n"); + if (value.size() > 2) { + result[key_value[0]] = value.substr(1, value.size() - 2); + } else { + result[key_value[0]] = ""; + } + } + } + + return result; +} + +bool readAndDiscardOrSendError(httpd_req_t* request, const std::string& toRead) { + size_t bytes_read; + auto buffer = receiveByteArray(request, toRead.length(), bytes_read); + if (bytes_read != toRead.length()) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "failed to read discardable data"); + return false; + } + + if (memcmp(buffer.get(), toRead.c_str(), bytes_read) != 0) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "discardable data mismatch"); + return false; + } + + return true; +} + +} + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Source/network/Url.cpp b/Tactility/Source/network/Url.cpp new file mode 100644 index 00000000..7cbdca85 --- /dev/null +++ b/Tactility/Source/network/Url.cpp @@ -0,0 +1,82 @@ +#include "Tactility/network/Url.h" + +#include + +namespace tt::network { + +std::map parseUrlQuery(std::string query) { + std::map result; + + if (query.empty()) { + return result; + } + + size_t current_index = query[0] == '?' ? 1U : 0U; + auto equals_index = query.find_first_of('=', current_index); + while (equals_index != std::string::npos) { + auto index_boundary = query.find_first_of('&', equals_index + 1); + if (index_boundary == std::string::npos) { + index_boundary = query.size(); + } + auto key = query.substr(current_index, (equals_index - current_index)); + auto decodedKey = urlDecode(key); + auto value = query.substr(equals_index + 1, (index_boundary - equals_index - 1)); + auto decodedValue = urlDecode(value); + + result[decodedKey] = decodedValue; + + // Find next token + current_index = index_boundary + 1; + equals_index = query.find_first_of('=', current_index); + } + + return result; +} + +// Adapted from https://stackoverflow.com/a/29962178/3848666 +std::string urlEncode(const std::string& input) { + std::string result = ""; + const char* characters = input.c_str(); + char hex_buffer[10]; + size_t input_length = input.length(); + + for (size_t i = 0;i < input_length;i++) { + unsigned char c = characters[i]; + // uncomment this if you want to encode spaces with + + if (c==' ') { + result += '+'; + } else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + sprintf(hex_buffer, "%%%02X", c); //%% means '%' literal, %02X means at least two digits, paddable with a leading zero + result += hex_buffer; + } + } + + return result; +} + +// Adapted from https://stackoverflow.com/a/29962178/3848666 +std::string urlDecode(const std::string& input) { + std::string result; + size_t conversion_buffer, input_length = input.length(); + + for (size_t i = 0; i < input_length; i++) { + if (input[i] != '%') { + if (input[i] == '+') { + result += ' '; + } else { + result += input[i]; + } + } else { + sscanf(input.substr(i + 1, 2).c_str(), "%x", &conversion_buffer); + char c = static_cast(conversion_buffer); + result += c; + i = i + 2; + } + } + + return result; +} + +} // namespace diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp new file mode 100644 index 00000000..27a0f0b4 --- /dev/null +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -0,0 +1,301 @@ +#ifdef ESP_PLATFORM + +#include "Tactility/service/development/DevelopmentService.h" + +#include "Tactility/network/HttpdReq.h" +#include "Tactility/network/Url.h" +#include "Tactility/TactilityHeadless.h" +#include "Tactility/service/ServiceManifest.h" +#include "Tactility/service/ServiceRegistry.h" +#include "Tactility/service/wifi/Wifi.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tt::service::development { + +extern const ServiceManifest manifest; + +constexpr const char* TAG = "DevService"; + +void DevelopmentService::onStart(ServiceContext& service) { + auto lock = mutex.asScopedLock(); + lock.lock(); + + networkConnectEventSubscription = kernel::subscribeSystemEvent( + kernel::SystemEvent::NetworkConnected, + [this](kernel::SystemEvent) { onNetworkConnected(); } + ); + networkConnectEventSubscription = kernel::subscribeSystemEvent( + kernel::SystemEvent::NetworkDisconnected, + [this](kernel::SystemEvent) { onNetworkDisconnected(); } + ); + + setEnabled(isEnabledOnStart()); +} + +void DevelopmentService::onStop(ServiceContext& service) { + auto lock = mutex.asScopedLock(); + lock.lock(); + + kernel::unsubscribeSystemEvent(networkConnectEventSubscription); + kernel::unsubscribeSystemEvent(networkDisconnectEventSubscription); + + if (isEnabled()) { + setEnabled(false); + } +} + +// region Enable/disable + +void DevelopmentService::setEnabled(bool enabled) { + auto lock = mutex.asScopedLock(); + lock.lock(); + this->enabled = enabled; + + // We might already have an IP address, so in case we do, we start the server manually + // Or we started the server while it shouldn't be + if (enabled && !isStarted() && wifi::getRadioState() == wifi::RadioState::ConnectionActive) { + startServer(); + } else if (!enabled && isStarted()) { + stopServer(); + } +} + +bool DevelopmentService::isEnabled() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return enabled; +} + +bool DevelopmentService::isEnabledOnStart() const { + Preferences preferences = Preferences(manifest.id.c_str()); + bool enabled_on_boot = false; + preferences.optBool("enabledOnBoot", enabled_on_boot); + return enabled_on_boot; +} + +void DevelopmentService::setEnabledOnStart(bool enabled) { + Preferences preferences = Preferences(manifest.id.c_str()); + preferences.putBool("enabledOnBoot", enabled); +} + +// region Enable/disable + +void DevelopmentService::startServer() { + auto lock = mutex.asScopedLock(); + lock.lock(); + + if (isStarted()) { + TT_LOG_W(TAG, "Already started"); + return; + } + + ESP_LOGI(TAG, "Starting server"); + + std::stringstream stream; + stream << "{"; + stream << "\"cpuFamily\":\"" << CONFIG_IDF_TARGET << "\", "; + stream << "\"osVersion\":\"" << TT_VERSION << "\", "; + stream << "\"protocolVersion\":\"1.0.0\""; + stream << "}"; + deviceResponse = stream.str(); + + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + config.server_port = 6666; + config.uri_match_fn = httpd_uri_match_wildcard; + + if (httpd_start(&server, &config) == ESP_OK) { + httpd_register_uri_handler(server, &handleGetInfoEndpoint); + httpd_register_uri_handler(server, &appRunEndpoint); + httpd_register_uri_handler(server, &appInstallEndpoint); + TT_LOG_I(TAG, "Started on port %d", config.server_port); + } else { + TT_LOG_E(TAG, "Failed to start"); + } +} + +void DevelopmentService::stopServer() { + auto lock = mutex.asScopedLock(); + lock.lock(); + + if (!isStarted()) { + TT_LOG_W(TAG, "Not started"); + return; + } + + TT_LOG_I(TAG, "Stopping server"); + if (httpd_stop(server) != ESP_OK) { + TT_LOG_W(TAG, "Error while stopping"); + } + server = nullptr; +} + +bool DevelopmentService::isStarted() const { + auto lock = mutex.asScopedLock(); + lock.lock(); + return server != nullptr; +} + +void DevelopmentService::onNetworkConnected() { + TT_LOG_I(TAG, "onNetworkConnected"); + mutex.withLock([this] { + if (isEnabled() && !isStarted()) { + startServer(); + } + }); +} + +void DevelopmentService::onNetworkDisconnected() { + TT_LOG_I(TAG, "onNetworkDisconnected"); + mutex.withLock([this] { + if (isStarted()) { + stopServer(); + } + }); +} + +// region endpoints + +esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) { + if (httpd_resp_set_type(request, "application/json") != ESP_OK) { + TT_LOG_W(TAG, "Failed to send header"); + return ESP_FAIL; + } + + auto* service = static_cast(request->user_ctx); + + if (httpd_resp_sendstr(request, service->deviceResponse.c_str()) != ESP_OK) { + TT_LOG_W(TAG, "Failed to send response body"); + return ESP_FAIL; + } + + TT_LOG_I(TAG, "[200] /device"); + return ESP_OK; +} + +esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { + std::string query; + if (!network::getQueryOrSendError(request, query)) { + return ESP_FAIL; + } + + auto parameters = network::parseUrlQuery(query); + auto id_key_pos = parameters.find("id"); + if (id_key_pos == parameters.end()) { + TT_LOG_W(TAG, "[400] /app/run id not specified"); + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "id not specified"); + return ESP_FAIL; + } + + auto app_id = id_key_pos->second; + if (app_id.ends_with(".app.elf")) { + app::registerElfApp(app_id); + app_id = app::getElfAppId(app_id); + } else if (!app::findAppById(app_id.c_str())) { + TT_LOG_W(TAG, "[400] /app/run app not found"); + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "app not found"); + return ESP_FAIL; + } + + app::start(app_id); + TT_LOG_I(TAG, "[200] /app/run %s", app_id.c_str()); + httpd_resp_send(request, nullptr, 0); + return ESP_OK; +} + +esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { + std::string boundary; + if (!network::getMultiPartBoundaryOrSendError(request, boundary)) { + return false; + } + + size_t content_left = request->content_len; + + // Skip newline after reading boundary + auto content_headers_data = network::receiveTextUntil(request, "\r\n\r\n"); + content_left -= content_headers_data.length(); + auto content_headers = string::split(content_headers_data, "\r\n") + | std::views::filter([](const std::string& line) { + return line.length() > 0; + }) + | std::ranges::to(); + + auto content_disposition_map = network::parseContentDisposition(content_headers); + if (content_disposition_map.empty()) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: invalid content disposition"); + return ESP_FAIL; + } + + auto name_entry = content_disposition_map.find("name"); + auto filename_entry = content_disposition_map.find("filename"); + if ( + name_entry == content_disposition_map.end() || + filename_entry == content_disposition_map.end() || + name_entry->second != "elf" + ) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: name or filename parameter missing or mismatching"); + return ESP_FAIL; + } + + // Receive file + size_t content_read; + auto part_after_file = std::format("\r\n--{}--\r\n", boundary); + auto file_size = content_left - part_after_file.length(); + auto buffer = network::receiveByteArray(request, file_size, content_read); + if (content_read != file_size) { + httpd_resp_send_err(request, HTTPD_400_BAD_REQUEST, "Multipart form error: file data not received"); + return ESP_FAIL; + } + content_left -= content_read; + + // Write file + auto file_path = std::format("/sdcard/{}", filename_entry->second); + auto* file = fopen(file_path.c_str(), "wb"); + auto file_bytes_written = fwrite(buffer.get(), 1, file_size, file); + fclose(file); + if (file_bytes_written != file_size) { + httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to save file"); + return ESP_FAIL; + } + + // Read and verify part + if (!network::readAndDiscardOrSendError(request, part_after_file)) { + return ESP_FAIL; + } + content_left -= part_after_file.length(); + + if (content_left != 0) { + TT_LOG_W(TAG, "We have more bytes at the end of the request parsing?!"); + } + + TT_LOG_I(TAG, "[200] /app/install -> %s", file_path.c_str()); + + httpd_resp_send(request, nullptr, 0); + return ESP_OK; +} + +// endregion + +std::shared_ptr findService() { + return std::static_pointer_cast( + findServiceById(manifest.id) + ); +} + +extern const ServiceManifest manifest = { + .id = "Development", + .createService = create +}; + +} + +#endif // ESP_PLATFORM diff --git a/Tactility/Source/service/gui/GuiDraw.cpp b/Tactility/Source/service/gui/GuiDraw.cpp index ed52c19e..a3ba2333 100644 --- a/Tactility/Source/service/gui/GuiDraw.cpp +++ b/Tactility/Source/service/gui/GuiDraw.cpp @@ -39,6 +39,17 @@ void redraw(Gui* gui) { if (gui->appToRender != nullptr) { + // Create a default group which adds all objects automatically, + // and assign all indevs to it. + // This enables navigation with limited input, such as encoder wheels. + lv_group_t* group = lv_group_create(); + auto* indev = lv_indev_get_next(nullptr); + while (indev) { + lv_indev_set_group(indev, group); + indev = lv_indev_get_next(indev); + } + lv_group_set_default(group); + app::Flags flags = std::static_pointer_cast(gui->appToRender)->getFlags(); if (flags.showStatusbar) { lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); @@ -61,4 +72,4 @@ void redraw(Gui* gui) { unlock(); } -} // namespace +} // namespace tt::service::gui diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index e8cd20bf..e7c343ab 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -47,17 +47,17 @@ static const char* appStateToString(app::State state) { class LoaderService final : public Service { -private: - std::shared_ptr pubsubExternal = std::make_shared(); Mutex mutex = Mutex(Mutex::Type::Recursive); std::stack> appStack; + app::LaunchId nextLaunchId = 0; + /** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods. * This includes full LVGL redraw via Gui::redraw() */ std::unique_ptr dispatcherThread = std::make_unique("loader_dispatcher", 6144); // Files app requires ~5k - void onStartAppMessage(const std::string& id, std::shared_ptr parameters); + void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr parameters); void onStopAppMessage(const std::string& id); void transitionAppToState(const std::shared_ptr& app, app::State state); @@ -75,7 +75,7 @@ public: }); } - void startApp(const std::string& id, std::shared_ptr parameters); + app::LaunchId startApp(const std::string& id, std::shared_ptr parameters); void stopApp(); std::shared_ptr _Nullable getCurrentAppContext(); @@ -86,8 +86,7 @@ std::shared_ptr _Nullable optScreenshotService() { return service::findServiceById(manifest.id); } -void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr parameters) { - +void LoaderService::onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr parameters) { TT_LOG_I(TAG, "Start by id %s", id.c_str()); auto app_manifest = app::findAppById(id); @@ -103,7 +102,7 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptr(app_manifest, parameters); + auto new_app = std::make_shared(app_manifest, launchId, parameters); new_app->mutableFlags().showStatusbar = (app_manifest->type != app::Type::Boot); @@ -123,7 +122,6 @@ void LoaderService::onStartAppMessage(const std::string& id, std::shared_ptrgetLaunchId(); + transitionAppToState(app_to_stop, app::State::Hiding); transitionAppToState(app_to_stop, app::State::Stopped); @@ -196,12 +196,14 @@ void LoaderService::onStopAppMessage(const std::string& id) { if (result_bundle != nullptr) { instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, result, std::move(result_bundle) ); } else { instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, result, nullptr ); @@ -210,6 +212,7 @@ void LoaderService::onStopAppMessage(const std::string& id) { const Bundle empty_bundle; instance_to_resume->getApp()->onResult( *instance_to_resume, + app_to_stop_launch_id, app::Result::Cancelled, nullptr ); @@ -255,10 +258,12 @@ void LoaderService::transitionAppToState(const std::shared_ptr app->setState(state); } -void LoaderService::startApp(const std::string& id, std::shared_ptr parameters) { - dispatcherThread->dispatch([this, id, parameters]() { - onStartAppMessage(id, parameters); +app::LaunchId LoaderService::startApp(const std::string& id, std::shared_ptr parameters) { + auto launch_id = nextLaunchId++; + dispatcherThread->dispatch([this, id, launch_id, parameters]() { + onStartAppMessage(id, launch_id, parameters); }); + return launch_id; } void LoaderService::stopApp() { @@ -278,11 +283,11 @@ std::shared_ptr _Nullable LoaderService::getCurrentAppContext() // region Public API -void startApp(const std::string& id, std::shared_ptr parameters) { +app::LaunchId startApp(const std::string& id, std::shared_ptr parameters) { TT_LOG_I(TAG, "Start app %s", id.c_str()); auto service = optScreenshotService(); assert(service); - service->startApp(id, std::move(parameters)); + return service->startApp(id, std::move(parameters)); } void stopApp() { diff --git a/Tactility/Source/service/wifi/WifiEsp.cpp b/Tactility/Source/service/wifi/WifiEsp.cpp index 1573b3b0..8fa75838 100644 --- a/Tactility/Source/service/wifi/WifiEsp.cpp +++ b/Tactility/Source/service/wifi/WifiEsp.cpp @@ -1,5 +1,6 @@ #ifdef ESP_PLATFORM +#include #include "Tactility/service/wifi/Wifi.h" #include "Tactility/TactilityHeadless.h" @@ -12,6 +13,7 @@ #include #include +#include #include namespace tt::service::wifi { @@ -71,6 +73,7 @@ public: }; bool pause_auto_connect = false; // Pause when manually disconnecting until manually connecting again bool connection_target_remember = false; // Whether to store the connection_target on successful connection or not + esp_netif_ip_info_t ip_info; RadioState getRadioState() const { auto lock = dataMutex.asScopedLock(); @@ -230,6 +233,19 @@ void disconnect() { getMainDispatcher().dispatch([wifi]() { dispatchDisconnectButKeepActive(wifi); }); } +void clearIp() { + auto wifi = wifi_singleton; + if (wifi == nullptr) { + return; + } + + auto lock = wifi->dataMutex.asScopedLock(); + if (!lock.lock(10 / portTICK_PERIOD_MS)) { + return; + } + + memset(&wifi->ip_info, 0, sizeof(esp_netif_ip_info_t)); +} void setScanRecords(uint16_t records) { TT_LOG_I(TAG, "setScanRecords(%d)", records); auto wifi = wifi_singleton; @@ -463,6 +479,7 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32 } } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { TT_LOG_I(TAG, "eventHandler: disconnected"); + clearIp(); switch (wifi->getRadioState()) { case RadioState::ConnectionPending: wifi->connection_wait_flags.set(WIFI_FAIL_BIT); @@ -476,8 +493,10 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32 } wifi->setRadioState(RadioState::On); publish_event_simple(wifi, EventType::Disconnected); + kernel::publishSystemEvent(kernel::SystemEvent::NetworkDisconnected); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { auto* event = static_cast(event_data); + memcpy(&wifi->ip_info, &event->ip_info, sizeof(esp_netif_ip_info_t)); TT_LOG_I(TAG, "eventHandler: got ip:" IPSTR, IP2STR(&event->ip_info.ip)); if (wifi->getRadioState() == RadioState::ConnectionPending) { wifi->connection_wait_flags.set(WIFI_CONNECTED_BIT); @@ -485,6 +504,7 @@ static void eventHandler(TT_UNUSED void* arg, esp_event_base_t event_base, int32 // TODO: Make thread-safe wifi->pause_auto_connect = false; // Resume auto-connection } + kernel::publishSystemEvent(kernel::SystemEvent::NetworkConnected); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { auto* event = static_cast(event_data); TT_LOG_I(TAG, "eventHandler: wifi scanning done (scan id %u)", event->scan_id); @@ -874,6 +894,15 @@ void onAutoConnectTimer() { } } +std::string getIp() { + auto wifi = std::static_pointer_cast(wifi_singleton); + + auto lock = wifi->dataMutex.asScopedLock(); + lock.lock(); + + return std::format("{}.{}.{}.{}", IP2STR(&wifi->ip_info.ip)); +} + class WifiService final : public Service { public: diff --git a/Tactility/Source/service/wifi/WifiMock.cpp b/Tactility/Source/service/wifi/WifiMock.cpp index af84a9e2..9828ca92 100644 --- a/Tactility/Source/service/wifi/WifiMock.cpp +++ b/Tactility/Source/service/wifi/WifiMock.cpp @@ -135,6 +135,10 @@ int getRssi() { } } +std::string getIp() { + return "192.168.1.2"; +} + // endregion Public functions class WifiService final : public Service { diff --git a/Tactility/Source/service/wifi/WifiSettingsEsp.cpp b/Tactility/Source/service/wifi/WifiSettingsEsp.cpp index eda470df..51f427dc 100644 --- a/Tactility/Source/service/wifi/WifiSettingsEsp.cpp +++ b/Tactility/Source/service/wifi/WifiSettingsEsp.cpp @@ -36,7 +36,7 @@ bool contains(const char* ssid) { return false; } - bool key_exists = nvs_find_key(handle, ssid, NULL) == ESP_OK; + bool key_exists = nvs_find_key(handle, ssid, nullptr) == ESP_OK; credentials_nvs_close(handle); return key_exists; diff --git a/TactilityC/Include/tt_app.h b/TactilityC/Include/tt_app.h index b1e97bd2..1e5dbbb0 100644 --- a/TactilityC/Include/tt_app.h +++ b/TactilityC/Include/tt_app.h @@ -18,7 +18,7 @@ BundleHandle _Nullable tt_app_get_parameters(AppHandle handle); * @param[in] result the result state to set * @param[in] bundle the result bundle to set */ -void tt_app_set_result(AppHandle handle, Result result, BundleHandle _Nullable bundle); +void tt_app_set_result(AppHandle handle, AppResult result, BundleHandle _Nullable bundle); /** @return true if a result was set for this app context */ bool tt_app_has_result(AppHandle handle); diff --git a/TactilityC/Include/tt_app_manifest.h b/TactilityC/Include/tt_app_manifest.h index c41b90f7..7551b4f3 100644 --- a/TactilityC/Include/tt_app_manifest.h +++ b/TactilityC/Include/tt_app_manifest.h @@ -9,13 +9,15 @@ extern "C" { /** Important: These values must map to tt::app::Result values exactly */ typedef enum { - AppResultOk = 0, - AppResultCancelled = 1, - AppResultError = 2 -} Result; + APP_RESULT_OK = 0, + APP_RESULT_CANCELLED = 1, + APP_RESULT_ERROR = 2 +} AppResult; typedef void* AppHandle; +typedef unsigned int AppLaunchId; + /** Important: These function types must map to t::app types exactly */ typedef void* (*AppCreateData)(); typedef void (*AppDestroyData)(void* data); @@ -23,7 +25,7 @@ typedef void (*AppOnCreate)(AppHandle app, void* _Nullable data); typedef void (*AppOnDestroy)(AppHandle app, void* _Nullable data); typedef void (*AppOnShow)(AppHandle app, void* _Nullable data, lv_obj_t* parent); typedef void (*AppOnHide)(AppHandle app, void* _Nullable data); -typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, Result result, BundleHandle resultData); +typedef void (*AppOnResult)(AppHandle app, void* _Nullable data, AppLaunchId launchId, AppResult result, BundleHandle resultData); typedef struct { /** The application's human-readable name */ diff --git a/TactilityC/Include/tt_gps.h b/TactilityC/Include/tt_gps.h new file mode 100644 index 00000000..8d9bcfae --- /dev/null +++ b/TactilityC/Include/tt_gps.h @@ -0,0 +1,21 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +bool tt_gps_has_coordinates(); + +bool tt_gps_get_coordinates( + float* longitude, + float* latitude, + float* speed, + float* course, + int* day, + int* month, + int* year +); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityC/Include/tt_preferences.h b/TactilityC/Include/tt_preferences.h new file mode 100644 index 00000000..735982b3 --- /dev/null +++ b/TactilityC/Include/tt_preferences.h @@ -0,0 +1,83 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** + * 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) + */ + +/** The handle that represents a Preferences instance */ +typedef void* PreferencesHandle; + +/** + * @param[in] identifier the name of the preferences. This determines the NVS namespace on ESP. + * @return a new preferences instance + */ +PreferencesHandle tt_preferences_alloc(const char* identifier); + +/** Dealloc an existing preferences instance */ +void tt_preferences_free(PreferencesHandle handle); + +/** + * Try to get a boolean value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[out] out the output value (only set when return value is set to true) + * @return true if "out" was set + */ +bool tt_preferences_opt_bool(PreferencesHandle handle, const char* key, bool* out); + +/** + * Try to get an int32_t value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[out] out the output value (only set when return value is set to true) + * @return true if "out" was set + */ +bool tt_preferences_opt_int32(PreferencesHandle handle, const char* key, int32_t* out); + +/** + * Try to get a string + * @warning outSize must be large enough to include null terminator. This means that your string has to be the expected text length + 1 extra character. + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[out] out the buffer to store the string in + * @param[in] outSize the size of the buffer + * @return true if "out" was set + */ +bool tt_preferences_opt_string(PreferencesHandle handle, const char* key, char* out, uint32_t outSize); + +/** + * Store a boolean value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[in] value the value to store + */ +void tt_preferences_put_bool(PreferencesHandle handle, const char* key, bool value); + +/** + * Store an int32_t value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[in] value the value to store + */ +void tt_preferences_put_int32(PreferencesHandle handle, const char* key, int32_t value); + +/** + * Store a string value + * @param[in] handle the handle that represents the preferences + * @param[in] key the identifier that represents the stored value (~variable name) + * @param[in] value the value to store + */ +void tt_preferences_put_string(PreferencesHandle handle, const char* key, const char* value); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/TactilityC/Include/tt_time.h b/TactilityC/Include/tt_time.h new file mode 100644 index 00000000..db9f972d --- /dev/null +++ b/TactilityC/Include/tt_time.h @@ -0,0 +1,42 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define TT_TIMEZONE_NAME_BUFFER_LENGTH 32 +#define TT_TIMEZONE_CODE_BUFFER_LENGTH 48 + +/** + * Set the timezone + * @param[in] name human-readable name + * @param[in] code the technical code (from timezones.csv) + */ +void tt_timezone_set(const char* name, const char* code); + +/** + * Get the name of the timezone + * @param[out] buffer the output buffer which will include a null terminator (should be TT_TIMEZONE_NAME_BUFFER_LENGTH) + * @param[in] bufferSize the size of the output buffer + */ +bool tt_timezone_get_name(char* buffer, size_t bufferSize); + +/** + * Get the code of the timezone (see timezones.csv) + */ +bool tt_timezone_get_code(char* buffer, size_t bufferSize); + +/** @return true when clocks should be shown as a 24 hours one instead of 12 hours */ +bool tt_timezone_is_format_24_hour(); + +/** Set whether clocks should be shown as a 24 hours instead of 12 hours + * @param[in] show24Hour + */ +void tt_timezone_set_format_24_hour(bool show24Hour); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityC/Include/tt_wifi.h b/TactilityC/Include/tt_wifi.h new file mode 100644 index 00000000..659fd750 --- /dev/null +++ b/TactilityC/Include/tt_wifi.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#define TT_WIFI_SSID_LIMIT 32 // 32 characters/octets, according to IEEE 802.11-2020 spec +#define TT_WIFI_CREDENTIALS_PASSWORD_LIMIT 64 // 64 characters/octets, according to IEEE 802.11-2020 spec + +#ifdef __cplusplus +extern "C" { +#endif + +/** Important: These values must map to tt::service::wifi::RadioState values exactly */ +typedef enum { + WIFI_RADIO_STATE_ON_PENDING, + WIFI_RADIO_STATE_ON, + WIFI_RADIO_STATE_CONNECTION_PENDING, + WIFI_RADIO_STATE_CONNECTION_ACTIVE, + WIFI_RADIO_STATE_OFF_PENDING, + WIFI_RADIO_STATE_OFF, +} WifiRadioState; + +/** @return the state of the WiFi radio */ +WifiRadioState tt_wifi_get_radio_state(); + +/** @return a textual representation of the WiFi radio state */ +const char* tt_wifi_radio_state_to_string(WifiRadioState state); + +/** Start scanning */ +void tt_wifi_scan(); + +/** @return true if a scan is active/pending */ +bool tt_wifi_is_scanning(); + +/** + * Return the WiFi SSID that the system tries to connect to, or is connected to. + * @param[out] buffer an allocated string buffer. Its size must be (WIFI_SSID_LIMIT + 1). + */ +void tt_wifi_get_connection_target(char* buffer); + +/** + * @brief Enable/disable the radio. Ignores input if desired state matches current state. + * @param[in] enabled + */ +void tt_wifi_set_enabled(bool enabled); + +/** + * + * @param ssid The access point identifier - maximal 32 characters/octets + * @param password the password - maximum 64 characters/octets + * @param channel 0 means "any" + * @param autoConnect whether we want to automatically reconnect if a disconnect occurs + * @param remember whether the record should be stored permanently on the device (it is only stored if this connection attempt succeeds) + */ +void tt_wifi_connect(const char* ssid, const char* password, int32_t channel, bool autoConnect, bool remember); + +/** + * If WiFi is connected, this disconnects it. + */ +void tt_wifi_disconnect(); + +/** + * @return true if WiFi is active and encrypted + */ +bool tt_wifi_is_connnection_secure(); + +/** + * @return the current radio connection link quality + */ +int tt_wifi_get_rssi(); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityC/Source/tt_app.cpp b/TactilityC/Source/tt_app.cpp index bf770720..2a50ddbf 100644 --- a/TactilityC/Source/tt_app.cpp +++ b/TactilityC/Source/tt_app.cpp @@ -10,9 +10,9 @@ BundleHandle _Nullable tt_app_get_parameters(AppHandle handle) { return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get(); } -void tt_app_set_result(AppHandle handle, Result result, BundleHandle _Nullable bundle) { - auto shared_bundle = std::unique_ptr((tt::Bundle*)bundle); - HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult((tt::app::Result)result, std::move(shared_bundle)); +void tt_app_set_result(AppHandle handle, AppResult result, BundleHandle _Nullable bundle) { + auto shared_bundle = std::unique_ptr(static_cast(bundle)); + HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult(static_cast(result), std::move(shared_bundle)); } bool tt_app_has_result(AppHandle handle) { @@ -24,7 +24,7 @@ void tt_app_start(const char* appId) { } void tt_app_start_with_bundle(const char* appId, BundleHandle parameters) { - tt::app::start(appId, std::shared_ptr((tt::Bundle*)parameters)); + tt::app::start(appId, std::shared_ptr(static_cast(parameters))); } void tt_app_stop() { diff --git a/TactilityC/Source/tt_app_manifest.cpp b/TactilityC/Source/tt_app_manifest.cpp index 4d77a2b8..d274cfd8 100644 --- a/TactilityC/Source/tt_app_manifest.cpp +++ b/TactilityC/Source/tt_app_manifest.cpp @@ -12,16 +12,16 @@ void tt_app_register( ) { #ifdef ESP_PLATFORM assert((manifest->createData == nullptr) == (manifest->destroyData == nullptr)); - tt::app::setElfAppManifest( + setElfAppManifest( manifest->name, manifest->icon, - (tt::app::CreateData)manifest->createData, - (tt::app::DestroyData)manifest->destroyData, - (tt::app::OnCreate)manifest->onCreate, - (tt::app::OnDestroy)manifest->onDestroy, - (tt::app::OnShow)manifest->onShow, - (tt::app::OnHide)manifest->onHide, - (tt::app::OnResult)manifest->onResult + manifest->createData, + manifest->destroyData, + manifest->onCreate, + manifest->onDestroy, + manifest->onShow, + manifest->onHide, + reinterpret_cast(manifest->onResult) ); #else tt_crash("TactilityC is not intended for PC/Simulator"); diff --git a/TactilityC/Source/tt_bundle.cpp b/TactilityC/Source/tt_bundle.cpp index 14530a32..4172bf2c 100644 --- a/TactilityC/Source/tt_bundle.cpp +++ b/TactilityC/Source/tt_bundle.cpp @@ -23,17 +23,19 @@ bool tt_bundle_opt_int32(BundleHandle handle, const char* key, int32_t* out) { } bool tt_bundle_opt_string(BundleHandle handle, const char* key, char* out, uint32_t outSize) { std::string out_string; - if (HANDLE_AS_BUNDLE(handle)->optString(key, out_string)) { - if (out_string.length() < outSize) { // Need 1 byte to add 0 at the end - memcpy(out, out_string.c_str(), out_string.length()); - out[out_string.length()] = 0x00; - return true; - } else { - return false; - } - } else { + + if (!HANDLE_AS_BUNDLE(handle)->optString(key, out_string)) { return false; } + + if (out_string.length() >= outSize) { + // Need 1 byte to add 0 at the end + return false; + } + + memcpy(out, out_string.c_str(), out_string.length()); + out[out_string.length()] = 0x00; + return true; } void tt_bundle_put_bool(BundleHandle handle, const char* key, bool value) { diff --git a/TactilityC/Source/tt_gps.cpp b/TactilityC/Source/tt_gps.cpp new file mode 100644 index 00000000..5131193c --- /dev/null +++ b/TactilityC/Source/tt_gps.cpp @@ -0,0 +1,45 @@ +#include "tt_gps.h" +#include + +using namespace tt::service; + +extern "C" { + +bool tt_gps_has_coordinates() { + auto service = gps::findGpsService(); + return service != nullptr && service->hasCoordinates(); +} + +bool tt_gps_get_coordinates( + float* longitude, + float* latitude, + float* speed, + float* course, + int* day, + int* month, + int* year +) { + auto service = gps::findGpsService(); + + if (service == nullptr) { + return false; + } + + minmea_sentence_rmc rmc; + + if (!service->getCoordinates(rmc)) { + return false; + } + + *longitude = minmea_tocoord(&rmc.longitude); + *latitude = minmea_tocoord(&rmc.latitude); + *speed = minmea_tocoord(&rmc.speed); + *course = minmea_tocoord(&rmc.course); + *day = rmc.date.day; + *month = rmc.date.month; + *year = rmc.date.year; + + return true; +} + +} \ No newline at end of file diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index a766a883..9c038e7c 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -5,23 +5,90 @@ #include "tt_app_manifest.h" #include "tt_app_selectiondialog.h" #include "tt_bundle.h" +#include "tt_gps.h" #include "tt_hal_i2c.h" #include "tt_lvgl_keyboard.h" #include "tt_lvgl_spinner.h" #include "tt_lvgl_toolbar.h" #include "tt_message_queue.h" #include "tt_mutex.h" +#include "tt_preferences.h" #include "tt_semaphore.h" #include "tt_thread.h" +#include "tt_time.h" #include "tt_timer.h" +#include "tt_wifi.h" +#include +#include #include +#include +#include #include extern "C" { +// Hidden functions work-around +extern void* _Znwj(uint32_t size); +extern void _ZdlPvj(void* p, uint64_t size); +extern double __adddf3(double a, double b); +extern double __subdf3(double a, double b); +extern double __muldf3 (double a, double b); +extern double __divdf3 (double a, double b); +extern int __nedf2 (double a, double b); + const struct esp_elfsym elf_symbols[] { + // Hidden functions work-around + ESP_ELFSYM_EXPORT(_ZdlPvj), // new? + ESP_ELFSYM_EXPORT(_Znwj), // delete? + ESP_ELFSYM_EXPORT(__adddf3), // Routines for floating point emulation: + ESP_ELFSYM_EXPORT(__subdf3), // See https://gcc.gnu.org/onlinedocs/gccint/Soft-float-library-routines.html + ESP_ELFSYM_EXPORT(__muldf3), + ESP_ELFSYM_EXPORT(__nedf2), + ESP_ELFSYM_EXPORT(__divdf3), + // + ESP_ELFSYM_EXPORT(__assert_func), + // + ESP_ELFSYM_EXPORT(fclose), + ESP_ELFSYM_EXPORT(feof), + ESP_ELFSYM_EXPORT(ferror), + ESP_ELFSYM_EXPORT(fflush), + ESP_ELFSYM_EXPORT(fgetc), + ESP_ELFSYM_EXPORT(fgetpos), + ESP_ELFSYM_EXPORT(fgets), + ESP_ELFSYM_EXPORT(fopen), + ESP_ELFSYM_EXPORT(fputc), + ESP_ELFSYM_EXPORT(fputs), + ESP_ELFSYM_EXPORT(fprintf), + ESP_ELFSYM_EXPORT(fread), + ESP_ELFSYM_EXPORT(fseek), + ESP_ELFSYM_EXPORT(fsetpos), + ESP_ELFSYM_EXPORT(fscanf), + ESP_ELFSYM_EXPORT(ftell), + ESP_ELFSYM_EXPORT(fwrite), + ESP_ELFSYM_EXPORT(getc), + ESP_ELFSYM_EXPORT(putc), + ESP_ELFSYM_EXPORT(puts), + ESP_ELFSYM_EXPORT(printf), + ESP_ELFSYM_EXPORT(sscanf), + ESP_ELFSYM_EXPORT(snprintf), + ESP_ELFSYM_EXPORT(sprintf), + ESP_ELFSYM_EXPORT(vsprintf), + // cstring + ESP_ELFSYM_EXPORT(strlen), + ESP_ELFSYM_EXPORT(strcmp), + ESP_ELFSYM_EXPORT(strncpy), + ESP_ELFSYM_EXPORT(strcpy), + ESP_ELFSYM_EXPORT(strcat), + ESP_ELFSYM_EXPORT(strstr), + ESP_ELFSYM_EXPORT(memset), + ESP_ELFSYM_EXPORT(memcpy), + // ctype + ESP_ELFSYM_EXPORT(isdigit), + // ESP-IDF + ESP_ELFSYM_EXPORT(esp_log_write), + ESP_ELFSYM_EXPORT(esp_log_timestamp), // Tactility ESP_ELFSYM_EXPORT(tt_app_register), ESP_ELFSYM_EXPORT(tt_app_get_parameters), @@ -39,6 +106,8 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_bundle_put_bool), ESP_ELFSYM_EXPORT(tt_bundle_put_int32), ESP_ELFSYM_EXPORT(tt_bundle_put_string), + ESP_ELFSYM_EXPORT(tt_gps_has_coordinates), + ESP_ELFSYM_EXPORT(tt_gps_get_coordinates), ESP_ELFSYM_EXPORT(tt_hal_i2c_start), ESP_ELFSYM_EXPORT(tt_hal_i2c_stop), ESP_ELFSYM_EXPORT(tt_hal_i2c_is_started), @@ -72,6 +141,14 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_mutex_free), ESP_ELFSYM_EXPORT(tt_mutex_lock), ESP_ELFSYM_EXPORT(tt_mutex_unlock), + ESP_ELFSYM_EXPORT(tt_preferences_alloc), + ESP_ELFSYM_EXPORT(tt_preferences_free), + ESP_ELFSYM_EXPORT(tt_preferences_opt_bool), + ESP_ELFSYM_EXPORT(tt_preferences_opt_int32), + ESP_ELFSYM_EXPORT(tt_preferences_opt_string), + ESP_ELFSYM_EXPORT(tt_preferences_put_bool), + ESP_ELFSYM_EXPORT(tt_preferences_put_int32), + ESP_ELFSYM_EXPORT(tt_preferences_put_string), ESP_ELFSYM_EXPORT(tt_semaphore_alloc), ESP_ELFSYM_EXPORT(tt_semaphore_free), ESP_ELFSYM_EXPORT(tt_semaphore_acquire), @@ -99,6 +176,21 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(tt_timer_get_expire_time), ESP_ELFSYM_EXPORT(tt_timer_set_pending_callback), ESP_ELFSYM_EXPORT(tt_timer_set_thread_priority), + ESP_ELFSYM_EXPORT(tt_timezone_set), + ESP_ELFSYM_EXPORT(tt_timezone_get_name), + ESP_ELFSYM_EXPORT(tt_timezone_get_code), + ESP_ELFSYM_EXPORT(tt_timezone_is_format_24_hour), + ESP_ELFSYM_EXPORT(tt_timezone_set_format_24_hour), + ESP_ELFSYM_EXPORT(tt_wifi_get_radio_state), + ESP_ELFSYM_EXPORT(tt_wifi_radio_state_to_string), + ESP_ELFSYM_EXPORT(tt_wifi_scan), + ESP_ELFSYM_EXPORT(tt_wifi_is_scanning), + ESP_ELFSYM_EXPORT(tt_wifi_get_connection_target), + ESP_ELFSYM_EXPORT(tt_wifi_set_enabled), + ESP_ELFSYM_EXPORT(tt_wifi_connect), + ESP_ELFSYM_EXPORT(tt_wifi_disconnect), + ESP_ELFSYM_EXPORT(tt_wifi_is_connnection_secure), + ESP_ELFSYM_EXPORT(tt_wifi_get_rssi), // tt::lvgl ESP_ELFSYM_EXPORT(tt_lvgl_spinner_create), // lv_event @@ -110,7 +202,10 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_event_get_user_data), ESP_ELFSYM_EXPORT(lv_event_get_target_obj), ESP_ELFSYM_EXPORT(lv_event_get_target), + ESP_ELFSYM_EXPORT(lv_event_get_current_target_obj), // lv_obj + ESP_ELFSYM_EXPORT(lv_obj_create), + ESP_ELFSYM_EXPORT(lv_obj_delete), ESP_ELFSYM_EXPORT(lv_obj_add_event_cb), ESP_ELFSYM_EXPORT(lv_obj_align), ESP_ELFSYM_EXPORT(lv_obj_align_to), @@ -128,7 +223,13 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_obj_remove_event_cb), ESP_ELFSYM_EXPORT(lv_obj_get_user_data), ESP_ELFSYM_EXPORT(lv_obj_set_user_data), + ESP_ELFSYM_EXPORT(lv_obj_remove_flag), + ESP_ELFSYM_EXPORT(lv_obj_add_flag), ESP_ELFSYM_EXPORT(lv_obj_set_pos), + ESP_ELFSYM_EXPORT(lv_obj_set_flex_align), + ESP_ELFSYM_EXPORT(lv_obj_set_flex_flow), + ESP_ELFSYM_EXPORT(lv_obj_set_flex_grow), + ESP_ELFSYM_EXPORT(lv_obj_set_style_bg_color), ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_hor), ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_ver), ESP_ELFSYM_EXPORT(lv_obj_set_style_margin_top), @@ -143,19 +244,36 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_bottom), ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_left), ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_right), + ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_column), + ESP_ELFSYM_EXPORT(lv_obj_set_style_pad_row), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_width), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_opa), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_post), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_side), ESP_ELFSYM_EXPORT(lv_obj_set_style_border_color), + ESP_ELFSYM_EXPORT(lv_obj_set_align), ESP_ELFSYM_EXPORT(lv_obj_set_x), ESP_ELFSYM_EXPORT(lv_obj_set_y), + ESP_ELFSYM_EXPORT(lv_obj_set_size), ESP_ELFSYM_EXPORT(lv_obj_set_width), ESP_ELFSYM_EXPORT(lv_obj_set_height), ESP_ELFSYM_EXPORT(lv_theme_get_color_primary), ESP_ELFSYM_EXPORT(lv_theme_get_color_secondary), // lv_button ESP_ELFSYM_EXPORT(lv_button_create), + // lv_buttonmatrix + ESP_ELFSYM_EXPORT(lv_buttonmatrix_create), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_button_text), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_map), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_one_checked), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_get_selected_button), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_ctrl), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_ctrl_all), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_ctrl_map), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_map), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_one_checked), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_button_width), + ESP_ELFSYM_EXPORT(lv_buttonmatrix_set_selected_button), // lv_label ESP_ELFSYM_EXPORT(lv_label_create), ESP_ELFSYM_EXPORT(lv_label_cut_text), @@ -222,6 +340,18 @@ const struct esp_elfsym elf_symbols[] { ESP_ELFSYM_EXPORT(lv_textarea_set_placeholder_text), ESP_ELFSYM_EXPORT(lv_textarea_set_text), ESP_ELFSYM_EXPORT(lv_textarea_set_text_selection), + // lv_palette + ESP_ELFSYM_EXPORT(lv_palette_main), + ESP_ELFSYM_EXPORT(lv_palette_darken), + ESP_ELFSYM_EXPORT(lv_palette_lighten), + // lv_display + ESP_ELFSYM_EXPORT(lv_display_get_horizontal_resolution), + ESP_ELFSYM_EXPORT(lv_display_get_vertical_resolution), + ESP_ELFSYM_EXPORT(lv_display_get_physical_horizontal_resolution), + ESP_ELFSYM_EXPORT(lv_display_get_physical_vertical_resolution), + // lv_pct + ESP_ELFSYM_EXPORT(lv_pct), + ESP_ELFSYM_EXPORT(lv_pct_to_px), // delimiter ESP_ELFSYM_END }; diff --git a/TactilityC/Source/tt_preferences.cpp b/TactilityC/Source/tt_preferences.cpp new file mode 100644 index 00000000..5aeb6d1d --- /dev/null +++ b/TactilityC/Source/tt_preferences.cpp @@ -0,0 +1,53 @@ +#include "tt_preferences.h" +#include +#include + +#define HANDLE_AS_PREFERENCES(handle) ((tt::Preferences*)(handle)) + +extern "C" { + +PreferencesHandle tt_preferences_alloc(const char* identifier) { + return new tt::Preferences(identifier); +} + +void tt_preferences_free(PreferencesHandle handle) { + delete HANDLE_AS_PREFERENCES(handle); +} + +bool tt_preferences_opt_bool(PreferencesHandle handle, const char* key, bool* out) { + return HANDLE_AS_PREFERENCES(handle)->optBool(key, *out); +} + +bool tt_preferences_opt_int32(PreferencesHandle handle, const char* key, int32_t* out) { + return HANDLE_AS_PREFERENCES(handle)->optInt32(key, *out); +} +bool tt_preferences_opt_string(PreferencesHandle handle, const char* key, char* out, uint32_t outSize) { + std::string out_string; + + if (!HANDLE_AS_PREFERENCES(handle)->optString(key, out_string)) { + return false; + } + + if (out_string.length() >= outSize) { + // Need 1 byte to add 0 at the end + return false; + } + + memcpy(out, out_string.c_str(), out_string.length()); + out[out_string.length()] = 0x00; + return true; +} + +void tt_preferences_put_bool(PreferencesHandle handle, const char* key, bool value) { + HANDLE_AS_PREFERENCES(handle)->putBool(key, value); +} + +void tt_preferences_put_int32(PreferencesHandle handle, const char* key, int32_t value) { + HANDLE_AS_PREFERENCES(handle)->putInt32(key, value); +} + +void tt_preferences_put_string(PreferencesHandle handle, const char* key, const char* value) { + HANDLE_AS_PREFERENCES(handle)->putString(key, value); +} + +} \ No newline at end of file diff --git a/TactilityC/Source/tt_time.cpp b/TactilityC/Source/tt_time.cpp new file mode 100644 index 00000000..9a255e8f --- /dev/null +++ b/TactilityC/Source/tt_time.cpp @@ -0,0 +1,42 @@ +#include "tt_time.h" + +#include +#include + +using namespace tt; + +extern "C" { + +void tt_timezone_set(const char* name, const char* code) { + time::setTimeZone(name, code); +} + +bool tt_timezone_get_name(char* buffer, size_t bufferSize) { + auto name = time::getTimeZoneName(); + if (bufferSize < (name.length() + 1)) { + return false; + } else { + strcpy(buffer, name.c_str()); + return true; + } +} + +bool tt_timezone_get_code(char* buffer, size_t bufferSize) { + auto code = time::getTimeZoneCode(); + if (bufferSize < (code.length() + 1)) { + return false; + } else { + strcpy(buffer, code.c_str()); + return true; + } +} + +bool tt_timezone_is_format_24_hour() { + return time::isTimeFormat24Hour(); +} + +void tt_timezone_set_format_24_hour(bool show24Hour) { + return time::setTimeFormat24Hour(show24Hour); +} + +} diff --git a/TactilityC/Source/tt_timer.cpp b/TactilityC/Source/tt_timer.cpp index 62ad2bbb..42143511 100644 --- a/TactilityC/Source/tt_timer.cpp +++ b/TactilityC/Source/tt_timer.cpp @@ -7,7 +7,6 @@ struct TimerWrapper { extern "C" { - TimerHandle tt_timer_alloc(TimerType type, TimerCallback callback, void* callbackContext) { auto wrapper = std::make_shared(); wrapper->timer = std::make_unique((tt::Timer::Type)type, [callback, callbackContext](){ callback(callbackContext); }); @@ -54,4 +53,3 @@ void tt_timer_set_thread_priority(TimerHandle handle, ThreadPriority priority) { } } - diff --git a/TactilityC/Source/tt_wifi.cpp b/TactilityC/Source/tt_wifi.cpp new file mode 100644 index 00000000..b64441f7 --- /dev/null +++ b/TactilityC/Source/tt_wifi.cpp @@ -0,0 +1,55 @@ +#include "tt_wifi.h" + +#include +#include +#include + +using namespace tt::service; + +extern "C" { + +WifiRadioState tt_wifi_get_radio_state() { + return static_cast(wifi::getRadioState()); +} +const char* tt_wifi_radio_state_to_string(WifiRadioState state) { + return wifi::radioStateToString(static_cast(state)); +} + +void tt_wifi_scan() { + wifi::scan(); +} + +bool tt_wifi_is_scanning() { + return wifi::isScanning(); +} + +void tt_wifi_get_connection_target(char* buffer) { + auto target = wifi::getConnectionTarget(); + strcpy(buffer, target.c_str()); +} + +void tt_wifi_set_enabled(bool enabled) { + wifi::setEnabled(enabled); +} + +void tt_wifi_connect(const char* ssid, const char* password, int32_t channel, bool autoConnect, bool remember) { + wifi::settings::WifiApSettings settings; + strcpy(settings.ssid, ssid); + strcpy(settings.password, password); + settings.channel = channel; + settings.auto_connect = autoConnect; +} + +void tt_wifi_disconnect() { + wifi::disconnect(); +} + +bool tt_wifi_is_connnection_secure() { + return wifi::isConnectionSecure(); +} + +int tt_wifi_get_rssi() { + return wifi::getRssi(); +} + +} diff --git a/TactilityCore/Include/Tactility/LogMessages.h b/TactilityCore/Include/Tactility/LogMessages.h index 48178061..e9c5ca10 100644 --- a/TactilityCore/Include/Tactility/LogMessages.h +++ b/TactilityCore/Include/Tactility/LogMessages.h @@ -4,6 +4,9 @@ */ #pragma once +// Alloc +#define LOG_MESSAGE_ALLOC_FAILED "Memory allocation failed" + // Mutex #define LOG_MESSAGE_MUTEX_LOCK_FAILED "Mutex acquisition timeout" #define LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT "Mutex acquisition timeout (%s)" diff --git a/TactilityCore/Include/Tactility/StringUtils.h b/TactilityCore/Include/Tactility/StringUtils.h index 3fefeb38..b92c3196 100644 --- a/TactilityCore/Include/Tactility/StringUtils.h +++ b/TactilityCore/Include/Tactility/StringUtils.h @@ -64,4 +64,12 @@ bool isAsciiHexString(const std::string& input); /** @return the first part of a file name right up (and excluding) the first period character. */ std::string removeFileExtension(const std::string& input); +/** + * Remove the given characters from the start and end of the specified string. + * @param[in] input the text to trim + * @param[in] characters the characters to remove from the input + * @return the input where the specified characters are removed from the start and end of the input string + */ +std::string trim(const std::string& input, const std::string& characters); + } // namespace diff --git a/TactilityCore/Include/Tactility/Timer.h b/TactilityCore/Include/Tactility/Timer.h index 445e23e1..285b5ea3 100644 --- a/TactilityCore/Include/Tactility/Timer.h +++ b/TactilityCore/Include/Tactility/Timer.h @@ -13,7 +13,6 @@ class Timer { public: typedef std::function Callback; -// typedef std::function PendingCallback; typedef void (*PendingCallback)(void* context, uint32_t arg); private: diff --git a/TactilityCore/Include/Tactility/file/File.h b/TactilityCore/Include/Tactility/file/File.h index 5f7d5b38..2aa83f1b 100644 --- a/TactilityCore/Include/Tactility/file/File.h +++ b/TactilityCore/Include/Tactility/file/File.h @@ -3,10 +3,34 @@ #include "Tactility/TactilityCore.h" #include +#include #include +#include namespace tt::file { +/** 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 +}; + #ifdef _WIN32 constexpr char SEPARATOR = '\\'; #else @@ -36,6 +60,13 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi */ std::unique_ptr readString(const std::string& filepath); +/** Write text to a file + * @param[in] path file path to write to + * @param[in] content file content to write + * @return true when operation is successful + */ +bool writeString(const std::string& filepath, const std::string& content); + /** Ensure a directory path exists. * @param[in] path the directory path to find, or to create recursively * @param[in] mode the mode to use when creating directories @@ -43,4 +74,41 @@ std::unique_ptr readString(const std::string& filepath); */ bool findOrCreateDirectory(std::string path, mode_t mode); +/** + * Concatenate a child path with a parent path, ensuring proper slash inbetween + * @param basePath an absolute path with or without trailing "/" + * @param childPath the name of the child path (e.g. subfolder or file) + * @return the concatenated path + */ +std::string getChildPath(const std::string& basePath, const std::string& childPath); + +typedef int (*ScandirFilter)(const dirent*); + +typedef bool (*ScandirSort)(const dirent&, const dirent&); + +/** Used for sorting by alphanumeric value and file type */ +bool direntSortAlphaAndType(const dirent& left, const dirent& right); + +/** A filter for filtering out "." and ".." */ +int direntFilterDotEntries(const 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& outList, + ScandirFilter _Nullable filter, + ScandirSort _Nullable sort +); + } diff --git a/TactilityCore/Source/StringUtils.cpp b/TactilityCore/Source/StringUtils.cpp index 945bef3e..e7f8969f 100644 --- a/TactilityCore/Source/StringUtils.cpp +++ b/TactilityCore/Source/StringUtils.cpp @@ -96,4 +96,14 @@ bool isAsciiHexString(const std::string& input) { }).empty(); } +std::string trim(const std::string& input, const std::string& characters) { + auto index = input.find_first_not_of(characters); + if (index == std::string::npos) { + return ""; + } else { + auto end_index = input.find_last_not_of(characters); + return input.substr(index, end_index - index + 1); + } +} + } // namespace diff --git a/TactilityCore/Source/file/File.cpp b/TactilityCore/Source/file/File.cpp index 464ec835..8ca16dbc 100644 --- a/TactilityCore/Source/file/File.cpp +++ b/TactilityCore/Source/file/File.cpp @@ -1,9 +1,66 @@ #include "Tactility/file/File.h" +#include +#include + namespace tt::file { #define TAG "file" +std::string getChildPath(const std::string& basePath, const std::string& childPath) { + // Postfix with "/" when the current path isn't "/" + if (basePath.length() != 1) { + return basePath + "/" + childPath; + } else { + return "/" + childPath; + } +} + +int direntFilterDotEntries(const dirent* entry) { + return (strcmp(entry->d_name, "..") == 0 || strcmp(entry->d_name, ".") == 0) ? -1 : 0; +} + +bool direntSortAlphaAndType(const dirent& left, const dirent& right) { + bool left_is_dir = left.d_type == TT_DT_DIR || left.d_type == TT_DT_CHR; + bool right_is_dir = right.d_type == TT_DT_DIR || right.d_type == TT_DT_CHR; + if (left_is_dir == right_is_dir) { + return strcmp(left.d_name, right.d_name) < 0; + } else { + return left_is_dir > right_is_dir; + } +} + +int scandir( + const std::string& path, + std::vector& outList, + ScandirFilter _Nullable filterMethod, + ScandirSort _Nullable sortMethod +) { + TT_LOG_I(TAG, "scandir start"); + DIR* dir = opendir(path.c_str()); + if (dir == nullptr) { + TT_LOG_E(TAG, "Failed to open dir %s", path.c_str()); + return -1; + } + + dirent* current_entry; + while ((current_entry = readdir(dir)) != nullptr) { + if (filterMethod(current_entry) == 0) { + outList.push_back(*current_entry); + } + } + + closedir(dir); + + if (sortMethod != nullptr) { + std::ranges::sort(outList, sortMethod); + } + + TT_LOG_I(TAG, "scandir finish"); + return outList.size(); +} + + long getSize(FILE* file) { long original_offset = ftell(file); @@ -80,12 +137,25 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi std::unique_ptr readString(const std::string& filepath) { size_t size = 0; auto data = readBinaryInternal(filepath, size, 1); - if (data != nullptr) { - data.get()[size] = 0; // Append null terminator - return data; - } else { + if (data == nullptr) { return nullptr; } + + data.get()[size] = 0; // Append null terminator + return data; +} + +bool writeString(const std::string& filepath, const std::string& content) { + std::ofstream fileStream(filepath); + + if (!fileStream.is_open()) { + return false; + } + + fileStream << content; + fileStream.close(); + + return true; } static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) { diff --git a/Tests/Tactility/UrlTest.cpp b/Tests/Tactility/UrlTest.cpp new file mode 100644 index 00000000..f2c2fe99 --- /dev/null +++ b/Tests/Tactility/UrlTest.cpp @@ -0,0 +1,61 @@ +#include "doctest.h" +#include + +using namespace tt; + +TEST_CASE("parseUrlQuery can handle a single key-value pair") { + auto map = network::parseUrlQuery("?key=value"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["key"], "value"); +} + +TEST_CASE("parseUrlQuery can handle empty value in the middle") { + auto map = network::parseUrlQuery("?a=1&b=&c=3"); + CHECK_EQ(map.size(), 3); + CHECK_EQ(map["a"], "1"); + CHECK_EQ(map["b"], ""); + CHECK_EQ(map["c"], "3"); +} + +TEST_CASE("parseUrlQuery can handle empty value at the end") { + auto map = network::parseUrlQuery("?a=1&b="); + CHECK_EQ(map.size(), 2); + CHECK_EQ(map["a"], "1"); + CHECK_EQ(map["b"], ""); +} + +TEST_CASE("parseUrlQuery returns empty map when query s questionmark with a key without a value") { + auto map = network::parseUrlQuery("?a"); + CHECK_EQ(map.size(), 0); +} + +TEST_CASE("parseUrlQuery returns empty map when query is a questionmark") { + auto map = network::parseUrlQuery("?"); + CHECK_EQ(map.size(), 0); +} + +TEST_CASE("parseUrlQuery should url-decode the value") { + auto map = network::parseUrlQuery("?key=Test%21Test"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["key"], "Test!Test"); +} + +TEST_CASE("parseUrlQuery should url-decode the key") { + auto map = network::parseUrlQuery("?Test%21Test=value"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["Test!Test"], "value"); +} + +TEST_CASE("urlDecode") { + auto input = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\"); + auto expected = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C"); + auto encoded = network::urlEncode(input); + CHECK_EQ(encoded, expected); +} + +TEST_CASE("urlDecode") { + auto input = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C"); + auto expected = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\"); + auto decoded = network::urlDecode(input); + CHECK_EQ(decoded, expected); +} \ No newline at end of file diff --git a/sdkconfig.board.lilygo-tlora-pager b/sdkconfig.board.lilygo-tlora-pager new file mode 100644 index 00000000..16fb1e30 --- /dev/null +++ b/sdkconfig.board.lilygo-tlora-pager @@ -0,0 +1,56 @@ +# Software defaults +# Increase stack size for WiFi (fixes crash after scan) +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 +CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_18=y +CONFIG_LV_USE_USER_DATA=y +CONFIG_LV_USE_FS_STDIO=y +CONFIG_LV_FS_STDIO_LETTER=65 +CONFIG_LV_FS_STDIO_PATH="" +CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 +CONFIG_LV_USE_LODEPNG=y +CONFIG_LV_USE_BUILTIN_MALLOC=n +CONFIG_LV_USE_CLIB_MALLOC=y +CONFIG_LV_USE_MSGBOX=n +CONFIG_LV_USE_SPINNER=n +CONFIG_LV_USE_WIN=n +CONFIG_LV_USE_SNAPSHOT=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 +CONFIG_FREERTOS_SMP=n +CONFIG_FREERTOS_UNICORE=n +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_VOLUME_COUNT=3 + +# Hardware: Main +CONFIG_TT_BOARD_LILYGO_TLORA_PAGER=y +CONFIG_TT_BOARD_NAME="LilyGo T-Lora Pager" +CONFIG_TT_BOARD_ID="lilygo-tlora-pager" +CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_FLASHMODE_DIO=y +# Hardware: SPI RAM +CONFIG_ESP32S3_SPIRAM_SUPPORT=y +#CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_SPIRAM_SPEED_120M=y +#CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# LVGL +CONFIG_LV_DPI_DEF=90 +CONFIG_LV_DISP_DEF_REFR_PERIOD=10 +CONFIG_LV_THEME_DEFAULT_DARK=y +# USB +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard" diff --git a/version.txt b/version.txt index 60a2d3e9..79a2734b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.5.0 \ No newline at end of file