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:
parent
81ece6f2e7
commit
d0ca3b16f8
@ -138,6 +138,5 @@ extern const Configuration crowpanel_advance_28 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
@ -138,6 +138,5 @@ extern const Configuration crowpanel_advance_35 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
@ -121,6 +121,5 @@ extern const Configuration crowpanel_advance_50 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
@ -116,6 +116,5 @@ extern const Configuration crowpanel_basic_28 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
@ -116,6 +116,5 @@ extern const Configuration crowpanel_basic_35 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
@ -86,6 +86,5 @@ extern const Configuration crowpanel_basic_50 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -104,13 +104,5 @@ extern const Configuration lilygo_tdeck = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {
|
||||
gps::GpsDevice::Configuration {
|
||||
.name = "Internal",
|
||||
.uartName = "Grove",
|
||||
.baudRate = 38400,
|
||||
.model = gps::GpsModel::UBLOX10
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -80,6 +80,5 @@ extern const Configuration waveshare_s3_touch_43 = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
.gps = {}
|
||||
}
|
||||
};
|
||||
|
||||
27
Documentation/gps.md
Normal file
27
Documentation/gps.md
Normal 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
|
||||
@ -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
23
Documentation/wiring.md
Normal 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
|
||||
1
Tactility/Include/Tactility/app/addgps/AddGps.h
Normal file
1
Tactility/Include/Tactility/app/addgps/AddGps.h
Normal file
@ -0,0 +1 @@
|
||||
#pragma once
|
||||
@ -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*);
|
||||
|
||||
@ -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);
|
||||
|
||||
179
Tactility/Source/app/addgps/AddGps.cpp
Normal file
179
Tactility/Source/app/addgps/AddGps.cpp
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
76
TactilityCore/Include/Tactility/file/ObjectFile.h
Normal file
76
TactilityCore/Include/Tactility/file/ObjectFile.h
Normal 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);
|
||||
};
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
207
TactilityCore/Source/file/ObjectFile.cpp
Normal file
207
TactilityCore/Source/file/ObjectFile.cpp
Normal 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
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
}
|
||||
@ -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
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
|
||||
51
TactilityHeadless/Source/hal/gps/GpsConfiguration.cpp
Normal file
51
TactilityHeadless/Source/hal/gps/GpsConfiguration.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include "Tactility/Partitions.h"
|
||||
|
||||
#define LVGL_PATH_PREFIX std::string("A:/")
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#define PARTITION_PREFIX std::string("/")
|
||||
#else
|
||||
|
||||
@ -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
|
||||
108
TactilityHeadless/Source/service/gps/GpsConfiguration.cpp
Normal file
108
TactilityHeadless/Source/service/gps/GpsConfiguration.cpp
Normal 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
|
||||
@ -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
|
||||
|
||||
14
Tests/TactilityCore/FileTest.cpp
Normal file
14
Tests/TactilityCore/FileTest.cpp
Normal 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
|
||||
}
|
||||
65
Tests/TactilityCore/ObjectFileTest.cpp
Normal file
65
Tests/TactilityCore/ObjectFileTest.cpp
Normal 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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user