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)
This commit is contained in:
Ken Van Hoeylandt 2025-03-30 01:14:22 +01:00 committed by GitHub
parent 81ece6f2e7
commit d0ca3b16f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1266 additions and 277 deletions

View File

@ -138,6 +138,5 @@ extern const Configuration crowpanel_advance_28 = {
}
}
}
},
.gps = {}
}
};

View File

@ -138,6 +138,5 @@ extern const Configuration crowpanel_advance_35 = {
}
}
}
},
.gps = {}
}
};

View File

@ -121,6 +121,5 @@ extern const Configuration crowpanel_advance_50 = {
}
}
}
},
.gps = {}
}
};

View File

@ -116,6 +116,5 @@ extern const Configuration crowpanel_basic_28 = {
}
}
}
},
.gps = {}
}
};

View File

@ -116,6 +116,5 @@ extern const Configuration crowpanel_basic_35 = {
}
}
}
},
.gps = {}
}
};

View File

@ -86,6 +86,5 @@ extern const Configuration crowpanel_basic_50 = {
}
}
}
},
.gps = {}
}
};

View File

@ -1,6 +1,9 @@
#include "PwmBacklight.h"
#include "Tactility/kernel/SystemEvents.h"
#include "Tactility/service/gps/GpsService.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/hal/gps/GpsConfiguration.h>
#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<tt::hal::gps::GpsConfiguration> gps_configurations;
gps_service->getGpsConfigurations(gps_configurations);
if (gps_configurations.empty()) {
if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {
.uartName = "Grove",
.baudRate = 38400,
.model = tt::hal::gps::GpsModel::UBLOX10
})) {
TT_LOG_I(TAG, "Configured internal GPS");
} else {
TT_LOG_E(TAG, "Failed to configure internal GPS");
}
}
}
});
return true;
}

View File

@ -104,13 +104,5 @@ extern const Configuration lilygo_tdeck = {
}
}
}
},
.gps = {
gps::GpsDevice::Configuration {
.name = "Internal",
.uartName = "Grove",
.baudRate = 38400,
.model = gps::GpsModel::UBLOX10
}
}
};

View File

@ -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 <Tactility/lvgl/LvglSync.h>
#include <Tactility/hal/uart/Uart.h>
#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,
}
}
}
}
};

View File

@ -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
}
}
};

View File

@ -80,6 +80,5 @@ extern const Configuration waveshare_s3_touch_43 = {
}
}
}
},
.gps = {}
}
};

27
Documentation/gps.md Normal file
View File

@ -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

View File

@ -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

23
Documentation/wiring.md Normal file
View File

@ -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)

@ -1 +1 @@
Subproject commit 0efeb82a28795e72594642251f1bddcb881aaa7f
Subproject commit 52146cf067ae950b1d431a84766fc7d0501f536a

View File

@ -0,0 +1 @@
#pragma once

View File

@ -2,11 +2,12 @@
#include <dirent.h>
#include <string>
#include <sys/stat.h>
#include <vector>
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*);

View File

@ -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);

View File

@ -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 <cstring>
#include <lvgl.h>
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<uint32_t, 6> 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<tt::hal::gps::GpsConfiguration> 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<AddGpsApp>
};
void start() {
app::start(manifest.id);
}
} // namespace

View File

@ -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

View File

@ -8,6 +8,8 @@
#include <cstring>
#include <unistd.h>
#include <vector>
#include <dirent.h>
#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

View File

@ -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 <Tactility/service/gps/Gps.h>
#include <Tactility/service/gps/GpsService.h>
#include <cstring>
#include <format>
#include <lvgl.h>
#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> timer;
std::shared_ptr<GpsSettingsApp*> appReference = std::make_shared<GpsSettingsApp*>(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::gps::GpsService> service;
static void onUpdateCallback(TT_UNUSED std::shared_ptr<void> context) {
auto appPtr = std::static_pointer_cast<GpsSettingsApp*>(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<tt::hal::gps::GpsConfiguration> 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<void*>(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<tt::hal::gps::GpsConfiguration> 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::gps::GpsService>(service)->startReceiving();
}, service);
} else {
getMainDispatcher().dispatch([](TT_UNUSED auto _) {
service::gps::stopReceiving();
}, nullptr);
getMainDispatcher().dispatch([](auto service) {
std::static_pointer_cast<service::gps::GpsService>(service)->stopReceiving();
}, service);
}
}
}
@ -170,6 +281,7 @@ public:
GpsSettingsApp() {
timer = std::make_unique<Timer>(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

View File

@ -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 <lvgl.h>
#define TAG "text_viewer"
namespace tt::app::serialconsole {
class SerialConsoleApp final : public App {

View File

@ -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 <Tactility/Mutex.h>
#include <Tactility/Tactility.h>
#include <Tactility/TactilityHeadless.h>
@ -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) {

View File

@ -3,9 +3,24 @@
#include "Tactility/TactilityCore.h"
#include <cstdio>
#include <sys/stat.h>
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<uint8_t[]> readBinary(const std::string& filepath, size_t& outSi
*/
std::unique_ptr<uint8_t[]> 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);
}

View File

@ -0,0 +1,76 @@
#pragma once
#include "File.h"
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
namespace tt::file {
class ObjectFileReader {
private:
const std::string filePath;
const uint32_t recordSize = 0;
std::unique_ptr<FILE, FileCloser> 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, FileCloser> 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);
};
}

View File

@ -88,4 +88,54 @@ std::unique_ptr<uint8_t[]> 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;
}
}

View File

@ -0,0 +1,207 @@
#include "Tactility/file/ObjectFile.h"
#include <Tactility/Log.h>
#include <unistd.h>
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<FILE, FileCloser>(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<FILE, FileCloser>(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
}

View File

@ -53,9 +53,6 @@ struct Configuration {
/** A list of UART interface configurations */
const std::vector<uart::Configuration> uart = {};
/** A list of GPS device configurations */
const std::vector<gps::GpsDevice::Configuration> gps = {};
};
} // namespace

View File

@ -0,0 +1,36 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
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<std::string> getModels();
struct GpsConfiguration {
char uartName[32]; // e.g. "Internal" or "/dev/ttyUSB0"
uint32_t baudRate;
GpsModel model; // Choosing "Unknown" will result in a probe
};
}

View File

@ -1,6 +1,7 @@
#pragma once
#include "../Device.h"
#include "GpsConfiguration.h"
#include "Satellites.h"
#include <Tactility/Mutex.h>
@ -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<std::function<void(Device::Id id, const minmea_sentence_rmc&)>> onData;
};
const Configuration configuration;
const GpsConfiguration configuration;
Mutex mutex = Mutex(Mutex::Type::Recursive);
std::unique_ptr<Thread> _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();

View File

@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <functional>
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<void(SystemEvent)> OnSystemEvent;
void systemEventPublish(SystemEvent event);
SystemEventSubscription systemEventAddListener(SystemEvent event, OnSystemEvent handler);
void systemEventRemoveListener(SystemEventSubscription subscription);
}

View File

@ -1,31 +0,0 @@
#pragma once
#include "Tactility/hal/gps/GpsDevice.h"
#include "GpsState.h"
#include <Tactility/PubSub.h>
namespace tt::service::gps {
/** Register a hardware device to the GPS service. */
void addGpsDevice(const std::shared_ptr<hal::gps::GpsDevice>& device);
/** Deregister a hardware device to the GPS service. */
void removeGpsDevice(const std::shared_ptr<hal::gps::GpsDevice>& 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<PubSub> getStatePubsub();
}

View File

@ -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<GpsDeviceRecord> deviceRecords;
std::shared_ptr<PubSub> statePubSub = std::make_shared<PubSub>();
std::unique_ptr<Paths> paths;
State state = State::Off;
bool startGpsDevice(GpsDeviceRecord& deviceRecord);
@ -37,13 +39,19 @@ private:
void setState(State newState);
void addGpsDevice(const std::shared_ptr<hal::gps::GpsDevice>& device);
void removeGpsDevice(const std::shared_ptr<hal::gps::GpsDevice>& 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<hal::gps::GpsDevice>& device);
void removeGpsDevice(const std::shared_ptr<hal::gps::GpsDevice>& device);
bool addGpsConfiguration(hal::gps::GpsConfiguration configuration);
bool removeGpsConfiguration(hal::gps::GpsConfiguration configuration);
bool getGpsConfigurations(std::vector<hal::gps::GpsConfiguration>& configurations) const;
bool startReceiving();
void stopReceiving();
@ -56,4 +64,6 @@ public:
std::shared_ptr<PubSub> getStatePubsub() const { return statePubSub; }
};
} // tt::hal::gps
std::shared_ptr<GpsService> findGpsService();
} // tt::service::gps

View File

@ -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<GpsDevice::Configuration>& configurations);
/**
* Init sequence on UART for a specific GPS model.
*/

View File

@ -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

View File

@ -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");

View File

@ -0,0 +1,51 @@
#include "Tactility/hal/gps/GpsConfiguration.h"
#include "Tactility/service/gps/GpsService.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/file/ObjectFile.h>
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<std::string> getModels() {
std::vector<std::string> result;
for (GpsModel model = GpsModel::Unknown; model <= GpsModel::UC6580; ++(int&)model) {
result.push_back(toString(model));
}
return result;
}
}

View File

@ -5,44 +5,10 @@
#include <cstring>
#include <minmea.h>
#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;

View File

@ -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 <cstring>
#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<GpsDevice::Configuration>& configurations) {
for (auto& configuration : configurations) {
auto device = std::make_shared<GpsDevice>(configuration);
hal::registerDevice(std::move(device));
}
return true;
}
bool init(uart::Uart& uart, GpsModel type) {
switch (type) {
case GpsModel::Unknown:

View File

@ -69,7 +69,7 @@ void systemEventPublish(SystemEvent event) {
}
}
SystemEventSubscription systemEventAddListener(SystemEvent event, OnSystemEvent handler) {
SystemEventSubscription systemEventAddListener(SystemEvent event, std::function<void(SystemEvent)> handler) {
if (mutex.lock(portMAX_DELAY)) {
auto id = ++subscriptionCounter;

View File

@ -3,6 +3,7 @@
#include "Tactility/Partitions.h"
#define LVGL_PATH_PREFIX std::string("A:/")
#ifdef ESP_PLATFORM
#define PARTITION_PREFIX std::string("/")
#else

View File

@ -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<GpsService> findGpsService() {
auto service = findServiceById(manifest.id);
assert(service != nullptr);
return std::static_pointer_cast<GpsService>(service);
}
void addGpsDevice(const std::shared_ptr<GpsDevice>& device) {
return findGpsService()->addGpsDevice(device);
}
void removeGpsDevice(const std::shared_ptr<GpsDevice>& 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<PubSub> getStatePubsub() {
return findGpsService()->getStatePubsub();
}
} // namespace tt::service::gps

View File

@ -0,0 +1,108 @@
#include "Tactility/service/gps/GpsService.h"
#include <Tactility/file/ObjectFile.h>
#include <cstring>
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<hal::gps::GpsConfiguration>& 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<hal::gps::GpsConfiguration> 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

View File

@ -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 <Tactility/Log.h>
#include <Tactility/file/File.h>
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<GpsDevice>& 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<GpsDevice>& 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<GpsDevice>(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<hal::gps::GpsConfiguration> 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<GpsDevice>(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<GpsService> findGpsService() {
auto service = findServiceById(manifest.id);
assert(service != nullptr);
return std::static_pointer_cast<GpsService>(service);
}
extern const ServiceManifest manifest = {
.id = "Gps",
.createService = create<GpsService>
};
} // tt::hal::gps
} // namespace tt::service::gps

View File

@ -0,0 +1,14 @@
#include "doctest.h"
#include <Tactility/file/File.h>
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
}

View File

@ -0,0 +1,65 @@
#include "doctest.h"
#include <Tactility/file/ObjectFile.h>
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);
}