From d0ca3b16f85c3a118edd9b54328036b453ce4f7f Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sun, 30 Mar 2025 01:14:22 +0100 Subject: [PATCH] GPS refactored (#262) - Refactored GPS service and HAL: GPS is no longer part of the HAL configuration. You can now add configure new GPS devices from the GPS settings app. - T-Deck adds a boot hook to check if a GPS configuration exists and adds it when the config is empty. - Implemented the concept of ObjectFile to read/write arrays of a raw data type (e.g. struct) to disk. - Implemented more file utils (e.g. to create all directories of a path) --- .../Source/CrowPanelAdvance28.cpp | 3 +- .../Source/CrowPanelAdvance35.cpp | 3 +- .../Source/CrowPanelAdvance50.cpp | 3 +- .../Source/CrowPanelBasic28.cpp | 3 +- .../Source/CrowPanelBasic35.cpp | 3 +- .../Source/CrowPanelBasic50.cpp | 3 +- Boards/LilygoTdeck/Source/Init.cpp | 21 ++ Boards/LilygoTdeck/Source/LilygoTdeck.cpp | 8 - Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp | 127 ++++++++++- Boards/Simulator/Source/Simulator.cpp | 8 - .../Source/WaveshareS3Touch43.cpp | 3 +- Documentation/gps.md | 27 +++ Documentation/ideas.md | 1 + Documentation/wiring.md | 23 ++ Libraries/SDL | 2 +- .../Include/Tactility/app/addgps/AddGps.h | 1 + .../Private/Tactility/app/files/FileUtils.h | 4 +- Tactility/Source/Tactility.cpp | 2 + Tactility/Source/app/addgps/AddGps.cpp | 179 +++++++++++++++ Tactility/Source/app/files/FileUtils.cpp | 16 +- Tactility/Source/app/files/State.cpp | 6 +- .../Source/app/gpssettings/GpsSettings.cpp | 179 +++++++++++++-- .../app/serialconsole/SerialConsole.cpp | 3 - .../Source/service/statusbar/Statusbar.cpp | 4 +- TactilityCore/Include/Tactility/file/File.h | 22 ++ .../Include/Tactility/file/ObjectFile.h | 76 +++++++ TactilityCore/Source/file/File.cpp | 50 +++++ TactilityCore/Source/file/ObjectFile.cpp | 207 ++++++++++++++++++ .../Include/Tactility/hal/Configuration.h | 3 - .../Tactility/hal/gps/GpsConfiguration.h | 36 +++ .../Include/Tactility/hal/gps/GpsDevice.h | 40 +--- .../Include/Tactility/kernel/SystemEvents.h | 5 +- .../Include/Tactility/service/gps/Gps.h | 31 --- .../Tactility/service/gps/GpsService.h | 16 +- .../Private/Tactility/hal/gps/GpsInit.h | 7 - .../Private/Tactility/hal/gps/UbloxMessages.h | 2 + TactilityHeadless/Source/hal/Hal.cpp | 2 - .../Source/hal/gps/GpsConfiguration.cpp | 51 +++++ .../Source/hal/gps/GpsDevice.cpp | 49 +---- TactilityHeadless/Source/hal/gps/GpsInit.cpp | 12 - .../Source/kernel/SystemEvents.cpp | 2 +- .../Source/service/ServiceInstancePaths.cpp | 1 + TactilityHeadless/Source/service/gps/Gps.cpp | 50 ----- .../Source/service/gps/GpsConfiguration.cpp | 108 +++++++++ .../Source/service/gps/GpsService.cpp | 62 ++++-- Tests/TactilityCore/FileTest.cpp | 14 ++ Tests/TactilityCore/ObjectFileTest.cpp | 65 ++++++ 47 files changed, 1266 insertions(+), 277 deletions(-) create mode 100644 Documentation/gps.md create mode 100644 Documentation/wiring.md create mode 100644 Tactility/Include/Tactility/app/addgps/AddGps.h create mode 100644 Tactility/Source/app/addgps/AddGps.cpp create mode 100644 TactilityCore/Include/Tactility/file/ObjectFile.h create mode 100644 TactilityCore/Source/file/ObjectFile.cpp create mode 100644 TactilityHeadless/Include/Tactility/hal/gps/GpsConfiguration.h delete mode 100644 TactilityHeadless/Include/Tactility/service/gps/Gps.h rename TactilityHeadless/{Private => Include}/Tactility/service/gps/GpsService.h (80%) create mode 100644 TactilityHeadless/Source/hal/gps/GpsConfiguration.cpp delete mode 100644 TactilityHeadless/Source/service/gps/Gps.cpp create mode 100644 TactilityHeadless/Source/service/gps/GpsConfiguration.cpp create mode 100644 Tests/TactilityCore/FileTest.cpp create mode 100644 Tests/TactilityCore/ObjectFileTest.cpp diff --git a/Boards/ElecrowCrowpanelAdvance28/Source/CrowPanelAdvance28.cpp b/Boards/ElecrowCrowpanelAdvance28/Source/CrowPanelAdvance28.cpp index b9c5c55a..0aaeaab2 100644 --- a/Boards/ElecrowCrowpanelAdvance28/Source/CrowPanelAdvance28.cpp +++ b/Boards/ElecrowCrowpanelAdvance28/Source/CrowPanelAdvance28.cpp @@ -138,6 +138,5 @@ extern const Configuration crowpanel_advance_28 = { } } } - }, - .gps = {} + } }; diff --git a/Boards/ElecrowCrowpanelAdvance35/Source/CrowPanelAdvance35.cpp b/Boards/ElecrowCrowpanelAdvance35/Source/CrowPanelAdvance35.cpp index 49247f45..4598a286 100644 --- a/Boards/ElecrowCrowpanelAdvance35/Source/CrowPanelAdvance35.cpp +++ b/Boards/ElecrowCrowpanelAdvance35/Source/CrowPanelAdvance35.cpp @@ -138,6 +138,5 @@ extern const Configuration crowpanel_advance_35 = { } } } - }, - .gps = {} + } }; diff --git a/Boards/ElecrowCrowpanelAdvance50/Source/CrowPanelAdvance50.cpp b/Boards/ElecrowCrowpanelAdvance50/Source/CrowPanelAdvance50.cpp index 118aa61f..ce58402a 100644 --- a/Boards/ElecrowCrowpanelAdvance50/Source/CrowPanelAdvance50.cpp +++ b/Boards/ElecrowCrowpanelAdvance50/Source/CrowPanelAdvance50.cpp @@ -121,6 +121,5 @@ extern const Configuration crowpanel_advance_50 = { } } } - }, - .gps = {} + } }; diff --git a/Boards/ElecrowCrowpanelBasic28/Source/CrowPanelBasic28.cpp b/Boards/ElecrowCrowpanelBasic28/Source/CrowPanelBasic28.cpp index c20a1e2d..b1902554 100644 --- a/Boards/ElecrowCrowpanelBasic28/Source/CrowPanelBasic28.cpp +++ b/Boards/ElecrowCrowpanelBasic28/Source/CrowPanelBasic28.cpp @@ -116,6 +116,5 @@ extern const Configuration crowpanel_basic_28 = { } } } - }, - .gps = {} + } }; diff --git a/Boards/ElecrowCrowpanelBasic35/Source/CrowPanelBasic35.cpp b/Boards/ElecrowCrowpanelBasic35/Source/CrowPanelBasic35.cpp index ad50f6b3..556e8e14 100644 --- a/Boards/ElecrowCrowpanelBasic35/Source/CrowPanelBasic35.cpp +++ b/Boards/ElecrowCrowpanelBasic35/Source/CrowPanelBasic35.cpp @@ -116,6 +116,5 @@ extern const Configuration crowpanel_basic_35 = { } } } - }, - .gps = {} + } }; diff --git a/Boards/ElecrowCrowpanelBasic50/Source/CrowPanelBasic50.cpp b/Boards/ElecrowCrowpanelBasic50/Source/CrowPanelBasic50.cpp index 2d43a7b6..6447c34a 100644 --- a/Boards/ElecrowCrowpanelBasic50/Source/CrowPanelBasic50.cpp +++ b/Boards/ElecrowCrowpanelBasic50/Source/CrowPanelBasic50.cpp @@ -86,6 +86,5 @@ extern const Configuration crowpanel_basic_50 = { } } } - }, - .gps = {} + } }; diff --git a/Boards/LilygoTdeck/Source/Init.cpp b/Boards/LilygoTdeck/Source/Init.cpp index 5b02cf4d..45f531a7 100644 --- a/Boards/LilygoTdeck/Source/Init.cpp +++ b/Boards/LilygoTdeck/Source/Init.cpp @@ -1,6 +1,9 @@ #include "PwmBacklight.h" +#include "Tactility/kernel/SystemEvents.h" +#include "Tactility/service/gps/GpsService.h" #include +#include #define TAG "tdeck" @@ -42,5 +45,23 @@ bool tdeckInit() { return false; } + tt::kernel::systemEventAddListener(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event){ + 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/LilygoTdeck/Source/LilygoTdeck.cpp b/Boards/LilygoTdeck/Source/LilygoTdeck.cpp index 3022008a..d88e4054 100644 --- a/Boards/LilygoTdeck/Source/LilygoTdeck.cpp +++ b/Boards/LilygoTdeck/Source/LilygoTdeck.cpp @@ -104,13 +104,5 @@ extern const Configuration lilygo_tdeck = { } } } - }, - .gps = { - gps::GpsDevice::Configuration { - .name = "Internal", - .uartName = "Grove", - .baudRate = 38400, - .model = gps::GpsModel::UBLOX10 - } } }; diff --git a/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp b/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp index 332b885e..1e0c120d 100644 --- a/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp +++ b/Boards/M5stackCoreS3/Source/M5stackCoreS3.cpp @@ -1,23 +1,27 @@ #include "M5stackCoreS3.h" #include "InitBoot.h" -#include "Tactility/lvgl/LvglSync.h" #include "hal/CoreS3Display.h" #include "hal/CoreS3DisplayConstants.h" #include "hal/CoreS3Power.h" #include "hal/CoreS3SdCard.h" +#include +#include + #define CORES3_TRANSACTION_SIZE (CORES3_LCD_DRAW_BUFFER_SIZE * LV_COLOR_DEPTH / 8) -const tt::hal::Configuration m5stack_cores3 = { +using namespace tt::hal; + +const Configuration m5stack_cores3 = { .initBoot = initBoot, .createDisplay = createDisplay, .sdcard = createSdCard(), .power = createPower, .i2c = { - tt::hal::i2c::Configuration { + i2c::Configuration { .name = "Internal", .port = I2C_NUM_0, - .initMode = tt::hal::i2c::InitMode::ByTactility, + .initMode = i2c::InitMode::ByTactility, .isMutable = false, .config = (i2c_config_t) { .mode = I2C_MODE_MASTER, @@ -31,10 +35,10 @@ const tt::hal::Configuration m5stack_cores3 = { .clk_flags = 0 } }, - tt::hal::i2c::Configuration { - .name = "External", // Grove + i2c::Configuration { + .name = "Port A", // Grove .port = I2C_NUM_1, - .initMode = tt::hal::i2c::InitMode::ByTactility, + .initMode = i2c::InitMode::Disabled, .isMutable = true, .config = (i2c_config_t) { .mode = I2C_MODE_MASTER, @@ -47,10 +51,44 @@ const tt::hal::Configuration m5stack_cores3 = { }, .clk_flags = 0 } + }, + i2c::Configuration { + .name = "Port B", // Grove + .port = I2C_NUM_1, + .initMode = i2c::InitMode::Disabled, + .isMutable = true, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_9, + .scl_io_num = GPIO_NUM_8, + .sda_pullup_en = true, + .scl_pullup_en = true, + .master = { + .clk_speed = 400000 + }, + .clk_flags = 0 + } + }, + i2c::Configuration { + .name = "Port C", // Grove + .port = I2C_NUM_1, + .initMode = i2c::InitMode::Disabled, + .isMutable = true, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_18, + .scl_io_num = GPIO_NUM_17, + .sda_pullup_en = true, + .scl_pullup_en = true, + .master = { + .clk_speed = 400000 + }, + .clk_flags = 0 + } } }, .spi { - tt::hal::spi::Configuration { + spi::Configuration { .device = SPI3_HOST, .dma = SPI_DMA_CH_AUTO, .config = { @@ -69,9 +107,80 @@ const tt::hal::Configuration m5stack_cores3 = { .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, .intr_flags = 0 }, - .initMode = tt::hal::spi::InitMode::ByTactility, + .initMode = spi::InitMode::ByTactility, .isMutable = false, .lock = tt::lvgl::getSyncLock() // esp_lvgl_port owns the lock for the display } + }, + .uart { + uart::Configuration { + .name = "Port A", + .port = UART_NUM_1, + .rxPin = GPIO_NUM_2, + .txPin = GPIO_NUM_1, + .rtsPin = GPIO_NUM_NC, + .ctsPin = GPIO_NUM_NC, + .rxBufferSize = 1024, + .txBufferSize = 1024, + .config = { + .baud_rate = 115200, + .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, + } + } + }, + uart::Configuration { + .name = "Port B", + .port = UART_NUM_1, + .rxPin = GPIO_NUM_9, + .txPin = GPIO_NUM_8, + .rtsPin = GPIO_NUM_NC, + .ctsPin = GPIO_NUM_NC, + .rxBufferSize = 1024, + .txBufferSize = 1024, + .config = { + .baud_rate = 115200, + .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, + } + } + }, + uart::Configuration { + .name = "Port C", + .port = UART_NUM_1, + .rxPin = GPIO_NUM_18, + .txPin = GPIO_NUM_17, + .rtsPin = GPIO_NUM_NC, + .ctsPin = GPIO_NUM_NC, + .rxBufferSize = 1024, + .txBufferSize = 1024, + .config = { + .baud_rate = 115200, + .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/Simulator/Source/Simulator.cpp b/Boards/Simulator/Source/Simulator.cpp index 2aaad3cf..4d942381 100644 --- a/Boards/Simulator/Source/Simulator.cpp +++ b/Boards/Simulator/Source/Simulator.cpp @@ -79,13 +79,5 @@ extern const Configuration hardware = { .name = "/dev/ttyACM0", .baudRate = 115200 } - }, - .gps = { - gps::GpsDevice::Configuration { - .name = "Internal", - .uartName = "/dev/ttyACM0", - .baudRate = 115200, - .model = gps::GpsModel::UBLOX10 - } } }; diff --git a/Boards/WaveshareS3Touch43/Source/WaveshareS3Touch43.cpp b/Boards/WaveshareS3Touch43/Source/WaveshareS3Touch43.cpp index b6906ff1..568ba8e4 100644 --- a/Boards/WaveshareS3Touch43/Source/WaveshareS3Touch43.cpp +++ b/Boards/WaveshareS3Touch43/Source/WaveshareS3Touch43.cpp @@ -80,6 +80,5 @@ extern const Configuration waveshare_s3_touch_43 = { } } } - }, - .gps = {} + } }; diff --git a/Documentation/gps.md b/Documentation/gps.md new file mode 100644 index 00000000..8372045b --- /dev/null +++ b/Documentation/gps.md @@ -0,0 +1,27 @@ +# GPS Reference + +## Devices + +### Beitian BH-222Q + +115200 baud, U-Blox 10 clone, defaults to binary messages + +https://www.beitian.com/en/pd.jsp?id=1677 + +### Beitian BN-357ZF + +9600 baud, L76K clone + +https://www.beitian.com/en/sys-pd/879.html + +### GY-GPS6MV2 + +9600 baud, U-Blox 6 (original/clone) + +https://www.datasheethub.com/gy-neo6mv2-flight-control-gps-module/ + +### M5Stack GPS + +9600 baud, AT6558 + MAX2659 (LNA) + +https://docs.m5stack.com/en/unit/gps diff --git a/Documentation/ideas.md b/Documentation/ideas.md index a48f5bde..f6be4a6e 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -1,4 +1,5 @@ # TODOs +- rename kernel::systemEventAddListener() etc to subscribe/unsubscribe - Split up boot stages, so the last stage can be done from the splash screen - Start using non_null (either via MS GSL, or custom) - `hal/Configuration.h` defines C function types: Use C++ std::function instead diff --git a/Documentation/wiring.md b/Documentation/wiring.md new file mode 100644 index 00000000..98211d91 --- /dev/null +++ b/Documentation/wiring.md @@ -0,0 +1,23 @@ +# 4 Pin connectors + +Many devices have 4-pin connectors: +Either a [Grove](https://docs.m5stack.com/en/start/interface/grove) connector or a [JST](https://en.wikipedia.org/wiki/JST_connector) one. + +Boards that implement an I2C/UART configuration for such a connector are advised to always implement both. +To make it consistent, it's important that the pins are mapped in the same manner: + +## I2C Pins + +Follow the Grove layout: + +Black(GND) - Red(5V) - Yellow(SDA) - White(SCL) + +## I2C Pins + +Follow this Grove layout on the MCU board: + +Black(GND) - Red(5V) - Yellow(RX) - White(TX) + +And this Grove layout on the peripheral: + +Black(GND) - Red(5V) - Yellow(TX) - White(RX) diff --git a/Libraries/SDL b/Libraries/SDL index 0efeb82a..52146cf0 160000 --- a/Libraries/SDL +++ b/Libraries/SDL @@ -1 +1 @@ -Subproject commit 0efeb82a28795e72594642251f1bddcb881aaa7f +Subproject commit 52146cf067ae950b1d431a84766fc7d0501f536a diff --git a/Tactility/Include/Tactility/app/addgps/AddGps.h b/Tactility/Include/Tactility/app/addgps/AddGps.h new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/Tactility/Include/Tactility/app/addgps/AddGps.h @@ -0,0 +1 @@ +#pragma once diff --git a/Tactility/Private/Tactility/app/files/FileUtils.h b/Tactility/Private/Tactility/app/files/FileUtils.h index c384de63..d05cba3c 100644 --- a/Tactility/Private/Tactility/app/files/FileUtils.h +++ b/Tactility/Private/Tactility/app/files/FileUtils.h @@ -2,11 +2,12 @@ #include #include +#include #include namespace tt::app::files { - /** File types for `dirent`'s `d_type`. */ +/** File types for `dirent`'s `d_type`. */ enum { TT_DT_UNKNOWN = 0, #define TT_DT_UNKNOWN TT_DT_UNKNOWN // Unknown type @@ -28,6 +29,7 @@ enum { #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*); diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index b14c3e2b..d97a2adb 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -30,6 +30,7 @@ namespace service { // region Default apps namespace app { + namespace addgps { extern const AppManifest manifest; } namespace alertdialog { extern const AppManifest manifest; } namespace applist { extern const AppManifest manifest; } namespace chat { extern const AppManifest manifest; } @@ -70,6 +71,7 @@ namespace app { // endregion static void registerSystemApps() { + addApp(app::addgps::manifest); addApp(app::alertdialog::manifest); addApp(app::applist::manifest); addApp(app::display::manifest); diff --git a/Tactility/Source/app/addgps/AddGps.cpp b/Tactility/Source/app/addgps/AddGps.cpp new file mode 100644 index 00000000..026d5fb1 --- /dev/null +++ b/Tactility/Source/app/addgps/AddGps.cpp @@ -0,0 +1,179 @@ +#include "Tactility/StringUtils.h" +#include "Tactility/app/AppManifest.h" +#include "Tactility/app/alertdialog/AlertDialog.h" +#include "Tactility/hal/gps/GpsDevice.h" +#include "Tactility/hal/uart/Uart.h" +#include "Tactility/lvgl/Style.h" +#include "Tactility/lvgl/Toolbar.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +namespace tt::app::addgps { + +constexpr const char* TAG = "AddGps"; + +class AddGpsApp final : public App { + +private: + + lv_obj_t* uartDropdown = nullptr; + lv_obj_t* modelDropdown = nullptr; + lv_obj_t* baudDropdown = nullptr; + + // Store as string instead of int, so app startup doesn't require parsing all entries. + // We only need to parse back to int when adding the new GPS entry + std::array baudRates = { 9600, 19200, 28800, 38400, 57600, 115200 }; + const char* baudRatesDropdownValues = "9600\n19200\n28800\n38400\n57600\n115200"; + + static void onAddGpsCallback(lv_event_t* event) { + auto* app = (AddGpsApp*)lv_event_get_user_data(event); + app->onAddGps(); + } + + void onAddGps() { + auto selected_baud_index = lv_dropdown_get_selected(baudDropdown); + + auto new_configuration = hal::gps::GpsConfiguration { + .uartName = { 0x00 }, + .baudRate = baudRates[selected_baud_index], + // Warning: This assumes that the enum is a regularly indexed one that starts at 0 + .model = (hal::gps::GpsModel)lv_dropdown_get_selected(modelDropdown) + }; + + lv_dropdown_get_selected_str(uartDropdown, new_configuration.uartName, sizeof(new_configuration.uartName)); + if (new_configuration.uartName[0] == 0x00) { + alertdialog::start("Error", "You must select a bus/uart."); + return; + } + + TT_LOG_I(TAG, "Saving: uart=%s, model=%lu, baud=%lu", new_configuration.uartName, (uint32_t)new_configuration.model, new_configuration.baudRate); + + auto service = service::gps::findGpsService(); + std::vector configurations; + if (service != nullptr) { + service->getGpsConfigurations(configurations); + for (auto& stored_configuration: configurations) { + if (strcmp(stored_configuration.uartName, new_configuration.uartName) == 0) { + auto message = std::string("Bus \"{}\" is already in use in another configuration", (const char*)new_configuration.uartName); + app::alertdialog::start("Error", message.c_str()); + return; + } + } + + if (!service->addGpsConfiguration(new_configuration)) { + app::alertdialog::start("Error", "Failed to add configuration"); + } else { + stop(); + } + } + } + +public: + + void onShow(AppContext& app, lv_obj_t* parent) final { + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + + lvgl::toolbar_create(parent, app); + + auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_width(main_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(main_wrapper, 1); + lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_all(main_wrapper, 0, 0); + lv_obj_set_style_border_width(main_wrapper, 0, 0); + lvgl::obj_set_style_bg_invisible(main_wrapper); + + // region Uart + + auto* uart_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(uart_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(uart_wrapper, 0, 0); + lv_obj_set_style_border_width(uart_wrapper, 0, 0); + lvgl::obj_set_style_bg_invisible(uart_wrapper); + + uartDropdown = lv_dropdown_create(uart_wrapper); + + auto uart_names = hal::uart::getNames(); + uart_names.insert(uart_names.begin(), ""); + auto uart_options = string::join(uart_names, "\n"); + lv_dropdown_set_options(uartDropdown, uart_options.c_str()); + lv_obj_align(uartDropdown, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_set_width(uartDropdown, LV_PCT(50)); + + auto* uart_label = lv_label_create(uart_wrapper); + lv_obj_align(uart_label, LV_ALIGN_TOP_LEFT, 0, 10); + lv_label_set_text(uart_label, "Bus"); + + // region Model + + auto* model_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(model_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(model_wrapper, 0, 0); + lv_obj_set_style_border_width(model_wrapper, 0, 0); + lvgl::obj_set_style_bg_invisible(model_wrapper); + + modelDropdown = lv_dropdown_create(model_wrapper); + + auto model_names = hal::gps::getModels(); + auto model_options = string::join(model_names, "\n"); + lv_dropdown_set_options(modelDropdown, model_options.c_str()); + lv_obj_align(modelDropdown, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_set_width(modelDropdown, LV_PCT(50)); + + auto* model_label = lv_label_create(model_wrapper); + lv_obj_align(model_label, LV_ALIGN_TOP_LEFT, 0, 10); + lv_label_set_text(model_label, "Model"); + + // endregion + + // region Baud + auto* baud_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(baud_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(baud_wrapper, 0, 0); + lv_obj_set_style_border_width(baud_wrapper, 0, 0); + lvgl::obj_set_style_bg_invisible(baud_wrapper); + + baudDropdown = lv_dropdown_create(baud_wrapper); + lv_dropdown_set_options(baudDropdown, baudRatesDropdownValues); + lv_obj_align(baudDropdown, LV_ALIGN_TOP_RIGHT, 0, 0); + lv_obj_set_width(baudDropdown, LV_PCT(50)); + + auto* baud_rate_label = lv_label_create(baud_wrapper); + lv_obj_align(baud_rate_label, LV_ALIGN_TOP_LEFT, 0, 10); + lv_label_set_text(baud_rate_label, "Baud"); + + // endregion + + // region Button + + auto* button_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(button_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_ver(button_wrapper, 0, 0); + lv_obj_set_style_border_width(button_wrapper, 0, 0); + lvgl::obj_set_style_bg_invisible(button_wrapper); + + auto* add_button = lv_button_create(button_wrapper); + lv_obj_align(add_button, LV_ALIGN_TOP_MID, 0, 0); + auto* add_label = lv_label_create(add_button); + lv_label_set_text(add_label, "Add"); + lv_obj_add_event_cb(add_button, onAddGpsCallback, LV_EVENT_SHORT_CLICKED, this); + + // endregion + } +}; + +extern const AppManifest manifest = { + .id = "AddGps", + .name = "Add GPS", + .icon = LV_SYMBOL_GPS, + .type = Type::Hidden, + .createApp = create +}; + +void start() { + app::start(manifest.id); +} + +} // namespace diff --git a/Tactility/Source/app/files/FileUtils.cpp b/Tactility/Source/app/files/FileUtils.cpp index 830e0d25..a649ea91 100644 --- a/Tactility/Source/app/files/FileUtils.cpp +++ b/Tactility/Source/app/files/FileUtils.cpp @@ -79,13 +79,13 @@ bool isSupportedImageFile(const std::string& filename) { 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"); + 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/files/State.cpp b/Tactility/Source/app/files/State.cpp index 00575b4f..fae8c301 100644 --- a/Tactility/Source/app/files/State.cpp +++ b/Tactility/Source/app/files/State.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #define TAG "files_app" @@ -48,12 +50,12 @@ bool State::setEntriesForPath(const std::string& path) { if (show_custom_root) { TT_LOG_I(TAG, "Setting custom root"); dir_entries.clear(); - dir_entries.push_back({ + dir_entries.push_back(dirent{ .d_ino = 0, .d_type = TT_DT_DIR, .d_name = SYSTEM_PARTITION_NAME }); - dir_entries.push_back({ + dir_entries.push_back(dirent{ .d_ino = 1, .d_type = TT_DT_DIR, .d_name = DATA_PARTITION_NAME diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index 8da3ddda..f53b5bd3 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -1,19 +1,25 @@ #include "Tactility/TactilityHeadless.h" #include "Tactility/Timer.h" #include "Tactility/app/AppManifest.h" +#include "Tactility/app/alertdialog/AlertDialog.h" #include "Tactility/lvgl/LvglSync.h" #include "Tactility/lvgl/Toolbar.h" #include "Tactility/service/gps/GpsUtil.h" #include "Tactility/service/loader/Loader.h" -#include +#include +#include #include #include -#define TAG "gps_settings" +namespace tt::app::addgps { +extern AppManifest manifest; +} namespace tt::app::gpssettings { +constexpr const char* TAG = "GpsSettings"; + extern const AppManifest manifest; class GpsSettingsApp final : public App { @@ -22,12 +28,16 @@ private: std::unique_ptr timer; std::shared_ptr appReference = std::make_shared(this); + lv_obj_t* statusWrapper = nullptr; lv_obj_t* statusLabelWidget = nullptr; lv_obj_t* switchWidget = nullptr; lv_obj_t* spinnerWidget = nullptr; lv_obj_t* infoContainerWidget = nullptr; + lv_obj_t* gpsConfigWrapper = nullptr; + lv_obj_t* addGpsWrapper = nullptr; bool hasSetInfo = false; PubSub::SubscriptionHandle serviceStateSubscription = nullptr; + std::shared_ptr service; static void onUpdateCallback(TT_UNUSED std::shared_ptr context) { auto appPtr = std::static_pointer_cast(context); @@ -54,6 +64,15 @@ private: app->onGpsToggled(event); } + static void onAddGpsCallback(lv_event_t* event) { + auto* app = (GpsSettingsApp*)lv_event_get_user_data(event); + app->onAddGps(); + } + + void onAddGps() { + app::start(addgps::manifest.id); + } + void startReceivingUpdates() { timer->start(kernel::secondsToTicks(1)); updateViews(); @@ -66,13 +85,83 @@ private: void createInfoView(hal::gps::GpsModel model) { auto* label = lv_label_create(infoContainerWidget); - lv_label_set_text_fmt(label, "Model: %s", toString(model)); + if (model == hal::gps::GpsModel::Unknown) { + lv_label_set_text(label, "Model: auto-detect"); + } else { + lv_label_set_text_fmt(label, "Model: %s", toString(model)); + } + } + + static void onDeleteConfiguration(lv_event_t* event) { + auto* app = (GpsSettingsApp*)lv_event_get_user_data(event); + + auto* button = lv_event_get_target_obj(event); + auto index_as_voidptr = lv_obj_get_user_data(button); // config index + int index; + // TODO: Find a better way to cast void* to int, or find a different way to pass the index + memcpy(&index, &index_as_voidptr, sizeof(int)); + + std::vector configurations; + auto gps_service = tt::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()) { + if (gps_service->removeGpsConfiguration(configurations[index])) { + app->updateViews(); + } else { + alertdialog::start("Error", "Failed to remove configuration"); + } + } + } + } + + void createGpsView(const hal::gps::GpsConfiguration& configuration, int index) { + auto* wrapper = lv_obj_create(gpsConfigWrapper); + lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW); + lv_obj_set_style_margin_hor(wrapper, 0, 0); + lv_obj_set_style_margin_bottom(wrapper, 8, 0); + + // Left wrapper + + auto* left_wrapper = lv_obj_create(wrapper); + lv_obj_set_style_border_width(left_wrapper, 0, 0); + lv_obj_set_style_pad_all(left_wrapper, 0, 0); + lv_obj_set_size(left_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_grow(left_wrapper, 1); + lv_obj_set_flex_flow(left_wrapper, LV_FLEX_FLOW_COLUMN); + + auto* uart_label = lv_label_create(left_wrapper); + lv_label_set_text_fmt(uart_label, "UART: %s", configuration.uartName); + + auto* baud_label = lv_label_create(left_wrapper); + lv_label_set_text_fmt(baud_label, "Baud: %lu", configuration.baudRate); + + auto* model_label = lv_label_create(left_wrapper); + if (configuration.model == hal::gps::GpsModel::Unknown) { + lv_label_set_text(model_label, "Model: auto-detect"); + } else { + lv_label_set_text_fmt(model_label, "Model: %s", toString(configuration.model)); + } + + // Right wrapper + auto* right_wrapper = lv_obj_create(wrapper); + lv_obj_set_style_border_width(right_wrapper, 0, 0); + lv_obj_set_style_pad_all(right_wrapper, 0, 0); + lv_obj_set_size(right_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(right_wrapper, LV_FLEX_FLOW_COLUMN); + + auto* delete_button = lv_button_create(right_wrapper); + lv_obj_add_event_cb(delete_button, onDeleteConfiguration, LV_EVENT_SHORT_CLICKED, this); + lv_obj_set_user_data(delete_button, reinterpret_cast(index)); + auto* delete_label = lv_label_create(delete_button); + lv_label_set_text_fmt(delete_label, LV_SYMBOL_TRASH); } void updateViews() { auto lock = lvgl::getSyncLock()->asScopedLock(); if (lock.lock(100 / portTICK_PERIOD_MS)) { - auto state = service::gps::getState(); + auto state = service->getState(); // Update toolbar switch (state) { @@ -81,24 +170,36 @@ private: lv_obj_remove_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_add_state(switchWidget, LV_STATE_CHECKED); lv_obj_add_state(switchWidget, LV_STATE_DISABLED); + lv_obj_remove_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN); break; case service::gps::State::On: TT_LOG_D(TAG, "On"); lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_add_state(switchWidget, LV_STATE_CHECKED); lv_obj_remove_state(switchWidget, LV_STATE_DISABLED); + lv_obj_remove_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN); break; case service::gps::State::OffPending: TT_LOG_D(TAG, "OffPending"); lv_obj_remove_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_state(switchWidget, LV_STATE_CHECKED); lv_obj_add_state(switchWidget, LV_STATE_DISABLED); + lv_obj_add_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN); break; case service::gps::State::Off: TT_LOG_D(TAG, "Off"); lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_state(switchWidget, LV_STATE_CHECKED); lv_obj_remove_state(switchWidget, LV_STATE_DISABLED); + lv_obj_add_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN); break; } @@ -113,7 +214,7 @@ private: } minmea_sentence_rmc rmc; - if (service::gps::getCoordinates(rmc)) { + if (service->getCoordinates(rmc)) { minmea_float latitude = { rmc.latitude.value, rmc.latitude.scale }; minmea_float longitude = { rmc.longitude.value, rmc.longitude.scale }; auto label_text = std::format("LAT {}\nLON {}", minmea_tocoord(&latitude), minmea_tocoord(&longitude)); @@ -130,12 +231,22 @@ private: lv_obj_add_flag(statusLabelWidget, LV_OBJ_FLAG_HIDDEN); } + + lv_obj_clean(gpsConfigWrapper); + std::vector configurations; + auto gps_service = tt::service::gps::findGpsService(); + if (gps_service && gps_service->getGpsConfigurations(configurations)) { + int index = 0; + for (auto& configuration : configurations) { + createGpsView(configuration, index++); + } + } } } /** @return true if the views were updated */ bool updateTimerState() { - bool is_on = service::gps::getState() == service::gps::State::On; + bool is_on = service->getState() == service::gps::State::On; if (is_on && !timer->isRunning()) { startReceivingUpdates(); return true; @@ -149,19 +260,19 @@ private: void onGpsToggled(TT_UNUSED lv_event_t* event) { bool wants_on = lv_obj_has_state(switchWidget, LV_STATE_CHECKED); - auto state = service::gps::getState(); + auto state = service->getState(); bool is_on = (state == service::gps::State::On) || (state == service::gps::State::OnPending); if (wants_on != is_on) { // start/stop are potentially blocking calls, so we use a dispatcher to not block the UI if (wants_on) { - getMainDispatcher().dispatch([](TT_UNUSED auto _) { - service::gps::startReceiving(); - }, nullptr); + getMainDispatcher().dispatch([](auto service) { + std::static_pointer_cast(service)->startReceiving(); + }, service); } else { - getMainDispatcher().dispatch([](TT_UNUSED auto _) { - service::gps::stopReceiving(); - }, nullptr); + getMainDispatcher().dispatch([](auto service) { + std::static_pointer_cast(service)->stopReceiving(); + }, service); } } } @@ -170,6 +281,7 @@ public: GpsSettingsApp() { timer = std::make_unique(Timer::Type::Periodic, onUpdateCallback, appReference); + service = service::gps::findGpsService(); } void onShow(AppContext& app, lv_obj_t* parent) final { @@ -190,16 +302,16 @@ public: lv_obj_set_style_border_width(main_wrapper, 0, 0); lv_obj_set_style_pad_all(main_wrapper, 0, 0); - auto* top_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_width(top_wrapper, LV_PCT(100)); - lv_obj_set_height(top_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(top_wrapper, 0, 0); - lv_obj_set_style_border_width(top_wrapper, 0, 0); + statusWrapper = lv_obj_create(main_wrapper); + lv_obj_set_width(statusWrapper, LV_PCT(100)); + lv_obj_set_height(statusWrapper, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(statusWrapper, 0, 0); + lv_obj_set_style_border_width(statusWrapper, 0, 0); - statusLabelWidget = lv_label_create(top_wrapper); + statusLabelWidget = lv_label_create(statusWrapper); lv_obj_align(statusLabelWidget, LV_ALIGN_TOP_LEFT, 0, 0); - infoContainerWidget = lv_obj_create(top_wrapper); + infoContainerWidget = lv_obj_create(statusWrapper); lv_obj_align_to(infoContainerWidget, statusLabelWidget, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 20); lv_obj_set_size(infoContainerWidget, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_flex_flow(infoContainerWidget, LV_FLEX_FLOW_COLUMN); @@ -207,14 +319,33 @@ public: lv_obj_set_style_pad_all(infoContainerWidget, 0, 0); hasSetInfo = false; + serviceStateSubscription = service->getStatePubsub()->subscribe(onServiceStateChangedCallback, this); + + gpsConfigWrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(gpsConfigWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(gpsConfigWrapper, 0, 0); + lv_obj_set_style_margin_all(gpsConfigWrapper, 0, 0); + lv_obj_set_style_pad_bottom(gpsConfigWrapper, 0, 0); + + addGpsWrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(addGpsWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_border_width(addGpsWrapper, 0, 0); + lv_obj_set_style_pad_all(addGpsWrapper, 0, 0); + lv_obj_set_style_margin_top(addGpsWrapper, 0, 0); + lv_obj_set_style_margin_bottom(addGpsWrapper, 8, 0); + + auto* add_gps_button = lv_button_create(addGpsWrapper); + auto* add_gps_label = lv_label_create(add_gps_button); + lv_label_set_text(add_gps_label, "Add GPS"); + lv_obj_add_event_cb(add_gps_button, onAddGpsCallback, LV_EVENT_SHORT_CLICKED, this); + lv_obj_align(add_gps_button, LV_ALIGN_TOP_MID, 0, 0); + updateTimerState(); updateViews(); - - serviceStateSubscription = service::gps::getStatePubsub()->subscribe(onServiceStateChangedCallback, this); } void onHide(AppContext& app) final { - service::gps::getStatePubsub()->unsubscribe(serviceStateSubscription); + service->getStatePubsub()->unsubscribe(serviceStateSubscription); serviceStateSubscription = nullptr; } }; @@ -228,7 +359,7 @@ extern const AppManifest manifest = { }; void start() { - service::loader::startApp(manifest.id); + app::start(manifest.id); } } // namespace diff --git a/Tactility/Source/app/serialconsole/SerialConsole.cpp b/Tactility/Source/app/serialconsole/SerialConsole.cpp index 41f433cd..782dde31 100644 --- a/Tactility/Source/app/serialconsole/SerialConsole.cpp +++ b/Tactility/Source/app/serialconsole/SerialConsole.cpp @@ -1,7 +1,6 @@ #include "Tactility/app/serialconsole/ConnectView.h" #include "Tactility/app/serialconsole/ConsoleView.h" -#include "Tactility/lvgl/LvglSync.h" #include "Tactility/lvgl/Style.h" #include "Tactility/lvgl/Toolbar.h" #include "Tactility/service/loader/Loader.h" @@ -10,8 +9,6 @@ #include -#define TAG "text_viewer" - namespace tt::app::serialconsole { class SerialConsoleApp final : public App { diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index d0170144..e06a049a 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -3,7 +3,7 @@ #include "Tactility/hal/power/PowerDevice.h" #include "Tactility/hal/sdcard/SdCardDevice.h" -#include "Tactility/service/gps/Gps.h" +#include "Tactility/service/gps/GpsService.h" #include #include #include @@ -154,7 +154,7 @@ private: } void updateGpsIcon() { - auto gps_state = gps::getState(); + auto gps_state = service::gps::findGpsService()->getState(); bool show_icon = (gps_state == gps::State::OnPending) || (gps_state == gps::State::On); if (gps_last_state != show_icon) { if (show_icon) { diff --git a/TactilityCore/Include/Tactility/file/File.h b/TactilityCore/Include/Tactility/file/File.h index 4dca8b45..5f7d5b38 100644 --- a/TactilityCore/Include/Tactility/file/File.h +++ b/TactilityCore/Include/Tactility/file/File.h @@ -3,9 +3,24 @@ #include "Tactility/TactilityCore.h" #include +#include namespace tt::file { +#ifdef _WIN32 +constexpr char SEPARATOR = '\\'; +#else +constexpr char SEPARATOR = '/'; +#endif + +struct FileCloser { + void operator()(FILE* file) { + if (file != nullptr) { + fclose(file); + } + } +}; + long getSize(FILE* file); /** Read a file and return its data. @@ -21,4 +36,11 @@ std::unique_ptr readBinary(const std::string& filepath, size_t& outSi */ std::unique_ptr readString(const std::string& filepath); +/** 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 + * @return true when the specified path was found, or otherwise creates the directories recursively with the specified mode. + */ +bool findOrCreateDirectory(std::string path, mode_t mode); + } diff --git a/TactilityCore/Include/Tactility/file/ObjectFile.h b/TactilityCore/Include/Tactility/file/ObjectFile.h new file mode 100644 index 00000000..9dd6bcba --- /dev/null +++ b/TactilityCore/Include/Tactility/file/ObjectFile.h @@ -0,0 +1,76 @@ +#pragma once + +#include "File.h" + +#include +#include +#include +#include + +namespace tt::file { + +class ObjectFileReader { + +private: + + const std::string filePath; + const uint32_t recordSize = 0; + + std::unique_ptr file; + uint32_t recordCount = 0; + uint32_t recordVersion = 0; + uint32_t recordsRead = 0; + +public: + + ObjectFileReader(std::string filePath, uint32_t recordSize) : + filePath(std::move(filePath)), + recordSize(recordSize) + {} + + bool open(); + void close(); + + bool hasNext() const { return recordsRead < recordCount; } + bool readNext(void* output); + + uint32_t getRecordCount() const { return recordCount; } + uint32_t getRecordSize() const { return recordSize; } + uint32_t getRecordVersion() const { return recordVersion; } +}; + +class ObjectFileWriter { + +private: + + const std::string filePath; + const uint32_t recordSize; + const uint32_t recordVersion; + const bool append; + + std::unique_ptr file; + uint32_t recordsWritten = 0; + +public: + + ObjectFileWriter(std::string filePath, uint32_t recordSize, uint32_t recordVersion, bool append) : + filePath(std::move(filePath)), + recordSize(recordSize), + recordVersion(recordVersion), + append(append) + {} + + + ~ObjectFileWriter() { + if (file != nullptr) { + close(); + } + } + + bool open(); + void close(); + + bool write(void* data); +}; + +} diff --git a/TactilityCore/Source/file/File.cpp b/TactilityCore/Source/file/File.cpp index 3bd28998..464ec835 100644 --- a/TactilityCore/Source/file/File.cpp +++ b/TactilityCore/Source/file/File.cpp @@ -88,4 +88,54 @@ std::unique_ptr readString(const std::string& filepath) { } } +static bool findOrCreateDirectoryInternal(std::string path, mode_t mode) { + struct stat dir_stat; + if (mkdir(path.c_str(), mode) == 0) { + return true; + } + + if (errno != EEXIST) { + return false; + } + + if (stat(path.c_str(), &dir_stat) != 0) { + return false; + } + + if (!S_ISDIR(dir_stat.st_mode)) { + return false; + } + + return true; +} + +bool findOrCreateDirectory(std::string path, mode_t mode) { + if (path.empty()) { + return true; + } + TT_LOG_D(TAG, "findOrCreate: %s %lu", path.c_str(), mode); + + const char separator_to_find[] = {SEPARATOR, 0x00}; + auto first_index = path[0] == SEPARATOR ? 1 : 0; + auto separator_index = path.find(separator_to_find, first_index); + + bool should_break = false; + while (!should_break) { + bool is_last_segment = (separator_index == std::string::npos); + auto to_create = is_last_segment ? path : path.substr(0, separator_index); + should_break = is_last_segment; + if (!findOrCreateDirectoryInternal(to_create, mode)) { + TT_LOG_E(TAG, "Failed to create %s", to_create.c_str()); + return false; + } else { + TT_LOG_D(TAG, " - got: %s", to_create.c_str()); + } + + // Find next file separator index + separator_index = path.find(separator_to_find, separator_index + 1); + } + + return true; +} + } diff --git a/TactilityCore/Source/file/ObjectFile.cpp b/TactilityCore/Source/file/ObjectFile.cpp new file mode 100644 index 00000000..9ee024da --- /dev/null +++ b/TactilityCore/Source/file/ObjectFile.cpp @@ -0,0 +1,207 @@ +#include "Tactility/file/ObjectFile.h" + +#include +#include + +namespace tt::file { + +constexpr const char* TAG = "ObjectFile"; + +constexpr uint32_t OBJECT_FILE_IDENTIFIER = 0x13371337; +constexpr uint32_t OBJECT_FILE_VERSION = 1; + +struct FileHeader { + uint32_t identifier = OBJECT_FILE_IDENTIFIER; + uint32_t version = OBJECT_FILE_VERSION; +}; + +struct ContentHeader { + uint32_t recordVersion = 0; + uint32_t recordSize = 0; + uint32_t recordCount = 0; +}; + +// region Reader + +bool ObjectFileReader::open() { + auto opening_file = std::unique_ptr(fopen(filePath.c_str(), "r")); + if (opening_file == nullptr) { + TT_LOG_E(TAG, "Failed to open file %s", filePath.c_str()); + return false; + } + + FileHeader file_header; + if (fread(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to read file header from %s", filePath.c_str()); + return false; + } + + if (file_header.identifier != OBJECT_FILE_IDENTIFIER) { + TT_LOG_E(TAG, "Invalid file type for %s", filePath.c_str()); + return false; + } + + if (file_header.version != OBJECT_FILE_VERSION) { + TT_LOG_E(TAG, "Unknown version for %s: %lu", filePath.c_str(), file_header.identifier); + return false; + } + + ContentHeader content_header; + if (fread(&content_header, sizeof(ContentHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to read content header from %s", filePath.c_str()); + return false; + } + + if (recordSize != content_header.recordSize) { + TT_LOG_E(TAG, "Record size mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordSize, content_header.recordSize); + return false; + } + + recordCount = content_header.recordCount; + recordVersion = content_header.recordVersion; + + file = std::move(opening_file); + + TT_LOG_D(TAG, "File version: %lu", file_header.version); + TT_LOG_D(TAG, "Content: version = %lu, size = %lu bytes, count = %lu", content_header.recordVersion, content_header.recordSize, content_header.recordCount); + + return true; +} + +void ObjectFileReader::close() { + recordCount = 0; + recordVersion = 0; + recordsRead = 0; + + file = nullptr; +} + +bool ObjectFileReader::readNext(void* output) { + if (file == nullptr) { + TT_LOG_E(TAG, "File not open"); + return false; + } + + bool result = fread(output, recordSize, 1, file.get()) == 1; + if (result) { + recordsRead++; + } + + return result; +} + +// endregion Reader + +// region Writer + +bool ObjectFileWriter::open() { + // If file exists + bool edit_existing = append && access(filePath.c_str(), F_OK) == 0; + + auto* mode = edit_existing ? "r+" : "w"; + auto opening_file = std::unique_ptr(fopen(filePath.c_str(), mode)); + if (opening_file == nullptr) { + TT_LOG_E(TAG, "Failed to open file %s", filePath.c_str()); + return false; + } + + auto file_size = getSize(opening_file.get()); + if (file_size > 0 && edit_existing) { + + // Read and parse file header + + FileHeader file_header; + if (fread(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to read file header from %s", filePath.c_str()); + return false; + } + + if (file_header.identifier != OBJECT_FILE_IDENTIFIER) { + TT_LOG_E(TAG, "Invalid file type for %s", filePath.c_str()); + return false; + } + + if (file_header.version != OBJECT_FILE_VERSION) { + TT_LOG_E(TAG, "Unknown version for %s: %lu", filePath.c_str(), file_header.identifier); + return false; + } + + // Read and parse content header + + ContentHeader content_header; + if (fread(&content_header, sizeof(ContentHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to read content header from %s", filePath.c_str()); + return false; + } + + if (recordSize != content_header.recordSize) { + TT_LOG_E(TAG, "Record size mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordSize, content_header.recordSize); + return false; + } + + if (recordVersion != content_header.recordVersion) { + TT_LOG_E(TAG, "Version mismatch for %s: expected %lu, got %lu", filePath.c_str(), recordVersion, content_header.recordVersion); + return false; + } + + recordsWritten = content_header.recordCount; + fseek(opening_file.get(), 0, SEEK_END); + } else { + FileHeader file_header; + if (fwrite(&file_header, sizeof(FileHeader), 1, opening_file.get()) != 1) { + TT_LOG_E(TAG, "Failed to write file header for %s", filePath.c_str()); + return false; + } + + // Seek forward (skip ContentHeader that will be written later) + fseek(opening_file.get(), sizeof(ContentHeader), SEEK_CUR); + } + + + file = std::move(opening_file); + return true; +} + +void ObjectFileWriter::close() { + if (file == nullptr) { + TT_LOG_E(TAG, "File not opened: %s", filePath.c_str()); + return; + } + + if (fseek(file.get(), sizeof(FileHeader), SEEK_SET) != 0) { + TT_LOG_E(TAG, "File seek failed: %s", filePath.c_str()); + return; + } + + ContentHeader content_header = { + .recordVersion = this->recordVersion, + .recordSize = this->recordSize, + .recordCount = this->recordsWritten + }; + + if (fwrite(&content_header, sizeof(ContentHeader), 1, file.get()) != 1) { + TT_LOG_E(TAG, "Failed to write content header to %s", filePath.c_str()); + } + + file = nullptr; +} + +bool ObjectFileWriter::write(void* data) { + if (file == nullptr) { + TT_LOG_E(TAG, "File not opened: %s", filePath.c_str()); + return false; + } + + if (fwrite(data, recordSize, 1, file.get()) != 1) { + TT_LOG_E(TAG, "Failed to write record to %s", filePath.c_str()); + return false; + } + + recordsWritten++; + + return true; +} + +// endregion Writer + +} \ No newline at end of file diff --git a/TactilityHeadless/Include/Tactility/hal/Configuration.h b/TactilityHeadless/Include/Tactility/hal/Configuration.h index e81edce4..2bad68b1 100644 --- a/TactilityHeadless/Include/Tactility/hal/Configuration.h +++ b/TactilityHeadless/Include/Tactility/hal/Configuration.h @@ -53,9 +53,6 @@ struct Configuration { /** A list of UART interface configurations */ const std::vector uart = {}; - - /** A list of GPS device configurations */ - const std::vector gps = {}; }; } // namespace diff --git a/TactilityHeadless/Include/Tactility/hal/gps/GpsConfiguration.h b/TactilityHeadless/Include/Tactility/hal/gps/GpsConfiguration.h new file mode 100644 index 00000000..bfcea9be --- /dev/null +++ b/TactilityHeadless/Include/Tactility/hal/gps/GpsConfiguration.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace tt::hal::gps { + +enum class GpsModel { + Unknown = 0, + AG3335, + AG3352, + ATGM336H, // Casic (might work with AT6558, Neoway N58 LTE Cat.1, Neoway G2, Neoway G7A) + LS20031, + MTK, + MTK_L76B, + MTK_PA1616S, + UBLOX6, + UBLOX7, + UBLOX8, + UBLOX9, + UBLOX10, + UC6580, +}; + +const char* toString(GpsModel model); + +std::vector getModels(); + +struct GpsConfiguration { + char uartName[32]; // e.g. "Internal" or "/dev/ttyUSB0" + uint32_t baudRate; + GpsModel model; // Choosing "Unknown" will result in a probe +}; + +} diff --git a/TactilityHeadless/Include/Tactility/hal/gps/GpsDevice.h b/TactilityHeadless/Include/Tactility/hal/gps/GpsDevice.h index ddfd600d..98e1ade3 100644 --- a/TactilityHeadless/Include/Tactility/hal/gps/GpsDevice.h +++ b/TactilityHeadless/Include/Tactility/hal/gps/GpsDevice.h @@ -1,6 +1,7 @@ #pragma once #include "../Device.h" +#include "GpsConfiguration.h" #include "Satellites.h" #include @@ -18,25 +19,6 @@ enum class GpsResponse { Ok, }; -enum class GpsModel { - Unknown = 0, - AG3335, - AG3352, - ATGM336H, // Casic (might work with AT6558, Neoway N58 LTE Cat.1, Neoway G2, Neoway G7A) - LS20031, - MTK, - MTK_L76B, - MTK_PA1616S, - UBLOX6, - UBLOX7, - UBLOX8, - UBLOX9, - UBLOX10, - UC6580, -}; - -const char* toString(GpsModel model); - class GpsDevice : public Device { public: @@ -44,13 +26,6 @@ public: typedef int GgaSubscriptionId; typedef int RmcSubscriptionId; - struct Configuration { - std::string name; - std::string uartName; // e.g. "Internal" or "/dev/ttyUSB0" - uint32_t baudRate; - GpsModel model; - }; - enum class State { PendingOn, On, @@ -71,7 +46,7 @@ private: std::shared_ptr> onData; }; - const Configuration configuration; + const GpsConfiguration configuration; Mutex mutex = Mutex(Mutex::Type::Recursive); std::unique_ptr _Nullable thread; bool threadInterrupted = false; @@ -91,13 +66,20 @@ private: public: - explicit GpsDevice(Configuration configuration) : configuration(std::move(configuration)) {} + explicit GpsDevice(GpsConfiguration configuration) : configuration(std::move(configuration)) {} ~GpsDevice() override = default; Type getType() const override { return Type::Gps; } - std::string getName() const override { return configuration.name; } + std::string getName() const override { + if (model != GpsModel::Unknown) { + return toString(model); + } else { + return "Unknown GPS"; + } + } + std::string getDescription() const override { return ""; } bool start(); diff --git a/TactilityHeadless/Include/Tactility/kernel/SystemEvents.h b/TactilityHeadless/Include/Tactility/kernel/SystemEvents.h index ffa12148..dda2ab5a 100644 --- a/TactilityHeadless/Include/Tactility/kernel/SystemEvents.h +++ b/TactilityHeadless/Include/Tactility/kernel/SystemEvents.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace tt::kernel { @@ -26,10 +27,12 @@ enum class SystemEvent { /** Value 0 mean "no subscription" */ typedef uint32_t SystemEventSubscription; -typedef void (*OnSystemEvent)(SystemEvent event); +typedef std::function OnSystemEvent; void systemEventPublish(SystemEvent event); + SystemEventSubscription systemEventAddListener(SystemEvent event, OnSystemEvent handler); + void systemEventRemoveListener(SystemEventSubscription subscription); } \ No newline at end of file diff --git a/TactilityHeadless/Include/Tactility/service/gps/Gps.h b/TactilityHeadless/Include/Tactility/service/gps/Gps.h deleted file mode 100644 index 2e230191..00000000 --- a/TactilityHeadless/Include/Tactility/service/gps/Gps.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "Tactility/hal/gps/GpsDevice.h" -#include "GpsState.h" - -#include - -namespace tt::service::gps { - -/** Register a hardware device to the GPS service. */ -void addGpsDevice(const std::shared_ptr& device); - -/** Deregister a hardware device to the GPS service. */ -void removeGpsDevice(const std::shared_ptr& device); - -/** @return true when GPS is set to receive updates from at least 1 device */ -bool startReceiving(); - -/** Turn GPS receiving off */ -void stopReceiving(); - -bool hasCoordinates(); - -bool getCoordinates(minmea_sentence_rmc& rmc); - -State getState(); - -/** @return GPS service pubsub that broadcasts State* objects */ -std::shared_ptr getStatePubsub(); - -} diff --git a/TactilityHeadless/Private/Tactility/service/gps/GpsService.h b/TactilityHeadless/Include/Tactility/service/gps/GpsService.h similarity index 80% rename from TactilityHeadless/Private/Tactility/service/gps/GpsService.h rename to TactilityHeadless/Include/Tactility/service/gps/GpsService.h index 9130aebd..de213e21 100644 --- a/TactilityHeadless/Private/Tactility/service/gps/GpsService.h +++ b/TactilityHeadless/Include/Tactility/service/gps/GpsService.h @@ -4,6 +4,7 @@ #include "Tactility/PubSub.h" #include "Tactility/hal/gps/GpsDevice.h" #include "Tactility/service/Service.h" +#include "Tactility/service/ServiceContext.h" #include "Tactility/service/gps/GpsState.h" namespace tt::service::gps { @@ -25,6 +26,7 @@ private: Mutex stateMutex; std::vector deviceRecords; std::shared_ptr statePubSub = std::make_shared(); + std::unique_ptr paths; State state = State::Off; bool startGpsDevice(GpsDeviceRecord& deviceRecord); @@ -37,13 +39,19 @@ private: void setState(State newState); + void addGpsDevice(const std::shared_ptr& device); + void removeGpsDevice(const std::shared_ptr& device); + + bool getConfigurationFilePath(std::string& output) const; + public: void onStart(tt::service::ServiceContext &serviceContext) final; void onStop(tt::service::ServiceContext &serviceContext) final; - void addGpsDevice(const std::shared_ptr& device); - void removeGpsDevice(const std::shared_ptr& device); + bool addGpsConfiguration(hal::gps::GpsConfiguration configuration); + bool removeGpsConfiguration(hal::gps::GpsConfiguration configuration); + bool getGpsConfigurations(std::vector& configurations) const; bool startReceiving(); void stopReceiving(); @@ -56,4 +64,6 @@ public: std::shared_ptr getStatePubsub() const { return statePubSub; } }; -} // tt::hal::gps +std::shared_ptr findGpsService(); + +} // tt::service::gps diff --git a/TactilityHeadless/Private/Tactility/hal/gps/GpsInit.h b/TactilityHeadless/Private/Tactility/hal/gps/GpsInit.h index 97e099fc..6d0b94fb 100644 --- a/TactilityHeadless/Private/Tactility/hal/gps/GpsInit.h +++ b/TactilityHeadless/Private/Tactility/hal/gps/GpsInit.h @@ -6,13 +6,6 @@ namespace tt::hal::uart { class Uart; } namespace tt::hal::gps { -/** - * Called by main HAL init to ready the internal state of the GPS HAL. - * @param[in] configurations HAL configuration for a board - * @return true on success - */ -bool init(const std::vector& configurations); - /** * Init sequence on UART for a specific GPS model. */ diff --git a/TactilityHeadless/Private/Tactility/hal/gps/UbloxMessages.h b/TactilityHeadless/Private/Tactility/hal/gps/UbloxMessages.h index 97846684..52092e9c 100644 --- a/TactilityHeadless/Private/Tactility/hal/gps/UbloxMessages.h +++ b/TactilityHeadless/Private/Tactility/hal/gps/UbloxMessages.h @@ -11,6 +11,8 @@ constexpr uint8_t _message_PMREQ[] = { 0x02, 0x00, 0x00, 0x00 // Bitfield, set backup = 1 }; +// Used for sleep mode +// See https://github.com/meshtastic/firmware/blob/af8b64e84ee60175d7a0e43c6c3458e3a3558708/src/gps/GPS.cpp#L939 constexpr uint8_t _message_PMREQ_10[] = { 0x00, // version (0 for this version) 0x00, 0x00, 0x00, // Reserved 1 diff --git a/TactilityHeadless/Source/hal/Hal.cpp b/TactilityHeadless/Source/hal/Hal.cpp index 82c588a3..b3868b9d 100644 --- a/TactilityHeadless/Source/hal/Hal.cpp +++ b/TactilityHeadless/Source/hal/Hal.cpp @@ -29,8 +29,6 @@ void init(const Configuration& configuration) { tt_check(uart::init(configuration.uart), "UART init failed"); kernel::systemEventPublish(kernel::SystemEvent::BootInitUartEnd); - tt_check(gps::init(configuration.gps), "GPS init failed"); - if (configuration.initBoot != nullptr) { TT_LOG_I(TAG, "Init power"); tt_check(configuration.initBoot(), "Init power failed"); diff --git a/TactilityHeadless/Source/hal/gps/GpsConfiguration.cpp b/TactilityHeadless/Source/hal/gps/GpsConfiguration.cpp new file mode 100644 index 00000000..a0a5ec8c --- /dev/null +++ b/TactilityHeadless/Source/hal/gps/GpsConfiguration.cpp @@ -0,0 +1,51 @@ +#include "Tactility/hal/gps/GpsConfiguration.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +namespace tt::hal::gps { + +const char* toString(GpsModel model) { + using enum GpsModel; + switch (model) { + case AG3335: + return TT_STRINGIFY(AG3335); + case AG3352: + return TT_STRINGIFY(AG3352); + case ATGM336H: + return TT_STRINGIFY(ATGM336H); + case LS20031: + return TT_STRINGIFY(LS20031); + case MTK: + return TT_STRINGIFY(MTK); + case MTK_L76B: + return TT_STRINGIFY(MTK_L76B); + case MTK_PA1616S: + return TT_STRINGIFY(MTK_PA1616S); + case UBLOX6: + return TT_STRINGIFY(UBLOX6); + case UBLOX7: + return TT_STRINGIFY(UBLOX7); + case UBLOX8: + return TT_STRINGIFY(UBLOX8); + case UBLOX9: + return TT_STRINGIFY(UBLOX9); + case UBLOX10: + return TT_STRINGIFY(UBLOX10); + case UC6580: + return TT_STRINGIFY(UC6580); + default: + return TT_STRINGIFY(Unknown); + } +} + +std::vector getModels() { + std::vector result; + for (GpsModel model = GpsModel::Unknown; model <= GpsModel::UC6580; ++(int&)model) { + result.push_back(toString(model)); + } + return result; +} + +} diff --git a/TactilityHeadless/Source/hal/gps/GpsDevice.cpp b/TactilityHeadless/Source/hal/gps/GpsDevice.cpp index 948d1e40..d2159f8d 100644 --- a/TactilityHeadless/Source/hal/gps/GpsDevice.cpp +++ b/TactilityHeadless/Source/hal/gps/GpsDevice.cpp @@ -5,44 +5,10 @@ #include #include -#define TAG "gps" -#define GPS_UART_BUFFER_SIZE 256 - namespace tt::hal::gps { -const char* toString(GpsModel model) { - using enum GpsModel; - switch (model) { - case AG3335: - return TT_STRINGIFY(AG3335); - case AG3352: - return TT_STRINGIFY(AG3352); - case ATGM336H: - return TT_STRINGIFY(ATGM336H); - case LS20031: - return TT_STRINGIFY(LS20031); - case MTK: - return TT_STRINGIFY(MTK); - case MTK_L76B: - return TT_STRINGIFY(MTK_L76B); - case MTK_PA1616S: - return TT_STRINGIFY(MTK_PA1616S); - case UBLOX6: - return TT_STRINGIFY(UBLOX6); - case UBLOX7: - return TT_STRINGIFY(UBLOX7); - case UBLOX8: - return TT_STRINGIFY(UBLOX8); - case UBLOX9: - return TT_STRINGIFY(UBLOX9); - case UBLOX10: - return TT_STRINGIFY(UBLOX10); - case UC6580: - return TT_STRINGIFY(UC6580); - default: - return TT_STRINGIFY(Unknown); - } -} +constexpr uint32_t GPS_UART_BUFFER_SIZE = 256; +constexpr const char* TAG = "GpsDevice"; int32_t GpsDevice::threadMainStatic(void* parameter) { auto* gps_device = (GpsDevice*)parameter; @@ -54,17 +20,17 @@ int32_t GpsDevice::threadMain() { auto uart = uart::open(configuration.uartName); if (uart == nullptr) { - TT_LOG_E(TAG, "Failed to open UART %s", configuration.uartName.c_str()); + TT_LOG_E(TAG, "Failed to open UART %s", configuration.uartName); return -1; } if (!uart->start()) { - TT_LOG_E(TAG, "Failed to start UART %s", configuration.uartName.c_str()); + TT_LOG_E(TAG, "Failed to start UART %s", configuration.uartName); return -1; } if (!uart->setBaudRate((int)configuration.baudRate)) { - TT_LOG_E(TAG, "Failed to set baud rate to %lu for UART %s", configuration.baudRate, configuration.uartName.c_str()); + TT_LOG_E(TAG, "Failed to set baud rate to %lu for UART %s", configuration.baudRate, configuration.uartName); return -1; } @@ -77,7 +43,6 @@ int32_t GpsDevice::threadMain() { return -1; } } - mutex.lock(); this->model = model; mutex.unlock(); @@ -101,7 +66,7 @@ int32_t GpsDevice::threadMain() { if (bytes_read > 0U) { - TT_LOG_D(TAG, "%s", buffer); + TT_LOG_I(TAG, "[%ul] %s", bytes_read, buffer); switch (minmea_sentence_id((char*)buffer, false)) { case MINMEA_SENTENCE_RMC: @@ -137,7 +102,7 @@ int32_t GpsDevice::threadMain() { } if (uart->isStarted() && !uart->stop()) { - TT_LOG_W(TAG, "Failed to stop UART %s", configuration.uartName.c_str()); + TT_LOG_W(TAG, "Failed to stop UART %s", configuration.uartName); } return 0; diff --git a/TactilityHeadless/Source/hal/gps/GpsInit.cpp b/TactilityHeadless/Source/hal/gps/GpsInit.cpp index 954abf66..352c7a56 100644 --- a/TactilityHeadless/Source/hal/gps/GpsInit.cpp +++ b/TactilityHeadless/Source/hal/gps/GpsInit.cpp @@ -1,7 +1,6 @@ #include "Tactility/hal/gps/Cas.h" #include "Tactility/hal/gps/GpsDevice.h" #include "Tactility/hal/gps/Ublox.h" -#include "Tactility/service/Service.h" #include #define TAG "gps" @@ -133,17 +132,6 @@ GpsResponse getACKCas(uart::Uart& uart, uint8_t class_id, uint8_t msg_id, uint32 // endregion - -/** Initialize the HAL with the provided configuration */ -bool init(const std::vector& configurations) { - for (auto& configuration : configurations) { - auto device = std::make_shared(configuration); - hal::registerDevice(std::move(device)); - } - - return true; -} - bool init(uart::Uart& uart, GpsModel type) { switch (type) { case GpsModel::Unknown: diff --git a/TactilityHeadless/Source/kernel/SystemEvents.cpp b/TactilityHeadless/Source/kernel/SystemEvents.cpp index fce4e2d8..dd8d59fa 100644 --- a/TactilityHeadless/Source/kernel/SystemEvents.cpp +++ b/TactilityHeadless/Source/kernel/SystemEvents.cpp @@ -69,7 +69,7 @@ void systemEventPublish(SystemEvent event) { } } -SystemEventSubscription systemEventAddListener(SystemEvent event, OnSystemEvent handler) { +SystemEventSubscription systemEventAddListener(SystemEvent event, std::function handler) { if (mutex.lock(portMAX_DELAY)) { auto id = ++subscriptionCounter; diff --git a/TactilityHeadless/Source/service/ServiceInstancePaths.cpp b/TactilityHeadless/Source/service/ServiceInstancePaths.cpp index 8fe7c9c3..bc7097d0 100644 --- a/TactilityHeadless/Source/service/ServiceInstancePaths.cpp +++ b/TactilityHeadless/Source/service/ServiceInstancePaths.cpp @@ -3,6 +3,7 @@ #include "Tactility/Partitions.h" #define LVGL_PATH_PREFIX std::string("A:/") + #ifdef ESP_PLATFORM #define PARTITION_PREFIX std::string("/") #else diff --git a/TactilityHeadless/Source/service/gps/Gps.cpp b/TactilityHeadless/Source/service/gps/Gps.cpp deleted file mode 100644 index 44a43a12..00000000 --- a/TactilityHeadless/Source/service/gps/Gps.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include "Tactility/PubSub.h" -#include "Tactility/service/ServiceManifest.h" -#include "Tactility/service/ServiceRegistry.h" -#include "Tactility/service/gps/GpsService.h" - -using tt::hal::gps::GpsDevice; - -namespace tt::service::gps { - -extern ServiceManifest manifest; - -static std::shared_ptr findGpsService() { - auto service = findServiceById(manifest.id); - assert(service != nullptr); - return std::static_pointer_cast(service); -} - -void addGpsDevice(const std::shared_ptr& device) { - return findGpsService()->addGpsDevice(device); -} - -void removeGpsDevice(const std::shared_ptr& device) { - return findGpsService()->removeGpsDevice(device); -} - -bool startReceiving() { - return findGpsService()->startReceiving(); -} - -void stopReceiving() { - findGpsService()->stopReceiving(); -} - -bool hasCoordinates() { - return findGpsService()->hasCoordinates(); -} - -bool getCoordinates(minmea_sentence_rmc& rmc) { - return findGpsService()->getCoordinates(rmc); -} - -State getState() { - return findGpsService()->getState(); -} - -std::shared_ptr getStatePubsub() { - return findGpsService()->getStatePubsub(); -} - -} // namespace tt::service::gps diff --git a/TactilityHeadless/Source/service/gps/GpsConfiguration.cpp b/TactilityHeadless/Source/service/gps/GpsConfiguration.cpp new file mode 100644 index 00000000..5a6ec6fd --- /dev/null +++ b/TactilityHeadless/Source/service/gps/GpsConfiguration.cpp @@ -0,0 +1,108 @@ +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +using tt::hal::gps::GpsDevice; + +namespace tt::service::gps { + +constexpr const char* TAG = "GpsService"; + +bool GpsService::getConfigurationFilePath(std::string& output) const { + if (paths == nullptr) { + TT_LOG_E(TAG, "Can't add configuration: service not started"); + return false; + } + + if (!file::findOrCreateDirectory(paths->getDataDirectory(), 0777)) { + TT_LOG_E(TAG, "Failed to find or create path %s", paths->getDataDirectory().c_str()); + return false; + } + + output = paths->getDataPath("config.bin"); + return true; +} + +bool GpsService::getGpsConfigurations(std::vector& configurations) const { + std::string path; + if (!getConfigurationFilePath(path)) { + return false; + } + + auto reader = file::ObjectFileReader(path, sizeof(hal::gps::GpsConfiguration)); + if (!reader.open()) { + TT_LOG_E(TAG, "Failed to open configuration file"); + return false; + } + + hal::gps::GpsConfiguration configuration; + while (reader.hasNext()) { + if (!reader.readNext(&configuration)) { + TT_LOG_E(TAG, "Failed to read configuration"); + reader.close(); + return false; + } else { + configurations.push_back(configuration); + } + } + + return true; +} + +bool GpsService::addGpsConfiguration(hal::gps::GpsConfiguration configuration) { + std::string path; + if (!getConfigurationFilePath(path)) { + return false; + } + + auto appender = file::ObjectFileWriter(path, sizeof(hal::gps::GpsConfiguration), 1, true); + if (!appender.open()) { + TT_LOG_E(TAG, "Failed to open/create configuration file"); + return false; + } + + if (!appender.write(&configuration)) { + TT_LOG_E(TAG, "Failed to add configuration"); + appender.close(); + return false; + } + + appender.close(); + return true; +} + +bool GpsService::removeGpsConfiguration(hal::gps::GpsConfiguration configuration) { + std::string path; + if (!getConfigurationFilePath(path)) { + return false; + } + + std::vector configurations; + if (!getGpsConfigurations(configurations)) { + TT_LOG_E(TAG, "Failed to get gps configurations"); + return false; + } + + auto count = std::erase_if(configurations, [&configuration](auto& item) { + return strcmp(item.uartName, configuration.uartName) == 0 && + item.baudRate == configuration.baudRate && + item.model == configuration.model; + }); + + auto writer = file::ObjectFileWriter(path, sizeof(hal::gps::GpsConfiguration), 1, false); + if (!writer.open()) { + TT_LOG_E(TAG, "Failed to open configuration file"); + return false; + } + + for (auto& configuration : configurations) { + writer.write(&configuration); + } + + writer.close(); + + return count > 0; +} + +} // namespace tt::service::gps diff --git a/TactilityHeadless/Source/service/gps/GpsService.cpp b/TactilityHeadless/Source/service/gps/GpsService.cpp index d8cd0a27..aa4a162f 100644 --- a/TactilityHeadless/Source/service/gps/GpsService.cpp +++ b/TactilityHeadless/Source/service/gps/GpsService.cpp @@ -1,12 +1,17 @@ #include "Tactility/service/gps/GpsService.h" #include "Tactility/service/ServiceManifest.h" +#include "Tactility/service/ServiceRegistry.h" -#define TAG "gps_service" +#include +#include using tt::hal::gps::GpsDevice; namespace tt::service::gps { +constexpr const char* TAG = "GpsService"; +extern const ServiceManifest manifest; + constexpr inline bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) { return (TickType_t)(now - timeInThePast) >= expireTimeInTicks; } @@ -15,7 +20,7 @@ GpsService::GpsDeviceRecord* _Nullable GpsService::findGpsRecord(const std::shar auto lock = mutex.asScopedLock(); lock.lock(); - auto result = std::views::filter(deviceRecords, [&device](auto& record){ + auto result = std::views::filter(deviceRecords, [&device](auto& record) { return record.device.get() == device.get(); }); if (!result.empty()) { @@ -29,7 +34,7 @@ void GpsService::addGpsDevice(const std::shared_ptr& device) { auto lock = mutex.asScopedLock(); lock.lock(); - GpsDeviceRecord record = { .device = device }; + GpsDeviceRecord record = {.device = device}; if (getState() == State::On) { // Ignore during OnPending due to risk of data corruptiohn startGpsDevice(record); @@ -48,25 +53,19 @@ void GpsService::removeGpsDevice(const std::shared_ptr& device) { stopGpsDevice(*record); } - std::erase_if(deviceRecords, [&device](auto& reference){ + std::erase_if(deviceRecords, [&device](auto& reference) { return reference.device.get() == device.get(); }); } -void GpsService::onStart(tt::service::ServiceContext &serviceContext) { +void GpsService::onStart(tt::service::ServiceContext& serviceContext) { auto lock = mutex.asScopedLock(); lock.lock(); - deviceRecords.clear(); - - auto devices = hal::findDevices(hal::Device::Type::Gps); - for (auto& device : devices) { - TT_LOG_I(TAG, "[device %lu] added", device->getId()); - addGpsDevice(device); - } + paths = serviceContext.getPaths(); } -void GpsService::onStop(tt::service::ServiceContext &serviceContext) { +void GpsService::onStop(tt::service::ServiceContext& serviceContext) { if (getState() == State::On) { stopReceiving(); } @@ -126,14 +125,39 @@ bool GpsService::stopGpsDevice(GpsDeviceRecord& record) { bool GpsService::startReceiving() { TT_LOG_I(TAG, "Start receiving"); + if (getState() != State::Off) { + TT_LOG_E(TAG, "Already receiving"); + return false; + } + setState(State::OnPending); auto lock = mutex.asScopedLock(); lock.lock(); + deviceRecords.clear(); + + std::vector configurations; + if (!getGpsConfigurations(configurations)) { + TT_LOG_E(TAG, "Failed to get GPS configurations"); + setState(State::Off); + return false; + } + + if (configurations.empty()) { + TT_LOG_E(TAG, "No GPS configurations"); + setState(State::Off); + return false; + } + + for (const auto& configuration: configurations) { + auto device = std::make_shared(configuration); + addGpsDevice(device); + } + bool started_one_or_more = false; - for (auto& record : deviceRecords) { + for (auto& record: deviceRecords) { started_one_or_more |= startGpsDevice(record); } @@ -156,7 +180,7 @@ void GpsService::stopReceiving() { auto lock = mutex.asScopedLock(); lock.lock(); - for (auto& record : deviceRecords) { + for (auto& record: deviceRecords) { stopGpsDevice(record); } @@ -202,9 +226,15 @@ bool GpsService::getCoordinates(minmea_sentence_rmc& rmc) const { } } +std::shared_ptr findGpsService() { + auto service = findServiceById(manifest.id); + assert(service != nullptr); + return std::static_pointer_cast(service); +} + extern const ServiceManifest manifest = { .id = "Gps", .createService = create }; -} // tt::hal::gps +} // namespace tt::service::gps diff --git a/Tests/TactilityCore/FileTest.cpp b/Tests/TactilityCore/FileTest.cpp new file mode 100644 index 00000000..94fa75f3 --- /dev/null +++ b/Tests/TactilityCore/FileTest.cpp @@ -0,0 +1,14 @@ +#include "doctest.h" +#include + +using namespace tt; + +TEST_CASE("findOrCreateDirectory can create a directory tree without prefix") { + CHECK_EQ(file::findOrCreateDirectory("test1/test1", 0777), true); + // TODO: delete dirs +} + +TEST_CASE("findOrCreateDirectory can create a directory tree with prefix") { + CHECK_EQ(file::findOrCreateDirectory("/test2/test2", 0777), true); + // TODO: delete dirs +} diff --git a/Tests/TactilityCore/ObjectFileTest.cpp b/Tests/TactilityCore/ObjectFileTest.cpp new file mode 100644 index 00000000..c07cf78a --- /dev/null +++ b/Tests/TactilityCore/ObjectFileTest.cpp @@ -0,0 +1,65 @@ +#include "doctest.h" +#include + +using tt::file::ObjectFileWriter; +using tt::file::ObjectFileReader; + +constexpr const char* TEMP_FILE = "test.tmp"; + +struct TestStruct { + uint32_t value; +}; + +TEST_CASE("Writing and reading multiple records to a file") { + ObjectFileWriter writer = ObjectFileWriter(TEMP_FILE, sizeof(TestStruct), 1, false); + + TestStruct record_out_1 = { .value = 0xAAAAAAAA }; + TestStruct record_out_2 = { .value = 0xBBBBBBBB }; + + CHECK_EQ(writer.open(), true); + CHECK_EQ(writer.write(&record_out_1), true); + CHECK_EQ(writer.write(&record_out_2), true); + writer.close(); + + TestStruct record_in; + ObjectFileReader reader = ObjectFileReader(TEMP_FILE, sizeof(TestStruct)); + CHECK_EQ(reader.open(), true); + CHECK_EQ(reader.hasNext(), true); + CHECK_EQ(reader.readNext(&record_in), true); + CHECK_EQ(reader.hasNext(), true); + CHECK_EQ(reader.readNext(&record_in), true); + CHECK_EQ(reader.hasNext(), false); + reader.close(); + + remove(TEMP_FILE); +} + +TEST_CASE("Appending records to a file") { + remove(TEMP_FILE); + + ObjectFileWriter writer = ObjectFileWriter(TEMP_FILE, sizeof(TestStruct), 1, false); + + TestStruct record_out_1 = { .value = 0xAAAAAAAA }; + TestStruct record_out_2 = { .value = 0xBBBBBBBB }; + + CHECK_EQ(writer.open(), true); + CHECK_EQ(writer.write(&record_out_1), true); + writer.close(); + + ObjectFileWriter appender = ObjectFileWriter(TEMP_FILE, sizeof(TestStruct), 1, true); + CHECK_EQ(appender.open(), true); + CHECK_EQ(appender.write(&record_out_2), true); + appender.close(); + + TestStruct record_in; + ObjectFileReader reader = ObjectFileReader(TEMP_FILE, sizeof(TestStruct)); + CHECK_EQ(reader.open(), true); + CHECK_EQ(reader.hasNext(), true); + CHECK_EQ(reader.readNext(&record_in), true); + CHECK_EQ(reader.hasNext(), true); + CHECK_EQ(reader.readNext(&record_in), true); + CHECK_EQ(reader.hasNext(), false); + reader.close(); + + remove(TEMP_FILE); +}