Compare commits

...

2 Commits

Author SHA1 Message Date
Ken Van Hoeylandt
c903c5c432 Fixes 2026-07-24 12:18:27 +02:00
Ken Van Hoeylandt
abc42be9be GPS migration and more 2026-07-24 12:05:48 +02:00
76 changed files with 1981 additions and 2581 deletions

View File

@ -96,9 +96,9 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Libraries/QRCode)
add_subdirectory(Libraries/minitar)
add_subdirectory(Libraries/minmea)
add_subdirectory(Modules/hal-device-module)
add_subdirectory(Modules/lvgl-module)
add_subdirectory(Modules/crypt-module)
add_subdirectory(Drivers/gps-module)
# FreeRTOS
set(FREERTOS_CONFIG_FILE_DIRECTORY ${PROJECT_SOURCE_DIR}/Devices/simulator/Source CACHE STRING "")

View File

@ -1,74 +1,79 @@
#include <tactility/module.h>
#include <tactility/error.h>
#include <tactility/drivers/gps.h>
#include <tactility/gps_service.h>
#include <tactility/log.h>
#include <tactility/lvgl_module.h>
#include <tactility/module.h>
#include <Tactility/SystemEvents.h>
#include <Tactility/LogMessages.h>
#include <Tactility/hal/gps/GpsConfiguration.h>
#include <Tactility/kernel/Kernel.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/settings/TrackballSettings.h>
#include <lilygo/drivers/trackball.h>
#include <lilygo/drivers/tdeck_power_on.h>
#include <tactility/delay.h>
#include <driver/gpio.h>
constexpr auto* TAG = "tdeck-plus";
extern "C" {
void subscribe_events() {
tt::kernel::subscribeSystemEvent(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 = "uart0", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) {
LOG_I(TAG, "Configured internal GPS");
} else {
LOG_E(TAG, "Failed to configure internal GPS");
}
}
}
static tt::kernel::SystemEventSubscription tdeck_boot_splash_subscription = 0;
void init_gps_configuration() {
bool has_configuration = false;
gps_service_for_each_configuration(&has_configuration, [](const GpsConfiguration*, size_t, void* context) {
*static_cast<bool*>(context) = true;
});
// The kernel trackball device is already started by kernel_init(); this just registers it as an
// LVGL input device and applies persisted settings, both of which require LVGL to be up first.
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto tbSettings = tt::settings::trackball::loadOrGetDefault();
lvgl_lock();
if (trackball::init() != nullptr) {
trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer
? trackball::Mode::Pointer
: trackball::Mode::Encoder);
trackball::setEncoderSensitivity(tbSettings.encoderSensitivity);
trackball::setPointerSensitivity(tbSettings.pointerSensitivity);
trackball::setEnabled(tbSettings.trackballEnabled);
if (!has_configuration) {
GpsConfiguration configuration = { .uart_name = "uart0", .baud_rate = 38400, .model = GpsModel::GPS_MODEL_UBLOX10 };
if (gps_service_add_configuration(&configuration) == ERROR_NONE) {
LOG_I(TAG, "Configured internal GPS");
} else {
LOG_E(TAG, "Failed to configure internal GPS");
}
lvgl_unlock();
});
}
}
void init_trackball() {
auto tbSettings = tt::settings::trackball::loadOrGetDefault();
lvgl_lock();
if (trackball::init() != nullptr) {
trackball::setMode(tbSettings.trackballMode == tt::settings::trackball::TrackballMode::Pointer
? trackball::Mode::Pointer
: trackball::Mode::Encoder);
trackball::setEncoderSensitivity(tbSettings.encoderSensitivity);
trackball::setPointerSensitivity(tbSettings.pointerSensitivity);
trackball::setEnabled(tbSettings.trackballEnabled);
}
lvgl_unlock();
}
static error_t start() {
LOG_I(TAG, LOG_MESSAGE_POWER_ON_START);
if (!tdeck_power_on()) {
LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED);
return ERROR_RESOURCE;
}
// Avoids crash when no SD card is inserted. It's unknown why, but likely is related to power draw.
tt::kernel::delayMillis(100);
delay_millis(100);
subscribe_events();
tdeck_boot_splash_subscription = tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
init_gps_configuration();
init_trackball();
});
return ERROR_NONE;
}
static error_t stop() {
tt::kernel::unsubscribeSystemEvent(tdeck_boot_splash_subscription);
tdeck_boot_splash_subscription = 0;
return ERROR_NONE;
}
@ -76,8 +81,6 @@ Module lilygo_tdeck_plus_module = {
.name = "lilygo-tdeck-plus",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
};
}

View File

@ -1,6 +1,5 @@
#include "Main.h"
#include <Tactility/Thread.h>
#include <Tactility/TactilityCore.h>
#include "FreeRTOS.h"
#include "task.h"

View File

@ -1,21 +0,0 @@
#include "hal/SdlDisplay.h"
#include "hal/SdlKeyboard.h"
#include "hal/SimulatorPower.h"
#define TAG "hardware"
using namespace tt::hal;
static std::vector<std::shared_ptr<tt::hal::Device>> createDevices() {
return {
std::make_shared<SdlDisplay>(),
std::make_shared<SdlKeyboard>(),
std::make_shared<SimulatorPower>(),
};
}
extern const Configuration hardwareConfiguration = {
.initBoot = nullptr,
.createDevices = createDevices
};

View File

@ -0,0 +1,160 @@
// SPDX-License-Identifier: Apache-2.0
#include "sdl_display.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <SDL2/SDL.h>
#include <cstdlib>
constexpr auto* TAG = "SdlDisplay";
#define GET_CONFIG(device) (static_cast<const SdlDisplayConfig*>((device)->config))
struct SdlDisplayInternal {
SDL_Window* window;
SDL_Renderer* renderer;
SDL_Texture* texture;
};
// region Driver lifecycle
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<SdlDisplayInternal*>(malloc(sizeof(SdlDisplayInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
LOG_E(TAG, "SDL_InitSubSystem failed: %s", SDL_GetError());
free(internal);
return ERROR_RESOURCE;
}
internal->window = SDL_CreateWindow(
"Tactility",
SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
config->horizontal_resolution, config->vertical_resolution,
SDL_WINDOW_SHOWN
);
internal->renderer = internal->window != nullptr
? SDL_CreateRenderer(internal->window, -1, SDL_RENDERER_ACCELERATED)
: nullptr;
internal->texture = internal->renderer != nullptr
? SDL_CreateTexture(internal->renderer, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING,
config->horizontal_resolution, config->vertical_resolution)
: nullptr;
if (internal->window == nullptr || internal->renderer == nullptr || internal->texture == nullptr) {
LOG_E(TAG, "Failed to create SDL window: %s", SDL_GetError());
if (internal->texture != nullptr) SDL_DestroyTexture(internal->texture);
if (internal->renderer != nullptr) SDL_DestroyRenderer(internal->renderer);
if (internal->window != nullptr) SDL_DestroyWindow(internal->window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
free(internal);
return ERROR_RESOURCE;
}
device_set_driver_data(device, internal);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<SdlDisplayInternal*>(device_get_driver_data(device));
SDL_DestroyTexture(internal->texture);
SDL_DestroyRenderer(internal->renderer);
SDL_DestroyWindow(internal->window);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// region DisplayApi
static error_t sdl_display_reset(Device*) { return ERROR_NONE; }
static error_t sdl_display_init(Device*) { return ERROR_NONE; }
static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<SdlDisplayInternal*>(device_get_driver_data(device));
SDL_Rect rect = { x_start, y_start, x_end - x_start, y_end - y_start };
// RGB565 = 2 bytes/pixel.
if (SDL_UpdateTexture(internal->texture, &rect, color_data, (x_end - x_start) * 2) != 0) {
return ERROR_RESOURCE;
}
SDL_RenderClear(internal->renderer);
SDL_RenderCopy(internal->renderer, internal->texture, nullptr, nullptr);
SDL_RenderPresent(internal->renderer);
return ERROR_NONE;
}
static enum DisplayColorFormat sdl_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_RGB565;
}
static uint16_t sdl_display_get_resolution_x(Device* device) {
return GET_CONFIG(device)->horizontal_resolution;
}
static uint16_t sdl_display_get_resolution_y(Device* device) {
return GET_CONFIG(device)->vertical_resolution;
}
static void sdl_display_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
*out_buffer = nullptr;
}
static uint8_t sdl_display_get_frame_buffer_count(Device*) {
return 0;
}
// endregion
static const DisplayApi sdl_display_api = {
.capabilities = 0,
.reset = sdl_display_reset,
.init = sdl_display_init,
.draw_bitmap = sdl_display_draw_bitmap,
.mirror = nullptr,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = nullptr,
.get_mirror_y = nullptr,
.set_gap = nullptr,
.get_gap_x = nullptr,
.get_gap_y = nullptr,
.invert_color = nullptr,
.disp_on_off = nullptr,
.disp_sleep = nullptr,
.get_color_format = sdl_display_get_color_format,
.get_resolution_x = sdl_display_get_resolution_x,
.get_resolution_y = sdl_display_get_resolution_y,
.get_frame_buffer = sdl_display_get_frame_buffer,
.get_frame_buffer_count = sdl_display_get_frame_buffer_count,
.get_backlight = nullptr,
.has_capability = nullptr,
};
extern Module simulator_module;
Driver sdl_display_driver = {
.name = "sdl-display",
.compatible = (const char*[]) { "tactility,sdl-display", nullptr },
.start_device = start,
.stop_device = stop,
.api = &sdl_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &simulator_module,
.internal = nullptr
};

View File

@ -5,7 +5,12 @@
extern "C" {
#endif
extern struct Module hal_device_module;
#include <stdint.h>
struct SdlDisplayConfig {
uint16_t horizontal_resolution;
uint16_t vertical_resolution;
};
#ifdef __cplusplus
}

View File

@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0
#include "sdl_input.h"
#include <lvgl.h>
#include <SDL2/SDL.h>
#include <cstdlib>
namespace {
constexpr size_t KEY_QUEUE_CAPACITY = 32;
SdlPointerState pointer_state = { 0, 0, false };
uint32_t key_queue[KEY_QUEUE_CAPACITY];
size_t key_queue_head = 0;
size_t key_queue_count = 0;
bool text_input_started = false;
void push_key(uint32_t key) {
if (key == 0 || key_queue_count >= KEY_QUEUE_CAPACITY) {
return;
}
key_queue[(key_queue_head + key_queue_count) % KEY_QUEUE_CAPACITY] = key;
key_queue_count++;
}
// Mirrors LVGL's own lv_sdl_keyboard.c keycode_to_ctrl_key(): maps navigation/control keys to
// LV_KEY_* constants. Printable characters arrive separately via SDL_TEXTINPUT.
uint32_t keycode_to_key(SDL_Keycode sdl_key) {
switch (sdl_key) {
case SDLK_RIGHT: return LV_KEY_RIGHT;
case SDLK_LEFT: return LV_KEY_LEFT;
case SDLK_UP: return LV_KEY_UP;
case SDLK_DOWN: return LV_KEY_DOWN;
case SDLK_ESCAPE: return LV_KEY_ESC;
case SDLK_BACKSPACE: return LV_KEY_BACKSPACE;
case SDLK_DELETE: return LV_KEY_DEL;
case SDLK_RETURN:
case SDLK_KP_ENTER: return LV_KEY_ENTER;
case SDLK_TAB: return LV_KEY_NEXT;
case SDLK_HOME: return LV_KEY_HOME;
case SDLK_END: return LV_KEY_END;
default: return 0;
}
}
}
void sdl_input_pump() {
if (!text_input_started) {
SDL_StartTextInput();
text_input_started = true;
}
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_MOUSEMOTION:
pointer_state.x = event.motion.x;
pointer_state.y = event.motion.y;
break;
case SDL_MOUSEBUTTONDOWN:
if (event.button.button == SDL_BUTTON_LEFT) {
pointer_state.x = event.button.x;
pointer_state.y = event.button.y;
pointer_state.pressed = true;
}
break;
case SDL_MOUSEBUTTONUP:
if (event.button.button == SDL_BUTTON_LEFT) {
pointer_state.pressed = false;
}
break;
case SDL_KEYDOWN:
push_key(keycode_to_key(event.key.keysym.sym));
break;
case SDL_TEXTINPUT:
// ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard.
push_key(static_cast<uint8_t>(event.text.text[0]));
break;
case SDL_QUIT:
exit(0);
default:
break;
}
}
}
void sdl_input_get_pointer_state(SdlPointerState* out_state) {
*out_state = pointer_state;
}
bool sdl_input_pop_key(uint32_t* out_key) {
if (key_queue_count == 0) {
return false;
}
*out_key = key_queue[key_queue_head];
key_queue_head = (key_queue_head + 1) % KEY_QUEUE_CAPACITY;
key_queue_count--;
return true;
}
bool sdl_input_has_queued_key() {
return key_queue_count > 0;
}

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
/**
* @brief Latest pointer (mouse) state as tracked by sdl_input_pump().
*/
struct SdlPointerState {
int32_t x;
int32_t y;
bool pressed;
};
/**
* @brief Drains all pending SDL events exactly once, updating the pointer state and key queue
* below. Safe to call from both the sdl-pointer and sdl-keyboard drivers' polling functions:
* SDL_PollEvent() drains a single global queue, so whichever driver is polled first on a given
* LVGL indev tick pumps events for both.
*/
void sdl_input_pump(void);
/**
* @brief Gets the pointer state as of the most recent sdl_input_pump() call.
*/
void sdl_input_get_pointer_state(struct SdlPointerState* out_state);
/**
* @brief Pops the next queued key event (produced by SDL_KEYDOWN/SDL_TEXTINPUT during
* sdl_input_pump()).
* @retval false when no key event is pending
*/
bool sdl_input_pop_key(uint32_t* out_key);
/**
* @brief Returns true if another key event is queued after the one just popped.
*/
bool sdl_input_has_queued_key(void);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: Apache-2.0
#include "sdl_input.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/module.h>
// region Driver lifecycle
static error_t start(Device*) { return ERROR_NONE; }
static error_t stop(Device*) { return ERROR_NONE; }
// endregion
// region KeyboardApi
static error_t sdl_keyboard_read_key(Device*, KeyboardKeyData* data) {
sdl_input_pump();
uint32_t key = 0;
if (sdl_input_pop_key(&key)) {
data->key = key;
data->pressed = true;
data->continue_reading = sdl_input_has_queued_key();
} else {
data->key = 0;
data->pressed = false;
data->continue_reading = false;
}
return ERROR_NONE;
}
// endregion
static const KeyboardApi sdl_keyboard_api = {
.read_key = sdl_keyboard_read_key,
.get_backlight = nullptr,
};
extern Module simulator_module;
Driver sdl_keyboard_driver = {
.name = "sdl-keyboard",
.compatible = (const char*[]) { "tactility,sdl-keyboard", nullptr },
.start_device = start,
.stop_device = stop,
.api = &sdl_keyboard_api,
.device_type = &KEYBOARD_TYPE,
.owner = &simulator_module,
.internal = nullptr
};

View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
#include "sdl_input.h"
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/pointer.h>
#include <tactility/module.h>
// region Driver lifecycle
static error_t start(Device*) { return ERROR_NONE; }
static error_t stop(Device*) { return ERROR_NONE; }
// endregion
// region PointerApi
static error_t sdl_pointer_read_data(Device*, TickType_t) {
sdl_input_pump();
return ERROR_NONE;
}
static bool sdl_pointer_get_touched_points(Device*, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
SdlPointerState state;
sdl_input_get_pointer_state(&state);
if (!state.pressed || max_point_count == 0) {
*point_count = 0;
return false;
}
x[0] = static_cast<uint16_t>(state.x);
y[0] = static_cast<uint16_t>(state.y);
if (strength != nullptr) {
strength[0] = 0xFFFF;
}
*point_count = 1;
return true;
}
// endregion
static const PointerApi sdl_pointer_api = {
.enter_sleep = nullptr,
.exit_sleep = nullptr,
.read_data = sdl_pointer_read_data,
.get_touched_points = sdl_pointer_get_touched_points,
.set_swap_xy = nullptr,
.get_swap_xy = nullptr,
.set_mirror_x = nullptr,
.get_mirror_x = nullptr,
.set_mirror_y = nullptr,
.get_mirror_y = nullptr,
};
extern Module simulator_module;
Driver sdl_pointer_driver = {
.name = "sdl-pointer",
.compatible = (const char*[]) { "tactility,sdl-pointer", nullptr },
.start_device = start,
.stop_device = stop,
.api = &sdl_pointer_api,
.device_type = &POINTER_TYPE,
.owner = &simulator_module,
.internal = nullptr
};

View File

@ -1,42 +0,0 @@
#pragma once
#include "SdlTouch.h"
#include <tactility/check.h>
#include <Tactility/hal/display/DisplayDevice.h>
class SdlDisplay final : public tt::hal::display::DisplayDevice {
lv_disp_t* displayHandle = nullptr;
public:
std::string getName() const override { return "SDL Display"; }
std::string getDescription() const override { return ""; }
bool start() override { return true; }
bool stop() override { return true; }
bool supportsLvgl() const override { return true; }
bool startLvgl() override {
if (displayHandle) return true; // already started
displayHandle = lv_sdl_window_create(320, 240);
lv_sdl_window_set_title(displayHandle, "Tactility");
return displayHandle != nullptr;
}
bool stopLvgl() override {
if (!displayHandle) return true;
lv_display_delete(displayHandle);
displayHandle = nullptr;
return true;
}
lv_display_t* getLvglDisplay() const override { return displayHandle; }
std::shared_ptr<tt::hal::touch::TouchDevice> getTouchDevice() override { return std::make_shared<SdlTouch>(); }
bool supportsDisplayDriver() const override { return false; }
std::shared_ptr<tt::hal::display::DisplayDriver> getDisplayDriver() override { return nullptr; }
};

View File

@ -1,26 +0,0 @@
#pragma once
#include <Tactility/TactilityCore.h>
#include <tactility/check.h>
#include <Tactility/hal/keyboard/KeyboardDevice.h>
class SdlKeyboard final : public tt::hal::keyboard::KeyboardDevice {
lv_indev_t* handle = nullptr;
public:
std::string getName() const override { return "SDL Keyboard"; }
std::string getDescription() const override { return "SDL keyboard device"; }
bool startLvgl(lv_display_t* display) override {
handle = lv_sdl_keyboard_create();
return handle != nullptr;
}
bool stopLvgl() override { check(false, "Not supported"); }
bool isAttached() const override { return true; }
lv_indev_t* getLvglIndev() override { return handle; }
};

View File

@ -1,36 +0,0 @@
#pragma once
#include "Tactility/hal/touch/TouchDevice.h"
#include <Tactility/TactilityCore.h>
#include <tactility/check.h>
class SdlTouch final : public tt::hal::touch::TouchDevice {
lv_indev_t* handle = nullptr;
public:
std::string getName() const override { return "SDL Mouse"; }
std::string getDescription() const override { return "SDL mouse/touch pointer device"; }
bool start() override { return true; }
bool stop() override { check(false, "Not supported"); }
bool supportsLvgl() const override { return true; }
bool startLvgl(lv_display_t* display) override {
handle = lv_sdl_mouse_create();
return handle != nullptr;
}
bool stopLvgl() override { check(false, "Not supported"); }
lv_indev_t* getLvglIndev() override { return handle; }
bool supportsTouchDriver() override { return false; }
std::shared_ptr<tt::hal::touch::TouchDriver> getTouchDriver() override { return nullptr; };
};

View File

@ -1,36 +0,0 @@
#include "SimulatorPower.h"
constexpr auto* TAG = "SimulatorPower";
bool SimulatorPower::supportsMetric(MetricType type) const {
switch (type) {
using enum MetricType;
case IsCharging:
case Current:
case BatteryVoltage:
case ChargeLevel:
return true;
}
return false; // Safety guard for when new enum values are introduced
}
bool SimulatorPower::getMetric(MetricType type, MetricData& data) {
switch (type) {
using enum MetricType;
case IsCharging:
data.valueAsBool = true;
return true;
case Current:
data.valueAsInt32 = 42;
return true;
case BatteryVoltage:
data.valueAsUint32 = 4032;
return true;
case ChargeLevel:
data.valueAsUint8 = 100;
return true;
}
return false; // Safety guard for when new enum values are introduced
}

View File

@ -1,26 +0,0 @@
#pragma once
#include <Tactility/hal/power/PowerDevice.h>
#include <memory>
using tt::hal::power::PowerDevice;
class SimulatorPower final : public PowerDevice {
bool allowedToCharge = false;
public:
SimulatorPower() = default;
~SimulatorPower() override = default;
std::string getName() const override { return "Power Mock"; }
std::string getDescription() const override { return ""; }
bool supportsMetric(MetricType type) const override;
bool getMetric(MetricType type, MetricData& data) override;
bool supportsChargeControl() const override { return true; }
bool isAllowedToCharge() const override { return allowedToCharge; }
void setAllowedToCharge(bool canCharge) override { allowedToCharge = canCharge; }
};

View File

@ -1,23 +1,108 @@
#include "drivers/sdl_display.h"
#include <tactility/device.h>
#include <tactility/device_listener.h>
#include <tactility/driver.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <cstring>
constexpr auto* TAG = "Simulator";
extern "C" {
extern Driver sdl_display_driver;
extern Driver sdl_pointer_driver;
extern Driver sdl_keyboard_driver;
static Driver* const simulator_drivers[] = {
&sdl_display_driver,
&sdl_pointer_driver,
&sdl_keyboard_driver,
nullptr
};
}
// These devices have no real bus to attach to (SDL has no notion of one), but every non-root
// device is still expected to have a parent (see Device::parent) - they're parented to root once
// it's available below.
static const SdlDisplayConfig sdl_display_config = { 320, 240 };
static Device sdl_display_device {};
static Device sdl_pointer_device {};
static Device sdl_keyboard_device {};
static bool construct_add_start(Device* device, Device* parent, const char* name, const void* config, const char* compatible) {
device->address = 0;
device->name = name;
device->config = config;
device->parent = nullptr;
device->internal = nullptr;
error_t error = device_construct(device);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to construct %s: %s", name, error_to_string(error));
return false;
}
device_set_parent(device, parent);
Driver* driver = driver_find_compatible(compatible);
if (driver == nullptr) {
LOG_E(TAG, "No driver registered for %s", compatible);
device_destruct(device);
return false;
}
device_set_driver(device, driver);
if (device_add(device) != ERROR_NONE) {
LOG_E(TAG, "Failed to add %s", name);
device_destruct(device);
return false;
}
if (device_start(device) != ERROR_NONE) {
LOG_E(TAG, "Failed to start %s", name);
device_remove(device);
device_destruct(device);
return false;
}
return true;
}
// Root is only constructed/added/started after all dts_modules (including this one) have already
// started (see kernel_init()), so it can't be looked up by name from this module's own start() -
// wait for its DEVICE_EVENT_STARTED instead, same as e.g. m5stack-tab5's display/keyboard detection.
static void on_root_started(Device* device, DeviceEvent event, void* context) {
if (event != DEVICE_EVENT_STARTED || strcmp(device->name, "/") != 0) {
return;
}
construct_add_start(&sdl_display_device, device, "display0", &sdl_display_config, "tactility,sdl-display");
construct_add_start(&sdl_pointer_device, device, "pointer0", nullptr, "tactility,sdl-pointer");
construct_add_start(&sdl_keyboard_device, device, "keyboard0", nullptr, "tactility,sdl-keyboard");
}
extern "C" {
static error_t start() {
// Empty for now
device_listener_add(on_root_started, nullptr);
return ERROR_NONE;
}
static error_t stop() {
// Empty for now
device_listener_remove(on_root_started);
return ERROR_NONE;
}
struct Module simulator_module = {
Module simulator_module = {
.name = "simulator",
.start = start,
.stop = stop,
.symbols = nullptr,
.internal = nullptr
.drivers = simulator_drivers
};
}

View File

@ -77,7 +77,7 @@ cd buildsim && ctest # run all tests
- **TactilityKernel** — C API kernel: device/driver/module lifecycle, concurrency primitives (thread, mutex, timer, dispatcher), filesystem, logging. Header convention: `<tactility/*.h>` (lowercase snake_case).
- **TactilityFreeRtos** — Thin C++ wrappers around FreeRTOS primitives.
- **Tactility** — Main OS layer: app framework, service framework, HAL (deprecated, replaced by TactilityKernel), LVGL integration, networking and services (Wi-Fi, BLE, NTP, ESP-NOW), settings, i18n.
- **Tactility** — Main OS layer: app framework, service framework, LVGL integration, networking and services (Wi-Fi, BLE, NTP, ESP-NOW), settings, i18n.
- **TactilityC** — C bindings (`tt_*.h`) for Tactility, used by side-loaded ELF apps on ESP32. Deprecated, replaced by TactilityKernel.
- **Firmware** — Entry point (`app_main`).
@ -100,34 +100,28 @@ Apps implement `tt::app::App` (or just provide callbacks). Each app has an `AppM
Services implement `tt::service::Service` with a `ServiceManifest`. Services are long-running background processes (GUI, Wi-Fi, loader, statusbar, GPS, etc.).
### HAL Layer
#### Deprecated HAL
Located in Tactility folder.
`tt::hal::Configuration` is declared per-device board (in `Devices/<id>/Source/Configuration.cpp`). It provides `initBoot` for early hardware setup and `createDevices` to instantiate HAL device wrappers (display, touch, power, keyboard, etc.).
#### Current HAL
Located in TactilityKernel. Based on Linux driver subsystems.
### Hardware Abstraction Layer
#### Driver
A driver generally consists of:
- Registration of driver in parent module (optional)
- Registration of driver in parent module (optional, but desirable)
- YAML bindings in the `bindings/` folder
- An `#include` that is used in the `.dts` file. The include is in `[projectname]/bindings/[drivername].h`
- The driver implementation: a `.cpp` and `.h` file. The implementation is C++, but the header exposes pure C functions.
- The driver implementation: a `.cpp` and `.h` file. The implementation is C++, but the header exposes pure C functions. C implementations are allowed, but C++ is preferred.
Drivers can be stored in:
Drivers are part of a kernel module.
Modules with drivers can be stored in:
- TactilityKernel
- A subproject in Platforms/ folder
- A subproject in Devices/ folder
- A subproject in Drivers/ folder. This is a kernel module. Naming is lower case and postfixed with `-module`
- A subproject in `Platforms` folder
- A subproject in `Devices` folder
- A subproject in `Drivers` folder
#### Kernel Modules
Kernel module names are lower case and postfixed with `-module`.
Projects that are kernel modules:
1. Declare a `struct Module`
@ -169,6 +163,6 @@ Pointers are expected to be non-null unless documented otherwise.
- `#ifdef ESP_PLATFORM` guards ESP32-specific code; the simulator uses POSIX equivalents.
- The `Drivers/` directory contains hardware drivers (display controllers, touch controllers, PMICs, etc.) — each is its own CMake component.
- `Modules/` contains cross-cutting modules: `hal-device-module` (device lifecycle) and `lvgl-module` (LVGL task management).
- `Modules/` contains cross-cutting modules. e.g.`lvgl-module` (LVGL task management).
- `Data/system/` and `Data/data/` are flashed as FAT filesystem images on ESP32.
- Translations are in `Translations/` as CSV files, generated via `generate.py`.

View File

@ -13,6 +13,7 @@
## Higher Priority
- Remove and migrate `Include/Tactility/kernel/Kernel.h` into `tactility/delay.h`
- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module.
- LilyGO T-Dongle S3: 1 button control, stop auto-launching web server
- Core2: support power off via software

View File

@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(gps-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel minmea
)

View File

@ -0,0 +1,2 @@
dependencies:
- TactilityKernel

View File

@ -0,0 +1,112 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <tactility/device.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <minmea.h>
/**
* @brief Supported GPS/GNSS receiver chipsets.
*/
enum GpsModel {
GPS_MODEL_UNKNOWN = 0,
GPS_MODEL_AG3335,
GPS_MODEL_AG3352,
/** Casic - might work with AT6558, Neoway N58 LTE Cat.1, Neoway G2, Neoway G7A */
GPS_MODEL_ATGM336H,
GPS_MODEL_LS20031,
GPS_MODEL_MTK,
GPS_MODEL_MTK_L76B,
GPS_MODEL_MTK_PA1616S,
GPS_MODEL_UBLOX6,
GPS_MODEL_UBLOX7,
GPS_MODEL_UBLOX8,
GPS_MODEL_UBLOX9,
GPS_MODEL_UBLOX10,
GPS_MODEL_UC6580,
};
/** @return a human-readable name for the model, e.g. "UBLOX8" or "Unknown" */
const char* gps_model_to_string(enum GpsModel model);
/**
* @brief Lifecycle state of a GPS_TYPE device.
*/
enum GpsState {
GPS_STATE_OFF,
GPS_STATE_PENDING_ON,
GPS_STATE_ON,
GPS_STATE_ERROR,
GPS_STATE_PENDING_OFF,
};
/**
* @brief Configuration for a GPS_TYPE device.
* @warning Set device_set_parent() to the UART_CONTROLLER_TYPE device this receiver is wired to
* before starting - the driver reads/writes through its parent.
*/
struct GpsConfig {
uint32_t baud_rate;
/** GPS_MODEL_UNKNOWN triggers an autoprobe on start(); the detected model is then available via get_model(). */
enum GpsModel model;
};
/**
* @brief API for GPS/GNSS receiver drivers.
*/
struct GpsApi {
/**
* @brief Gets the most recently parsed RMC (position/velocity/time) sentence.
* @param[in] device the GPS device
* @param[out] out the parsed sentence
* @param[in] max_age the maximum acceptable age of the cached sentence
* @retval ERROR_NONE when a sentence younger than max_age was copied into out
* @retval ERROR_NOT_FOUND when no RMC sentence has ever been parsed
* @retval ERROR_TIMEOUT when the cached sentence is older than max_age
*/
error_t (*get_rmc)(struct Device* device, struct minmea_sentence_rmc* out, TickType_t max_age);
/**
* @brief Gets the most recently parsed GGA (fix data) sentence.
* @see GpsApi::get_rmc
*/
error_t (*get_gga)(struct Device* device, struct minmea_sentence_gga* out, TickType_t max_age);
/**
* @brief Gets the model in use - the autodetected model when configured with GPS_MODEL_UNKNOWN.
* @param[in] device the GPS device
*/
enum GpsModel (*get_model)(struct Device* device);
/**
* @brief Gets the current lifecycle state.
* @param[in] device the GPS device
*/
enum GpsState (*get_state)(struct Device* device);
};
/** @copydoc GpsApi::get_rmc */
error_t gps_get_rmc(struct Device* device, struct minmea_sentence_rmc* out, TickType_t max_age);
/** @copydoc GpsApi::get_gga */
error_t gps_get_gga(struct Device* device, struct minmea_sentence_gga* out, TickType_t max_age);
/** @copydoc GpsApi::get_model */
enum GpsModel gps_get_model(struct Device* device);
/** @copydoc GpsApi::get_state */
enum GpsState gps_get_state(struct Device* device);
extern const struct DeviceType GPS_TYPE;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <tactility/drivers/gps.h>
#include <tactility/error.h>
#include <minmea.h>
struct Module;
#define GPS_SERVICE_ID "gps"
/**
* @brief Aggregate receive state of the GPS service (across all configured receivers).
*/
enum GpsServiceState {
GPS_SERVICE_STATE_ON_PENDING,
GPS_SERVICE_STATE_ON,
GPS_SERVICE_STATE_OFF_PENDING,
GPS_SERVICE_STATE_OFF,
};
/**
* @brief A persisted GPS receiver configuration.
*/
struct GpsConfiguration {
/** UART controller device name, e.g. "uart0" - resolved via device_get_by_name(). */
char uart_name[32];
uint32_t baud_rate;
/** GPS_MODEL_UNKNOWN triggers an autoprobe. */
enum GpsModel model;
};
/**
* @brief Persists a new GPS configuration.
* @retval ERROR_RESOURCE if the configuration file could not be opened/written
*/
error_t gps_service_add_configuration(const struct GpsConfiguration* configuration);
/**
* @brief Removes a persisted GPS configuration that matches by value.
* @retval ERROR_NOT_FOUND if no matching configuration was found
* @retval ERROR_RESOURCE if the configuration file could not be read/written
*/
error_t gps_service_remove_configuration(const struct GpsConfiguration* configuration);
/**
* @brief Iterates over all persisted GPS configurations.
* @param[in] context passed through to on_configuration, can be NULL
* @param[in] on_configuration called once per configuration, in file order, with its index
*/
void gps_service_for_each_configuration(void* context, void (*on_configuration)(const struct GpsConfiguration* configuration, size_t index, void* context));
/**
* @brief Iterates over the GPS_TYPE devices currently constructed by gps_service_start_receiving().
*/
void gps_service_for_each_device(void* context, void (*on_device)(struct Device* device, void* context));
/**
* @brief Constructs and starts a GPS_TYPE device for every persisted configuration and begins receiving.
* @retval ERROR_INVALID_STATE if already receiving
* @retval ERROR_NOT_FOUND if there are no persisted configurations, or none of their UART devices could be found
*/
error_t gps_service_start_receiving(void);
/** Stops and destroys every GPS_TYPE device constructed by gps_service_start_receiving(). */
void gps_service_stop_receiving(void);
enum GpsServiceState gps_service_get_state(void);
/** @return true when a coordinate fix is available and is not older than 10 seconds */
bool gps_service_has_coordinates(void);
/** @copydoc gps_service_has_coordinates */
bool gps_service_get_coordinates(struct minmea_sentence_rmc* out);
/** @return true when GGA fix data is available and is not older than 10 seconds */
bool gps_service_get_gga(struct minmea_sentence_gga* out);
extern struct Module gps_module;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,327 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/gps.h>
#include "init.h"
#include "probe.h"
#include <tactility/check.h>
#include <tactility/concurrent/recursive_mutex.h>
#include <tactility/concurrent/thread.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <tactility/time.h>
#include <minmea.h>
#include <cstdlib>
constexpr auto* TAG = "Gps";
#define GET_CONFIG(device) (static_cast<const GpsConfig*>((device)->config))
constexpr uint32_t GPS_UART_BUFFER_SIZE = 256;
constexpr TickType_t GPS_THREAD_STOP_TIMEOUT_TICKS = pdMS_TO_TICKS(5000);
constexpr TickType_t GPS_THREAD_STOP_POLL_TICKS = pdMS_TO_TICKS(10);
struct GpsInternal {
RecursiveMutex mutex;
Thread* thread;
volatile bool interrupt_requested;
GpsState state;
// Mirrors GpsConfig::model, but overwritten with the autodetected model once probing succeeds.
GpsModel model;
bool has_rmc;
minmea_sentence_rmc rmc;
TickType_t rmc_time;
bool has_gga;
minmea_sentence_gga gga;
TickType_t gga_time;
};
static void set_state(GpsInternal* internal, GpsState state) {
recursive_mutex_lock(&internal->mutex);
internal->state = state;
recursive_mutex_unlock(&internal->mutex);
}
static bool is_interrupted(GpsInternal* internal) {
recursive_mutex_lock(&internal->mutex);
bool result = internal->interrupt_requested;
recursive_mutex_unlock(&internal->mutex);
return result;
}
// region Driver lifecycle
static int32_t gps_thread_main(void* context) {
auto* device = static_cast<Device*>(context);
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
auto* uart = device_get_parent(device);
check(device_get_type(uart) == &UART_CONTROLLER_TYPE);
const auto* config = GET_CONFIG(device);
UartConfig uart_config = {
.baud_rate = config->baud_rate,
.data_bits = UART_CONTROLLER_DATA_8_BITS,
.parity = UART_CONTROLLER_PARITY_DISABLE,
.stop_bits = UART_CONTROLLER_STOP_BITS_1
};
if (uart_controller_set_config(uart, &uart_config) != ERROR_NONE) {
LOG_E(TAG, "Failed to configure UART %s", uart->name);
set_state(internal, GpsState::GPS_STATE_ERROR);
return -1;
}
if (uart_controller_open(uart) != ERROR_NONE) {
LOG_E(TAG, "Failed to open UART %s", uart->name);
set_state(internal, GpsState::GPS_STATE_ERROR);
return -1;
}
GpsModel model = internal->model;
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
model = gps_probe(uart);
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
LOG_E(TAG, "Probe failed");
set_state(internal, GpsState::GPS_STATE_ERROR);
return -1;
}
recursive_mutex_lock(&internal->mutex);
internal->model = model;
recursive_mutex_unlock(&internal->mutex);
}
if (!gps_init(uart, model)) {
LOG_E(TAG, "Init failed");
set_state(internal, GpsState::GPS_STATE_ERROR);
return -1;
}
set_state(internal, GpsState::GPS_STATE_ON);
// Reference: https://gpsd.gitlab.io/gpsd/NMEA.html
uint8_t buffer[GPS_UART_BUFFER_SIZE];
while (!is_interrupted(internal)) {
size_t bytes_read = 0;
uart_controller_read_until(uart, buffer, sizeof(buffer), '\n', true, &bytes_read, pdMS_TO_TICKS(100));
// Thread might've been interrupted in the meanwhile
if (is_interrupted(internal)) {
break;
}
if (bytes_read > 0U) {
switch (minmea_sentence_id((char*)buffer, false)) {
case MINMEA_SENTENCE_RMC: {
minmea_sentence_rmc rmc_frame;
if (minmea_parse_rmc(&rmc_frame, (char*)buffer)) {
recursive_mutex_lock(&internal->mutex);
internal->has_rmc = true;
internal->rmc = rmc_frame;
internal->rmc_time = get_ticks();
recursive_mutex_unlock(&internal->mutex);
} else {
LOG_E(TAG, "RMC parse error: %s", reinterpret_cast<const char*>(buffer));
}
break;
}
case MINMEA_SENTENCE_GGA: {
minmea_sentence_gga gga_frame;
if (minmea_parse_gga(&gga_frame, (char*)buffer)) {
recursive_mutex_lock(&internal->mutex);
internal->has_gga = true;
internal->gga = gga_frame;
internal->gga_time = get_ticks();
recursive_mutex_unlock(&internal->mutex);
} else {
LOG_E(TAG, "GGA parse error: %s", reinterpret_cast<const char*>(buffer));
}
break;
}
default:
break;
}
}
}
if (uart_controller_close(uart) != ERROR_NONE) {
LOG_W(TAG, "Failed to close UART %s", uart->name);
}
set_state(internal, GpsState::GPS_STATE_OFF);
return 0;
}
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<GpsInternal*>(calloc(1, sizeof(GpsInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
recursive_mutex_construct(&internal->mutex);
internal->model = config->model;
internal->state = GpsState::GPS_STATE_PENDING_ON;
internal->thread = thread_alloc_full("gps", 4096, gps_thread_main, device, -1);
if (internal->thread == nullptr) {
recursive_mutex_destruct(&internal->mutex);
free(internal);
return ERROR_OUT_OF_MEMORY;
}
thread_set_priority(internal->thread, THREAD_PRIORITY_HIGH);
device_set_driver_data(device, internal);
if (thread_start(internal->thread) != ERROR_NONE) {
thread_free(internal->thread);
recursive_mutex_destruct(&internal->mutex);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_RESOURCE;
}
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
recursive_mutex_lock(&internal->mutex);
internal->interrupt_requested = true;
internal->state = GpsState::GPS_STATE_PENDING_OFF;
recursive_mutex_unlock(&internal->mutex);
if (thread_join(internal->thread, GPS_THREAD_STOP_TIMEOUT_TICKS, GPS_THREAD_STOP_POLL_TICKS) != ERROR_NONE) {
LOG_W(TAG, "GPS thread for %s did not stop in time", device->name);
}
thread_free(internal->thread);
recursive_mutex_destruct(&internal->mutex);
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
// region GpsApi
static error_t gps_api_get_rmc(Device* device, minmea_sentence_rmc* out, TickType_t max_age) {
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
recursive_mutex_lock(&internal->mutex);
error_t result;
if (!internal->has_rmc) {
result = ERROR_NOT_FOUND;
} else if (get_ticks() - internal->rmc_time > max_age) {
result = ERROR_TIMEOUT;
} else {
*out = internal->rmc;
result = ERROR_NONE;
}
recursive_mutex_unlock(&internal->mutex);
return result;
}
static error_t gps_api_get_gga(Device* device, minmea_sentence_gga* out, TickType_t max_age) {
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
recursive_mutex_lock(&internal->mutex);
error_t result;
if (!internal->has_gga) {
result = ERROR_NOT_FOUND;
} else if (get_ticks() - internal->gga_time > max_age) {
result = ERROR_TIMEOUT;
} else {
*out = internal->gga;
result = ERROR_NONE;
}
recursive_mutex_unlock(&internal->mutex);
return result;
}
static GpsModel gps_api_get_model(Device* device) {
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
recursive_mutex_lock(&internal->mutex);
auto model = internal->model;
recursive_mutex_unlock(&internal->mutex);
return model;
}
static GpsState gps_api_get_state(Device* device) {
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
recursive_mutex_lock(&internal->mutex);
auto state = internal->state;
recursive_mutex_unlock(&internal->mutex);
return state;
}
// endregion
const char* gps_model_to_string(enum GpsModel model) {
switch (model) {
case GPS_MODEL_AG3335: return "AG3335";
case GPS_MODEL_AG3352: return "AG3352";
case GPS_MODEL_ATGM336H: return "ATGM336H";
case GPS_MODEL_LS20031: return "LS20031";
case GPS_MODEL_MTK: return "MTK";
case GPS_MODEL_MTK_L76B: return "MTK_L76B";
case GPS_MODEL_MTK_PA1616S: return "MTK_PA1616S";
case GPS_MODEL_UBLOX6: return "UBLOX6";
case GPS_MODEL_UBLOX7: return "UBLOX7";
case GPS_MODEL_UBLOX8: return "UBLOX8";
case GPS_MODEL_UBLOX9: return "UBLOX9";
case GPS_MODEL_UBLOX10: return "UBLOX10";
case GPS_MODEL_UC6580: return "UC6580";
default: return "Unknown";
}
}
error_t gps_get_rmc(Device* device, minmea_sentence_rmc* out, TickType_t max_age) {
const auto* driver = device_get_driver(device);
return static_cast<const GpsApi*>(driver->api)->get_rmc(device, out, max_age);
}
error_t gps_get_gga(Device* device, minmea_sentence_gga* out, TickType_t max_age) {
const auto* driver = device_get_driver(device);
return static_cast<const GpsApi*>(driver->api)->get_gga(device, out, max_age);
}
enum GpsModel gps_get_model(Device* device) {
const auto* driver = device_get_driver(device);
return static_cast<const GpsApi*>(driver->api)->get_model(device);
}
enum GpsState gps_get_state(Device* device) {
const auto* driver = device_get_driver(device);
return static_cast<const GpsApi*>(driver->api)->get_state(device);
}
const DeviceType GPS_TYPE {
.name = "gps"
};
static const GpsApi gps_api = {
.get_rmc = gps_api_get_rmc,
.get_gga = gps_api_get_gga,
.get_model = gps_api_get_model,
.get_state = gps_api_get_state,
};
extern Module gps_module;
Driver gps_driver = {
.name = "gps",
.compatible = (const char*[]) { "generic,gps", nullptr },
.start_device = start,
.stop_device = stop,
.api = &gps_api,
.device_type = &GPS_TYPE,
.owner = &gps_module
};

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
// Internal-only result of waiting for a chip's ACK/NACK response during probing/initialization.
// Not part of the public API (see tactility/drivers/gps.h) - callers only ever see GpsState/GpsModel.
enum class GpsResponse {
None,
NotAck,
FrameErrors,
Ok,
};

View File

@ -0,0 +1,398 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/gps_service.h>
#include "gps_service_internal.h"
#include <tactility/concurrent/recursive_mutex.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/gps.h>
#include <tactility/log.h>
#include <tactility/service/service_instance.h>
#include <tactility/service/service_manager.h>
#include <tactility/service/service_paths.h>
#include <tactility/time.h>
#include <sys/stat.h>
#include <cstdio>
#include <cstring>
#include <new>
#include <vector>
constexpr auto* TAG = "GpsService";
// A dynamically-constructed GPS_TYPE device, one per active GpsConfiguration. Device is the first
// member so `reinterpret_cast<GpsDeviceEntry*>(device)` is never needed - callers just keep the
// Device* and, once done with it, `delete` via a GpsDeviceEntry* they already have.
struct GpsDeviceEntry {
Device device {};
GpsConfig config {};
};
struct GpsServiceData {
RecursiveMutex mutex {};
std::vector<GpsDeviceEntry*> devices;
GpsServiceState state = GPS_SERVICE_STATE_OFF;
};
static GpsServiceData* get_data() {
auto* instance = service_manager_find_instance(GPS_SERVICE_ID);
if (instance == nullptr) {
return nullptr;
}
return static_cast<GpsServiceData*>(service_instance_get_data(instance));
}
static void set_state(GpsServiceData* data, GpsServiceState state) {
recursive_mutex_lock(&data->mutex);
data->state = state;
recursive_mutex_unlock(&data->mutex);
}
// region Configuration persistence
// Recursively creates every missing directory component of `path` (best-effort - mkdir() failures
// other than "already exists" are surfaced later, when the actual config file open fails).
static void ensure_directory_exists(const char* path) {
char buffer[224];
std::strncpy(buffer, path, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
for (char* p = buffer + 1; *p != '\0'; p++) {
if (*p == '/') {
*p = '\0';
mkdir(buffer, 0777);
*p = '/';
}
}
mkdir(buffer, 0777);
}
static bool get_configuration_path(char* out_path, size_t out_path_size) {
return service_paths_get_user_data_path(GPS_SERVICE_ID, "config.bin", out_path, out_path_size) == ERROR_NONE;
}
void gps_service_for_each_configuration(void* context, void (*on_configuration)(const GpsConfiguration* configuration, size_t index, void* context)) {
char path[224];
if (!get_configuration_path(path, sizeof(path))) {
return;
}
FILE* file = fopen(path, "rb");
if (file == nullptr) {
return; // No configurations saved yet
}
GpsConfiguration configuration;
size_t index = 0;
while (fread(&configuration, sizeof(configuration), 1, file) == 1) {
on_configuration(&configuration, index, context);
index++;
}
fclose(file);
}
static void collect_configuration(const GpsConfiguration* configuration, size_t, void* context) {
static_cast<std::vector<GpsConfiguration>*>(context)->push_back(*configuration);
}
static void load_configurations(std::vector<GpsConfiguration>& out) {
gps_service_for_each_configuration(&out, collect_configuration);
}
static error_t write_configurations(const std::vector<GpsConfiguration>& configurations) {
char directory[224];
if (service_paths_get_user_data_directory(GPS_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE) {
return ERROR_RESOURCE;
}
ensure_directory_exists(directory);
char path[256];
if (!get_configuration_path(path, sizeof(path))) {
return ERROR_RESOURCE;
}
FILE* file = fopen(path, "wb");
if (file == nullptr) {
LOG_E(TAG, "Failed to open %s for writing", path);
return ERROR_RESOURCE;
}
bool ok = true;
for (auto& configuration : configurations) {
if (fwrite(&configuration, sizeof(configuration), 1, file) != 1) {
ok = false;
break;
}
}
fclose(file);
return ok ? ERROR_NONE : ERROR_RESOURCE;
}
static bool configurations_equal(const GpsConfiguration& a, const GpsConfiguration& b) {
return strcmp(a.uart_name, b.uart_name) == 0 &&
a.baud_rate == b.baud_rate &&
a.model == b.model;
}
error_t gps_service_add_configuration(const GpsConfiguration* configuration) {
std::vector<GpsConfiguration> configurations;
load_configurations(configurations);
configurations.push_back(*configuration);
return write_configurations(configurations);
}
error_t gps_service_remove_configuration(const GpsConfiguration* configuration) {
std::vector<GpsConfiguration> configurations;
load_configurations(configurations);
size_t original_size = configurations.size();
std::erase_if(configurations, [configuration](const GpsConfiguration& item) {
return configurations_equal(item, *configuration);
});
if (configurations.size() == original_size) {
return ERROR_NOT_FOUND;
}
return write_configurations(configurations);
}
// endregion
// region Receiving
static bool construct_add_start(Device* device, Device* parent, const char* name, const void* config, const char* compatible) {
device->address = 0;
device->name = name;
device->config = config;
device->parent = nullptr;
device->internal = nullptr;
if (device_construct(device) != ERROR_NONE) {
LOG_E(TAG, "Failed to construct %s", name);
return false;
}
device_set_parent(device, parent);
Driver* driver = driver_find_compatible(compatible);
if (driver == nullptr) {
LOG_E(TAG, "No driver registered for %s", compatible);
device_destruct(device);
return false;
}
device_set_driver(device, driver);
if (device_add(device) != ERROR_NONE) {
LOG_E(TAG, "Failed to add %s", name);
device_destruct(device);
return false;
}
if (device_start(device) != ERROR_NONE) {
LOG_E(TAG, "Failed to start %s", name);
device_remove(device);
device_destruct(device);
return false;
}
return true;
}
error_t gps_service_start_receiving() {
auto* data = get_data();
if (data == nullptr) {
return ERROR_INVALID_STATE;
}
recursive_mutex_lock(&data->mutex);
if (data->state != GpsServiceState::GPS_SERVICE_STATE_OFF) {
recursive_mutex_unlock(&data->mutex);
return ERROR_INVALID_STATE;
}
data->state = GpsServiceState::GPS_SERVICE_STATE_ON_PENDING;
recursive_mutex_unlock(&data->mutex);
std::vector<GpsConfiguration> configurations;
load_configurations(configurations);
if (configurations.empty()) {
LOG_E(TAG, "No GPS configurations");
set_state(data, GpsServiceState::GPS_SERVICE_STATE_OFF);
return ERROR_NOT_FOUND;
}
static uint32_t next_device_index = 0;
bool started_one_or_more = false;
for (auto& configuration : configurations) {
Device* uart = nullptr;
if (device_get_by_name(configuration.uart_name, &uart) != ERROR_NONE) {
LOG_E(TAG, "Failed to find device %s", configuration.uart_name);
continue;
}
auto* entry = new(std::nothrow) GpsDeviceEntry();
if (entry == nullptr) {
device_put(uart);
continue;
}
entry->config = GpsConfig { .baud_rate = configuration.baud_rate, .model = configuration.model };
char name[16];
snprintf(name, sizeof(name), "gps%u", (unsigned)next_device_index++);
bool started = construct_add_start(&entry->device, uart, name, &entry->config, "generic,gps");
device_put(uart);
if (started) {
recursive_mutex_lock(&data->mutex);
data->devices.push_back(entry);
recursive_mutex_unlock(&data->mutex);
started_one_or_more = true;
} else {
delete entry;
}
}
if (!started_one_or_more) {
set_state(data, GPS_SERVICE_STATE_OFF);
return ERROR_RESOURCE;
}
set_state(data, GPS_SERVICE_STATE_ON);
return ERROR_NONE;
}
void gps_service_stop_receiving() {
auto* data = get_data();
if (data == nullptr) {
return;
}
recursive_mutex_lock(&data->mutex);
if (data->state != GPS_SERVICE_STATE_ON) {
recursive_mutex_unlock(&data->mutex);
return;
}
data->state = GPS_SERVICE_STATE_OFF_PENDING;
for (auto* entry : data->devices) {
device_stop(&entry->device);
device_remove(&entry->device);
device_destruct(&entry->device);
delete entry;
}
data->devices.clear();
data->state = GPS_SERVICE_STATE_OFF;
recursive_mutex_unlock(&data->mutex);
}
void gps_service_for_each_device(void* context, void (*on_device)(Device* device, void* context)) {
auto* data = get_data();
if (data == nullptr) {
return;
}
recursive_mutex_lock(&data->mutex);
for (auto* entry : data->devices) {
on_device(&entry->device, context);
}
recursive_mutex_unlock(&data->mutex);
}
GpsServiceState gps_service_get_state() {
auto* data = get_data();
if (data == nullptr) {
return GPS_SERVICE_STATE_OFF;
}
recursive_mutex_lock(&data->mutex);
auto state = data->state;
recursive_mutex_unlock(&data->mutex);
return state;
}
bool gps_service_get_coordinates(minmea_sentence_rmc* out) {
auto* data = get_data();
if (data == nullptr) {
return false;
}
recursive_mutex_lock(&data->mutex);
bool found = false;
for (auto* entry : data->devices) {
if (gps_get_rmc(&entry->device, out, seconds_to_ticks(10)) == ERROR_NONE) {
found = true;
break;
}
}
recursive_mutex_unlock(&data->mutex);
return found;
}
bool gps_service_has_coordinates() {
minmea_sentence_rmc rmc;
return gps_service_get_coordinates(&rmc);
}
bool gps_service_get_gga(minmea_sentence_gga* out) {
auto* data = get_data();
if (data == nullptr) {
return false;
}
recursive_mutex_lock(&data->mutex);
bool found = false;
for (auto* entry : data->devices) {
if (gps_get_gga(&entry->device, out, seconds_to_ticks(10)) == ERROR_NONE) {
found = true;
break;
}
}
recursive_mutex_unlock(&data->mutex);
return found;
}
// endregion
// region ServiceManifest
static void* create_service(const ServiceManifest*) {
auto* data = new(std::nothrow) GpsServiceData();
if (data == nullptr) {
return nullptr;
}
recursive_mutex_construct(&data->mutex);
return data;
}
static void destroy_service(const ServiceManifest*, void* data) {
auto* service_data = static_cast<GpsServiceData*>(data);
recursive_mutex_destruct(&service_data->mutex);
delete service_data;
}
static void on_stop(ServiceInstance*, void* data) {
auto* service_data = static_cast<GpsServiceData*>(data);
if (service_data->state != GpsServiceState::GPS_SERVICE_STATE_OFF) {
gps_service_stop_receiving();
}
}
static const ServiceManifest gps_service_manifest = {
.id = GPS_SERVICE_ID,
.create_service = create_service,
.destroy_service = destroy_service,
.on_start = nullptr,
.on_stop = on_stop,
};
error_t gps_service_register() {
return service_manager_add(&gps_service_manifest, true);
}
// endregion

View File

@ -0,0 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
// Registers the GPS service manifest with the kernel service manager (auto-started). Called once
// from module.cpp's Module::start().
error_t gps_service_register();

View File

@ -1,31 +1,31 @@
#include <Tactility/hal/gps/Cas.h>
#include <Tactility/hal/gps/GpsDevice.h>
#include <Tactility/hal/gps/Ublox.h>
#include <Tactility/kernel/Kernel.h>
// SPDX-License-Identifier: Apache-2.0
#include "init.h"
#include "cas_messages.h"
#include "gps_response.h"
#include "ublox.h"
#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <cstring>
namespace tt::hal::gps {
constexpr auto* TAG = "Gps";
bool initMtk(::Device* uart);
bool initMtkL76b(::Device* uart);
bool initMtkPa1616s(::Device* uart);
bool initAtgm336h(::Device* uart);
bool initUc6580(::Device* uart);
bool initAg33xx(::Device* uart);
bool init_mtk(Device* uart);
bool init_mtk_l76b(Device* uart);
bool init_mtk_pa1616s(Device* uart);
bool init_atgm336h(Device* uart);
bool init_uc6580(Device* uart);
bool init_ag33xx(Device* uart);
// region CAS
// Calculate the checksum for a CAS packet
static void CASChecksum(uint8_t *message, size_t length)
{
static void cas_checksum(uint8_t* message, size_t length) {
uint32_t cksum = ((uint32_t)message[5] << 24); // Message ID
cksum += ((uint32_t)message[4]) << 16; // Class
cksum += message[2]; // Payload Len
@ -46,8 +46,7 @@ static void CASChecksum(uint8_t *message, size_t length)
}
// Function to create a CAS packet for editing in memory
static uint8_t makeCASPacket(uint8_t* buffer, uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t *msg)
{
static uint8_t make_cas_packet(uint8_t* buffer, uint8_t class_id, uint8_t msg_id, uint8_t payload_size, const uint8_t* msg) {
// General CAS structure
// | H1 | H2 | payload_len | cls | msg | Payload ... | Checksum |
// Size: | 1 | 1 | 2 | 1 | 1 | payload_len | 4 |
@ -71,17 +70,16 @@ static uint8_t makeCASPacket(uint8_t* buffer, uint8_t class_id, uint8_t msg_id,
for (int i = 0; i < payload_size; i++) {
buffer[6 + i] = msg[i];
}
CASChecksum(buffer, (payload_size + 10));
cas_checksum(buffer, (payload_size + 10));
return (payload_size + 10);
}
GpsResponse getACKCas(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t waitMillis)
{
uint32_t startTime = kernel::getMillis();
static GpsResponse get_ack_cas(Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wait_millis) {
uint32_t start_time = get_millis();
uint8_t buffer[CAS_ACK_NACK_MSG_SIZE] = {0};
uint8_t bufferPos = 0;
TickType_t waitTicks = pdMS_TO_TICKS(waitMillis);
uint8_t buffer_pos = 0;
TickType_t wait_ticks = pdMS_TO_TICKS(wait_millis);
// CAS-ACK-(N)ACK structure
// | H1 | H2 | Payload Len | cls | msg | Payload | Checksum (4) |
@ -90,26 +88,26 @@ GpsResponse getACKCas(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t
// ACK-NACK| 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x00 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX |
// ACK-ACK | 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x01 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX |
while (kernel::getTicks() - startTime < waitTicks) {
while (get_ticks() - start_time < wait_ticks) {
size_t available = 0;
uart_controller_get_available(uart, &available);
if (available > 0) {
uart_controller_read_byte(uart, &buffer[bufferPos++], 1);
uart_controller_read_byte(uart, &buffer[buffer_pos++], 1);
// keep looking at the first two bytes of buffer until
// we have found the CAS frame header (0xBA, 0xCE), if not
// keep reading bytes until we find a frame header or we run
// out of time.
if ((bufferPos == 2) && !(buffer[0] == 0xBA && buffer[1] == 0xCE)) {
if ((buffer_pos == 2) && !(buffer[0] == 0xBA && buffer[1] == 0xCE)) {
buffer[0] = buffer[1];
buffer[1] = 0;
bufferPos = 1;
buffer_pos = 1;
}
}
// we have read all the bytes required for the Ack/Nack (14-bytes)
// and we must have found a frame to get this far
if (bufferPos == sizeof(buffer) - 1) {
if (buffer_pos == sizeof(buffer) - 1) {
uint8_t msg_cls = buffer[4]; // message class should be 0x05
uint8_t msg_msg_id = buffer[5]; // message id should be 0x00 or 0x01
uint8_t payload_cls = buffer[6]; // payload class id
@ -117,24 +115,18 @@ GpsResponse getACKCas(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t
// Check for an ACK-ACK for the specified class and message id
if ((msg_cls == 0x05) && (msg_msg_id == 0x01) && payload_cls == class_id && payload_msg == msg_id) {
#ifdef GPS_DEBUG
LOG_I(TAG, "Got ACK for class %02X message %02X in %zu ms", class_id, msg_id, kernel::getMillis() - startTime);
#endif
return GpsResponse::Ok;
}
// Check for an ACK-NACK for the specified class and message id
if ((msg_cls == 0x05) && (msg_msg_id == 0x00) && payload_cls == class_id && payload_msg == msg_id) {
#ifdef GPS_DEBUG
LOG_W(TAG, "Got NACK for class %02X message %02X in %zu ms", class_id, msg_id, millis() - startTime);
#endif
return GpsResponse::NotAck;
}
// This isn't the frame we are looking for, clear the buffer
// and try again until we run out of time.
memset(buffer, 0x0, sizeof(buffer));
bufferPos = 0;
buffer_pos = 0;
}
}
return GpsResponse::None;
@ -142,38 +134,38 @@ GpsResponse getACKCas(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t
// endregion
bool init(::Device* uart, GpsModel type) {
bool gps_init(Device* uart, GpsModel type) {
switch (type) {
case GpsModel::Unknown:
case GPS_MODEL_UNKNOWN:
check(false);
case GpsModel::AG3335:
case GpsModel::AG3352:
return initAg33xx(uart);
case GpsModel::ATGM336H:
return initAtgm336h(uart);
case GpsModel::LS20031:
case GPS_MODEL_AG3335:
case GPS_MODEL_AG3352:
return init_ag33xx(uart);
case GPS_MODEL_ATGM336H:
return init_atgm336h(uart);
case GPS_MODEL_LS20031:
return true;
case GpsModel::MTK:
return initMtk(uart);
case GpsModel::MTK_L76B:
return initMtkL76b(uart);
case GpsModel::MTK_PA1616S:
return initMtkPa1616s(uart);
case GpsModel::UBLOX6:
case GpsModel::UBLOX7:
case GpsModel::UBLOX8:
case GpsModel::UBLOX9:
case GpsModel::UBLOX10:
return ublox::init(uart, type);
case GpsModel::UC6580:
return initUc6580(uart);
case GPS_MODEL_MTK:
return init_mtk(uart);
case GPS_MODEL_MTK_L76B:
return init_mtk_l76b(uart);
case GPS_MODEL_MTK_PA1616S:
return init_mtk_pa1616s(uart);
case GPS_MODEL_UBLOX6:
case GPS_MODEL_UBLOX7:
case GPS_MODEL_UBLOX8:
case GPS_MODEL_UBLOX9:
case GPS_MODEL_UBLOX10:
return gps_ublox::init(uart, type);
case GPS_MODEL_UC6580:
return init_uc6580(uart);
}
LOG_I(TAG, "Init not implemented %d", static_cast<int>(type));
return false;
}
bool initAg33xx(::Device* uart) {
bool init_ag33xx(Device* uart) {
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR066,1,0,1,0,0,1*3B\r\n", 25, 250); // Enable GPS+GALILEO+NAVIC
// Configure NMEA (sentences will output once per fix)
@ -185,47 +177,47 @@ bool initAg33xx(::Device* uart) {
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR062,5,0*3B\r\n", 17, 250); // VTG OFF
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR062,6,0*38\r\n", 17, 250); // ZDA ON
kernel::delayMillis(250);
delay_millis(250);
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR513*3D\r\n", 13, 250); // save configuration
return true;
}
bool initUc6580(::Device* uart) {
bool init_uc6580(Device* uart) {
// The Unicore UC6580 can use a lot of sat systems, enable it to
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
// This will reset the receiver, so wait a bit afterwards
// The paranoid will wait for the OK*04 confirmation response after each command.
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGSYS,h35155\r\n", 16, 250);
kernel::delayMillis(750);
delay_millis(750);
// Must be done after the CFGSYS command
// Turn off GSV messages, we don't really care about which and where the sats are, maybe someday.
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,0,3,0\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
// Turn off GSA messages, TinyGPS++ doesn't use this message.
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,0,2,0\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
// Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care.
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,6,0,0\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,6,1,0\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
return true;
}
bool initAtgm336h(::Device* uart) {
bool init_atgm336h(Device* uart) {
uint8_t buffer[256];
// Set the intial configuration of the device - these _should_ work for most AT6558 devices
int msglen = makeCASPacket(buffer, 0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
int msglen = make_cas_packet(buffer, 0x06, 0x07, sizeof(_message_CAS_CFG_NAVX_CONF), _message_CAS_CFG_NAVX_CONF);
uart_controller_write_bytes(uart, buffer, msglen, 250);
if (getACKCas(uart, 0x06, 0x07, 250) != GpsResponse::Ok) {
if (get_ack_cas(uart, 0x06, 0x07, 250) != GpsResponse::Ok) {
LOG_W(TAG, "ATGM336H: Could not set Config");
}
// Set the update frequence to 1Hz
msglen = makeCASPacket(buffer, 0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
msglen = make_cas_packet(buffer, 0x06, 0x04, sizeof(_message_CAS_CFG_RATE_1HZ), _message_CAS_CFG_RATE_1HZ);
uart_controller_write_bytes(uart, buffer, msglen, 250);
if (getACKCas(uart, 0x06, 0x04, 250) != GpsResponse::Ok) {
if (get_ack_cas(uart, 0x06, 0x04, 250) != GpsResponse::Ok) {
LOG_W(TAG, "ATGM336H: Could not set Update Frequency");
}
@ -235,64 +227,62 @@ bool initAtgm336h(::Device* uart) {
for (unsigned int i = 0; i < sizeof(fields); i++) {
// Construct a CAS-CFG-MSG packet
uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00};
msglen = makeCASPacket(buffer, 0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
msglen = make_cas_packet(buffer, 0x06, 0x01, sizeof(cas_cfg_msg_packet), cas_cfg_msg_packet);
uart_controller_write_bytes(uart, buffer, msglen, 250);
if (getACKCas(uart, 0x06, 0x01, 250) != GpsResponse::Ok) {
if (get_ack_cas(uart, 0x06, 0x01, 250) != GpsResponse::Ok) {
LOG_W(TAG, "ATGM336H: Could not enable NMEA MSG: %u", fields[i]);
}
}
return true;
}
bool initMtkPa1616s(::Device* uart) {
bool init_mtk_pa1616s(Device* uart) {
// PA1616S is used in some GPS breakout boards from Adafruit
// PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here.
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK353,1,0,0,0,0*2A\r\n", 23, 250);
// Above command will reset the GPS and takes longer before it will accept new commands
kernel::delayMillis(1000);
delay_millis(1000);
// Only ask for RMC and GGA (GNRMC and GNGGA)
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n", 51, 250);
kernel::delayMillis(250);
delay_millis(250);
// Enable SBAS / WAAS
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK301,2*2E\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
return true;
}
bool initMtkL76b(::Device* uart) {
bool init_mtk_l76b(Device* uart) {
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
// Initialize the L76B Chip, use GPS + GLONASS
// See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK353,1,1,0,0,0*2B\r\n", 23, 250);
// Above command will reset the GPS and takes longer before it will accept new commands
kernel::delayMillis(1000);
delay_millis(1000);
// only ask for RMC and GGA (GNRMC and GNGGA)
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n", 51, 250);
kernel::delayMillis(250);
delay_millis(250);
// Enable SBAS
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK301,2*2E\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
// Enable PPS for 2D/3D fix only
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK285,3,100*3F\r\n", 19, 250);
kernel::delayMillis(250);
delay_millis(250);
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK886,1*29\r\n", 15, 250);
kernel::delayMillis(250);
delay_millis(250);
return true;
}
bool initMtk(::Device* uart) {
bool init_mtk(Device* uart) {
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS04,7*1E\r\n", 14, 250);
kernel::delayMillis(250);
delay_millis(250);
// only ask for RMC and GGA
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS03,1,0,0,0,1,0,0,0,0,0,,,0,0*02\r\n", 38, 250);
kernel::delayMillis(250);
delay_millis(250);
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS11,3*1E\r\n", 14, 250);
kernel::delayMillis(250);
delay_millis(250);
return true;
}
} // namespace tt::hal::gps

View File

@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/drivers/gps.h>
struct Device;
/**
* Sends the init sequence for a specific, already-probed GPS model over uart.
*/
bool gps_init(Device* uart, GpsModel model);

View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
#include "gps_service_internal.h"
#include <tactility/check.h>
#include <tactility/driver.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/module.h>
constexpr auto* TAG = "GpsModule";
extern "C" {
extern Driver gps_driver;
static Driver* const gps_drivers[] = {
&gps_driver,
nullptr
};
static error_t start() {
error_t error = gps_service_register();
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to register GPS service: %s", error_to_string(error));
return error;
}
return ERROR_NONE;
}
Module gps_module = {
.name = "gps",
.start = start,
.stop = nullptr,
.drivers = gps_drivers,
.symbols = nullptr,
.internal = nullptr
};
}

View File

@ -1,26 +1,22 @@
#include "Tactility/hal/gps/GpsDevice.h"
#include "Tactility/hal/gps/Ublox.h"
#include <Tactility/kernel/Kernel.h>
// SPDX-License-Identifier: Apache-2.0
#include "probe.h"
#include "gps_response.h"
#include "ublox.h"
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <cstring>
constexpr auto* TAG = "Gps";
#define GPS_UART_BUFFER_SIZE 256
using namespace tt;
using namespace tt::hal;
namespace tt::hal::gps {
/**
* From: https://github.com/meshtastic/firmware/blob/3b0232de1b6282eacfbff6e50b68fca7e67b8511/src/meshUtils.cpp#L40
*/
char* strnstr(const char* s, const char* find, size_t slen) {
static char* probe_strnstr(const char* s, const char* find, size_t slen) {
char c;
if ((c = *find++) != '\0') {
char sc;
@ -43,36 +39,24 @@ char* strnstr(const char* s, const char* find, size_t slen) {
/**
* From: https://github.com/meshtastic/firmware/blob/f81d3b045dd1b7e3ca7870af3da915ff4399ea98/src/gps/GPS.cpp
*/
GpsResponse getAck(::Device* uart, const char* message, uint32_t waitMillis) {
static GpsResponse get_ack(Device* uart, const char* message, uint32_t wait_millis) {
uint8_t buffer[768] = {0};
uint8_t b;
int bytesRead = 0;
uint32_t startTimeout = kernel::getMillis() + waitMillis;
#ifdef GPS_DEBUG
std::string debugmsg = "";
#endif
while (kernel::getMillis() < startTimeout) {
int bytes_read = 0;
uint32_t start_timeout = get_millis() + wait_millis;
while (get_millis() < start_timeout) {
size_t available = 0;
uart_controller_get_available(uart, &available);
if (available > 0) {
uart_controller_read_byte(uart, &b, 1);
#ifdef GPS_DEBUG
debugmsg += vformat("%c", (b >= 32 && b <= 126) ? b : '.');
#endif
buffer[bytesRead] = b;
bytesRead++;
if ((bytesRead == 767) || (b == '\r')) {
if (strnstr((char*)buffer, message, bytesRead) != nullptr) {
#ifdef GPS_DEBUG
LOG_D(TAG, "Found: %s", message); // Log the found message
#endif
buffer[bytes_read] = b;
bytes_read++;
if ((bytes_read == 767) || (b == '\r')) {
if (probe_strnstr((char*)buffer, message, bytes_read) != nullptr) {
return GpsResponse::Ok;
} else {
bytesRead = 0;
#ifdef GPS_DEBUG
LOG_D(TAG, "%s", debugmsg.c_str());
#endif
bytes_read = 0;
}
}
}
@ -88,7 +72,7 @@ GpsResponse getAck(::Device* uart, const char* message, uint32_t waitMillis) {
LOG_I(TAG, "Probing for %s (%s)", CHIP, TOWRITE); \
uart_controller_flush_input(UART); \
uart_controller_write_bytes(UART, (const uint8_t*)(TOWRITE "\r\n"), strlen(TOWRITE "\r\n"), TIMEOUT); \
if (getAck(UART, RESPONSE, TIMEOUT) == GpsResponse::Ok) { \
if (get_ack(UART, RESPONSE, TIMEOUT) == GpsResponse::Ok) { \
LOG_I(TAG, "Probe detected %s %s", CHIP, #DRIVER); \
return DRIVER; \
} \
@ -97,50 +81,48 @@ GpsResponse getAck(::Device* uart, const char* message, uint32_t waitMillis) {
/**
* From: https://github.com/meshtastic/firmware/blob/f81d3b045dd1b7e3ca7870af3da915ff4399ea98/src/gps/GPS.cpp
*/
GpsModel probe(::Device* uart) {
GpsModel gps_probe(Device* uart) {
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n", 40, 500);
kernel::delayMillis(20);
delay_millis(20);
// Close NMEA sequences on Ublox
uart_controller_write_bytes(uart, (const uint8_t*)"$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n", 29, 500);
uart_controller_write_bytes(uart, (const uint8_t*)"$PUBX,40,GSV,0,0,0,0,0,0*59\r\n", 29, 500);
uart_controller_write_bytes(uart, (const uint8_t*)"$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n", 29, 500);
kernel::delayMillis(20);
delay_millis(20);
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
PROBE_SIMPLE(uart, "UC6580", "$PDTINFO", "UC6580", GpsModel::UC6580, 500);
PROBE_SIMPLE(uart, "UM600", "$PDTINFO", "UM600", GpsModel::UC6580, 500);
PROBE_SIMPLE(uart, "ATGM336H", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM336H", GpsModel::ATGM336H, 500);
PROBE_SIMPLE(uart, "UC6580", "$PDTINFO", "UC6580", GpsModel::GPS_MODEL_UC6580, 500);
PROBE_SIMPLE(uart, "UM600", "$PDTINFO", "UM600", GpsModel::GPS_MODEL_UC6580, 500);
PROBE_SIMPLE(uart, "ATGM336H", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM336H", GpsModel::GPS_MODEL_ATGM336H, 500);
/* ATGM332D series (-11(GPS), -21(BDS), -31(GPS+BDS), -51(GPS+GLONASS), -71-0(GPS+BDS+GLONASS))
based on AT6558 */
PROBE_SIMPLE(uart, "ATGM332D", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM332D", GpsModel::ATGM336H, 500);
PROBE_SIMPLE(uart, "ATGM332D", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM332D", GpsModel::GPS_MODEL_ATGM336H, 500);
/* Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M */
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR062,2,0*3C\r\n", 17, 500); // GSA OFF to reduce volume
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR062,3,0*3D\r\n", 17, 500); // GSV OFF to reduce volume
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR513*3D\r\n", 13, 500); // save configuration
PROBE_SIMPLE(uart, "AG3335", "$PAIR021*39", "$PAIR021,AG3335", GpsModel::AG3335, 500);
PROBE_SIMPLE(uart, "AG3352", "$PAIR021*39", "$PAIR021,AG3352", GpsModel::AG3352, 500);
PROBE_SIMPLE(uart, "LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GpsModel::AG3352, 500);
PROBE_SIMPLE(uart, "AG3335", "$PAIR021*39", "$PAIR021,AG3335", GpsModel::GPS_MODEL_AG3335, 500);
PROBE_SIMPLE(uart, "AG3352", "$PAIR021*39", "$PAIR021,AG3352", GpsModel::GPS_MODEL_AG3352, 500);
PROBE_SIMPLE(uart, "LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GpsModel::GPS_MODEL_AG3352, 500);
PROBE_SIMPLE(uart, "L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GpsModel::MTK, 500);
PROBE_SIMPLE(uart, "L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GpsModel::GPS_MODEL_MTK, 500);
// Close all NMEA sentences, valid for L76B MTK platform (Waveshare Pico GPS)
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK514,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*2E\r\n", 51, 500);
kernel::delayMillis(20);
delay_millis(20);
PROBE_SIMPLE(uart, "L76B", "$PMTK605*31", "Quectel-L76B", GpsModel::MTK_L76B, 500);
PROBE_SIMPLE(uart, "PA1616S", "$PMTK605*31", "1616S", GpsModel::MTK_PA1616S, 500);
PROBE_SIMPLE(uart, "L76B", "$PMTK605*31", "Quectel-L76B", GpsModel::GPS_MODEL_MTK_L76B, 500);
PROBE_SIMPLE(uart, "PA1616S", "$PMTK605*31", "1616S", GpsModel::GPS_MODEL_MTK_PA1616S, 500);
auto ublox_result = ublox::probe(uart);
if (ublox_result != GpsModel::Unknown) {
auto ublox_result = gps_ublox::probe(uart);
if (ublox_result != GpsModel::GPS_MODEL_UNKNOWN) {
return ublox_result;
} else {
LOG_W(TAG, "No GNSS Module");
return GpsModel::Unknown;
return GpsModel::GPS_MODEL_UNKNOWN;
}
}
} // namespace tt::hal::gps

View File

@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/drivers/gps.h>
struct Device;
/**
* Attempts to auto-detect the GPS/GNSS chipset connected via uart.
* @return GPS_MODEL_UNKNOWN when no supported chipset responded
*/
GpsModel gps_probe(Device* uart);

View File

@ -1,26 +1,30 @@
#include <Tactility/hal/gps/Ublox.h>
#include <Tactility/hal/gps/UbloxMessages.h>
#include <Tactility/kernel/Kernel.h>
// SPDX-License-Identifier: Apache-2.0
#include "ublox.h"
#include "gps_response.h"
#include "ublox_messages.h"
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <cstring>
#include <cstdlib>
namespace tt::hal::gps::ublox {
namespace gps_ublox {
constexpr auto* TAG = "Ublox";
bool initUblox6(::Device* uart);
bool initUblox789(::Device* uart, GpsModel model);
bool initUblox10(::Device* uart);
bool init_ublox_6(Device* uart);
bool init_ublox_789(Device* uart, GpsModel model);
bool init_ublox_10(Device* uart);
#define SEND_UBX_PACKET(UART, BUFFER, TYPE, ID, DATA, ERRMSG, TIMEOUT_MILLIS) \
do { \
auto msglen = makePacket(TYPE, ID, DATA, sizeof(DATA), BUFFER); \
auto msglen = make_packet(TYPE, ID, DATA, sizeof(DATA), BUFFER); \
uart_controller_write_bytes(UART, BUFFER, msglen, TIMEOUT_MILLIS / portTICK_PERIOD_MS); \
if (getAck(UART, TYPE, ID, TIMEOUT_MILLIS) != GpsResponse::Ok) { \
if (get_ack(UART, TYPE, ID, TIMEOUT_MILLIS) != GpsResponse::Ok) { \
LOG_I(TAG, "Sending packet failed: %s", #ERRMSG); \
} \
} while (0)
@ -39,37 +43,34 @@ void checksum(uint8_t* message, size_t length) {
message[length - 1] = CK_B;
}
uint8_t makePacket(uint8_t classId, uint8_t messageId, const uint8_t* payload, uint8_t payloadSize, uint8_t* bufferOut) {
uint8_t make_packet(uint8_t class_id, uint8_t message_id, const uint8_t* payload, uint8_t payload_size, uint8_t* buffer_out) {
// Construct the UBX packet
bufferOut[0] = 0xB5U; // header
bufferOut[1] = 0x62U; // header
bufferOut[2] = classId; // class
bufferOut[3] = messageId; // id
bufferOut[4] = payloadSize; // length
bufferOut[5] = 0x00U;
buffer_out[0] = 0xB5U; // header
buffer_out[1] = 0x62U; // header
buffer_out[2] = class_id; // class
buffer_out[3] = message_id; // id
buffer_out[4] = payload_size; // length
buffer_out[5] = 0x00U;
bufferOut[6 + payloadSize] = 0x00U; // CK_A
bufferOut[7 + payloadSize] = 0x00U; // CK_B
buffer_out[6 + payload_size] = 0x00U; // CK_A
buffer_out[7 + payload_size] = 0x00U; // CK_B
for (int i = 0; i < payloadSize; i++) {
bufferOut[6 + i] = payload[i];
for (int i = 0; i < payload_size; i++) {
buffer_out[6 + i] = payload[i];
}
checksum(bufferOut, (payloadSize + 8U));
return (payloadSize + 8U);
checksum(buffer_out, (payload_size + 8U));
return (payload_size + 8U);
}
GpsResponse getAck(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t waitMillis) {
GpsResponse get_ack(Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wait_millis) {
uint8_t b;
uint8_t ack = 0;
const uint8_t ackP[2] = {class_id, msg_id};
uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00};
uint32_t startTime = kernel::getTicks();
TickType_t waitTicks = pdMS_TO_TICKS(waitMillis);
uint32_t start_time = get_ticks();
TickType_t wait_ticks = pdMS_TO_TICKS(wait_millis);
const char frame_errors[] = "More than 100 frame errors";
int sCounter = 0;
#ifdef GPS_DEBUG
std::string debugmsg = "";
#endif
for (int j = 2; j < 6; j++) {
buf[8] += buf[j];
@ -82,11 +83,8 @@ GpsResponse getAck(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wa
buf[9] += buf[8];
}
while (kernel::getTicks() - startTime < waitTicks) {
while (get_ticks() - start_time < wait_ticks) {
if (ack > 9) {
#ifdef GPS_DEBUG
LOG_I(TAG, "Got ACK for class %02X message %02X in %zums", class_id, msg_id, kernel::getMillis() - startTime);
#endif
return GpsResponse::Ok; // ACK received
}
size_t available = 0;
@ -96,25 +94,15 @@ GpsResponse getAck(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wa
if (b == frame_errors[sCounter]) {
sCounter++;
if (sCounter == 26) {
#ifdef GPS_DEBUG
LOG_I(TAG, "%s", debugmsg.c_str());
#endif
return GpsResponse::FrameErrors;
}
} else {
sCounter = 0;
}
#ifdef GPS_DEBUG
debugmsg += std::format("%02X", b);
#endif
if (b == buf[ack]) {
ack++;
} else {
if (ack == 3 && b == 0x00) { // UBX-ACK-NAK message
#ifdef GPS_DEBUG
LOG_I(TAG, "%s", debugmsg.c_str());
#endif
LOG_W(TAG, "Got NAK for class %02X message %02X", class_id, msg_id);
return GpsResponse::NotAck; // NAK received
}
@ -122,20 +110,17 @@ GpsResponse getAck(::Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wa
}
}
}
#ifdef GPS_DEBUG
LOG_I(TAG, "%s", debugmsg.c_str());
LOG_W(TAG, "No response for class %02X message %02X", class_id, msg_id);
#endif
return GpsResponse::None; // No response received within timeout
}
static int getAck(::Device* uart, uint8_t* buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedId, uint32_t timeoutMillis) {
uint16_t ubxFrameCounter = 0;
TickType_t startTime = kernel::getTicks();
TickType_t timeoutTicks = pdMS_TO_TICKS(timeoutMillis);
uint16_t needRead = 0;
static int get_ack(Device* uart, uint8_t* buffer, uint16_t size, uint8_t requested_class, uint8_t requested_id, uint32_t timeout_millis) {
uint16_t ubx_frame_counter = 0;
TickType_t start_time = get_ticks();
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_millis);
uint16_t need_read = 0;
while ((kernel::getTicks() - startTime) < timeoutTicks) {
while ((get_ticks() - start_time) < timeout_ticks) {
size_t available = 0;
uart_controller_get_available(uart, &available);
while (available > 0) {
@ -143,56 +128,53 @@ static int getAck(::Device* uart, uint8_t* buffer, uint16_t size, uint8_t reques
uart_controller_read_byte(uart, &c, 1);
available--;
switch (ubxFrameCounter) {
switch (ubx_frame_counter) {
case 0:
if (c == 0xB5) {
ubxFrameCounter++;
ubx_frame_counter++;
}
break;
case 1:
if (c == 0x62) {
ubxFrameCounter++;
ubx_frame_counter++;
} else {
ubxFrameCounter = 0;
ubx_frame_counter = 0;
}
break;
case 2:
if (c == requestedClass) {
ubxFrameCounter++;
if (c == requested_class) {
ubx_frame_counter++;
} else {
ubxFrameCounter = 0;
ubx_frame_counter = 0;
}
break;
case 3:
if (c == requestedId) {
ubxFrameCounter++;
if (c == requested_id) {
ubx_frame_counter++;
} else {
ubxFrameCounter = 0;
ubx_frame_counter = 0;
}
break;
case 4:
needRead = c;
ubxFrameCounter++;
need_read = c;
ubx_frame_counter++;
break;
case 5: {
// Payload length msb
needRead |= (c << 8);
ubxFrameCounter++;
need_read |= (c << 8);
ubx_frame_counter++;
// Check for buffer overflow
if (needRead >= size) {
ubxFrameCounter = 0;
if (need_read >= size) {
ubx_frame_counter = 0;
break;
}
auto read_bytes = 0U;
uart_controller_read_bytes(uart, buffer, needRead, 250 / portTICK_PERIOD_MS);
if (read_bytes != needRead) {
ubxFrameCounter = 0;
uart_controller_read_bytes(uart, buffer, need_read, 250 / portTICK_PERIOD_MS);
if (read_bytes != need_read) {
ubx_frame_counter = 0;
} else {
// return payload length
#ifdef GPS_DEBUG
LOG_I(TAG, "Got ACK for class %02X message %02X in %zums", requestedClass, requestedId, kernel::getMillis() - startTime);
#endif
return needRead;
return need_read;
}
break;
}
@ -205,7 +187,7 @@ static int getAck(::Device* uart, uint8_t* buffer, uint16_t size, uint8_t reques
return 0;
}
static struct uBloxGnssModelInfo {
static struct UbloxGnssModelInfo {
char swVersion[30];
char hwVersion[10];
uint8_t extensionNo;
@ -213,7 +195,7 @@ static struct uBloxGnssModelInfo {
uint8_t protocol_version;
} ublox_info;
GpsModel probe(::Device* uart) {
GpsModel probe(Device* uart) {
LOG_I(TAG, "Probing for U-blox");
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
@ -221,28 +203,28 @@ GpsModel probe(::Device* uart) {
uart_controller_flush_input(uart);
uart_controller_write_bytes(uart, cfg_rate, sizeof(cfg_rate), 500 / portTICK_PERIOD_MS);
// Check that the returned response class and message ID are correct
GpsResponse response = getAck(uart, 0x06, 0x08, 750);
GpsResponse response = get_ack(uart, 0x06, 0x08, 750);
if (response == GpsResponse::None) {
LOG_W(TAG, "No GNSS Module");
return GpsModel::Unknown;
return GpsModel::GPS_MODEL_UNKNOWN;
} else if (response == GpsResponse::FrameErrors) {
LOG_W(TAG, "UBlox Frame Errors");
}
uint8_t buffer[256];
memset(buffer, 0, sizeof(buffer));
uint8_t _message_MONVER[8] = {
uint8_t message_monver[8] = {
0xB5, 0x62, // Sync message for UBX protocol
0x0A, 0x04, // Message class and ID (UBX-MON-VER)
0x00, 0x00, // Length of payload (we're asking for an answer, so no payload)
0x00, 0x00 // Checksum
};
// Get Ublox gnss module hardware and software info
checksum(_message_MONVER, sizeof(_message_MONVER));
checksum(message_monver, sizeof(message_monver));
uart_controller_flush_input(uart);
uart_controller_write_bytes(uart, _message_MONVER, sizeof(_message_MONVER), 500);
uart_controller_write_bytes(uart, message_monver, sizeof(message_monver), 500);
uint16_t ack_response_len = getAck(uart, buffer, sizeof(buffer), 0x0A, 0x04, 1200);
uint16_t ack_response_len = get_ack(uart, buffer, sizeof(buffer), 0x0A, 0x04, 1200);
if (ack_response_len) {
uint16_t position = 0;
for (char& i: ublox_info.swVersion) {
@ -294,86 +276,86 @@ GpsModel probe(::Device* uart) {
#define DETECTED_MESSAGE "%s detected, using %s Module"
if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 6", "6");
return GpsModel::UBLOX6;
return GPS_MODEL_UBLOX6;
} else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 7", "7");
return GpsModel::UBLOX7;
return GPS_MODEL_UBLOX7;
} else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 8", "8");
return GpsModel::UBLOX8;
return GPS_MODEL_UBLOX8;
} else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 9", "9");
return GpsModel::UBLOX9;
return GPS_MODEL_UBLOX9;
} else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 10", "10");
return GpsModel::UBLOX10;
return GPS_MODEL_UBLOX10;
}
}
return GpsModel::Unknown;
return GPS_MODEL_UNKNOWN;
}
bool init(::Device* uart, GpsModel model) {
bool init(Device* uart, GpsModel model) {
LOG_I(TAG, "U-blox init");
switch (model) {
case GpsModel::UBLOX6:
return initUblox6(uart);
case GpsModel::UBLOX7:
case GpsModel::UBLOX8:
case GpsModel::UBLOX9:
return initUblox789(uart, model);
case GpsModel::UBLOX10:
return initUblox10(uart);
case GPS_MODEL_UBLOX6:
return init_ublox_6(uart);
case GPS_MODEL_UBLOX7:
case GPS_MODEL_UBLOX8:
case GPS_MODEL_UBLOX9:
return init_ublox_789(uart, model);
case GPS_MODEL_UBLOX10:
return init_ublox_10(uart);
default:
LOG_E(TAG, "Unknown or unsupported U-blox model");
return false;
}
}
bool initUblox10(::Device* uart) {
bool init_ublox_10(Device* uart) {
uint8_t buffer[256];
kernel::delayMillis(1000);
delay_millis(1000);
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
kernel::delayMillis(750);
delay_millis(750);
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
kernel::delayMillis(750);
delay_millis(750);
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
kernel::delayMillis(750);
delay_millis(750);
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
kernel::delayMillis(750);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
kernel::delayMillis(750);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
kernel::delayMillis(750);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
kernel::delayMillis(750);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
kernel::delayMillis(750);
delay_millis(750);
// Here is where the init commands should go to do further M10 initialization.
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
kernel::delayMillis(750); // will cause a receiver restart so wait a bit
delay_millis(750); // will cause a receiver restart so wait a bit
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
kernel::delayMillis(750); // will cause a receiver restart so wait a bit
delay_millis(750); // will cause a receiver restart so wait a bit
// Done with initialization
// Enable wanted NMEA messages in BBR layer so they will survive a periodic sleep
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
kernel::delayMillis(750);
delay_millis(750);
// Enable wanted NMEA messages in RAM layer
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
kernel::delayMillis(750);
delay_millis(750);
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
// BBR will survive a restart, and power off for a while, but modules with small backup
// batteries or super caps will not retain the config for a long power off time.
auto packet_size = makePacket(0x06, 0x09, _message_SAVE_10, sizeof(_message_SAVE_10), buffer);
auto packet_size = make_packet(0x06, 0x09, _message_SAVE_10, sizeof(_message_SAVE_10), buffer);
uart_controller_write_bytes(uart, buffer, packet_size, 2000 / portTICK_PERIOD_MS);
if (getAck(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
if (get_ack(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
LOG_W(TAG, "Unable to save GNSS module config");
} else {
LOG_I(TAG, "GNSS module configuration saved!");
@ -381,36 +363,36 @@ bool initUblox10(::Device* uart) {
return true;
}
bool initUblox789(::Device* uart, GpsModel model) {
bool init_ublox_789(Device* uart, GpsModel model) {
uint8_t buffer[256];
if (model == GpsModel::UBLOX7) {
if (model == GpsModel::GPS_MODEL_UBLOX7) {
LOG_D(TAG, "Set GPS+SBAS");
auto msglen = makePacket(0x06, 0x3e, _message_GNSS_7, sizeof(_message_GNSS_7), buffer);
auto msglen = make_packet(0x06, 0x3e, _message_GNSS_7, sizeof(_message_GNSS_7), buffer);
uart_controller_write_bytes(uart, buffer, msglen, 800 / portTICK_PERIOD_MS);
} else { // 8,9
auto msglen = makePacket(0x06, 0x3e, _message_GNSS_8, sizeof(_message_GNSS_8), buffer);
auto msglen = make_packet(0x06, 0x3e, _message_GNSS_8, sizeof(_message_GNSS_8), buffer);
uart_controller_write_bytes(uart, buffer, msglen, 800 / portTICK_PERIOD_MS);
}
if (getAck(uart, 0x06, 0x3e, 800) == GpsResponse::NotAck) {
if (get_ack(uart, 0x06, 0x3e, 800) == GpsResponse::NotAck) {
// It's not critical if the module doesn't acknowledge this configuration.
LOG_D(TAG, "reconfigure GNSS - defaults maintained. Is this module GPS-only?");
} else {
if (model == GpsModel::UBLOX7) {
if (model == GpsModel::GPS_MODEL_UBLOX7) {
LOG_I(TAG, "GPS+SBAS configured");
} else { // 8,9
LOG_I(TAG, "GPS+SBAS+GLONASS+Galileo configured");
}
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
// commands for the M8 it tends to be more. 1 sec should be enough
kernel::delayMillis(1000);
delay_millis(1000);
}
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
if (model == GpsModel::UBLOX8) { // 8
if (model == GpsModel::GPS_MODEL_UBLOX8) { // 8
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
@ -436,7 +418,7 @@ bool initUblox789(::Device* uart, GpsModel model) {
SEND_UBX_PACKET(uart, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
// For M8 we want to enable NMEA version 4.10 so we can see the additional satellites.
if (model == GpsModel::UBLOX8) {
if (model == GpsModel::GPS_MODEL_UBLOX8) {
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
}
@ -445,9 +427,9 @@ bool initUblox789(::Device* uart, GpsModel model) {
SEND_UBX_PACKET(uart, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
}
auto packet_size = makePacket(0x06, 0x09, _message_SAVE, sizeof(_message_SAVE), buffer);
auto packet_size = make_packet(0x06, 0x09, _message_SAVE, sizeof(_message_SAVE), buffer);
uart_controller_write_bytes(uart, buffer, packet_size, 2000 / portTICK_PERIOD_MS);
if (getAck(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
if (get_ack(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
LOG_W(TAG, "Unable to save GNSS module config");
} else {
LOG_I(TAG, "GNSS module configuration saved!");
@ -455,7 +437,7 @@ bool initUblox789(::Device* uart, GpsModel model) {
return true;
}
bool initUblox6(::Device* uart) {
bool init_ublox_6(Device* uart) {
uint8_t buffer[256];
uart_controller_flush_input(uart);
@ -479,9 +461,9 @@ bool initUblox6(::Device* uart) {
SEND_UBX_PACKET(uart, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_AID, "disable UBX-AID", 500);
auto packet_size = makePacket(0x06, 0x09, _message_SAVE, sizeof(_message_SAVE), buffer);
auto packet_size = make_packet(0x06, 0x09, _message_SAVE, sizeof(_message_SAVE), buffer);
uart_controller_write_bytes(uart, buffer, packet_size, 2000);
if (getAck(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
if (get_ack(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
LOG_W(TAG, "Unable to save GNSS module config");
} else {
LOG_I(TAG, "GNSS module config saved!");
@ -489,4 +471,4 @@ bool initUblox6(::Device* uart) {
return true;
}
} // namespace tt::hal::gps::ublox
}

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/drivers/gps.h>
#include <cstdint>
#include <cstddef>
struct Device;
namespace gps_ublox {
void checksum(uint8_t* message, size_t length);
// From https://github.com/meshtastic/firmware/blob/7648391f91f2b84e367ae2b38220b30936fb45b1/src/gps/GPS.cpp#L128
uint8_t make_packet(uint8_t class_id, uint8_t message_id, const uint8_t* payload, uint8_t payload_size, uint8_t* buffer_out);
GpsModel probe(Device* uart);
bool init(Device* uart, GpsModel model);
}

View File

@ -1,8 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <cstdint>
namespace tt::hal::gps::ublox {
namespace gps_ublox {
// Power Management
@ -466,4 +467,4 @@ BBR layer config message:
b5 62 06 8a 0e 00 00 02 00 00 20 00 31 10 00 05 00 31 10 00 47 94
*/
}
}

View File

@ -95,9 +95,9 @@ else ()
list(APPEND REQUIRES_LIST
Tactility
TactilityFreeRtos
hal-device-module
lvgl-module
crypt-module
gps-module
SDL2::SDL2-static
SDL2-static
)

View File

@ -1,20 +0,0 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
list(APPEND REQUIRES_LIST
TactilityKernel
TactilityFreeRtos
)
if (NOT DEFINED ENV{ESP_IDF_VERSION})
list(APPEND REQUIRES_LIST freertos_kernel)
endif ()
tactility_add_module(hal-device-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES ${REQUIRES_LIST}
)

View File

@ -1,195 +0,0 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,10 +0,0 @@
# hal-device-module
**WARNING: This module contains deprecated code**
This module is the basis for the old Tactility HAL.
This HAL existed before TactilityKernel.
The C++ `tt::hal::Device` class is replaced by `struct Device` from TactilityKernel.
License: [Apache v2.0](LICENSE-Apache-2.0.md)

View File

@ -1,28 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
enum class HalDeviceType {
HAL_DEVICE_TYPE_I2C,
HAL_DEVICE_TYPE_DISPLAY,
HAL_DEVICE_TYPE_TOUCH,
HAL_DEVICE_TYPE_SDCARD,
HAL_DEVICE_TYPE_KEYBOARD,
HAL_DEVICE_TYPE_ENCODER,
HAL_DEVICE_TYPE_POWER,
HAL_DEVICE_TYPE_GPS,
HAL_DEVICE_TYPE_OTHER
};
HalDeviceType hal_device_get_type(struct Device* device);
void hal_device_for_each_of_type(HalDeviceType type, void* context, bool(*onDevice)(struct Device* device, void* context));
extern const struct DeviceType HAL_DEVICE_TYPE;
#ifdef __cplusplus
}
#endif

View File

@ -1,21 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "hal_device.h"
#include <memory>
#include <tactility/hal/Device.h>
namespace tt::hal {
/**
* @brief Get a tt::hal::Device object from a Kernel device.
* @warning The input device must be of type HAL_DEVICE_TYPE
* @param kernelDevice The kernel device
* @return std::shared_ptr<Device>
*/
std::shared_ptr<Device> hal_device_get_device(::Device* kernelDevice);
void hal_device_set_device(::Device* kernelDevice, std::shared_ptr<Device> halDevice);
}

View File

@ -1,144 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <functional>
#include <memory>
#include <ranges>
#include <string>
#include <vector>
#include <cassert>
#include <tactility/device.h>
typedef ::Device KernelDevice;
namespace tt::hal {
/** Base class for HAL-related devices. */
class Device {
public:
enum class Type {
I2c,
Display,
Touch,
SdCard,
Keyboard,
Encoder,
Power,
Gps,
Other
};
typedef uint32_t Id;
struct KernelDeviceHolder {
const std::string name;
std::shared_ptr<KernelDevice> device = std::make_shared<KernelDevice>();
explicit KernelDeviceHolder(std::string name) : name(name) {
device->name = this->name.c_str();
}
};
private:
Id id;
std::shared_ptr<KernelDeviceHolder> kernelDeviceHolder;
public:
Device();
virtual ~Device() = default;
/** Unique identifier */
Id getId() const { return id; }
/** The type of device */
virtual Type getType() const = 0;
/** The part number or hardware name e.g. TdeckTouch, TdeckDisplay, BQ24295, etc. */
virtual std::string getName() const = 0;
/** A short description of what this device does.
* e.g. "USB charging controller with I2C interface."
*/
virtual std::string getDescription() const = 0;
void setKernelDeviceHolder(std::shared_ptr<KernelDeviceHolder> kernelDeviceHolder) { this->kernelDeviceHolder = kernelDeviceHolder; }
std::shared_ptr<KernelDeviceHolder> getKernelDeviceHolder() const { return kernelDeviceHolder; }
};
/**
* Adds a device to the registry.
* @warning This will leak memory if you want to destroy a device and don't call deregisterDevice()!
*/
void registerDevice(const std::shared_ptr<Device>& device);
/** Remove a device from the registry. */
void deregisterDevice(const std::shared_ptr<Device>& device);
/** Find a single device with a custom filter. Could return nullptr if not found. */
std::shared_ptr<Device> findDevice(const std::function<bool(const std::shared_ptr<Device>&)>& filterFunction);
/** Find devices with a custom filter */
std::vector<std::shared_ptr<Device>> findDevices(const std::function<bool(const std::shared_ptr<Device>&)>& filterFunction);
/** Find a device in the registry by its name. Could return nullptr if not found. */
std::shared_ptr<Device> findDevice(std::string name);
/** Find a device in the registry by its identifier. Could return nullptr if not found.*/
std::shared_ptr<Device> findDevice(Device::Id id);
/** Find 0, 1 or more devices in the registry by type. */
std::vector<std::shared_ptr<Device>> findDevices(Device::Type type);
/** Get a copy of the entire device registry in its current state. */
std::vector<std::shared_ptr<Device>> getDevices();
/** Find devices of a certain type and cast them to the specified class */
template<class DeviceType>
std::vector<std::shared_ptr<DeviceType>> findDevices(Device::Type type) {
auto devices = findDevices(type);
if (devices.empty()) {
return {};
} else {
std::vector<std::shared_ptr<DeviceType>> result;
result.reserve(devices.size());
for (auto& device : devices) {
auto target_device = std::static_pointer_cast<DeviceType>(device);
assert(target_device != nullptr);
result.push_back(target_device);
}
return result;
}
}
template<class DeviceType>
void findDevices(Device::Type type, std::function<bool(const std::shared_ptr<DeviceType>&)> onDeviceFound) {
auto devices_view = findDevices(type);
for (auto& device : devices_view) {
auto typed_device = std::static_pointer_cast<DeviceType>(device);
if (!onDeviceFound(typed_device)) {
break;
}
}
}
/** Find the first device of the specified type and cast it to the specified class */
template<class DeviceType>
std::shared_ptr<DeviceType> findFirstDevice(Device::Type type) {
auto devices = findDevices(type);
if (devices.empty()) {
return {};
} else {
auto& first = devices[0];
return std::static_pointer_cast<DeviceType>(first);
}
}
/** @return true if there are 1 or more devices of the specified type */
bool hasDevice(Device::Type type);
}

View File

@ -1,130 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/drivers/hal_device.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
#include <tactility/hal/Device.h>
#include <memory>
#include <utility>
#define TAG "HalDevice"
struct HalDevicePrivate {
std::shared_ptr<tt::hal::Device> halDevice;
};
#define GET_DATA(device) ((HalDevicePrivate*)device_get_driver_data(device))
static enum HalDeviceType getHalDeviceType(tt::hal::Device::Type type) {
switch (type) {
case tt::hal::Device::Type::I2c:
return HalDeviceType::HAL_DEVICE_TYPE_I2C;
case tt::hal::Device::Type::Display:
return HalDeviceType::HAL_DEVICE_TYPE_DISPLAY;
case tt::hal::Device::Type::Touch:
return HalDeviceType::HAL_DEVICE_TYPE_TOUCH;
case tt::hal::Device::Type::SdCard:
return HalDeviceType::HAL_DEVICE_TYPE_SDCARD;
case tt::hal::Device::Type::Keyboard:
return HalDeviceType::HAL_DEVICE_TYPE_KEYBOARD;
case tt::hal::Device::Type::Encoder:
return HalDeviceType::HAL_DEVICE_TYPE_ENCODER;
case tt::hal::Device::Type::Power:
return HalDeviceType::HAL_DEVICE_TYPE_POWER;
case tt::hal::Device::Type::Gps:
return HalDeviceType::HAL_DEVICE_TYPE_GPS;
case tt::hal::Device::Type::Other:
return HalDeviceType::HAL_DEVICE_TYPE_OTHER;
default:
LOG_W(TAG, "Device type %d is not implemented", static_cast<int>(type));
return HalDeviceType::HAL_DEVICE_TYPE_OTHER;
}
}
extern "C" {
HalDeviceType hal_device_get_type(struct Device* device) {
auto type = GET_DATA(device)->halDevice->getType();
return getHalDeviceType(type);
}
void hal_device_for_each_of_type(HalDeviceType type, void* context, bool(*onDevice)(struct Device* device, void* context)) {
struct InternalContext {
HalDeviceType typeParam;
void* contextParam;
bool(*onDeviceParam)(struct Device* device, void* context);
};
InternalContext internal_context = {
.typeParam = type,
.contextParam = context,
.onDeviceParam = onDevice
};
device_for_each_of_type(&HAL_DEVICE_TYPE, &internal_context, [](Device* device, void* context){
auto* hal_device_private = GET_DATA(device);
auto* internal_context = static_cast<InternalContext*>(context);
auto hal_device_type = getHalDeviceType(hal_device_private->halDevice->getType());
if (hal_device_type == internal_context->typeParam) {
if (!internal_context->onDeviceParam(device, internal_context->contextParam)) {
return false;
}
}
return true;
});
}
}
namespace tt::hal {
std::shared_ptr<Device> hal_device_get_device(::Device* device) {
auto* hal_device_private = GET_DATA(device);
return hal_device_private->halDevice;
}
void hal_device_set_device(::Device* kernelDevice, std::shared_ptr<Device> halDevice) {
GET_DATA(kernelDevice)->halDevice = std::move(halDevice);
}
}
#pragma region Lifecycle
static error_t start(Device* device) {
LOG_I(TAG, "start %s", device->name);
auto hal_device_data = new(std::nothrow) HalDevicePrivate();
if (hal_device_data == nullptr) return ERROR_OUT_OF_MEMORY;
device_set_driver_data(device, hal_device_data);
return ERROR_NONE;
}
static error_t stop(Device* device) {
LOG_I(TAG, "stop %s", device->name);
delete GET_DATA(device);
return ERROR_NONE;
}
#pragma endregion
extern "C" {
const struct DeviceType HAL_DEVICE_TYPE {
"hal-device"
};
extern struct Module hal_device_module;
Driver hal_device_driver = {
.name = "hal-device",
.compatible = (const char*[]) {"hal-device", nullptr},
.start_device = start,
.stop_device = stop,
.api = nullptr,
.device_type = &HAL_DEVICE_TYPE,
.owner = &hal_device_module,
.internal = nullptr
};
}

View File

@ -1,145 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/drivers/hal_device.hpp>
#include <tactility/hal/Device.h>
#include <tactility/log.h>
#include <Tactility/RecursiveMutex.h>
#include <algorithm>
#include <format>
namespace tt::hal {
RecursiveMutex mutex;
static Device::Id nextId = 0;
constexpr auto* TAG = "Devices";
Device::Device() : id(nextId++) {}
static std::shared_ptr<Device::KernelDeviceHolder> createKernelDeviceHolder(const std::shared_ptr<Device>& device) {
auto kernel_device_name = std::format("hal-device-{}", device->getId());
LOG_I(TAG, "Registering %s with id %u as kernel device %s", device->getName().c_str(), (unsigned)device->getId(), kernel_device_name.c_str());
auto kernel_device_holder = std::make_shared<Device::KernelDeviceHolder>(kernel_device_name);
auto* kernel_device = kernel_device_holder->device.get();
check(device_construct(kernel_device) == ERROR_NONE);
check(device_add(kernel_device) == ERROR_NONE);
auto* driver = driver_find_compatible("hal-device");
check(driver);
device_set_driver(kernel_device, driver);
check(device_start(kernel_device) == ERROR_NONE);
hal_device_set_device(kernel_device, device);
return kernel_device_holder;
}
static void destroyKernelDeviceHolder(std::shared_ptr<Device::KernelDeviceHolder>& holder) {
auto kernel_device = holder->device.get();
hal_device_set_device(kernel_device, nullptr);
check(device_stop(kernel_device) == ERROR_NONE);
check(device_remove(kernel_device) == ERROR_NONE);
check(device_destruct(kernel_device) == ERROR_NONE);
holder->device = nullptr;
}
void registerDevice(const std::shared_ptr<Device>& device) {
auto scoped_mutex = mutex.asScopedLock();
scoped_mutex.lock();
if (device->getKernelDeviceHolder() == nullptr) {
// Kernel device
auto kernel_device_holder = createKernelDeviceHolder(device);
device->setKernelDeviceHolder(kernel_device_holder);
} else {
LOG_W(TAG, "Device %s with id %u was already registered", device->getName().c_str(), (unsigned)device->getId());
}
}
void deregisterDevice(const std::shared_ptr<Device>& device) {
auto scoped_mutex = mutex.asScopedLock();
scoped_mutex.lock();
// Kernel device
auto kernel_device_holder = device->getKernelDeviceHolder();
if (kernel_device_holder) {
destroyKernelDeviceHolder(kernel_device_holder);
device->setKernelDeviceHolder(nullptr);
} else {
LOG_W(TAG, "Device %s with id %u was not registered", device->getName().c_str(), (unsigned)device->getId());
}
}
template<typename R>
auto toVector(R&& range) {
using T = std::ranges::range_value_t<R>;
std::vector<T> result;
if constexpr (std::ranges::common_range<R>) {
result.reserve(std::ranges::distance(range));
}
std::ranges::copy(range, std::back_inserter(result));
return result;
}
std::vector<std::shared_ptr<Device>> findDevices(const std::function<bool(const std::shared_ptr<Device>&)>& filterFunction) {
auto scoped_mutex = mutex.asScopedLock();
scoped_mutex.lock();
auto devices_view = getDevices() | std::views::filter([&filterFunction](auto& device) {
return filterFunction(device);
});
return toVector(devices_view);
}
std::shared_ptr<Device> findDevice(const std::function<bool(const std::shared_ptr<Device>&)>& filterFunction) {
auto scoped_mutex = mutex.asScopedLock();
scoped_mutex.lock();
auto result_set = getDevices() | std::views::filter([&filterFunction](auto& device) {
return filterFunction(device);
});
if (!result_set.empty()) {
return result_set.front();
} else {
return nullptr;
}
}
std::shared_ptr<Device> findDevice(std::string name) {
return findDevice([&name](auto& device){
return device->getName() == name;
});
}
std::shared_ptr<Device> findDevice(Device::Id id) {
return findDevice([id](auto& device){
return device->getId() == id;
});
}
std::vector<std::shared_ptr<Device>> findDevices(Device::Type type) {
return findDevices([type](auto& device) {
return device->getType() == type;
});
}
std::vector<std::shared_ptr<Device>> getDevices() {
std::vector<std::shared_ptr<Device>> devices;
device_for_each_of_type(&HAL_DEVICE_TYPE, &devices ,[](auto* kernelDevice, auto* context) {
auto devices_ptr = static_cast<std::vector<std::shared_ptr<Device>>*>(context);
auto hal_device = hal_device_get_device(kernelDevice);
(*devices_ptr).push_back(hal_device);
return true;
});
return devices;
}
bool hasDevice(Device::Type type) {
auto scoped_mutex = mutex.asScopedLock();
scoped_mutex.lock();
auto result_set = getDevices() | std::views::filter([&type](auto& device) {
return device->getType() == type;
});
return !result_set.empty();
}
}

View File

@ -1,19 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver hal_device_driver;
static Driver* const hal_device_drivers[] = {
&hal_device_driver,
nullptr
};
Module hal_device_module = {
.name = "hal-device",
.drivers = hal_device_drivers
};
}

View File

@ -13,6 +13,8 @@
#include <tactility/lvgl_module.h>
extern struct LvglModuleConfig lvgl_module_config;
extern void lvgl_devices_attach();
extern void lvgl_devices_detach();
// Mutex for LVGL drawing
static struct RecursiveMutex lvgl_mutex;
@ -71,6 +73,9 @@ static void lvgl_task(void* arg) {
check(!lvgl_task_is_interrupt_requested());
// Must run from this task (like on_start below), otherwise the display doesn't work.
lvgl_devices_attach();
// on_start must be called from the task, otherwise the display doesn't work
if (lvgl_module_config.on_start) lvgl_module_config.on_start();
@ -89,6 +94,8 @@ static void lvgl_task(void* arg) {
if (lvgl_module_config.on_stop) lvgl_module_config.on_stop();
lvgl_devices_detach();
task_lock();
lvgl_task_handle = NULL;
task_unlock();

View File

@ -7,12 +7,11 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
list(APPEND REQUIRES_LIST
TactilityKernel
TactilityFreeRtos
hal-device-module
lvgl-module
crypt-module
gps-module
lv_screenshot
minitar
minmea
)
if (DEFINED ENV{ESP_IDF_VERSION})

View File

@ -1,36 +0,0 @@
#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,135 +0,0 @@
#pragma once
#include <tactility/hal/Device.h>
#include "GpsConfiguration.h"
#include "Satellites.h"
#include <Tactility/Thread.h>
#include <Tactility/RecursiveMutex.h>
#include <minmea.h>
#include <utility>
namespace tt::hal::gps {
enum class GpsResponse {
None,
NotAck,
FrameErrors,
Ok,
};
class GpsDevice : public Device {
public:
typedef int GgaSubscriptionId;
typedef int RmcSubscriptionId;
enum class State {
PendingOn,
On,
Error,
PendingOff,
Off
};
private:
struct GgaSubscription {
GgaSubscriptionId id;
std::shared_ptr<std::function<void(Device::Id id, const minmea_sentence_gga&)>> onData;
};
struct RmcSubscription {
RmcSubscriptionId id;
std::shared_ptr<std::function<void(Device::Id id, const minmea_sentence_rmc&)>> onData;
};
::Device* uartDevice;
uint32_t baudRate;
GpsModel model;
RecursiveMutex mutex;
std::unique_ptr<Thread> thread;
bool threadInterrupted = false;
std::vector<GgaSubscription> ggaSubscriptions;
std::vector<RmcSubscription> rmcSubscriptions;
GgaSubscriptionId lastSatelliteSubscriptionId = 0;
RmcSubscriptionId lastRmcSubscriptionId = 0;
State state = State::Off;
int32_t threadMain();
bool isThreadInterrupted() const;
void setState(State newState);
public:
explicit GpsDevice(
::Device* uartDevice,
uint32_t baudRate,
GpsModel model // Choosing "Unknown" will result in a probe
) : uartDevice(uartDevice), baudRate(baudRate), model(model) {
assert(uartDevice != nullptr);
device_get(uartDevice);
};
~GpsDevice() override {
device_put(uartDevice);
}
Type getType() const override { return Type::Gps; }
std::string getName() const override {
if (model != GpsModel::Unknown) {
return toString(model);
} else {
return "Unknown GPS";
}
}
std::string getDescription() const override { return ""; }
bool start();
bool stop();
GgaSubscriptionId subscribeGga(const std::function<void(Device::Id deviceId, const minmea_sentence_gga&)>& onData) {
auto lock = mutex.asScopedLock();
lock.lock();
ggaSubscriptions.push_back({
.id = ++lastSatelliteSubscriptionId,
.onData = std::make_shared<std::function<void(Device::Id, const minmea_sentence_gga&)>>(onData)
});
return lastSatelliteSubscriptionId;
}
void unsubscribeGga(GgaSubscriptionId subscriptionId) {
auto lock = mutex.asScopedLock();
lock.lock();
std::erase_if(ggaSubscriptions, [subscriptionId](auto& subscription) { return subscription.id == subscriptionId; });
}
RmcSubscriptionId subscribeRmc(const std::function<void(Device::Id deviceId, const minmea_sentence_rmc&)>& onData) {
auto lock = mutex.asScopedLock();
lock.lock();
rmcSubscriptions.push_back({
.id = ++lastRmcSubscriptionId,
.onData = std::make_shared<std::function<void(Device::Id, const minmea_sentence_rmc&)>>(onData)
});
return lastRmcSubscriptionId;
}
void unsubscribeRmc(RmcSubscriptionId subscriptionId) {
auto lock = mutex.asScopedLock();
lock.lock();
std::erase_if(rmcSubscriptions, [subscriptionId](auto& subscription) { return subscription.id == subscriptionId; });
}
GpsModel getModel() const;
State getState() const;
};
}

View File

@ -1,59 +0,0 @@
#pragma once
#include <Tactility/freertoscompat/RTOS.h>
#include <Tactility/RecursiveMutex.h>
#include <minmea.h>
#include <ranges>
#include <memory>
namespace tt::hal::gps {
/** Thread-safe storage of recent satellites */
class SatelliteStorage {
public:
static constexpr size_t recordCount = 32;
private:
struct SatelliteRecord {
minmea_sat_info data {
.nr = 0,
.elevation = 0,
.azimuth = 0,
.snr = 0
};
TickType_t lastUpdated = 0;
bool inUse = false;
};
RecursiveMutex mutex;
std::array<SatelliteRecord, recordCount> records;
uint16_t recycleTimeSeconds;
uint16_t recentTimeSeconds;
SatelliteRecord* findRecord(int number);
SatelliteRecord* findUnusedRecord();
SatelliteRecord* findRecordToRecycle();
/** Tries to find an existing record, otherwise return a free one, otherwise return the oldest active one */
SatelliteRecord* findWithFallback(int number);
public:
explicit SatelliteStorage(
uint16_t recycleTimeSeconds = 120,
uint16_t recentTimeSeconds = 60
) : recycleTimeSeconds(recycleTimeSeconds), recentTimeSeconds(recentTimeSeconds) {}
void notify(const minmea_sat_info& info);
void getRecords(const std::function<void(const minmea_sat_info&)>& onRecord) const;
};
} // namespace tt::hal::gps

View File

@ -1,73 +0,0 @@
#pragma once
#include <Tactility/PubSub.h>
#include <Tactility/Mutex.h>
#include <Tactility/RecursiveMutex.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 {
class GpsService final : public Service {
struct GpsDeviceRecord {
std::shared_ptr<hal::gps::GpsDevice> device = nullptr;
hal::gps::GpsDevice::GgaSubscriptionId satelliteSubscriptionId = -1;
hal::gps::GpsDevice::RmcSubscriptionId rmcSubscriptionId = -1;
};
minmea_sentence_rmc rmcRecord;
TickType_t rmcTime = 0;
minmea_sentence_gga ggaRecord;
TickType_t ggaTime = 0;
RecursiveMutex mutex;
Mutex stateMutex;
std::vector<GpsDeviceRecord> deviceRecords;
std::shared_ptr<PubSub<State>> statePubSub = std::make_shared<PubSub<State>>();
std::unique_ptr<ServicePaths> paths;
State state = State::Off;
bool startGpsDevice(GpsDeviceRecord& deviceRecord);
static bool stopGpsDevice(GpsDeviceRecord& deviceRecord);
/** return nullptr when not found */
GpsDeviceRecord* findGpsRecord(const std::shared_ptr<hal::gps::GpsDevice>& record);
void onGgaSentence(hal::Device::Id deviceId, const minmea_sentence_gga& gga);
void onRmcSentence(hal::Device::Id deviceId, const minmea_sentence_rmc& rmc);
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:
bool onStart(ServiceContext &serviceContext) override;
void onStop(ServiceContext &serviceContext) override;
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();
State getState() const;
bool hasCoordinates() const;
bool getCoordinates(minmea_sentence_rmc& rmc) const;
bool getGga(minmea_sentence_gga& gga) const;
/** @return GPS service pubsub that broadcasts State* objects */
std::shared_ptr<PubSub<State>> getStatePubsub() const { return statePubSub; }
};
std::shared_ptr<GpsService> findGpsService();
} // tt::service::gps

View File

@ -1,12 +0,0 @@
#pragma once
namespace tt::service::gps {
enum class State {
OnPending,
On,
OffPending,
Off
};
}

View File

@ -1,10 +0,0 @@
#pragma once
#include <minmea.h>
namespace tt::hal::gps {
/** @return true when the input float is valid (contains non-zero values) */
inline bool isValid(const minmea_float& inFloat) { return inFloat.value != 0 && inFloat.scale != 0; }
}

View File

@ -1,14 +0,0 @@
#pragma once
#include "Tactility/hal/gps/GpsDevice.h"
struct Device;
namespace tt::hal::gps {
/**
* Init sequence on UART for a specific GPS model.
*/
bool init(::Device* uart, GpsModel type);
}

View File

@ -1,9 +0,0 @@
#pragma once
struct Device;
namespace tt::hal::gps {
GpsModel probe(::Device* uart);
}

View File

@ -1,21 +0,0 @@
#pragma once
#include "Tactility/hal/gps/GpsDevice.h"
#include <cstdint>
#include <cstddef>
struct Device;
namespace tt::hal::gps::ublox {
void checksum(uint8_t* message, size_t length);
// From https://github.com/meshtastic/firmware/blob/7648391f91f2b84e367ae2b38220b30936fb45b1/src/gps/GPS.cpp#L128
uint8_t makePacket(uint8_t classId, uint8_t messageId, const uint8_t* payload, uint8_t payloadSize, uint8_t* bufferOut);
GpsModel probe(::Device* uart);
bool init(::Device* uart, GpsModel model);
}

View File

@ -1,7 +1,6 @@
#include "Tactility/MountPoints.h"
#include "Tactility/TactilityConfig.h"
#include <tactility/hal/Device.h>
#include <Tactility/file/File.h>

View File

@ -29,7 +29,7 @@
#include <tactility/drivers/rtc.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/hal_device_module.h>
#include <tactility/gps_service.h>
#include <tactility/kernel_init.h>
#include <tactility/log.h>
#include <tactility/lvgl_module.h>
@ -74,7 +74,6 @@ bool MainDispatcher::dispatch(Function function, TickType_t timeout) const {
namespace service {
// Primary
namespace audio { extern const ServiceManifest manifest; }
namespace gps { extern const ServiceManifest manifest; }
namespace wifi { extern const ServiceManifest manifest; }
#ifdef ESP_PLATFORM
namespace development { extern const ServiceManifest manifest; }
@ -319,7 +318,6 @@ static void registerAndStartPrimaryServices() {
if (device_exists_of_type(&AUDIO_STREAM_TYPE)) {
addService(service::audio::manifest);
}
addService(service::gps::manifest);
addService(service::wifi::manifest);
#ifdef ESP_PLATFORM
addService(service::development::manifest);
@ -372,12 +370,12 @@ void run(Module* dtsModules[], DtsDevice dtsDevices[]) {
return;
}
// hal-device-module
check(module_construct_add_start(&hal_device_module) == ERROR_NONE);
// crypt-module
check(module_construct_add_start(&crypt_module) == ERROR_NONE);
// gps-module
check(module_construct_add_start(&gps_module) == ERROR_NONE);
#ifdef ESP_PLATFORM
initEsp();
#endif
@ -408,7 +406,7 @@ void run(Module* dtsModules[], DtsDevice dtsDevices[]) {
});
check(module_construct(&lvgl_module) == ERROR_NONE);
check(module_add(&lvgl_module) == ERROR_NONE);
module_start(&lvgl_module);
check(module_start(&lvgl_module) == ERROR_NONE);
registerAndStartSecondaryServices();

View File

@ -4,7 +4,6 @@
#include <Tactility/app/AppRegistration.h>
#include <Tactility/file/File.h>
#include <Tactility/file/FileLock.h>
#include <tactility/hal/Device.h>
#include <Tactility/Paths.h>
#include <cerrno>

View File

@ -1,12 +1,13 @@
#include <Tactility/StringUtils.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/hal/gps/GpsDevice.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/gps/GpsService.h>
#include "tactility/drivers/uart_controller.h"
#include <tactility/drivers/gps.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/gps_service.h>
#include <cstring>
#include <lvgl.h>
#include <tactility/log.h>
@ -29,6 +30,26 @@ class AddGpsApp final : public App {
std::array<uint32_t, 6> baudRates = { 9600, 19200, 28800, 38400, 57600, 115200 };
const char* baudRatesDropdownValues = "9600\n19200\n28800\n38400\n57600\n115200";
struct DuplicateCheckContext {
const char* uartName;
bool found;
};
static void onCheckDuplicateUart(const GpsConfiguration* configuration, size_t, void* context) {
auto* ctx = static_cast<DuplicateCheckContext*>(context);
if (strcmp(configuration->uart_name, ctx->uartName) == 0) {
ctx->found = true;
}
}
static std::vector<std::string> getModelNames() {
std::vector<std::string> result;
for (int model = GpsModel::GPS_MODEL_UNKNOWN; model <= GpsModel::GPS_MODEL_UC6580; model++) {
result.emplace_back(gps_model_to_string(static_cast<GpsModel>(model)));
}
return result;
}
static void onAddGpsCallback(lv_event_t* event) {
auto* app = (AddGpsApp*)lv_event_get_user_data(event);
app->onAddGps();
@ -37,38 +58,33 @@ class AddGpsApp final : public App {
void onAddGps() {
auto selected_baud_index = lv_dropdown_get_selected(baudDropdown);
auto new_configuration = hal::gps::GpsConfiguration {
.uartName = { 0x00 },
.baudRate = baudRates[selected_baud_index],
GpsConfiguration new_configuration = {
.uart_name = { 0x00 },
.baud_rate = 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)
.model = (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) {
lv_dropdown_get_selected_str(uartDropdown, new_configuration.uart_name, sizeof(new_configuration.uart_name));
if (new_configuration.uart_name[0] == 0x00) {
alertdialog::start("Error", "You must select a bus/uart.");
return;
}
LOG_I(TAG, "Saving: uart=%s, model=%d, baud=%u", new_configuration.uartName, (int)new_configuration.model, (unsigned)new_configuration.baudRate);
LOG_I(TAG, "Saving: uart=%s, model=%d, baud=%u", new_configuration.uart_name, (int)new_configuration.model, (unsigned)new_configuration.baud_rate);
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;
}
}
DuplicateCheckContext duplicate_check = { .uartName = new_configuration.uart_name, .found = false };
gps_service_for_each_configuration(&duplicate_check, onCheckDuplicateUart);
if (duplicate_check.found) {
auto message = std::string("Bus \"") + new_configuration.uart_name + "\" is already in use in another configuration";
app::alertdialog::start("Error", message.c_str());
return;
}
if (!service->addGpsConfiguration(new_configuration)) {
app::alertdialog::start("Error", "Failed to add configuration");
} else {
stop();
}
if (gps_service_add_configuration(&new_configuration) != ERROR_NONE) {
app::alertdialog::start("Error", "Failed to add configuration");
} else {
stop();
}
}
@ -137,7 +153,7 @@ public:
modelDropdown = lv_dropdown_create(model_wrapper);
auto model_names = hal::gps::getModels();
auto model_names = getModelNames();
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);

View File

@ -3,12 +3,12 @@
#include <Tactility/app/crashdiagnostics/QrHelpers.h>
#include <Tactility/app/crashdiagnostics/QrUrl.h>
#include <Tactility/app/launcher/Launcher.h>
#include <tactility/hal/Device.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/service/loader/Loader.h>
#include <lvgl.h>
#include <qrcode.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
namespace tt::app::crashdiagnostics {
@ -36,7 +36,7 @@ public:
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);
auto* bottom_label = lv_label_create(parent);
if (hal::hasDevice(hal::Device::Type::Touch)) {
if (device_has_active_by_type(&POINTER_TYPE)) {
lv_label_set_text(bottom_label, "Tap screen to continue");
} else {
lv_label_set_text(bottom_label, "Reboot device to continue");

View File

@ -5,10 +5,10 @@
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/service/gps/GpsState.h>
#include <Tactility/service/loader/Loader.h>
#include <tactility/drivers/gps.h>
#include <tactility/gps_service.h>
#include <tactility/log.h>
#include <tactility/lvgl_icon_shared.h>
@ -29,7 +29,6 @@ class GpsSettingsApp final : public App {
static constexpr auto* TAG = "GpsSettings";
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* statusLatitudeValue = nullptr;
@ -44,17 +43,6 @@ class GpsSettingsApp final : public App {
lv_obj_t* gpsConfigWrapper = nullptr;
lv_obj_t* addGpsWrapper = nullptr;
bool hasSetInfo = false;
PubSub<service::gps::State>::SubscriptionHandle serviceStateSubscription = nullptr;
std::shared_ptr<service::gps::GpsService> service;
void onServiceStateChanged() {
auto lock = lvgl::getSyncLock()->asScopedLock();
if (lock.lock(100 / portTICK_PERIOD_MS)) {
if (!updateTimerState()) {
updateViews();
}
}
}
static void onGpsToggledCallback(lv_event_t* event) {
auto* app = (GpsSettingsApp*)lv_event_get_user_data(event);
@ -70,22 +58,12 @@ class GpsSettingsApp final : public App {
app::start(addgps::manifest.appId);
}
void startReceivingUpdates() {
timer->start();
updateViews();
}
void stopReceivingUpdates() {
timer->stop();
updateViews();
}
void createInfoView(hal::gps::GpsModel model) {
void createInfoView(GpsModel model) {
auto* label = lv_label_create(infoContainerWidget);
if (model == hal::gps::GpsModel::Unknown) {
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
lv_label_set_text(label, "Model: auto-detect");
} else {
lv_label_set_text_fmt(label, "Model: %s", toString(model));
lv_label_set_text_fmt(label, "Model: %s", gps_model_to_string(model));
}
}
@ -98,21 +76,21 @@ class GpsSettingsApp final : public App {
// 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 = service::gps::findGpsService();
if (gps_service && gps_service->getGpsConfigurations(configurations)) {
LOG_I(TAG, "Found service and configs %d %d", index, (int)configurations.size());
if (index < configurations.size()) {
if (gps_service->removeGpsConfiguration(configurations[index])) {
app->updateViews();
} else {
alertdialog::start("Error", "Failed to remove configuration");
}
std::vector<GpsConfiguration> configurations;
gps_service_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) {
static_cast<std::vector<GpsConfiguration>*>(context)->push_back(*configuration);
});
if (index < (int)configurations.size()) {
if (gps_service_remove_configuration(&configurations[index]) == ERROR_NONE) {
app->updateViews();
} else {
alertdialog::start("Error", "Failed to remove configuration");
}
}
}
void createGpsView(const hal::gps::GpsConfiguration& configuration, int index) {
void createGpsView(const 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);
@ -129,16 +107,16 @@ class GpsSettingsApp final : public App {
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);
lv_label_set_text_fmt(uart_label, "UART: %s", configuration.uart_name);
auto* baud_label = lv_label_create(left_wrapper);
lv_label_set_text_fmt(baud_label, "Baud: %lu", configuration.baudRate);
lv_label_set_text_fmt(baud_label, "Baud: %lu", configuration.baud_rate);
auto* model_label = lv_label_create(left_wrapper);
if (configuration.model == hal::gps::GpsModel::Unknown) {
if (configuration.model == GpsModel::GPS_MODEL_UNKNOWN) {
lv_label_set_text(model_label, "Model: auto-detect");
} else {
lv_label_set_text_fmt(model_label, "Model: %s", toString(configuration.model));
lv_label_set_text_fmt(model_label, "Model: %s", gps_model_to_string(configuration.model));
}
// Right wrapper
@ -158,11 +136,11 @@ class GpsSettingsApp final : public App {
void updateViews() {
auto lock = lvgl::getSyncLock()->asScopedLock();
if (lock.lock(100 / portTICK_PERIOD_MS)) {
auto state = service->getState();
auto state = gps_service_get_state();
// Update toolbar
switch (state) {
case service::gps::State::OnPending:
case GpsServiceState::GPS_SERVICE_STATE_ON_PENDING:
LOG_D(TAG, "OnPending");
lv_obj_remove_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_state(switchWidget, LV_STATE_CHECKED);
@ -171,7 +149,7 @@ class GpsSettingsApp final : public App {
lv_obj_add_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
case service::gps::State::On:
case GpsServiceState::GPS_SERVICE_STATE_ON:
LOG_D(TAG, "On");
lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_state(switchWidget, LV_STATE_CHECKED);
@ -180,7 +158,7 @@ class GpsSettingsApp final : public App {
lv_obj_add_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
case service::gps::State::OffPending:
case GpsServiceState::GPS_SERVICE_STATE_OFF_PENDING:
LOG_D(TAG, "OffPending");
lv_obj_remove_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_state(switchWidget, LV_STATE_CHECKED);
@ -189,7 +167,7 @@ class GpsSettingsApp final : public App {
lv_obj_remove_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
case service::gps::State::Off:
case GpsServiceState::GPS_SERVICE_STATE_OFF:
LOG_D(TAG, "Off");
lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_state(switchWidget, LV_STATE_CHECKED);
@ -201,18 +179,17 @@ class GpsSettingsApp final : public App {
}
// Update status label and device info
if (state == service::gps::State::On) {
if (state == GpsServiceState::GPS_SERVICE_STATE_ON) {
if (!hasSetInfo) {
auto devices = hal::findDevices<hal::gps::GpsDevice>(hal::Device::Type::Gps);
for (auto& device : devices) {
createInfoView(device->getModel());
hasSetInfo = true;
}
gps_service_for_each_device(this, [](Device* device, void* context) {
static_cast<GpsSettingsApp*>(context)->createInfoView(gps_get_model(device));
});
hasSetInfo = true;
}
minmea_sentence_rmc rmc;
char buffer[64];
if (service->getCoordinates(rmc)) {
if (gps_service_get_coordinates(&rmc)) {
lv_label_set_text(statusLabelWidget, "Lock acquired");
lv_obj_set_style_text_color(statusLabelWidget, lv_color_hex(0x00ff00), 0);
@ -268,7 +245,7 @@ class GpsSettingsApp final : public App {
}
minmea_sentence_gga gga;
if (service->getGga(gga)) {
if (gps_service_get_gga(&gga)) {
float altitude = minmea_tofloat(&gga.altitude);
if (!isnan(altitude)) {
snprintf(buffer, sizeof(buffer), "%.1f m", altitude);
@ -296,13 +273,13 @@ class GpsSettingsApp final : public App {
if (!lv_obj_has_flag(gpsConfigWrapper, 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++);
}
std::vector<GpsConfiguration> configurations;
gps_service_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) {
static_cast<std::vector<GpsConfiguration>*>(context)->push_back(*configuration);
});
int index = 0;
for (auto& configuration : configurations) {
createGpsView(configuration, index++);
}
} else {
lv_obj_clean(gpsConfigWrapper);
@ -310,34 +287,20 @@ class GpsSettingsApp final : public App {
}
}
/** @return true if the views were updated */
bool updateTimerState() {
bool is_on = service->getState() == service::gps::State::On;
if (is_on && !timer->isRunning()) {
startReceivingUpdates();
return true;
} else if (!is_on && timer->isRunning()) {
stopReceivingUpdates();
return true;
} else {
return false;
}
}
void onGpsToggled(lv_event_t* event) {
bool wants_on = lv_obj_has_state(switchWidget, LV_STATE_CHECKED);
auto state = service->getState();
bool is_on = (state == service::gps::State::On) || (state == service::gps::State::OnPending);
auto state = gps_service_get_state();
bool is_on = (state == GpsServiceState::GPS_SERVICE_STATE_ON) || (state == GpsServiceState::GPS_SERVICE_STATE_ON_PENDING);
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([this] {
service->startReceiving();
gps_service_start_receiving();
});
} else {
getMainDispatcher().dispatch([this] {
service->stopReceiving();
gps_service_stop_receiving();
});
}
}
@ -368,10 +331,11 @@ class GpsSettingsApp final : public App {
public:
GpsSettingsApp() {
// Runs continuously while the screen is shown - there's no push notification for GPS
// service state changes, so this is the only way this screen finds out about them.
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::secondsToTicks(1), [this] {
updateViews();
});
service = service::gps::findGpsService();
}
void onShow(AppContext& app, lv_obj_t* parent) override {
@ -419,10 +383,6 @@ public:
statusHeadingValue = createInfoRow(infoContainerWidget, "Heading", lv_color_hex(0xff88ff));
statusSatellitesValue = createInfoRow(infoContainerWidget, "Satellites", lv_color_hex(0xffffff));
serviceStateSubscription = service->getStatePubsub()->subscribe([this](auto) {
onServiceStateChanged();
});
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);
@ -442,13 +402,12 @@ public:
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();
timer->start();
updateViews();
}
void onHide(AppContext& app) override {
service->getStatePubsub()->unsubscribe(serviceStateSubscription);
serviceStateSubscription = nullptr;
timer->stop();
}
};

View File

@ -1,49 +0,0 @@
#include "Tactility/hal/gps/GpsConfiguration.h"
#include "Tactility/service/gps/GpsService.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

@ -1,191 +0,0 @@
#include <Tactility/hal/gps/GpsDevice.h>
#include <Tactility/hal/gps/GpsInit.h>
#include <Tactility/hal/gps/Probe.h>
#include <tactility/log.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <minmea.h>
namespace tt::hal::gps {
constexpr uint32_t GPS_UART_BUFFER_SIZE = 256;
constexpr auto* TAG = "GpsDevice";
int32_t GpsDevice::threadMain() {
uint8_t buffer[GPS_UART_BUFFER_SIZE];
UartConfig uartConfig = {
.baud_rate = baudRate,
.data_bits = UART_CONTROLLER_DATA_8_BITS,
.parity = UART_CONTROLLER_PARITY_DISABLE,
.stop_bits = UART_CONTROLLER_STOP_BITS_1
};
error_t error = uart_controller_set_config(uartDevice, &uartConfig);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to configure UART %s: %s", uartDevice->name, error_to_string(error));
return -1;
}
error = uart_controller_open(uartDevice);
if (error != ERROR_NONE) {
LOG_E(TAG, "Failed to open UART %s: %s", uartDevice->name, error_to_string(error));
return -1;
}
if (model == GpsModel::Unknown) {
model = probe(uartDevice);
if (model == GpsModel::Unknown) {
LOG_E(TAG, "Probe failed");
setState(State::Error);
return -1;
}
}
mutex.lock();
mutex.unlock();
if (!init(uartDevice, model)) {
LOG_E(TAG, "Init failed");
setState(State::Error);
return -1;
}
setState(State::On);
// Reference: https://gpsd.gitlab.io/gpsd/NMEA.html
while (!isThreadInterrupted()) {
size_t bytes_read = 0;
uart_controller_read_until(uartDevice, buffer, GPS_UART_BUFFER_SIZE, '\n', true, &bytes_read, 100 / portTICK_PERIOD_MS);
// Thread might've been interrupted in the meanwhile
if (isThreadInterrupted()) {
break;
}
if (bytes_read > 0U) {
LOG_I(TAG, "[%d] %s", (int)bytes_read, reinterpret_cast<const char*>(buffer));
switch (minmea_sentence_id((char*)buffer, false)) {
case MINMEA_SENTENCE_RMC:
minmea_sentence_rmc rmc_frame;
if (minmea_parse_rmc(&rmc_frame, (char*)buffer)) {
mutex.lock();
for (auto& subscription : rmcSubscriptions) {
(*subscription.onData)(getId(), rmc_frame);
}
mutex.unlock();
LOG_D(TAG, "RMC %f lat, %f lon, %f m/s", minmea_tocoord(&rmc_frame.latitude), minmea_tocoord(&rmc_frame.longitude), minmea_tofloat(&rmc_frame.speed));
} else {
LOG_E(TAG, "RMC parse error: %s", reinterpret_cast<const char*>(buffer));
}
break;
case MINMEA_SENTENCE_GGA:
minmea_sentence_gga gga_frame;
if (minmea_parse_gga(&gga_frame, (char*)buffer)) {
mutex.lock();
for (auto& subscription : ggaSubscriptions) {
(*subscription.onData)(getId(), gga_frame);
}
mutex.unlock();
LOG_D(TAG, "GGA %f lat, %f lon", minmea_tocoord(&gga_frame.latitude), minmea_tocoord(&gga_frame.longitude));
} else {
LOG_E(TAG, "GGA parse error: %s", reinterpret_cast<const char*>(buffer));
}
break;
default:
break;
}
}
}
if (uart_controller_close(uartDevice) != ERROR_NONE) {
LOG_W(TAG, "Failed to stop UART %s", uartDevice->name);
}
return 0;
}
bool GpsDevice::start() {
auto lock = mutex.asScopedLock();
lock.lock();
if (thread != nullptr && thread->getState() != Thread::State::Stopped) {
LOG_W(TAG, "Already started");
return true;
}
threadInterrupted = false;
LOG_I(TAG, "Starting thread");
setState(State::PendingOn);
thread = std::make_unique<Thread>(
"gps",
4096,
[this]() {
return this->threadMain();
}
);
thread->setPriority(tt::Thread::Priority::High);
thread->start();
LOG_I(TAG, "Starting finished");
return true;
}
bool GpsDevice::stop() {
auto lock = mutex.asScopedLock();
lock.lock();
setState(State::PendingOff);
if (thread != nullptr) {
threadInterrupted = true;
// Detach thread, it will auto-delete when leaving the current scope
auto old_thread = std::move(thread);
if (old_thread->getState() != Thread::State::Stopped) {
// Unlock so thread can lock
lock.unlock();
// Wait for thread to finish
old_thread->join();
// Re-lock to continue logic below
lock.lock();
}
}
setState(State::Off);
return true;
}
bool GpsDevice::isThreadInterrupted() const {
auto lock = mutex.asScopedLock();
lock.lock();
return threadInterrupted;
}
GpsModel GpsDevice::getModel() const {
auto lock = mutex.asScopedLock();
lock.lock();
return model; // Make copy because of thread safety
}
GpsDevice::State GpsDevice::getState() const {
auto lock = mutex.asScopedLock();
lock.lock();
return state; // Make copy because of thread safety
}
void GpsDevice::setState(State newState) {
auto lock = mutex.asScopedLock();
lock.lock();
state = newState;
}
} // namespace tt::hal::gps

View File

@ -1,115 +0,0 @@
#include <Tactility/hal/gps/Satellites.h>
#include <Tactility/kernel/Kernel.h>
#include <tactility/log.h>
namespace tt::hal::gps {
constexpr auto* TAG = "Satellites";
constexpr bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) {
return (TickType_t)(now - timeInThePast) >= expireTimeInTicks;
}
SatelliteStorage::SatelliteRecord* SatelliteStorage::findRecord(int number) {
auto result = records | std::views::filter([number](auto& record) {
return record.inUse && record.data.nr == number;
});
if (!result.empty()) {
return &result.front();
} else {
return nullptr;
}
}
SatelliteStorage::SatelliteRecord* SatelliteStorage::findUnusedRecord() {
auto lock = mutex.asScopedLock();
lock.lock();
auto result = records | std::views::filter([](auto& record) {
return !record.inUse;
});
if (!result.empty()) {
auto* record = &result.front();
record->inUse = true;
LOG_D(TAG, "Found unused record");
return record;
} else {
return nullptr;
}
}
SatelliteStorage::SatelliteRecord* SatelliteStorage::findRecordToRecycle() {
auto lock = mutex.asScopedLock();
lock.lock();
int candidate_index = -1;
auto candidate_age = kernel::MAX_TICKS;
TickType_t expire_duration = kernel::secondsToTicks(recycleTimeSeconds);
TickType_t now = kernel::getTicks();
for (int i = 0; i < records.size(); ++i) {
// First try to find a record that is "old enough"
if (hasTimeElapsed(now, records[i].lastUpdated, expire_duration)) {
LOG_D(TAG, "! [%d] %u < %u", i, records[i].lastUpdated, expire_duration);
candidate_index = i;
break;
}
// Otherwise keep finding the oldest record
if (records[i].inUse && records[i].lastUpdated < candidate_age) {
candidate_index = i;
candidate_age = records[i].lastUpdated;
LOG_D(TAG, "? [%d] %u < %u", i, records[i].lastUpdated, candidate_age);
}
}
assert(candidate_index != -1);
LOG_D(TAG, "Recycled record %d", candidate_index);
return &records[candidate_index];
}
SatelliteStorage::SatelliteRecord* SatelliteStorage::findWithFallback(int number) {
auto lock = mutex.asScopedLock();
lock.lock();
if (auto* found_record = findRecord(number)) {
return found_record;
} else if (auto* unused_record = findUnusedRecord()) {
return unused_record;
} else {
return findRecordToRecycle();
}
}
void SatelliteStorage::notify(const minmea_sat_info& data) {
auto lock = mutex.asScopedLock();
lock.lock();
auto* record = findWithFallback(data.nr);
if (record != nullptr) {
record->inUse = true;
record->lastUpdated = kernel::getTicks();
record->data = data;
LOG_D(TAG, "Updated satellite %d: elevation %d, azimuth %d, snr %d", record->data.nr, record->data.elevation, record->data.elevation, record->data.snr);
}
}
void SatelliteStorage::getRecords(const std::function<void(const minmea_sat_info&)>& onRecord) const {
auto lock = mutex.asScopedLock();
lock.lock();
TickType_t expire_duration = kernel::secondsToTicks(recentTimeSeconds);
TickType_t now = kernel::getTicks();
for (auto& record: records) {
if (record.inUse && !hasTimeElapsed(now, record.lastUpdated, expire_duration)) {
onRecord(record.data);
}
}
}
} // namespace tt::hal::gps

View File

@ -1,119 +0,0 @@
#include <Tactility/file/ObjectFile.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/service/ServicePaths.h>
#include <cstring>
#include <unistd.h>
#include <tactility/log.h>
using tt::hal::gps::GpsDevice;
namespace tt::service::gps {
constexpr auto* TAG = "GpsService";
bool GpsService::getConfigurationFilePath(std::string& output) const {
if (paths == nullptr) {
LOG_E(TAG, "Can't add configuration: service not started");
return false;
}
if (!file::findOrCreateDirectory(paths->getUserDataDirectory(), 0777)) {
LOG_E(TAG, "Failed to find or create path %s", paths->getUserDataDirectory().c_str());
return false;
}
output = paths->getUserDataPath("config.bin");
return true;
}
bool GpsService::getGpsConfigurations(std::vector<hal::gps::GpsConfiguration>& configurations) const {
std::string path;
if (!getConfigurationFilePath(path)) {
return false;
}
// If file does not exist, return empty list
if (access(path.c_str(), F_OK) != 0) {
LOG_W(TAG, "No configurations (file not found: %s)", path.c_str());
return true;
}
LOG_I(TAG, "Reading configuration file %s", path.c_str());
auto reader = file::ObjectFileReader(path, sizeof(hal::gps::GpsConfiguration));
if (!reader.open()) {
LOG_E(TAG, "Failed to open configuration file");
return false;
}
hal::gps::GpsConfiguration configuration;
while (reader.hasNext()) {
if (!reader.readNext(&configuration)) {
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()) {
LOG_E(TAG, "Failed to open/create configuration file");
return false;
}
if (!appender.write(&configuration)) {
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)) {
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()) {
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,268 +0,0 @@
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/file/File.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <tactility/log.h>
using tt::hal::gps::GpsDevice;
namespace tt::service::gps {
constexpr auto* TAG = "GpsService";
extern const ServiceManifest manifest;
constexpr bool hasTimeElapsed(TickType_t now, TickType_t timeInThePast, TickType_t expireTimeInTicks) {
return (now - timeInThePast) >= expireTimeInTicks;
}
GpsService::GpsDeviceRecord* GpsService::findGpsRecord(const std::shared_ptr<GpsDevice>& device) {
auto lock = mutex.asScopedLock();
lock.lock();
auto result = std::views::filter(deviceRecords, [&device](auto& record) {
return record.device.get() == device.get();
});
if (!result.empty()) {
return &result.front();
} else {
return nullptr;
}
}
void GpsService::addGpsDevice(const std::shared_ptr<GpsDevice>& device) {
auto lock = mutex.asScopedLock();
lock.lock();
GpsDeviceRecord record = {.device = device};
if (getState() == State::On) { // Ignore during OnPending due to risk of data corruption
startGpsDevice(record);
}
deviceRecords.push_back(record);
}
void GpsService::removeGpsDevice(const std::shared_ptr<GpsDevice>& device) {
auto lock = mutex.asScopedLock();
lock.lock();
GpsDeviceRecord* record = findGpsRecord(device);
if (getState() == State::On) { // Ignore during OnPending due to risk of data corruption
stopGpsDevice(*record);
}
std::erase_if(deviceRecords, [&device](auto& reference) {
return reference.device.get() == device.get();
});
}
bool GpsService::onStart(ServiceContext& serviceContext) {
auto lock = mutex.asScopedLock();
lock.lock();
paths = serviceContext.getPaths();
return true;
}
void GpsService::onStop(ServiceContext& serviceContext) {
if (getState() == State::On) {
stopReceiving();
}
}
bool GpsService::startGpsDevice(GpsDeviceRecord& record) {
LOG_I(TAG, "[device %u] starting", (unsigned)record.device->getId());
auto lock = mutex.asScopedLock();
lock.lock();
auto device = record.device;
if (!device->start()) {
LOG_E(TAG, "[device %u] starting failed", (unsigned)record.device->getId());
return false;
}
record.satelliteSubscriptionId = device->subscribeGga([this](hal::Device::Id deviceId, auto& record) {
mutex.lock();
if (record.fix_quality > 0) {
ggaRecord = record;
ggaTime = kernel::getTicks();
}
onGgaSentence(deviceId, record);
mutex.unlock();
});
record.rmcSubscriptionId = device->subscribeRmc([this](hal::Device::Id deviceId, auto& record) {
mutex.lock();
if (record.longitude.value != 0 && record.longitude.scale != 0) {
rmcRecord = record;
rmcTime = kernel::getTicks();
}
onRmcSentence(deviceId, record);
mutex.unlock();
});
return true;
}
bool GpsService::stopGpsDevice(GpsDeviceRecord& record) {
LOG_I(TAG, "[device %u] stopping", (unsigned)record.device->getId());
auto device = record.device;
device->unsubscribeGga(record.satelliteSubscriptionId);
device->unsubscribeRmc(record.rmcSubscriptionId);
record.satelliteSubscriptionId = -1;
record.rmcSubscriptionId = -1;
if (!device->stop()) {
LOG_E(TAG, "[device %u] stopping failed", (unsigned)record.device->getId());
return false;
}
return true;
}
bool GpsService::startReceiving() {
LOG_I(TAG, "Start receiving");
if (getState() != State::Off) {
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)) {
LOG_E(TAG, "Failed to get GPS configurations");
setState(State::Off);
return false;
}
if (configurations.empty()) {
LOG_E(TAG, "No GPS configurations");
setState(State::Off);
return false;
}
for (const auto& configuration: configurations) {
::Device* uart_device;
if (device_get_by_name(configuration.uartName, &uart_device) == ERROR_NONE) {
auto gps_device = std::make_shared<GpsDevice>(
uart_device,
configuration.baudRate,
configuration.model
);
addGpsDevice(gps_device);
} else {
LOG_E(TAG, "Failed to find device %s", configuration.uartName);
}
}
// Reset times before starting devices to avoid race with incoming data
rmcTime = 0;
ggaTime = 0;
bool started_one_or_more = false;
for (auto& record: deviceRecords) {
started_one_or_more |= startGpsDevice(record);
}
if (started_one_or_more) {
setState(State::On);
return true;
} else {
setState(State::Off);
return false;
}
}
void GpsService::stopReceiving() {
LOG_I(TAG, "Stop receiving");
setState(State::OffPending);
auto lock = mutex.asScopedLock();
lock.lock();
for (auto& record: deviceRecords) {
stopGpsDevice(record);
}
rmcTime = 0;
ggaTime = 0;
setState(State::Off);
}
void GpsService::onGgaSentence(hal::Device::Id deviceId, const minmea_sentence_gga& gga) {
LOG_D(TAG, "[device %u] LAT %f LON %f, satellites: %d", (unsigned)deviceId, minmea_tocoord(&gga.latitude), minmea_tocoord(&gga.longitude), gga.satellites_tracked);
}
void GpsService::onRmcSentence(hal::Device::Id deviceId, const minmea_sentence_rmc& rmc) {
LOG_D(TAG, "[device %u] LAT %f LON %f, speed: %f", (unsigned)deviceId, minmea_tocoord(&rmc.latitude), minmea_tocoord(&rmc.longitude), minmea_tofloat(&rmc.speed));
}
State GpsService::getState() const {
auto lock = stateMutex.asScopedLock();
lock.lock();
return state;
}
void GpsService::setState(State newState) {
auto lock = stateMutex.asScopedLock();
lock.lock();
state = newState;
lock.unlock();
statePubSub->publish(state);
}
bool GpsService::hasCoordinates() const {
auto lock = mutex.asScopedLock();
lock.lock();
return getState() == State::On && rmcTime != 0 && !hasTimeElapsed(kernel::getTicks(), rmcTime, kernel::secondsToTicks(10));
}
bool GpsService::getCoordinates(minmea_sentence_rmc& rmc) const {
if (hasCoordinates()) {
rmc = rmcRecord;
return true;
} else {
return false;
}
}
bool GpsService::getGga(minmea_sentence_gga& gga) const {
auto lock = mutex.asScopedLock();
lock.lock();
if (getState() == State::On && ggaTime != 0 && !hasTimeElapsed(kernel::getTicks(), ggaTime, kernel::secondsToTicks(10))) {
gga = ggaRecord;
return true;
}
return false;
}
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>
};
} // namespace tt::service::gps

View File

@ -22,6 +22,7 @@ class KeyboardIdleService final : public Service {
bool keyboardDimmed = false;
settings::keyboard::KeyboardSettings cachedKeyboardSettings;
// TODO: This only works for the fist active keyboard. Update it so it works for all keyboards with a backlight.
static Device* getKeyboardBacklight() {
::Device* keyboard;
if (device_get_first_active_by_type(&KEYBOARD_TYPE, &keyboard) == ERROR_NONE) {
@ -31,13 +32,19 @@ class KeyboardIdleService final : public Service {
return backlight; // WARNING: did not increase refcount
}
// TODO: Remove after all drivers are migrated
return device_find_by_name("keyboard_backlight");
::Device* backlight;
if (device_get_by_name("keyboard_backlight", &backlight) != ERROR_NONE) {
return nullptr;
}
return backlight;
}
void setKeyboardBacklightBrightness(uint8_t brightness) {
Device* backlight = getKeyboardBacklight();
if (backlight != nullptr) {
backlight_set_brightness(backlight, brightness);
device_put(backlight);
}
}
@ -50,6 +57,9 @@ class KeyboardIdleService final : public Service {
if (lvgl_try_lock(100)) {
inactive_ms = lv_display_get_inactive_time(nullptr);
lvgl_unlock();
} else {
// Assume it's not used
inactive_ms = 100;
}
// Handle keyboard backlight

View File

@ -10,8 +10,8 @@
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/service/wifi/Wifi.h>
#include <tactility/gps_service.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/bluetooth.h>
@ -154,8 +154,8 @@ class StatusbarService final : public Service {
}
void updateGpsIcon() {
auto gps_state = gps::findGpsService()->getState();
bool show_icon = (gps_state == gps::State::OnPending) || (gps_state == gps::State::On);
auto gps_state = gps_service_get_state();
bool show_icon = (gps_state == GpsServiceState::GPS_SERVICE_STATE_ON_PENDING) || (gps_state == GpsServiceState::GPS_SERVICE_STATE_ON);
if (gps_last_state != show_icon) {
if (show_icon) {
lvgl::statusbar_icon_set_image(gps_icon_id, LVGL_ICON_STATUSBAR_LOCATION_ON);

View File

@ -11,7 +11,6 @@
#include <Tactility/Mutex.h>
#include <Tactility/TactilityConfig.h>
#include <tactility/hal/Device.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/App.h>

View File

@ -3,7 +3,6 @@
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/Paths.h>
#include <tactility/hal/Device.h>
#include <map>
#include <string>

View File

@ -388,7 +388,7 @@ error_t device_get_first_by_type(const struct DeviceType* type, struct Device**
* @param[in] type non-null device type pointer
* @param[out] out_device receives the found device on success; untouched on failure
* @retval ERROR_NOT_FOUND if no started device of that type exists
* @retval ERROR_NONE on success; caller must call device_put(*out_device) exactly once
* @retval ERROR_NONE if a started device of that type exists; must call device_put() exactly once afterwards.
*/
error_t device_get_first_active_by_type(const struct DeviceType* type, struct Device** out_device);
@ -397,7 +397,7 @@ error_t device_get_first_active_by_type(const struct DeviceType* type, struct De
*
* @param[in] type non-null device type pointer
* @retval ERROR_NOT_FOUND if no started device of that type exists
* @retval ERROR_NONE on success; caller must call device_put(*out_device) exactly once
* @retval ERROR_NONE if a started device of that type exists
*/
bool device_has_active_by_type(const struct DeviceType* type);

View File

@ -40,6 +40,7 @@ struct KeyboardApi {
/**
* @brief Returns the baclight if the keyboard has one.
* @warning Returns a referenced device. Must call device_put() afterwards.
* @param[in] device the keyboard device
* @param[out] backlight_device the output backlight device
* @retval ERROR_NONE when the backlight_device was set
@ -55,6 +56,7 @@ error_t keyboard_read_key(struct Device* device, struct KeyboardKeyData* data);
/**
* @brief Returns the backlight if the keyboard has one.
* @warning Returns a referenced device. Must call device_put() afterwards.
* @param[in] device the keyboard device
* @param[out] backlight_device the output backlight device
* @retval ERROR_NONE when the backlight_device was set

View File

@ -14,9 +14,9 @@ target_link_libraries(TactilityTests PRIVATE
Tactility
TactilityKernel
platform-posix
hal-device-module
lvgl-module
crypt-module
gps-module
lvgl
SDL2::SDL2-static SDL2-static
)

View File

@ -1,98 +0,0 @@
#include "doctest.h"
#include <tactility/hal/Device.h>
#include <utility>
using namespace tt;
class TestDevice final : public hal::Device {
private:
hal::Device::Type type;
std::string name;
std::string description;
public:
TestDevice(hal::Device::Type type, std::string name, std::string description) :
type(type),
name(std::move(name)),
description(std::move(description))
{}
TestDevice() : TestDevice(hal::Device::Type::Power, "PowerMock", "PowerMock description") {}
~TestDevice() final = default;
Type getType() const final { return type; }
std::string getName() const final { return name; }
std::string getDescription() const final { return description; }
};
class DeviceAutoRegistration {
std::shared_ptr<hal::Device> device;
public:
explicit DeviceAutoRegistration(std::shared_ptr<hal::Device> inDevice) : device(std::move(inDevice)) {
hal::registerDevice(device);
}
~DeviceAutoRegistration() {
hal::deregisterDevice(device);
}
};
/** We add 3 tests into 1 to ensure cleanup happens */
TEST_CASE("registering and deregistering a device works") {
auto device = std::make_shared<TestDevice>();
// Pre-registration
CHECK_EQ(hal::findDevice(device->getId()), nullptr);
// Registration
hal::registerDevice(device);
auto found_device = hal::findDevice(device->getId());
CHECK_NE(found_device, nullptr);
CHECK_EQ(found_device->getId(), device->getId());
// Deregistration
hal::deregisterDevice(device);
CHECK_EQ(hal::findDevice(device->getId()), nullptr);
found_device = nullptr; // to decrease use count
CHECK_EQ(device.use_count(), 1);
}
TEST_CASE("find device by id") {
auto device = std::make_shared<TestDevice>();
DeviceAutoRegistration auto_registration(device);
auto found_device = hal::findDevice(device->getId());
CHECK_NE(found_device, nullptr);
CHECK_EQ(found_device->getId(), device->getId());
}
TEST_CASE("find device by name") {
auto device = std::make_shared<TestDevice>();
DeviceAutoRegistration auto_registration(device);
auto found_device = hal::findDevice(device->getName());
CHECK_NE(found_device, nullptr);
CHECK_EQ(found_device->getId(), device->getId());
}
TEST_CASE("find device by type") {
// Headless mode shouldn't have a display, so we want to create one to find only our own display as unique device
// We first verify the initial assumption that there is no display:
auto unexpected_display = hal::findFirstDevice<TestDevice>(hal::Device::Type::Display);
CHECK_EQ(unexpected_display, nullptr);
auto device = std::make_shared<TestDevice>(hal::Device::Type::Display, "DisplayMock", "");
DeviceAutoRegistration auto_registration(device);
auto found_device = hal::findFirstDevice<TestDevice>(hal::Device::Type::Display);
CHECK_NE(found_device, nullptr);
CHECK_EQ(found_device->getId(), device->getId());
}

View File

@ -7,7 +7,6 @@
#include <tactility/check.h>
#include <tactility/dts.h>
#include <tactility/hal_device_module.h>
#include <tactility/kernel_init.h>
typedef struct {
@ -29,7 +28,7 @@ void test_task(void* parameter) {
// overrides
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
Module* dts_modules[] = { &platform_posix_module, &hal_device_module, nullptr };
Module* dts_modules[] = { &platform_posix_module, nullptr };
DtsDevice dts_devices[] = { DTS_DEVICE_TERMINATOR };
check(kernel_init(dts_modules, dts_devices) == ERROR_NONE);