mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Location services, kernel Device improvements, de-incubate devices, and more (#691)
**New Features** * Added location services with GPS and network-based positioning, approximate or precise accuracy, and live status updates. * Added a Location Settings app to view status, control location access and accuracy, and manage GPS devices. * Added location choices during device setup and a location toggle in the system menu. * Added support for thread stacks in external memory when available, with fallback to existing memory options. **Improvements** * Wi-Fi and Bluetooth enabled states are saved when changed and applied at startup; separate “Enable on boot” switches are no longer shown in their management screens. * GPS setup can stop promptly when interrupted. * Improved device removal handling during startup and while references are outstanding. * Updated several device listings to no longer be marked as incubating.
This commit is contained in:
parent
3a7db7a801
commit
6e437d3288
@ -173,6 +173,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
|||||||
add_subdirectory(Modules/gps-module)
|
add_subdirectory(Modules/gps-module)
|
||||||
add_subdirectory(Modules/graphics-module)
|
add_subdirectory(Modules/graphics-module)
|
||||||
add_subdirectory(Modules/http-module)
|
add_subdirectory(Modules/http-module)
|
||||||
|
add_subdirectory(Modules/location-module)
|
||||||
add_subdirectory(Modules/mbedtls-module)
|
add_subdirectory(Modules/mbedtls-module)
|
||||||
add_subdirectory(Modules/mesh-module)
|
add_subdirectory(Modules/mesh-module)
|
||||||
add_subdirectory(Modules/mesh-meshcore-module)
|
add_subdirectory(Modules/mesh-meshcore-module)
|
||||||
@ -203,6 +204,11 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
|
|||||||
TARGET_DIRECTORY freertos_kernel
|
TARGET_DIRECTORY freertos_kernel
|
||||||
PROPERTIES COMPILE_DEFINITIONS "xTaskCreate=freertos_real_xTaskCreate;vTaskDelete=freertos_real_vTaskDelete"
|
PROPERTIES COMPILE_DEFINITIONS "xTaskCreate=freertos_real_xTaskCreate;vTaskDelete=freertos_real_vTaskDelete"
|
||||||
)
|
)
|
||||||
|
# vPortCancelThread() is wrapped by freertos_task_hooks.c, see its header
|
||||||
|
set_source_files_properties(Libraries/FreeRTOS-Kernel/portable/ThirdParty/GCC/Posix/port.c
|
||||||
|
TARGET_DIRECTORY freertos_kernel_port
|
||||||
|
PROPERTIES COMPILE_DEFINITIONS "vPortCancelThread=freertos_real_vPortCancelThread"
|
||||||
|
)
|
||||||
target_sources(freertos_kernel PRIVATE Platforms/platform-posix/freertos/freertos_task_hooks.c)
|
target_sources(freertos_kernel PRIVATE Platforms/platform-posix/freertos/freertos_task_hooks.c)
|
||||||
target_include_directories(freertos_kernel PUBLIC Platforms/platform-posix/freertos)
|
target_include_directories(freertos_kernel PUBLIC Platforms/platform-posix/freertos)
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=LilyGO
|
general.vendor=LilyGO
|
||||||
general.name=T5 4.7 Inch E-Paper S3
|
general.name=T5 4.7 Inch E-Paper S3
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
hardware.flashSize=16MB
|
hardware.flashSize=16MB
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
general.vendor=LilyGO
|
general.vendor=LilyGO
|
||||||
general.name=T-Deck Max
|
general.name=T-Deck Max
|
||||||
# SD card support is not yet working, display driver is untested since refactoring; remove once it is functional.
|
# SD card support is not yet working, display driver is untested since refactoring; remove once it is functional.
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
hardware.flashSize=16MB
|
hardware.flashSize=16MB
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
general.vendor=LilyGO
|
general.vendor=LilyGO
|
||||||
general.name=T-Deck Pro
|
general.name=T-Deck Pro
|
||||||
# The display driver is not reliable yet
|
# The display driver is not reliable yet
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
hardware.flashSize=16MB
|
hardware.flashSize=16MB
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=LilyGO
|
general.vendor=LilyGO
|
||||||
general.name=T-Display
|
general.name=T-Display
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
apps.autoStartAppId=tactility.apwebserver
|
apps.autoStartAppId=tactility.apwebserver
|
||||||
|
|
||||||
hardware.target=ESP32
|
hardware.target=ESP32
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=LilyGO
|
general.vendor=LilyGO
|
||||||
general.name=T-Dongle S3
|
general.name=T-Dongle S3
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
apps.autoStartAppId=tactility.apwebserver
|
apps.autoStartAppId=tactility.apwebserver
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=M5Stack
|
general.vendor=M5Stack
|
||||||
general.name=PaperS3
|
general.name=PaperS3
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
|
|
||||||
hardware.target=esp32s3
|
hardware.target=esp32s3
|
||||||
hardware.flashSize=16MB
|
hardware.flashSize=16MB
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=Waveshare
|
general.vendor=Waveshare
|
||||||
general.name=ESP32 S3 GEEK
|
general.name=ESP32 S3 GEEK
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
apps.autoStartAppId=tactility.apwebserver
|
apps.autoStartAppId=tactility.apwebserver
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=WaveShare
|
general.vendor=WaveShare
|
||||||
general.name=S3 LCD 1.3"
|
general.name=S3 LCD 1.3"
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
apps.autoStartAppId=tactility.apwebserver
|
apps.autoStartAppId=tactility.apwebserver
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=WaveShare
|
general.vendor=WaveShare
|
||||||
general.name=S3 Touch LCD 1.28"
|
general.name=S3 Touch LCD 1.28"
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
apps.autoStartAppId=tactility.apwebserver
|
apps.autoStartAppId=tactility.apwebserver
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
general.vendor=WaveShare
|
general.vendor=WaveShare
|
||||||
general.name=S3 Touch LCD 1.47"
|
general.name=S3 Touch LCD 1.47"
|
||||||
general.incubating=true
|
general.incubating=false
|
||||||
|
|
||||||
hardware.target=ESP32S3
|
hardware.target=ESP32S3
|
||||||
hardware.flashSize=16MB
|
hardware.flashSize=16MB
|
||||||
|
|||||||
@ -7,11 +7,13 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## Higher Priority
|
||||||
|
|
||||||
- Create location service, which works with location providers. GPS is a location provider. IP is also a location provider (must have wifi connected)
|
- Fix Wi-Fi so it actually connects to something
|
||||||
|
- Support http/https on posix
|
||||||
|
- Vi apps are both named "Vi" in the App Hub
|
||||||
- Use GPS time to set/update the current time: when enabled, at boot, when wifi is not enabled, start the gps device and wait until a lock is acquired, then get the time and sync it. Then stop the GPS device.
|
- Use GPS time to set/update the current time: when enabled, at boot, when wifi is not enabled, start the gps device and wait until a lock is acquired, then get the time and sync it. Then stop the GPS device.
|
||||||
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project, should be sized based on DPI
|
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project, should be sized based on DPI
|
||||||
- Cursor is hard-coded, needs size based on DPI
|
- Cursor is hard-coded, needs size based on DPI
|
||||||
- Improve Setup: Show progress of some kind
|
- Improve Setup: Show progress of some kind. Put skip/next buttons closer? And add outline button style (update docs!)
|
||||||
- Files app should have a page indicator like Launcher.
|
- Files app should have a page indicator like Launcher.
|
||||||
- Move Bluetooth code from kernel to Modules/bluetooth-module, and have includes in path bluetooth/ then make a bluetooth_command.cpp like wifi_command.cpp
|
- Move Bluetooth code from kernel to Modules/bluetooth-module, and have includes in path bluetooth/ then make a bluetooth_command.cpp like wifi_command.cpp
|
||||||
- Inspect code for memory improvements: replace `std::*` with `tt::std::*`
|
- Inspect code for memory improvements: replace `std::*` with `tt::std::*`
|
||||||
@ -30,6 +32,8 @@
|
|||||||
|
|
||||||
## Medium Priority
|
## Medium Priority
|
||||||
|
|
||||||
|
- CL-32 display driver performance
|
||||||
|
- Move more window management code from Tactility/ to window-manager
|
||||||
- Implement signal/raise for terminal apps (for CTRL+C and window resize signals)
|
- Implement signal/raise for terminal apps (for CTRL+C and window resize signals)
|
||||||
- Build the simulator with `-fno-exceptions` to match ESP32. First make app exit() work without C++ unwinding there:
|
- Build the simulator with `-fno-exceptions` to match ESP32. First make app exit() work without C++ unwinding there:
|
||||||
scheduler.cpp's DeferredUnload relies on pthread_exit()'s forced unwind running its destructor.
|
scheduler.cpp's DeferredUnload relies on pthread_exit()'s forced unwind running its destructor.
|
||||||
|
|||||||
@ -3,9 +3,12 @@
|
|||||||
|
|
||||||
#include <gps/gps.h>
|
#include <gps/gps.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
struct Device;
|
struct Device;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends the init sequence for a specific, already-probed GPS model over uart.
|
* Sends the init sequence for a specific, already-probed GPS model over uart.
|
||||||
|
* @return false on failure or when interrupted
|
||||||
*/
|
*/
|
||||||
bool gps_init(Device* uart, GpsModel model);
|
bool gps_init(Device* uart, GpsModel model, const std::atomic<bool>& interrupted);
|
||||||
|
|||||||
@ -0,0 +1,11 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delays unless the GPS thread is interrupted.
|
||||||
|
* @return false when interrupted
|
||||||
|
*/
|
||||||
|
bool gps_delay_millis(const std::atomic<bool>& interrupted, uint32_t millis);
|
||||||
@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
#include <gps/gps.h>
|
#include <gps/gps.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
struct Device;
|
struct Device;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to auto-detect the GPS/GNSS chipset connected via uart.
|
* Attempts to auto-detect the GPS/GNSS chipset connected via uart.
|
||||||
* @return GPS_MODEL_UNKNOWN when no supported chipset responded
|
* @return GPS_MODEL_UNKNOWN when no supported chipset responded or when interrupted
|
||||||
*/
|
*/
|
||||||
GpsModel gps_probe(Device* uart);
|
GpsModel gps_probe(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <gps/gps.h>
|
#include <gps/gps.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
@ -15,8 +16,8 @@ void checksum(uint8_t* message, size_t length);
|
|||||||
// From https://github.com/meshtastic/firmware/blob/7648391f91f2b84e367ae2b38220b30936fb45b1/src/gps/GPS.cpp#L128
|
// 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);
|
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);
|
GpsModel probe(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
|
|
||||||
bool init(Device* uart, GpsModel model);
|
bool init(Device* uart, GpsModel model, const std::atomic<bool>& interrupted);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include <minmea.h>
|
#include <minmea.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib> // For calloc() in PC builds
|
#include <cstdlib> // For calloc() in PC builds
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ constexpr TickType_t GPS_THREAD_STOP_POLL_TICKS = pdMS_TO_TICKS(1000);
|
|||||||
struct GpsInternal {
|
struct GpsInternal {
|
||||||
RecursiveMutex mutex;
|
RecursiveMutex mutex;
|
||||||
Thread* thread;
|
Thread* thread;
|
||||||
volatile bool interrupt_requested;
|
std::atomic<bool> interrupt_requested;
|
||||||
GpsState state;
|
GpsState state;
|
||||||
// Mirrors GpsConfig::model, but overwritten with the autodetected model once probing succeeds.
|
// Mirrors GpsConfig::model, but overwritten with the autodetected model once probing succeeds.
|
||||||
GpsModel model;
|
GpsModel model;
|
||||||
@ -105,6 +106,31 @@ static bool is_interrupted(GpsInternal* internal) {
|
|||||||
|
|
||||||
// region Driver lifecycle
|
// region Driver lifecycle
|
||||||
|
|
||||||
|
// Returns false when probing or init failed, or when the thread was interrupted
|
||||||
|
static bool setup_receiver(GpsInternal* internal, Device* uart) {
|
||||||
|
GpsModel model = internal->model;
|
||||||
|
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
|
||||||
|
model = gps_probe(uart, internal->interrupt_requested);
|
||||||
|
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
|
||||||
|
if (!is_interrupted(internal)) {
|
||||||
|
LOG_E(TAG, "Probe failed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
recursive_mutex_lock(&internal->mutex);
|
||||||
|
internal->model = model;
|
||||||
|
recursive_mutex_unlock(&internal->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gps_init(uart, model, internal->interrupt_requested)) {
|
||||||
|
if (!is_interrupted(internal)) {
|
||||||
|
LOG_E(TAG, "Init failed");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static int32_t gps_thread_main(void* context) {
|
static int32_t gps_thread_main(void* context) {
|
||||||
auto* device = static_cast<Device*>(context);
|
auto* device = static_cast<Device*>(context);
|
||||||
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
|
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
|
||||||
@ -132,26 +158,12 @@ static int32_t gps_thread_main(void* context) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
GpsModel model = internal->model;
|
if (setup_receiver(internal, uart)) {
|
||||||
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);
|
set_state(internal, GpsState::GPS_STATE_ON);
|
||||||
|
} else if (!is_interrupted(internal)) {
|
||||||
|
set_state(internal, GpsState::GPS_STATE_ERROR);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
// Reference: https://gpsd.gitlab.io/gpsd/NMEA.html
|
// Reference: https://gpsd.gitlab.io/gpsd/NMEA.html
|
||||||
uint8_t buffer[GPS_UART_BUFFER_SIZE];
|
uint8_t buffer[GPS_UART_BUFFER_SIZE];
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
#include <gps_generic/private/cas_messages.h>
|
#include <gps_generic/private/cas_messages.h>
|
||||||
#include <gps_generic/private/init.h>
|
#include <gps_generic/private/init.h>
|
||||||
|
#include <gps_generic/private/interrupt.h>
|
||||||
#include <gps_generic/private/ublox.h>
|
#include <gps_generic/private/ublox.h>
|
||||||
#include <gps_generic/private/gps_response.h>
|
#include <gps_generic/private/gps_response.h>
|
||||||
|
|
||||||
#include <tactility/check.h>
|
#include <tactility/check.h>
|
||||||
#include <tactility/delay.h>
|
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/uart_controller.h>
|
#include <tactility/drivers/uart_controller.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
@ -15,12 +15,12 @@
|
|||||||
|
|
||||||
constexpr auto* TAG = "gps-meshtastic";
|
constexpr auto* TAG = "gps-meshtastic";
|
||||||
|
|
||||||
bool init_mtk(Device* uart);
|
bool init_mtk(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
bool init_mtk_l76b(Device* uart);
|
bool init_mtk_l76b(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
bool init_mtk_pa1616s(Device* uart);
|
bool init_mtk_pa1616s(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
bool init_atgm336h(Device* uart);
|
bool init_atgm336h(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
bool init_uc6580(Device* uart);
|
bool init_uc6580(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
bool init_ag33xx(Device* uart);
|
bool init_ag33xx(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
|
|
||||||
// region CAS
|
// region CAS
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ static uint8_t make_cas_packet(uint8_t* buffer, uint8_t class_id, uint8_t msg_id
|
|||||||
return (payload_size + 10);
|
return (payload_size + 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GpsResponse get_ack_cas(Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wait_millis) {
|
static GpsResponse get_ack_cas(Device* uart, const std::atomic<bool>& interrupted, uint8_t class_id, uint8_t msg_id, uint32_t wait_millis) {
|
||||||
uint32_t start_time = get_millis();
|
uint32_t start_time = get_millis();
|
||||||
uint8_t buffer[CAS_MESSAGE_ACK_NACK_SIZE] = {0};
|
uint8_t buffer[CAS_MESSAGE_ACK_NACK_SIZE] = {0};
|
||||||
uint8_t buffer_pos = 0;
|
uint8_t buffer_pos = 0;
|
||||||
@ -88,7 +88,7 @@ static GpsResponse get_ack_cas(Device* uart, uint8_t class_id, uint8_t msg_id, u
|
|||||||
// ACK-NACK| 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x00 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX |
|
// 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 |
|
// ACK-ACK | 0xBA | 0xCE | 0x04 | 0x00 | 0x05 | 0x01 | 0xXX | 0xXX | 0x00 | 0x00 | 0xXX | 0xXX | 0xXX | 0xXX |
|
||||||
|
|
||||||
while (get_ticks() - start_time < wait_ticks) {
|
while (get_ticks() - start_time < wait_ticks && !interrupted) {
|
||||||
size_t available = 0;
|
size_t available = 0;
|
||||||
uart_controller_get_available(uart, &available);
|
uart_controller_get_available(uart, &available);
|
||||||
if (available > 0) {
|
if (available > 0) {
|
||||||
@ -134,38 +134,38 @@ static GpsResponse get_ack_cas(Device* uart, uint8_t class_id, uint8_t msg_id, u
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
bool gps_init(Device* uart, GpsModel type) {
|
bool gps_init(Device* uart, GpsModel type, const std::atomic<bool>& interrupted) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case GPS_MODEL_UNKNOWN:
|
case GPS_MODEL_UNKNOWN:
|
||||||
check(false);
|
check(false);
|
||||||
case GPS_MODEL_AG3335:
|
case GPS_MODEL_AG3335:
|
||||||
case GPS_MODEL_AG3352:
|
case GPS_MODEL_AG3352:
|
||||||
return init_ag33xx(uart);
|
return init_ag33xx(uart, interrupted);
|
||||||
case GPS_MODEL_ATGM336H:
|
case GPS_MODEL_ATGM336H:
|
||||||
return init_atgm336h(uart);
|
return init_atgm336h(uart, interrupted);
|
||||||
case GPS_MODEL_LS20031:
|
case GPS_MODEL_LS20031:
|
||||||
return true;
|
return true;
|
||||||
case GPS_MODEL_MTK:
|
case GPS_MODEL_MTK:
|
||||||
return init_mtk(uart);
|
return init_mtk(uart, interrupted);
|
||||||
case GPS_MODEL_MTK_L76B:
|
case GPS_MODEL_MTK_L76B:
|
||||||
return init_mtk_l76b(uart);
|
return init_mtk_l76b(uart, interrupted);
|
||||||
case GPS_MODEL_MTK_PA1616S:
|
case GPS_MODEL_MTK_PA1616S:
|
||||||
return init_mtk_pa1616s(uart);
|
return init_mtk_pa1616s(uart, interrupted);
|
||||||
case GPS_MODEL_UBLOX6:
|
case GPS_MODEL_UBLOX6:
|
||||||
case GPS_MODEL_UBLOX7:
|
case GPS_MODEL_UBLOX7:
|
||||||
case GPS_MODEL_UBLOX8:
|
case GPS_MODEL_UBLOX8:
|
||||||
case GPS_MODEL_UBLOX9:
|
case GPS_MODEL_UBLOX9:
|
||||||
case GPS_MODEL_UBLOX10:
|
case GPS_MODEL_UBLOX10:
|
||||||
return gps_ublox::init(uart, type);
|
return gps_ublox::init(uart, type, interrupted);
|
||||||
case GPS_MODEL_UC6580:
|
case GPS_MODEL_UC6580:
|
||||||
return init_uc6580(uart);
|
return init_uc6580(uart, interrupted);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_I(TAG, "Init not implemented %d", static_cast<int>(type));
|
LOG_I(TAG, "Init not implemented %d", static_cast<int>(type));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_ag33xx(Device* uart) {
|
bool init_ag33xx(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR066,1,0,1,0,0,1*3B\r\n", 25, 250); // Enable GPS+GALILEO+NAVIC
|
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)
|
// Configure NMEA (sentences will output once per fix)
|
||||||
@ -177,47 +177,67 @@ bool init_ag33xx(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,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
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR062,6,0*38\r\n", 17, 250); // ZDA ON
|
||||||
|
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR513*3D\r\n", 13, 250); // save configuration
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PAIR513*3D\r\n", 13, 250); // save configuration
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_uc6580(Device* uart) {
|
bool init_uc6580(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
// The Unicore UC6580 can use a lot of sat systems, enable it to
|
// 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
|
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
|
||||||
// This will reset the receiver, so wait a bit afterwards
|
// This will reset the receiver, so wait a bit afterwards
|
||||||
// The paranoid will wait for the OK*04 confirmation response after each command.
|
// 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);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGSYS,h35155\r\n", 16, 250);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Must be done after the CFGSYS command
|
// 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.
|
// 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);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,0,3,0\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Turn off GSA messages, TinyGPS++ doesn't use this message.
|
// 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);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,0,2,0\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Turn off NOTICE __TXT messages, these may provide Unicore some info but we don't care.
|
// 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);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,6,0,0\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,6,1,0\r\n", 15, 250);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,6,1,0\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_atgm336h(Device* uart) {
|
bool init_atgm336h(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
uint8_t buffer[256];
|
uint8_t buffer[256];
|
||||||
|
|
||||||
// Set the intial configuration of the device - these _should_ work for most AT6558 devices
|
// Set the intial configuration of the device - these _should_ work for most AT6558 devices
|
||||||
int msglen = make_cas_packet(buffer, 0x06, 0x07, sizeof(CAS_MESSAGE_CFG_NAVX_CONF), CAS_MESSAGE_CFG_NAVX_CONF);
|
int msglen = make_cas_packet(buffer, 0x06, 0x07, sizeof(CAS_MESSAGE_CFG_NAVX_CONF), CAS_MESSAGE_CFG_NAVX_CONF);
|
||||||
uart_controller_write_bytes(uart, buffer, msglen, 250);
|
uart_controller_write_bytes(uart, buffer, msglen, 250);
|
||||||
if (get_ack_cas(uart, 0x06, 0x07, 250) != GpsResponse::Ok) {
|
if (get_ack_cas(uart, interrupted, 0x06, 0x07, 250) != GpsResponse::Ok) {
|
||||||
|
if (interrupted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
LOG_W(TAG, "ATGM336H: Could not set Config");
|
LOG_W(TAG, "ATGM336H: Could not set Config");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the update frequence to 1Hz
|
// Set the update frequence to 1Hz
|
||||||
msglen = make_cas_packet(buffer, 0x06, 0x04, sizeof(CAS_MESSAGE_CFG_RATE_1HZ), CAS_MESSAGE_CFG_RATE_1HZ);
|
msglen = make_cas_packet(buffer, 0x06, 0x04, sizeof(CAS_MESSAGE_CFG_RATE_1HZ), CAS_MESSAGE_CFG_RATE_1HZ);
|
||||||
uart_controller_write_bytes(uart, buffer, msglen, 250);
|
uart_controller_write_bytes(uart, buffer, msglen, 250);
|
||||||
if (get_ack_cas(uart, 0x06, 0x04, 250) != GpsResponse::Ok) {
|
if (get_ack_cas(uart, interrupted, 0x06, 0x04, 250) != GpsResponse::Ok) {
|
||||||
|
if (interrupted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
LOG_W(TAG, "ATGM336H: Could not set Update Frequency");
|
LOG_W(TAG, "ATGM336H: Could not set Update Frequency");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,60 +249,85 @@ bool init_atgm336h(Device* uart) {
|
|||||||
uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00};
|
uint8_t cas_cfg_msg_packet[] = {0x4e, fields[i], 0x01, 0x00};
|
||||||
msglen = make_cas_packet(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);
|
uart_controller_write_bytes(uart, buffer, msglen, 250);
|
||||||
if (get_ack_cas(uart, 0x06, 0x01, 250) != GpsResponse::Ok) {
|
if (get_ack_cas(uart, interrupted, 0x06, 0x01, 250) != GpsResponse::Ok) {
|
||||||
|
if (interrupted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
LOG_W(TAG, "ATGM336H: Could not enable NMEA MSG: %u", fields[i]);
|
LOG_W(TAG, "ATGM336H: Could not enable NMEA MSG: %u", fields[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_mtk_pa1616s(Device* uart) {
|
bool init_mtk_pa1616s(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
// PA1616S is used in some GPS breakout boards from Adafruit
|
// PA1616S is used in some GPS breakout boards from Adafruit
|
||||||
// PA1616S does not have GLONASS capability. PA1616D does, but is not implemented here.
|
// 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);
|
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
|
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||||
delay_millis(1000);
|
if (!gps_delay_millis(interrupted, 1000)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Only ask for RMC and GGA (GNRMC and GNGGA)
|
// 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);
|
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);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Enable SBAS / WAAS
|
// Enable SBAS / WAAS
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK301,2*2E\r\n", 15, 250);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK301,2*2E\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_mtk_l76b(Device* uart) {
|
bool init_mtk_l76b(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
|
// Waveshare Pico-GPS hat uses the L76B with 9600 baud
|
||||||
// Initialize the L76B Chip, use GPS + GLONASS
|
// Initialize the L76B Chip, use GPS + GLONASS
|
||||||
// See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29
|
// 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);
|
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
|
// Above command will reset the GPS and takes longer before it will accept new commands
|
||||||
delay_millis(1000);
|
if (!gps_delay_millis(interrupted, 1000)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// only ask for RMC and GGA (GNRMC and GNGGA)
|
// only ask for RMC and GGA (GNRMC and GNGGA)
|
||||||
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
|
// 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);
|
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);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Enable SBAS
|
// Enable SBAS
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK301,2*2E\r\n", 15, 250);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK301,2*2E\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Enable PPS for 2D/3D fix only
|
// Enable PPS for 2D/3D fix only
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK285,3,100*3F\r\n", 19, 250);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK285,3,100*3F\r\n", 19, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Switch to Fitness Mode, for running and walking purpose with low speed (<5 m/s)
|
// 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);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK886,1*29\r\n", 15, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_mtk(Device* uart) {
|
bool init_mtk(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
|
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS04,7*1E\r\n", 14, 250);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS04,7*1E\r\n", 14, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// only ask for RMC and GGA
|
// 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);
|
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);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
|
// Switch to Vehicle Mode, since SoftRF enables Aviation < 2g
|
||||||
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS11,3*1E\r\n", 14, 250);
|
uart_controller_write_bytes(uart, (const uint8_t*)"$PCAS11,3*1E\r\n", 14, 250);
|
||||||
delay_millis(250);
|
if (!gps_delay_millis(interrupted, 250)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
20
Drivers/gps-meshtastic-module/source/interrupt.cpp
Normal file
20
Drivers/gps-meshtastic-module/source/interrupt.cpp
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
|
#include <gps_generic/private/interrupt.h>
|
||||||
|
|
||||||
|
#include <tactility/delay.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
constexpr uint32_t DELAY_SLICE_MILLIS = 50;
|
||||||
|
|
||||||
|
bool gps_delay_millis(const std::atomic<bool>& interrupted, uint32_t millis) {
|
||||||
|
while (millis > 0) {
|
||||||
|
if (interrupted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint32_t slice = std::min(millis, DELAY_SLICE_MILLIS);
|
||||||
|
delay_millis(slice);
|
||||||
|
millis -= slice;
|
||||||
|
}
|
||||||
|
return !interrupted;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
#include <gps_generic/private/gps_response.h>
|
#include <gps_generic/private/gps_response.h>
|
||||||
|
#include <gps_generic/private/interrupt.h>
|
||||||
#include <gps_generic/private/probe.h>
|
#include <gps_generic/private/probe.h>
|
||||||
#include <gps_generic/private/ublox.h>
|
#include <gps_generic/private/ublox.h>
|
||||||
|
|
||||||
@ -33,12 +34,12 @@ static char* probe_strnstr(const char* s, const char* find, size_t slen) {
|
|||||||
return ((char*)s);
|
return ((char*)s);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GpsResponse get_ack(Device* uart, const char* message, uint32_t wait_millis) {
|
static GpsResponse get_ack(Device* uart, const std::atomic<bool>& interrupted, const char* message, uint32_t wait_millis) {
|
||||||
uint8_t buffer[768] = {0};
|
uint8_t buffer[768] = {0};
|
||||||
uint8_t b;
|
uint8_t b;
|
||||||
int bytes_read = 0;
|
int bytes_read = 0;
|
||||||
uint32_t start_timeout = get_millis() + wait_millis;
|
uint32_t start_timeout = get_millis() + wait_millis;
|
||||||
while (get_millis() < start_timeout) {
|
while (get_millis() < start_timeout && !interrupted) {
|
||||||
size_t available = 0;
|
size_t available = 0;
|
||||||
uart_controller_get_available(uart, &available);
|
uart_controller_get_available(uart, &available);
|
||||||
if (available > 0) {
|
if (available > 0) {
|
||||||
@ -58,18 +59,21 @@ static GpsResponse get_ack(Device* uart, const char* message, uint32_t wait_mill
|
|||||||
return GpsResponse::None;
|
return GpsResponse::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define PROBE_SIMPLE(UART, CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \
|
#define PROBE_SIMPLE(UART, INTERRUPTED, CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \
|
||||||
do { \
|
do { \
|
||||||
|
if (INTERRUPTED) { \
|
||||||
|
return GPS_MODEL_UNKNOWN; \
|
||||||
|
} \
|
||||||
LOG_I(TAG, "Probing for %s (%s)", CHIP, TOWRITE); \
|
LOG_I(TAG, "Probing for %s (%s)", CHIP, TOWRITE); \
|
||||||
uart_controller_flush_input(UART); \
|
uart_controller_flush_input(UART); \
|
||||||
uart_controller_write_bytes(UART, (const uint8_t*)(TOWRITE "\r\n"), strlen(TOWRITE "\r\n"), TIMEOUT); \
|
uart_controller_write_bytes(UART, (const uint8_t*)(TOWRITE "\r\n"), strlen(TOWRITE "\r\n"), TIMEOUT); \
|
||||||
if (get_ack(UART, RESPONSE, TIMEOUT) == GpsResponse::Ok) { \
|
if (get_ack(UART, INTERRUPTED, RESPONSE, TIMEOUT) == GpsResponse::Ok) { \
|
||||||
LOG_I(TAG, "Probe detected %s %s", CHIP, #DRIVER); \
|
LOG_I(TAG, "Probe detected %s %s", CHIP, #DRIVER); \
|
||||||
return DRIVER; \
|
return DRIVER; \
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
GpsModel gps_probe(Device* uart) {
|
GpsModel gps_probe(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
// Close all NMEA sentences
|
// Close all NMEA sentences
|
||||||
// Valid for L76K, ATGM336H and likely other AT6558 devices
|
// Valid for L76K, ATGM336H and likely other AT6558 devices
|
||||||
uart_controller_write_bytes(uart, reinterpret_cast<const uint8_t*>("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"), 40, 500);
|
uart_controller_write_bytes(uart, reinterpret_cast<const uint8_t*>("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"), 40, 500);
|
||||||
@ -82,12 +86,12 @@ GpsModel gps_probe(Device* uart) {
|
|||||||
delay_millis(20);
|
delay_millis(20);
|
||||||
|
|
||||||
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
|
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
|
||||||
PROBE_SIMPLE(uart, "UC6580", "$PDTINFO", "UC6580", GpsModel::GPS_MODEL_UC6580, 500);
|
PROBE_SIMPLE(uart, interrupted, "UC6580", "$PDTINFO", "UC6580", GpsModel::GPS_MODEL_UC6580, 500);
|
||||||
PROBE_SIMPLE(uart, "UM600", "$PDTINFO", "UM600", GpsModel::GPS_MODEL_UC6580, 500);
|
PROBE_SIMPLE(uart, interrupted, "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);
|
PROBE_SIMPLE(uart, interrupted, "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
|
// 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::GPS_MODEL_ATGM336H, 500);
|
PROBE_SIMPLE(uart, interrupted, "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
|
// Airoha (Mediatek) AG3335A/M/S, A3352Q, Quectel L89 2.0, SimCom SIM65M
|
||||||
// GSA OFF, reduce volume
|
// GSA OFF, reduce volume
|
||||||
@ -96,21 +100,21 @@ GpsModel gps_probe(Device* uart) {
|
|||||||
uart_controller_write_bytes(uart, reinterpret_cast<const uint8_t*>("$PAIR062,3,0*3D\r\n"), 17, 500);
|
uart_controller_write_bytes(uart, reinterpret_cast<const uint8_t*>("$PAIR062,3,0*3D\r\n"), 17, 500);
|
||||||
// Save configuration
|
// Save configuration
|
||||||
uart_controller_write_bytes(uart, reinterpret_cast<const uint8_t*>("$PAIR513*3D\r\n"), 13, 500);
|
uart_controller_write_bytes(uart, reinterpret_cast<const uint8_t*>("$PAIR513*3D\r\n"), 13, 500);
|
||||||
PROBE_SIMPLE(uart, "AG3335", "$PAIR021*39", "$PAIR021,AG3335", GpsModel::GPS_MODEL_AG3335, 500);
|
PROBE_SIMPLE(uart, interrupted, "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, interrupted, "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, interrupted, "LC86", "$PQTMVERNO*58", "$PQTMVERNO,LC86", GpsModel::GPS_MODEL_AG3352, 500);
|
||||||
|
|
||||||
PROBE_SIMPLE(uart, "L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GpsModel::GPS_MODEL_MTK, 500);
|
PROBE_SIMPLE(uart, interrupted, "L76K", "$PCAS06,0*1B", "$GPTXT,01,01,02,SW=", GpsModel::GPS_MODEL_MTK, 500);
|
||||||
|
|
||||||
// Close all NMEA sentences
|
// Close all NMEA sentences
|
||||||
// Valid for L76B MTK
|
// Valid for L76B MTK
|
||||||
uart_controller_write_bytes(uart, reinterpret_cast<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);
|
uart_controller_write_bytes(uart, reinterpret_cast<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);
|
||||||
delay_millis(20);
|
delay_millis(20);
|
||||||
|
|
||||||
PROBE_SIMPLE(uart, "L76B", "$PMTK605*31", "Quectel-L76B", GpsModel::GPS_MODEL_MTK_L76B, 500);
|
PROBE_SIMPLE(uart, interrupted, "L76B", "$PMTK605*31", "Quectel-L76B", GpsModel::GPS_MODEL_MTK_L76B, 500);
|
||||||
PROBE_SIMPLE(uart, "PA1616S", "$PMTK605*31", "1616S", GpsModel::GPS_MODEL_MTK_PA1616S, 500);
|
PROBE_SIMPLE(uart, interrupted, "PA1616S", "$PMTK605*31", "1616S", GpsModel::GPS_MODEL_MTK_PA1616S, 500);
|
||||||
|
|
||||||
auto ublox_result = gps_ublox::probe(uart);
|
auto ublox_result = gps_ublox::probe(uart, interrupted);
|
||||||
if (ublox_result != GPS_MODEL_UNKNOWN) {
|
if (ublox_result != GPS_MODEL_UNKNOWN) {
|
||||||
return ublox_result;
|
return ublox_result;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
#include <gps_generic/private/ublox.h>
|
#include <gps_generic/private/ublox.h>
|
||||||
#include <gps_generic/private/gps_response.h>
|
#include <gps_generic/private/gps_response.h>
|
||||||
|
#include <gps_generic/private/interrupt.h>
|
||||||
#include <gps_generic/private/ublox_messages.h>
|
#include <gps_generic/private/ublox_messages.h>
|
||||||
|
|
||||||
#include <gps/gps.h>
|
#include <gps/gps.h>
|
||||||
|
|
||||||
#include <tactility/delay.h>
|
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/drivers/uart_controller.h>
|
#include <tactility/drivers/uart_controller.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/memory.h>
|
||||||
#include <tactility/time.h>
|
#include <tactility/time.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -18,15 +19,18 @@ namespace gps_ublox {
|
|||||||
|
|
||||||
constexpr auto* TAG = "ublox";
|
constexpr auto* TAG = "ublox";
|
||||||
|
|
||||||
bool init_ublox_6(Device* uart);
|
bool init_ublox_6(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
bool init_ublox_789(Device* uart, GpsModel model);
|
bool init_ublox_789(Device* uart, GpsModel model, const std::atomic<bool>& interrupted);
|
||||||
bool init_ublox_10(Device* uart);
|
bool init_ublox_10(Device* uart, const std::atomic<bool>& interrupted);
|
||||||
|
|
||||||
#define SEND_UBX_PACKET(UART, BUFFER, TYPE, ID, DATA, ERRMSG, TIMEOUT_MILLIS) \
|
#define SEND_UBX_PACKET(UART, INTERRUPTED, BUFFER, TYPE, ID, DATA, ERRMSG, TIMEOUT_MILLIS) \
|
||||||
do { \
|
do { \
|
||||||
auto msglen = make_packet(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); \
|
uart_controller_write_bytes(UART, BUFFER, msglen, TIMEOUT_MILLIS / portTICK_PERIOD_MS); \
|
||||||
if (get_ack(UART, TYPE, ID, TIMEOUT_MILLIS) != GpsResponse::Ok) { \
|
if (get_ack(UART, INTERRUPTED, TYPE, ID, TIMEOUT_MILLIS) != GpsResponse::Ok) { \
|
||||||
|
if (INTERRUPTED) { \
|
||||||
|
return false; \
|
||||||
|
} \
|
||||||
LOG_I(TAG, "Sending packet failed: %s", #ERRMSG); \
|
LOG_I(TAG, "Sending packet failed: %s", #ERRMSG); \
|
||||||
} \
|
} \
|
||||||
} while (0)
|
} while (0)
|
||||||
@ -64,7 +68,7 @@ uint8_t make_packet(uint8_t class_id, uint8_t message_id, const uint8_t* payload
|
|||||||
return (payload_size + 8U);
|
return (payload_size + 8U);
|
||||||
}
|
}
|
||||||
|
|
||||||
GpsResponse get_ack(Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wait_millis) {
|
GpsResponse get_ack(Device* uart, const std::atomic<bool>& interrupted, uint8_t class_id, uint8_t msg_id, uint32_t wait_millis) {
|
||||||
uint8_t b;
|
uint8_t b;
|
||||||
uint8_t ack = 0;
|
uint8_t ack = 0;
|
||||||
const uint8_t ackP[2] = {class_id, msg_id};
|
const uint8_t ackP[2] = {class_id, msg_id};
|
||||||
@ -85,7 +89,7 @@ GpsResponse get_ack(Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wai
|
|||||||
buf[9] += buf[8];
|
buf[9] += buf[8];
|
||||||
}
|
}
|
||||||
|
|
||||||
while (get_ticks() - start_time < wait_ticks) {
|
while (get_ticks() - start_time < wait_ticks && !interrupted) {
|
||||||
if (ack > 9) {
|
if (ack > 9) {
|
||||||
return GpsResponse::Ok; // ACK received
|
return GpsResponse::Ok; // ACK received
|
||||||
}
|
}
|
||||||
@ -112,17 +116,19 @@ GpsResponse get_ack(Device* uart, uint8_t class_id, uint8_t msg_id, uint32_t wai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!interrupted) {
|
||||||
LOG_W(TAG, "No response for class %02X message %02X", class_id, msg_id);
|
LOG_W(TAG, "No response for class %02X message %02X", class_id, msg_id);
|
||||||
|
}
|
||||||
return GpsResponse::None; // No response received within timeout
|
return GpsResponse::None; // No response received within timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
static int get_ack(Device* uart, uint8_t* buffer, uint16_t size, uint8_t requested_class, uint8_t requested_id, uint32_t timeout_millis) {
|
static int get_ack(Device* uart, const std::atomic<bool>& interrupted, uint8_t* buffer, uint16_t size, uint8_t requested_class, uint8_t requested_id, uint32_t timeout_millis) {
|
||||||
uint16_t ubx_frame_counter = 0;
|
uint16_t ubx_frame_counter = 0;
|
||||||
TickType_t start_time = get_ticks();
|
TickType_t start_time = get_ticks();
|
||||||
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_millis);
|
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_millis);
|
||||||
uint16_t need_read = 0;
|
uint16_t need_read = 0;
|
||||||
|
|
||||||
while ((get_ticks() - start_time) < timeout_ticks) {
|
while ((get_ticks() - start_time) < timeout_ticks && !interrupted) {
|
||||||
size_t available = 0;
|
size_t available = 0;
|
||||||
uart_controller_get_available(uart, &available);
|
uart_controller_get_available(uart, &available);
|
||||||
while (available > 0) {
|
while (available > 0) {
|
||||||
@ -189,45 +195,19 @@ static int get_ack(Device* uart, uint8_t* buffer, uint16_t size, uint8_t request
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct UbloxGnssModelInfo {
|
struct UbloxGnssModelInfo {
|
||||||
char swVersion[30];
|
char swVersion[30];
|
||||||
char hwVersion[10];
|
char hwVersion[10];
|
||||||
uint8_t extensionNo;
|
uint8_t extensionNo;
|
||||||
char extension[10][30];
|
char extension[10][30];
|
||||||
uint8_t protocol_version;
|
uint8_t protocol_version;
|
||||||
} ublox_info;
|
|
||||||
|
|
||||||
GpsModel probe(Device* uart) {
|
|
||||||
LOG_I(TAG, "Probing for U-blox");
|
|
||||||
|
|
||||||
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
|
||||||
checksum(cfg_rate, sizeof(cfg_rate));
|
|
||||||
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 = get_ack(uart, 0x06, 0x08, 750);
|
|
||||||
if (response == GpsResponse::None) {
|
|
||||||
LOG_W(TAG, "No GNSS Module");
|
|
||||||
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] = {
|
|
||||||
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));
|
|
||||||
uart_controller_flush_input(uart);
|
|
||||||
uart_controller_write_bytes(uart, message_monver, sizeof(message_monver), 500);
|
|
||||||
|
|
||||||
uint16_t ack_response_len = get_ack(uart, buffer, sizeof(buffer), 0x0A, 0x04, 1200);
|
// The protocol version of the last probed module, used by init
|
||||||
if (ack_response_len) {
|
static uint8_t protocol_version = 0;
|
||||||
|
|
||||||
|
/** Reads the hardware and software info of a UBX-MON-VER response */
|
||||||
|
static GpsModel parse_module_info(UbloxGnssModelInfo& ublox_info, uint8_t (&buffer)[256], uint16_t ack_response_len) {
|
||||||
uint16_t position = 0;
|
uint16_t position = 0;
|
||||||
for (char& i: ublox_info.swVersion) {
|
for (char& i: ublox_info.swVersion) {
|
||||||
i = buffer[position];
|
i = buffer[position];
|
||||||
@ -292,72 +272,143 @@ GpsModel probe(Device* uart) {
|
|||||||
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 10", "10");
|
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 10", "10");
|
||||||
return GPS_MODEL_UBLOX10;
|
return GPS_MODEL_UBLOX10;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return GPS_MODEL_UNKNOWN;
|
return GPS_MODEL_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init(Device* uart, GpsModel model) {
|
GpsModel probe(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
|
LOG_I(TAG, "Probing for U-blox");
|
||||||
|
|
||||||
|
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||||
|
checksum(cfg_rate, sizeof(cfg_rate));
|
||||||
|
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 = get_ack(uart, interrupted, 0x06, 0x08, 750);
|
||||||
|
if (interrupted) {
|
||||||
|
return GpsModel::GPS_MODEL_UNKNOWN;
|
||||||
|
} else if (response == GpsResponse::None) {
|
||||||
|
LOG_W(TAG, "No GNSS Module");
|
||||||
|
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] = {
|
||||||
|
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));
|
||||||
|
uart_controller_flush_input(uart);
|
||||||
|
uart_controller_write_bytes(uart, message_monver, sizeof(message_monver), 500);
|
||||||
|
|
||||||
|
uint16_t ack_response_len = get_ack(uart, interrupted, buffer, sizeof(buffer), 0x0A, 0x04, 1200);
|
||||||
|
if (ack_response_len == 0) {
|
||||||
|
return GPS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
static constexpr MemoryPolicy policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
||||||
|
auto* info = static_cast<UbloxGnssModelInfo*>(memory_calloc_with_policy(1, sizeof(UbloxGnssModelInfo), &policy));
|
||||||
|
if (info == nullptr) {
|
||||||
|
return GPS_MODEL_UNKNOWN;
|
||||||
|
}
|
||||||
|
const GpsModel model = parse_module_info(*info, buffer, ack_response_len);
|
||||||
|
protocol_version = info->protocol_version;
|
||||||
|
memory_free(info);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool init(Device* uart, GpsModel model, const std::atomic<bool>& interrupted) {
|
||||||
LOG_I(TAG, "U-blox init");
|
LOG_I(TAG, "U-blox init");
|
||||||
switch (model) {
|
switch (model) {
|
||||||
case GPS_MODEL_UBLOX6:
|
case GPS_MODEL_UBLOX6:
|
||||||
return init_ublox_6(uart);
|
return init_ublox_6(uart, interrupted);
|
||||||
case GPS_MODEL_UBLOX7:
|
case GPS_MODEL_UBLOX7:
|
||||||
case GPS_MODEL_UBLOX8:
|
case GPS_MODEL_UBLOX8:
|
||||||
case GPS_MODEL_UBLOX9:
|
case GPS_MODEL_UBLOX9:
|
||||||
return init_ublox_789(uart, model);
|
return init_ublox_789(uart, model, interrupted);
|
||||||
case GPS_MODEL_UBLOX10:
|
case GPS_MODEL_UBLOX10:
|
||||||
return init_ublox_10(uart);
|
return init_ublox_10(uart, interrupted);
|
||||||
default:
|
default:
|
||||||
LOG_E(TAG, "Unknown or unsupported U-blox model");
|
LOG_E(TAG, "Unknown or unsupported U-blox model");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_ublox_10(Device* uart) {
|
bool init_ublox_10(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
uint8_t buffer[256];
|
uint8_t buffer[256];
|
||||||
delay_millis(1000);
|
if (!gps_delay_millis(interrupted, 1000)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
uart_controller_flush_input(uart);
|
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);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
uart_controller_flush_input(uart);
|
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);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
|
return false;
|
||||||
delay_millis(750);
|
}
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
|
return false;
|
||||||
delay_millis(750);
|
}
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
|
||||||
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
|
||||||
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Here is where the init commands should go to do further M10 initialization.
|
// 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);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
|
||||||
delay_millis(750); // will cause a receiver restart so wait a bit
|
if (!gps_delay_millis(interrupted, 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);
|
return false;
|
||||||
delay_millis(750); // will cause a receiver restart so wait a bit
|
}
|
||||||
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
|
||||||
|
if (!gps_delay_millis(interrupted, 750)) { // will cause a receiver restart so wait a bit
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Done with initialization
|
// Done with initialization
|
||||||
|
|
||||||
// Enable wanted NMEA messages in BBR layer so they will survive a periodic sleep
|
// 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);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Enable wanted NMEA messages in RAM layer
|
// 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);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
|
||||||
delay_millis(750);
|
if (!gps_delay_millis(interrupted, 750)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// As the M10 has no flash, the best we can do to preserve the config is to set it in RAM and BBR.
|
// 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
|
// 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.
|
// batteries or super caps will not retain the config for a long power off time.
|
||||||
auto packet_size = make_packet(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);
|
uart_controller_write_bytes(uart, buffer, packet_size, 2000 / portTICK_PERIOD_MS);
|
||||||
if (get_ack(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
|
if (get_ack(uart, interrupted, 0x06, 0x09, 2000) != GpsResponse::Ok) {
|
||||||
LOG_W(TAG, "Unable to save GNSS module config");
|
LOG_W(TAG, "Unable to save GNSS module config");
|
||||||
} else {
|
} else {
|
||||||
LOG_I(TAG, "GNSS module configuration saved!");
|
LOG_I(TAG, "GNSS module configuration saved!");
|
||||||
@ -365,7 +416,7 @@ bool init_ublox_10(Device* uart) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_ublox_789(Device* uart, GpsModel model) {
|
bool init_ublox_789(Device* uart, GpsModel model, const std::atomic<bool>& interrupted) {
|
||||||
uint8_t buffer[256];
|
uint8_t buffer[256];
|
||||||
if (model == GpsModel::GPS_MODEL_UBLOX7) {
|
if (model == GpsModel::GPS_MODEL_UBLOX7) {
|
||||||
LOG_D(TAG, "Set GPS+SBAS");
|
LOG_D(TAG, "Set GPS+SBAS");
|
||||||
@ -376,7 +427,11 @@ bool init_ublox_789(Device* uart, GpsModel model) {
|
|||||||
uart_controller_write_bytes(uart, buffer, msglen, 800 / portTICK_PERIOD_MS);
|
uart_controller_write_bytes(uart, buffer, msglen, 800 / portTICK_PERIOD_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_ack(uart, 0x06, 0x3e, 800) == GpsResponse::NotAck) {
|
const GpsResponse gnss_response = get_ack(uart, interrupted, 0x06, 0x3e, 800);
|
||||||
|
if (interrupted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (gnss_response == GpsResponse::NotAck) {
|
||||||
// It's not critical if the module doesn't acknowledge this configuration.
|
// It's not critical if the module doesn't acknowledge this configuration.
|
||||||
LOG_D(TAG, "reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
LOG_D(TAG, "reconfigure GNSS - defaults maintained. Is this module GPS-only?");
|
||||||
} else {
|
} else {
|
||||||
@ -387,51 +442,53 @@ bool init_ublox_789(Device* uart, GpsModel model) {
|
|||||||
}
|
}
|
||||||
// Documentation say, we need wait at least 0.5s after reconfiguration of GNSS module, before sending next
|
// 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
|
// commands for the M8 it tends to be more. 1 sec should be enough
|
||||||
delay_millis(1000);
|
if (!gps_delay_millis(interrupted, 1000)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
|
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
||||||
|
|
||||||
if (model == GpsModel::GPS_MODEL_UBLOX8) { // 8
|
if (model == GpsModel::GPS_MODEL_UBLOX8) { // 8
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x39, _message_JAM_8, "enable interference resistance", 500);
|
||||||
|
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x23, _message_NAVX5_8, "configure NAVX5_8 settings", 500);
|
||||||
} else { // 6,7,9
|
} else { // 6,7,9
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn off unwanted NMEA messages, set update rate
|
// Turn off unwanted NMEA messages, set update rate
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||||
|
|
||||||
if (ublox_info.protocol_version >= 18) {
|
if (protocol_version >= 18) {
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x86, _message_PMS, "enable powersave for GPS", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
SEND_UBX_PACKET(uart, interrupted, 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.
|
// For M8 we want to enable NMEA version 4.10 so we can see the additional satellites.
|
||||||
if (model == GpsModel::GPS_MODEL_UBLOX8) {
|
if (model == GpsModel::GPS_MODEL_UBLOX8) {
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x11, _message_CFG_RXM_PSM, "enable powersave mode for GPS", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto packet_size = make_packet(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);
|
uart_controller_write_bytes(uart, buffer, packet_size, 2000 / portTICK_PERIOD_MS);
|
||||||
if (get_ack(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
|
if (get_ack(uart, interrupted, 0x06, 0x09, 2000) != GpsResponse::Ok) {
|
||||||
LOG_W(TAG, "Unable to save GNSS module config");
|
LOG_W(TAG, "Unable to save GNSS module config");
|
||||||
} else {
|
} else {
|
||||||
LOG_I(TAG, "GNSS module configuration saved!");
|
LOG_I(TAG, "GNSS module configuration saved!");
|
||||||
@ -439,33 +496,33 @@ bool init_ublox_789(Device* uart, GpsModel model) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init_ublox_6(Device* uart) {
|
bool init_ublox_6(Device* uart, const std::atomic<bool>& interrupted) {
|
||||||
uint8_t buffer[256];
|
uint8_t buffer[256];
|
||||||
|
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
|
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
|
||||||
|
|
||||||
// Turn off unwanted NMEA messages, set update rate
|
// Turn off unwanted NMEA messages, set update rate
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x08, _message_1HZ, "set GPS update rate", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
|
||||||
|
|
||||||
uart_controller_flush_input(uart);
|
uart_controller_flush_input(uart);
|
||||||
|
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
|
||||||
SEND_UBX_PACKET(uart, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 500);
|
SEND_UBX_PACKET(uart, interrupted, 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);
|
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_AID, "disable UBX-AID", 500);
|
||||||
|
|
||||||
auto packet_size = make_packet(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);
|
uart_controller_write_bytes(uart, buffer, packet_size, 2000);
|
||||||
if (get_ack(uart, 0x06, 0x09, 2000) != GpsResponse::Ok) {
|
if (get_ack(uart, interrupted, 0x06, 0x09, 2000) != GpsResponse::Ok) {
|
||||||
LOG_W(TAG, "Unable to save GNSS module config");
|
LOG_W(TAG, "Unable to save GNSS module config");
|
||||||
} else {
|
} else {
|
||||||
LOG_I(TAG, "GNSS module config saved!");
|
LOG_I(TAG, "GNSS module config saved!");
|
||||||
|
|||||||
45
LICENSE.md
45
LICENSE.md
@ -1,5 +1,30 @@
|
|||||||
# License Grant
|
# License Grant
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In short:
|
||||||
|
- The main firmware project uses `GNU Public License v3.0`
|
||||||
|
- TactilitySDK and related libraries use `Apache License v2.0`
|
||||||
|
- License of subprojects may vary.
|
||||||
|
|
||||||
|
Below is an overview of the licenses of some of the subprojects.
|
||||||
|
|
||||||
|
| Project | License |
|
||||||
|
|--------------------|-------------------------|
|
||||||
|
| Tactility | GNU Public License v3.0 |
|
||||||
|
| TactilityFreeRTOS | Apache License v2.0 |
|
||||||
|
| TactilityKernel | Apache License v2.0 |
|
||||||
|
| Tests | (varies) |
|
||||||
|
| Devices/* | (varies) |
|
||||||
|
| Drivers/* | (varies) |
|
||||||
|
| Modules/* | Apache License v2.0 |
|
||||||
|
| DevicetreeCompiler | Apache License v2.0 |
|
||||||
|
| Platforms/* | Apache License v2.0 |
|
||||||
|
|
||||||
|
Subprojects and directories in this project can contain license files.
|
||||||
|
|
||||||
|
The presence of such a license file indicates that this license applies to all the files and folders that are contained by the folder at the level where the license file resides.
|
||||||
|
|
||||||
## Definitions
|
## Definitions
|
||||||
|
|
||||||
"external apps" or "external applications" refers to applications that are built with the TactilitySDK.
|
"external apps" or "external applications" refers to applications that are built with the TactilitySDK.
|
||||||
@ -58,26 +83,6 @@ Some projects were rewritten entirely. Some examples:
|
|||||||
- Drivers that were written for the `Tactility/` subproject (C++ interface, GPL) and were rewritten from scratch based on `TactilityKernel/` (C interface, Apache)
|
- Drivers that were written for the `Tactility/` subproject (C++ interface, GPL) and were rewritten from scratch based on `TactilityKernel/` (C interface, Apache)
|
||||||
- Device subprojects were rewritten from a purely code-focused configuration project to an empty module declaration (can't really copyright this) and a DTS file.
|
- Device subprojects were rewritten from a purely code-focused configuration project to an empty module declaration (can't really copyright this) and a DTS file.
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Below is an overview of the licenses of some of the subprojects.
|
|
||||||
|
|
||||||
| Project | License |
|
|
||||||
|--------------------|-------------------------|
|
|
||||||
| Tactility | GNU Public License v3.0 |
|
|
||||||
| TactilityFreeRTOS | Apache License v2.0 |
|
|
||||||
| TactilityKernel | Apache License v2.0 |
|
|
||||||
| Tests | (varies) |
|
|
||||||
| Devices/* | (varies) |
|
|
||||||
| Drivers/* | (varies) |
|
|
||||||
| Modules/* | Apache License v2.0 |
|
|
||||||
| DevicetreeCompiler | Apache License v2.0 |
|
|
||||||
| Platforms/* | Apache License v2.0 |
|
|
||||||
|
|
||||||
Subprojects and directories in this project can contain license files.
|
|
||||||
|
|
||||||
The presence of such a license file indicates that this license applies to all the files and folders that are contained by the folder at the level where the license file resides.
|
|
||||||
|
|
||||||
## Logo
|
## Logo
|
||||||
|
|
||||||
The Tactility logo copyrights are owned by Ken Van Hoeylandt.
|
The Tactility logo copyrights are owned by Ken Van Hoeylandt.
|
||||||
|
|||||||
@ -4,9 +4,11 @@
|
|||||||
#include <gps/gps.h>
|
#include <gps/gps.h>
|
||||||
#include <gps/gps_settings.h>
|
#include <gps/gps_settings.h>
|
||||||
#include <tactility/concurrent/mutex.h>
|
#include <tactility/concurrent/mutex.h>
|
||||||
|
#include <tactility/delay.h>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/driver.h>
|
#include <tactility/driver.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/time.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@ -14,6 +16,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
constexpr auto* TAG = "gps_ledger";
|
constexpr auto* TAG = "gps_ledger";
|
||||||
|
constexpr uint32_t DESTROY_RETRY_INTERVAL_MS = 10;
|
||||||
|
constexpr uint32_t DESTROY_TIMEOUT_MS = 10 * 1000;
|
||||||
|
|
||||||
struct GpsConfig {
|
struct GpsConfig {
|
||||||
uint32_t baud_rate;
|
uint32_t baud_rate;
|
||||||
@ -43,6 +47,9 @@ static LedgerMutex ledger_mutex;
|
|||||||
// Unique across every device the ledger creates, so device names ("gpsN") never collide.
|
// Unique across every device the ledger creates, so device names ("gpsN") never collide.
|
||||||
static uint32_t next_device_index = 0;
|
static uint32_t next_device_index = 0;
|
||||||
|
|
||||||
|
// Removed devices that were still referenced when they were destroyed. Guarded by ledger_mutex.
|
||||||
|
static std::vector<Device*> pending_destruction;
|
||||||
|
|
||||||
static bool device_matches_configuration(Device* device, const GpsConfiguration& configuration) {
|
static bool device_matches_configuration(Device* device, const GpsConfiguration& configuration) {
|
||||||
auto* parent = device_get_parent(device);
|
auto* parent = device_get_parent(device);
|
||||||
if (parent == nullptr || strcmp(parent->name, configuration.uart_name) != 0) {
|
if (parent == nullptr || strcmp(parent->name, configuration.uart_name) != 0) {
|
||||||
@ -108,13 +115,40 @@ static bool create_device(const GpsConfiguration& configuration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stops (if needed), removes and destructs a ledger-owned device, and frees its entry.
|
// Stops (if needed), removes and destructs a ledger-owned device, and frees its entry.
|
||||||
|
// Another task can be starting or stopping the device, or hold a reference to it, and the driver can fail to stop
|
||||||
|
// while its thread is still exiting. So each step is retried until it succeeds.
|
||||||
|
// A removed device that is still referenced after the timeout is destructed by a later ledger operation.
|
||||||
|
// Must be called with ledger_mutex held.
|
||||||
static void destroy_device(Device* device) {
|
static void destroy_device(Device* device) {
|
||||||
if (device_is_ready(device)) {
|
const uint64_t deadline_us = get_micros_since_boot() + DESTROY_TIMEOUT_MS * 1000ULL;
|
||||||
device_stop(device);
|
while (get_micros_since_boot() < deadline_us) {
|
||||||
}
|
if (device_is_added(device) && device_stop(device) == ERROR_NONE) {
|
||||||
device_remove(device);
|
device_remove(device);
|
||||||
device_destruct(device);
|
}
|
||||||
|
if (!device_is_added(device) && device_destruct(device) == ERROR_NONE) {
|
||||||
delete reinterpret_cast<GpsLedgerEntry*>(device);
|
delete reinterpret_cast<GpsLedgerEntry*>(device);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delay_millis(DESTROY_RETRY_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
if (device_is_added(device)) {
|
||||||
|
// Still in the device ledger, so a later ledger operation finds it again
|
||||||
|
LOG_E(TAG, "Failed to stop %s", device->name);
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "%s is still referenced, destroying it later", device->name);
|
||||||
|
pending_destruction.push_back(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be called with ledger_mutex held
|
||||||
|
static void destroy_pending_devices() {
|
||||||
|
std::erase_if(pending_destruction, [](Device* device) {
|
||||||
|
if (device_destruct(device) != ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
delete reinterpret_cast<GpsLedgerEntry*>(device);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool is_ledger_owned(const Device* device) {
|
static bool is_ledger_owned(const Device* device) {
|
||||||
@ -136,6 +170,7 @@ static std::vector<Device*> collect_owned_devices() {
|
|||||||
|
|
||||||
void gps_ledger_sync() {
|
void gps_ledger_sync() {
|
||||||
mutex_lock(&ledger_mutex.mutex);
|
mutex_lock(&ledger_mutex.mutex);
|
||||||
|
destroy_pending_devices();
|
||||||
|
|
||||||
std::vector<GpsConfiguration> configurations;
|
std::vector<GpsConfiguration> configurations;
|
||||||
gps_settings_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) {
|
gps_settings_for_each_configuration(&configurations, [](const GpsConfiguration* configuration, size_t, void* context) {
|
||||||
@ -176,6 +211,7 @@ void gps_ledger_sync() {
|
|||||||
|
|
||||||
void gps_ledger_clear() {
|
void gps_ledger_clear() {
|
||||||
mutex_lock(&ledger_mutex.mutex);
|
mutex_lock(&ledger_mutex.mutex);
|
||||||
|
destroy_pending_devices();
|
||||||
for (auto* device : collect_owned_devices()) {
|
for (auto* device : collect_owned_devices()) {
|
||||||
destroy_device(device);
|
destroy_device(device);
|
||||||
}
|
}
|
||||||
@ -184,6 +220,7 @@ void gps_ledger_clear() {
|
|||||||
|
|
||||||
void gps_ledger_remove_children_of(Device* parent) {
|
void gps_ledger_remove_children_of(Device* parent) {
|
||||||
mutex_lock(&ledger_mutex.mutex);
|
mutex_lock(&ledger_mutex.mutex);
|
||||||
|
destroy_pending_devices();
|
||||||
for (auto* device : collect_owned_devices()) {
|
for (auto* device : collect_owned_devices()) {
|
||||||
if (device_get_parent(device) == parent) {
|
if (device_get_parent(device) == parent) {
|
||||||
destroy_device(device);
|
destroy_device(device);
|
||||||
|
|||||||
24
Modules/location-module/CMakeLists.txt
Normal file
24
Modules/location-module/CMakeLists.txt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
TactilityKernelCpp
|
||||||
|
service-module
|
||||||
|
gps-module
|
||||||
|
minmea
|
||||||
|
)
|
||||||
|
|
||||||
|
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||||
|
list(APPEND REQUIRES_LIST lwip)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
tactility_add_module(location-module
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
PRIV_INCLUDE_DIRS private/
|
||||||
|
INCLUDE_DIRS include/
|
||||||
|
REQUIRES ${REQUIRES_LIST}
|
||||||
|
)
|
||||||
195
Modules/location-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/location-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
Apache License
|
||||||
|
==============
|
||||||
|
|
||||||
|
_Version 2.0, January 2004_
|
||||||
|
_<<http://www.apache.org/licenses/>>_
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
120
Modules/location-module/include/location/location.h
Normal file
120
Modules/location-module/include/location/location.h
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
|
#include <tactility/error.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location service. Location providers (e.g. GPS) report fixes to the service.
|
||||||
|
* The most recent fix of a regular provider is used while it's less than 2 minutes old.
|
||||||
|
* Otherwise, the most recent fix of a fallback provider (e.g. IP geolocation) is used, if there is one.
|
||||||
|
* Providers only run while there is at least one subscription.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define LOCATION_PROVIDER_ID_MAX_LENGTH 15
|
||||||
|
|
||||||
|
/** Horizontal accuracy in meters of a fix with LOCATION_PRECISION_APPROXIMATE */
|
||||||
|
#define LOCATION_APPROXIMATE_ACCURACY_METERS 3000.0f
|
||||||
|
|
||||||
|
enum LocationPrecision {
|
||||||
|
/** Providers only report fixes with a horizontal accuracy of LOCATION_APPROXIMATE_ACCURACY_METERS or worse */
|
||||||
|
LOCATION_PRECISION_APPROXIMATE,
|
||||||
|
/** Providers report fixes as accurate as they can */
|
||||||
|
LOCATION_PRECISION_PRECISE,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A location fix. Values the provider doesn't know are NAN for floating point fields and 0 for the others.
|
||||||
|
* Accuracies are the radius of 68% confidence, in the unit of the value they belong to.
|
||||||
|
*/
|
||||||
|
struct Location {
|
||||||
|
/** Degrees in WGS84, positive is north */
|
||||||
|
double latitude;
|
||||||
|
/** Degrees in WGS84, positive is east */
|
||||||
|
double longitude;
|
||||||
|
/** Meters */
|
||||||
|
float horizontal_accuracy;
|
||||||
|
/** Meters above mean sea level */
|
||||||
|
float altitude;
|
||||||
|
/** Meters above the WGS84 ellipsoid */
|
||||||
|
float ellipsoidal_altitude;
|
||||||
|
/** Meters */
|
||||||
|
float vertical_accuracy;
|
||||||
|
/** Speed over ground in meters per second */
|
||||||
|
float speed;
|
||||||
|
/** Meters per second */
|
||||||
|
float speed_accuracy;
|
||||||
|
/** Direction of travel in degrees clockwise from true north, in range [0, 360) */
|
||||||
|
float course;
|
||||||
|
/** Degrees */
|
||||||
|
float course_accuracy;
|
||||||
|
/** Number of satellites used for the fix */
|
||||||
|
uint8_t satellites;
|
||||||
|
/** Time of the fix according to the provider, in milliseconds since the Unix epoch */
|
||||||
|
int64_t time_ms;
|
||||||
|
/** Milliseconds since boot when the service received the fix */
|
||||||
|
uint64_t uptime_ms;
|
||||||
|
/** Id of the provider that reported the fix */
|
||||||
|
char provider[LOCATION_PROVIDER_ID_MAX_LENGTH + 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow or disallow the use of location services. The service only runs while allowed. The choice is persisted.
|
||||||
|
* Location services are disallowed by default.
|
||||||
|
*/
|
||||||
|
error_t location_set_enabled(bool enabled);
|
||||||
|
|
||||||
|
bool location_is_enabled(void);
|
||||||
|
|
||||||
|
/** Set the precision that providers should report. The choice is persisted. */
|
||||||
|
error_t location_set_precision(enum LocationPrecision precision);
|
||||||
|
|
||||||
|
enum LocationPrecision location_get_precision(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current fix. Without subscriptions, this is the last known fix.
|
||||||
|
* @param[out] location the fix
|
||||||
|
* @retval ERROR_NOT_FOUND when no fix was reported since the service started, or location services are disabled
|
||||||
|
*/
|
||||||
|
error_t location_get(struct Location* location);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caller-owned subscription: the service signals @a bit on the event group whenever a new current fix is reported.
|
||||||
|
* Read the fix with location_get().
|
||||||
|
* @warning Fields other than `bit` are for internal use only.
|
||||||
|
*/
|
||||||
|
struct LocationSubscription {
|
||||||
|
uint32_t bit;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
struct TaskEventGroup* event_group;
|
||||||
|
struct LocationSubscription* next;
|
||||||
|
} internal;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribing starts the providers when location services are enabled. They stop when the last subscription is removed.
|
||||||
|
* Providers start and stop in the background: subscribing and unsubscribing don't wait for them.
|
||||||
|
* @param[in,out] sub subscription to register, must stay alive until unsubscribed
|
||||||
|
* @param[in] event_group caller-owned group to wait on, must outlive @a sub
|
||||||
|
* @retval ERROR_RESOURCE @a event_group has no free bits left
|
||||||
|
* @retval ERROR_INVALID_STATE @a sub is already registered
|
||||||
|
*/
|
||||||
|
error_t location_subscribe(struct LocationSubscription* sub, struct TaskEventGroup* event_group);
|
||||||
|
|
||||||
|
/** @retval ERROR_NOT_FOUND when @a sub is not registered */
|
||||||
|
error_t location_unsubscribe(struct LocationSubscription* sub);
|
||||||
|
|
||||||
|
/** @return true when location services are enabled and there is at least one subscription */
|
||||||
|
bool location_is_in_use(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
14
Modules/location-module/include/location/module.h
Normal file
14
Modules/location-module/include/location/module.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <tactility/module.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern struct Module location_module;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
79
Modules/location-module/include/location/provider.h
Normal file
79
Modules/location-module/include/location/provider.h
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
|
||||||
|
#include <tactility/freertos/freertos.h>
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface between the location service and a location provider. Not part of the app SDK.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** The service, as seen by a provider */
|
||||||
|
struct LocationHost;
|
||||||
|
|
||||||
|
struct LocationProviderApi {
|
||||||
|
/** Unique id, at most LOCATION_PROVIDER_ID_MAX_LENGTH characters */
|
||||||
|
const char* id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fallback provider (e.g. IP geolocation) is only used while no other provider reported a fix in the last
|
||||||
|
* 2 minutes. It should only work when location_host_needs_fallback() returns true.
|
||||||
|
*/
|
||||||
|
bool fallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the provider. Called on the service's worker task while the service runs and has subscriptions.
|
||||||
|
* It must not block: the work happens on the provider's own task.
|
||||||
|
* @return the provider state, NULL on failure
|
||||||
|
*/
|
||||||
|
void* (*create)(struct LocationHost* host);
|
||||||
|
|
||||||
|
/** Ask the provider to stop. It must not block. */
|
||||||
|
void (*request_stop)(void* provider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait at most @a timeout for the provider to stop and release its resources.
|
||||||
|
* It must not report anymore once this returned true. The provider is only created again after that.
|
||||||
|
* @return true when stopped
|
||||||
|
*/
|
||||||
|
bool (*wait_stopped)(void* provider, TickType_t timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Maximum number of registered providers */
|
||||||
|
#define LOCATION_PROVIDER_MAX_COUNT 4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a provider. Usually called from the provider module's start().
|
||||||
|
* @retval ERROR_ALREADY_EXISTS when a provider with the same id is registered
|
||||||
|
* @retval ERROR_RESOURCE when LOCATION_PROVIDER_MAX_COUNT providers are registered
|
||||||
|
*/
|
||||||
|
error_t location_provider_register(const struct LocationProviderApi* api);
|
||||||
|
|
||||||
|
/** @retval ERROR_NOT_FOUND when @a api is not registered */
|
||||||
|
error_t location_provider_unregister(const struct LocationProviderApi* api);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report a new fix. Can be called from any task.
|
||||||
|
* The service sets the `provider` and `uptime_ms` fields.
|
||||||
|
*/
|
||||||
|
void location_host_report(struct LocationHost* host, const struct Location* location);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce a fix to LOCATION_PRECISION_APPROXIMATE: the position snaps to a grid and only position and time remain.
|
||||||
|
* Providers that can report precise fixes apply this when location_get_precision() is LOCATION_PRECISION_APPROXIMATE.
|
||||||
|
*/
|
||||||
|
void location_make_approximate(struct Location* location);
|
||||||
|
|
||||||
|
/** @return true when no provider that isn't a fallback reported a fix in the last 2 minutes */
|
||||||
|
bool location_host_needs_fallback(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
17
Modules/location-module/private/location/gps_provider.h
Normal file
17
Modules/location-module/private/location/gps_provider.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
#include <location/provider.h>
|
||||||
|
|
||||||
|
#include <minmea.h>
|
||||||
|
|
||||||
|
/** Uses the first started GPS device */
|
||||||
|
extern const LocationProviderApi gps_location_provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the sentences of one GPS epoch into a fix.
|
||||||
|
* @param[in] gga the GGA sentence of the same epoch, NULL when there is none
|
||||||
|
* @return false when @a rmc has no valid fix
|
||||||
|
*/
|
||||||
|
bool gps_location_from_nmea(const minmea_sentence_rmc* rmc, const minmea_sentence_gga* gga, Location* location);
|
||||||
41
Modules/location-module/private/location/ip_provider.h
Normal file
41
Modules/location-module/private/location/ip_provider.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <location/provider.h>
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback provider that looks up the location of the public IP address with ip-api.com.
|
||||||
|
* The result is reused while the network (local IP address and gateway) is unchanged.
|
||||||
|
*/
|
||||||
|
extern const LocationProviderApi ip_location_provider;
|
||||||
|
|
||||||
|
/** Start tracking the network that the device is connected to, also while the provider doesn't run */
|
||||||
|
error_t ip_provider_track_network(void);
|
||||||
|
|
||||||
|
/** Stop tracking the network and release the cached result. The provider must not run. */
|
||||||
|
void ip_provider_untrack_network(void);
|
||||||
|
|
||||||
|
#define IP_API_MESSAGE_MAX_LENGTH 63
|
||||||
|
|
||||||
|
struct IpApiResponse {
|
||||||
|
/** HTTP status code */
|
||||||
|
int status_code;
|
||||||
|
/** Requests left in the rate limit window, -1 when unknown */
|
||||||
|
int rate_remaining;
|
||||||
|
/** Seconds until the rate limit window resets, -1 when unknown */
|
||||||
|
int rate_reset_seconds;
|
||||||
|
bool success;
|
||||||
|
/** Reason of failure, empty on success */
|
||||||
|
char message[IP_API_MESSAGE_MAX_LENGTH + 1];
|
||||||
|
double latitude;
|
||||||
|
double longitude;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an HTTP response of the newline separated ip-api endpoint, requested with fields "status,message,lat,lon"
|
||||||
|
* @param[in] response the NUL-terminated response, including the status line and headers
|
||||||
|
* @return false when the response is malformed
|
||||||
|
*/
|
||||||
|
bool ip_api_parse_response(const char* response, IpApiResponse* out);
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <service/manifest.h>
|
||||||
|
|
||||||
|
extern const ServiceManifest location_service_manifest;
|
||||||
16
Modules/location-module/private/location/location_settings.h
Normal file
16
Modules/location-module/private/location/location_settings.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
|
||||||
|
#define LOCATION_SERVICE_ID "tactility.location"
|
||||||
|
|
||||||
|
struct LocationSettings {
|
||||||
|
bool enabled;
|
||||||
|
enum LocationPrecision precision;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Loads the settings, or defaults when there are none */
|
||||||
|
void location_settings_load(struct LocationSettings* settings);
|
||||||
|
|
||||||
|
error_t location_settings_save(const struct LocationSettings* settings);
|
||||||
337
Modules/location-module/source/gps_provider.cpp
Normal file
337
Modules/location-module/source/gps_provider.cpp
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include <location/gps_provider.h>
|
||||||
|
|
||||||
|
#include <gps/gps.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/recursive_mutex.h>
|
||||||
|
#include <tactility/concurrent/thread.h>
|
||||||
|
#include <tactility/delay.h>
|
||||||
|
#include <tactility/device.h>
|
||||||
|
#include <tactility/device_listener.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/time.h>
|
||||||
|
|
||||||
|
#include <TactilityCpp/Memory.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
#define TAG "location_gps"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr uint32_t AWAIT_TIMEOUT_MS = 250;
|
||||||
|
constexpr uint32_t DEVICE_POLL_INTERVAL_MS = 1000;
|
||||||
|
constexpr uint64_t START_RETRY_INTERVAL_MS = 30 * 1000;
|
||||||
|
constexpr float METERS_PER_SECOND_PER_KNOT = 0.514444f;
|
||||||
|
// Typical user equivalent range error of consumer receivers, which turns dilution of precision into meters
|
||||||
|
constexpr float RANGE_ERROR_METERS = 5.0f;
|
||||||
|
|
||||||
|
/** The sentences received for one GPS epoch */
|
||||||
|
struct Epoch {
|
||||||
|
minmea_time time;
|
||||||
|
bool hasRmc;
|
||||||
|
bool hasGga;
|
||||||
|
bool reported;
|
||||||
|
minmea_sentence_rmc rmc;
|
||||||
|
minmea_sentence_gga gga;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The state of a provider run. Allocated in external memory when available. */
|
||||||
|
struct GpsProvider {
|
||||||
|
LocationHost* host = nullptr;
|
||||||
|
Thread* thread = nullptr;
|
||||||
|
std::atomic<bool> running = false;
|
||||||
|
// Guarded by mutex
|
||||||
|
Device* device = nullptr;
|
||||||
|
// The device that this provider started, which it stops when it's done. Guarded by mutex.
|
||||||
|
Device* startedDevice = nullptr;
|
||||||
|
uint64_t nextStartAttemptMs = 0;
|
||||||
|
// Guarded by mutex
|
||||||
|
GpsSubscription subscription {};
|
||||||
|
Epoch epoch {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GlobalRecursiveMutex {
|
||||||
|
RecursiveMutex handle {};
|
||||||
|
GlobalRecursiveMutex() { recursive_mutex_construct(&handle); }
|
||||||
|
~GlobalRecursiveMutex() { recursive_mutex_destruct(&handle); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guards instance and the provider state that the device listener uses.
|
||||||
|
// Recursive, because stopping startedDevice notifies the device listener on the same thread.
|
||||||
|
GlobalRecursiveMutex mutex;
|
||||||
|
// The running provider, for the device listener. A listener call can still be in progress after it's removed.
|
||||||
|
GpsProvider* instance = nullptr;
|
||||||
|
|
||||||
|
double toDegrees(const minmea_float& value) {
|
||||||
|
const double raw = static_cast<double>(value.value) / value.scale;
|
||||||
|
const int degrees = static_cast<int>(raw / 100);
|
||||||
|
return degrees + (raw - degrees * 100) / 60.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days from 1970-01-01 to a date in the proleptic Gregorian calendar
|
||||||
|
int64_t daysSinceEpoch(int year, int month, int day) {
|
||||||
|
year -= month <= 2;
|
||||||
|
const int64_t era = (year >= 0 ? year : year - 399) / 400;
|
||||||
|
const int yearOfEra = static_cast<int>(year - era * 400);
|
||||||
|
const int dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
|
||||||
|
const int dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear;
|
||||||
|
return era * 146097 + dayOfEra - 719468;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t getUptimeMs() {
|
||||||
|
return get_micros_since_boot() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSameTime(const minmea_time& a, const minmea_time& b) {
|
||||||
|
return a.hours == b.hours && a.minutes == b.minutes && a.seconds == b.seconds && a.microseconds == b.microseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void report(GpsProvider& provider, Epoch& epoch) {
|
||||||
|
Location location;
|
||||||
|
if (gps_location_from_nmea(&epoch.rmc, epoch.hasGga ? &epoch.gga : nullptr, &location)) {
|
||||||
|
if (location_get_precision() == LOCATION_PRECISION_APPROXIMATE) {
|
||||||
|
location_make_approximate(&location);
|
||||||
|
}
|
||||||
|
location_host_report(provider.host, &location);
|
||||||
|
}
|
||||||
|
epoch.reported = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSentence(GpsProvider& provider, const GpsEvent& event) {
|
||||||
|
auto& epoch = provider.epoch;
|
||||||
|
const minmea_time& time = event.type == GPS_EVENT_MESSAGE_RMC ? event.data.rmc.time : event.data.gga.time;
|
||||||
|
if (!isSameTime(epoch.time, time)) {
|
||||||
|
// Receivers that don't send GGA only get reported once the next epoch starts
|
||||||
|
if (epoch.hasRmc && !epoch.reported) {
|
||||||
|
report(provider, epoch);
|
||||||
|
}
|
||||||
|
epoch = {};
|
||||||
|
epoch.time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type == GPS_EVENT_MESSAGE_RMC) {
|
||||||
|
epoch.rmc = event.data.rmc;
|
||||||
|
epoch.hasRmc = true;
|
||||||
|
} else {
|
||||||
|
epoch.gga = event.data.gga;
|
||||||
|
epoch.hasGga = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (epoch.hasRmc && epoch.hasGga && !epoch.reported) {
|
||||||
|
report(provider, epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribes before the driver stops, as the subscription can't be removed afterwards
|
||||||
|
void onDeviceEvent(Device* device, DeviceEvent event, void*) {
|
||||||
|
if (event != DEVICE_EVENT_STOPPING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recursive_mutex_lock(&mutex.handle);
|
||||||
|
if (instance != nullptr) {
|
||||||
|
if (device == instance->device) {
|
||||||
|
gps_event_unsubscribe(device, &instance->subscription);
|
||||||
|
instance->device = nullptr;
|
||||||
|
}
|
||||||
|
if (device == instance->startedDevice) {
|
||||||
|
instance->startedDevice = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recursive_mutex_unlock(&mutex.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with the mutex held */
|
||||||
|
void startFirstDevice(GpsProvider& provider) {
|
||||||
|
const uint64_t now = getUptimeMs();
|
||||||
|
if (now < provider.nextStartAttemptMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The reference keeps the device from being freed while it's started. A removed device fails to start.
|
||||||
|
Device* device;
|
||||||
|
if (device_get_first_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!device_is_ready(device)) {
|
||||||
|
if (device_start(device) == ERROR_NONE) {
|
||||||
|
provider.startedDevice = device;
|
||||||
|
} else {
|
||||||
|
LOG_W(TAG, "Failed to start %s", device->name);
|
||||||
|
provider.nextStartAttemptMs = now + START_RETRY_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device_put(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with the mutex held */
|
||||||
|
bool subscribe(GpsProvider& provider) {
|
||||||
|
Device* device;
|
||||||
|
if (device_get_first_active_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||||
|
startFirstDevice(provider);
|
||||||
|
if (device_get_first_active_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const error_t error = gps_event_subscribe(device, &provider.subscription);
|
||||||
|
device_put(device);
|
||||||
|
if (error != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to subscribe to %s", device->name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LOG_I(TAG, "Using %s", device->name);
|
||||||
|
provider.device = device;
|
||||||
|
provider.epoch = {};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with the mutex held and a device subscribed */
|
||||||
|
void receive(GpsProvider& provider) {
|
||||||
|
auto& sub = provider.subscription;
|
||||||
|
if (sub.sequence != sub.consumed_sequence) {
|
||||||
|
sub.consumed_sequence = sub.sequence;
|
||||||
|
} else if (gps_event_await(provider.device, &sub, millis_to_ticks(AWAIT_TIMEOUT_MS)) != ERROR_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sub.event.type) {
|
||||||
|
case GPS_EVENT_MESSAGE_RMC:
|
||||||
|
case GPS_EVENT_MESSAGE_GGA:
|
||||||
|
onSentence(provider, sub.event);
|
||||||
|
break;
|
||||||
|
case GPS_EVENT_UNSUBSCRIBED:
|
||||||
|
provider.device = nullptr;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t threadMain(void* context) {
|
||||||
|
auto& provider = *static_cast<GpsProvider*>(context);
|
||||||
|
while (provider.running) {
|
||||||
|
recursive_mutex_lock(&mutex.handle);
|
||||||
|
const bool subscribed = provider.device != nullptr || subscribe(provider);
|
||||||
|
if (subscribed) {
|
||||||
|
receive(provider);
|
||||||
|
}
|
||||||
|
recursive_mutex_unlock(&mutex.handle);
|
||||||
|
|
||||||
|
if (!subscribed) {
|
||||||
|
delay_millis(DEVICE_POLL_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recursive_mutex_lock(&mutex.handle);
|
||||||
|
if (provider.device != nullptr) {
|
||||||
|
gps_event_unsubscribe(provider.device, &provider.subscription);
|
||||||
|
provider.device = nullptr;
|
||||||
|
}
|
||||||
|
if (provider.startedDevice != nullptr) {
|
||||||
|
Device* device = provider.startedDevice;
|
||||||
|
provider.startedDevice = nullptr;
|
||||||
|
if (device_stop(device) != ERROR_NONE) {
|
||||||
|
LOG_W(TAG, "Failed to stop %s", device->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recursive_mutex_unlock(&mutex.handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* create(LocationHost* host) {
|
||||||
|
auto* provider = tt::newObject<GpsProvider>();
|
||||||
|
if (provider == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
provider->host = host;
|
||||||
|
provider->running = true;
|
||||||
|
provider->thread = thread_alloc_full("location_gps", 4096, threadMain, provider, -1);
|
||||||
|
if (provider->thread == nullptr) {
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
// Settings are loaded before providers start, so the thread doesn't access flash
|
||||||
|
thread_set_stack_external(provider->thread, true);
|
||||||
|
|
||||||
|
recursive_mutex_lock(&mutex.handle);
|
||||||
|
instance = provider;
|
||||||
|
recursive_mutex_unlock(&mutex.handle);
|
||||||
|
device_listener_add(onDeviceEvent, nullptr);
|
||||||
|
|
||||||
|
if (thread_start(provider->thread) != ERROR_NONE) {
|
||||||
|
device_listener_remove(onDeviceEvent, nullptr);
|
||||||
|
recursive_mutex_lock(&mutex.handle);
|
||||||
|
instance = nullptr;
|
||||||
|
recursive_mutex_unlock(&mutex.handle);
|
||||||
|
thread_free(provider->thread);
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestStop(void* context) {
|
||||||
|
static_cast<GpsProvider*>(context)->running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool waitStopped(void* context, TickType_t timeout) {
|
||||||
|
auto* provider = static_cast<GpsProvider*>(context);
|
||||||
|
if (thread_join(provider->thread, timeout, millis_to_ticks(10)) != ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
device_listener_remove(onDeviceEvent, nullptr);
|
||||||
|
recursive_mutex_lock(&mutex.handle);
|
||||||
|
instance = nullptr;
|
||||||
|
recursive_mutex_unlock(&mutex.handle);
|
||||||
|
thread_free(provider->thread);
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool gps_location_from_nmea(const minmea_sentence_rmc* rmc, const minmea_sentence_gga* gga, Location* location) {
|
||||||
|
if (!rmc->valid || rmc->latitude.scale == 0 || rmc->longitude.scale == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*location = Location {
|
||||||
|
.latitude = toDegrees(rmc->latitude),
|
||||||
|
.longitude = toDegrees(rmc->longitude),
|
||||||
|
.horizontal_accuracy = NAN,
|
||||||
|
.altitude = NAN,
|
||||||
|
.ellipsoidal_altitude = NAN,
|
||||||
|
.vertical_accuracy = NAN,
|
||||||
|
.speed = minmea_tofloat(&rmc->speed) * METERS_PER_SECOND_PER_KNOT,
|
||||||
|
.speed_accuracy = NAN,
|
||||||
|
.course = minmea_tofloat(&rmc->course),
|
||||||
|
.course_accuracy = NAN,
|
||||||
|
.satellites = 0,
|
||||||
|
.time_ms = 0,
|
||||||
|
.uptime_ms = 0,
|
||||||
|
.provider = {},
|
||||||
|
};
|
||||||
|
|
||||||
|
tm dateTime;
|
||||||
|
if (minmea_getdatetime(&dateTime, &rmc->date, &rmc->time) == 0) {
|
||||||
|
const int64_t seconds = daysSinceEpoch(dateTime.tm_year + 1900, dateTime.tm_mon + 1, dateTime.tm_mday) * 86400 +
|
||||||
|
dateTime.tm_hour * 3600 + dateTime.tm_min * 60 + dateTime.tm_sec;
|
||||||
|
location->time_ms = seconds * 1000 + rmc->time.microseconds / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gga != nullptr && gga->fix_quality > 0) {
|
||||||
|
location->horizontal_accuracy = minmea_tofloat(&gga->hdop) * RANGE_ERROR_METERS;
|
||||||
|
location->altitude = minmea_tofloat(&gga->altitude);
|
||||||
|
location->ellipsoidal_altitude = location->altitude + minmea_tofloat(&gga->height);
|
||||||
|
location->satellites = static_cast<uint8_t>(gga->satellites_tracked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationProviderApi gps_location_provider = {
|
||||||
|
.id = "gps",
|
||||||
|
.fallback = false,
|
||||||
|
.create = create,
|
||||||
|
.request_stop = requestStop,
|
||||||
|
.wait_stopped = waitStopped,
|
||||||
|
};
|
||||||
445
Modules/location-module/source/ip_provider.cpp
Normal file
445
Modules/location-module/source/ip_provider.cpp
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include <location/ip_provider.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
|
#include <tactility/concurrent/thread.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/system_event.h>
|
||||||
|
#include <tactility/time.h>
|
||||||
|
|
||||||
|
#include <TactilityCpp/Memory.h>
|
||||||
|
#include <TactilityCpp/std/string.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string_view>
|
||||||
|
#include <strings.h>
|
||||||
|
|
||||||
|
#define TAG "location_ip"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The free ip-api endpoint only supports plain HTTP
|
||||||
|
constexpr auto* API_HOST = "ip-api.com";
|
||||||
|
constexpr auto* API_PATH = "/line/?fields=status,message,lat,lon";
|
||||||
|
constexpr uint64_t RETRY_INTERVAL_MS = 60 * 1000;
|
||||||
|
// Bounds the request rate when the network connects repeatedly
|
||||||
|
constexpr uint64_t MIN_INTERVAL_MS = 10 * 1000;
|
||||||
|
// Wake up interval to check whether the fallback became needed
|
||||||
|
constexpr uint32_t CHECK_INTERVAL_MS = 10 * 1000;
|
||||||
|
constexpr int SOCKET_TIMEOUT_SECONDS = 5;
|
||||||
|
constexpr size_t RESPONSE_MAX_LENGTH = 1024;
|
||||||
|
// IP geolocation is accurate to roughly city level
|
||||||
|
constexpr float ACCURACY_METERS = 25000.0f;
|
||||||
|
|
||||||
|
/** A lookup result */
|
||||||
|
struct CachedLookup {
|
||||||
|
// The network of the lookup, see trackedNetwork
|
||||||
|
uint64_t network = 0;
|
||||||
|
double latitude = NAN;
|
||||||
|
double longitude = NAN;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IpProvider {
|
||||||
|
LocationHost* host = nullptr;
|
||||||
|
Thread* thread = nullptr;
|
||||||
|
std::atomic<bool> running = false;
|
||||||
|
TaskEventGroup group {};
|
||||||
|
uint32_t stopBit = 0;
|
||||||
|
SystemEventSubscription networkSubscription {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// The last lookup result, kept across provider runs until the network isn't tracked anymore.
|
||||||
|
// Only accessed by the provider thread and by ip_provider_untrack_network() after the provider stopped.
|
||||||
|
CachedLookup* cache = nullptr;
|
||||||
|
|
||||||
|
// The local IP address and gateway of the last connected network, 0 when unknown.
|
||||||
|
// A disconnect keeps the last known network, so reconnecting to it doesn't need a new lookup.
|
||||||
|
std::atomic<uint64_t> trackedNetwork = 0;
|
||||||
|
|
||||||
|
void onNetworkConnected(SystemEvent* event, void*) {
|
||||||
|
NetworkConnectedEvent connected;
|
||||||
|
if (event->data_len < sizeof(connected)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memcpy(&connected, event->data, sizeof(connected));
|
||||||
|
trackedNetwork = (static_cast<uint64_t>(connected.ipv4_addr) << 32) | connected.gateway;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t getUptimeMs() {
|
||||||
|
return get_micros_since_boot() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The FreeRTOS POSIX port's tick signal can interrupt blocking socket calls, so they retry on EINTR
|
||||||
|
|
||||||
|
bool connectWithTimeout(int fd, const sockaddr* address, socklen_t addressLength) {
|
||||||
|
const int flags = fcntl(fd, F_GETFL, 0);
|
||||||
|
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
int result = connect(fd, address, addressLength);
|
||||||
|
if (result < 0 && errno == EINPROGRESS) {
|
||||||
|
fd_set writeSet;
|
||||||
|
do {
|
||||||
|
FD_ZERO(&writeSet);
|
||||||
|
FD_SET(fd, &writeSet);
|
||||||
|
timeval timeout { .tv_sec = SOCKET_TIMEOUT_SECONDS, .tv_usec = 0 };
|
||||||
|
result = select(fd + 1, nullptr, &writeSet, nullptr, &timeout);
|
||||||
|
} while (result < 0 && errno == EINTR);
|
||||||
|
int error = 0;
|
||||||
|
socklen_t errorLength = sizeof(error);
|
||||||
|
if (result == 1 && getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &errorLength) == 0 && error == 0) {
|
||||||
|
result = 0;
|
||||||
|
} else {
|
||||||
|
result = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fcntl(fd, F_SETFL, flags);
|
||||||
|
return result == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendAll(int fd, const char* data, size_t length) {
|
||||||
|
size_t sent = 0;
|
||||||
|
while (sent < length) {
|
||||||
|
const ssize_t result = send(fd, data + sent, length - sent, 0);
|
||||||
|
if (result < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sent += static_cast<size_t>(result);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Receive until the server closes the connection */
|
||||||
|
bool receiveAll(int fd, char* buffer, size_t bufferSize) {
|
||||||
|
size_t received = 0;
|
||||||
|
while (received < bufferSize - 1) {
|
||||||
|
const ssize_t result = recv(fd, buffer + received, bufferSize - 1 - received, 0);
|
||||||
|
if (result < 0) {
|
||||||
|
if (errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
received += static_cast<size_t>(result);
|
||||||
|
}
|
||||||
|
buffer[received] = '\0';
|
||||||
|
return received > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool httpGet(const char* host, const char* path, char* response, size_t responseSize) {
|
||||||
|
addrinfo hints {};
|
||||||
|
hints.ai_family = AF_INET;
|
||||||
|
hints.ai_socktype = SOCK_STREAM;
|
||||||
|
addrinfo* addresses = nullptr;
|
||||||
|
if (getaddrinfo(host, "80", &hints, &addresses) != 0 || addresses == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int fd = socket(addresses->ai_family, addresses->ai_socktype, addresses->ai_protocol);
|
||||||
|
if (fd < 0) {
|
||||||
|
freeaddrinfo(addresses);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
timeval timeout { .tv_sec = SOCKET_TIMEOUT_SECONDS, .tv_usec = 0 };
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
|
||||||
|
|
||||||
|
char request[160];
|
||||||
|
const int requestLength = snprintf(request, sizeof(request), "GET %s HTTP/1.0\r\nHost: %s\r\nConnection: close\r\n\r\n", path, host);
|
||||||
|
|
||||||
|
const bool success = connectWithTimeout(fd, addresses->ai_addr, addresses->ai_addrlen) &&
|
||||||
|
sendAll(fd, request, static_cast<size_t>(requestLength)) &&
|
||||||
|
receiveAll(fd, response, responseSize);
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
freeaddrinfo(addresses);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
void report(IpProvider& provider, double latitude, double longitude) {
|
||||||
|
Location location {
|
||||||
|
.latitude = latitude,
|
||||||
|
.longitude = longitude,
|
||||||
|
.horizontal_accuracy = ACCURACY_METERS,
|
||||||
|
.altitude = NAN,
|
||||||
|
.ellipsoidal_altitude = NAN,
|
||||||
|
.vertical_accuracy = NAN,
|
||||||
|
.speed = NAN,
|
||||||
|
.speed_accuracy = NAN,
|
||||||
|
.course = NAN,
|
||||||
|
.course_accuracy = NAN,
|
||||||
|
.satellites = 0,
|
||||||
|
.time_ms = 0,
|
||||||
|
.uptime_ms = 0,
|
||||||
|
.provider = {},
|
||||||
|
};
|
||||||
|
if (location_get_precision() == LOCATION_PRECISION_APPROXIMATE) {
|
||||||
|
location_make_approximate(&location);
|
||||||
|
}
|
||||||
|
location_host_report(provider.host, &location);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the delay until the next lookup is allowed */
|
||||||
|
uint64_t lookup(IpProvider& provider, uint64_t network) {
|
||||||
|
tt::std::string response(RESPONSE_MAX_LENGTH, '\0');
|
||||||
|
if (!httpGet(API_HOST, API_PATH, response.data(), response.size())) {
|
||||||
|
LOG_W(TAG, "Request failed");
|
||||||
|
return RETRY_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpApiResponse parsed;
|
||||||
|
if (!ip_api_parse_response(response.c_str(), &parsed)) {
|
||||||
|
LOG_W(TAG, "Malformed response");
|
||||||
|
return RETRY_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t delay = RETRY_INTERVAL_MS;
|
||||||
|
if (parsed.status_code != 200) {
|
||||||
|
LOG_W(TAG, "HTTP status %d", parsed.status_code);
|
||||||
|
} else if (!parsed.success) {
|
||||||
|
LOG_W(TAG, "Lookup failed: %s", parsed.message);
|
||||||
|
} else {
|
||||||
|
if (cache == nullptr) {
|
||||||
|
cache = tt::newObject<CachedLookup>();
|
||||||
|
}
|
||||||
|
if (cache != nullptr) {
|
||||||
|
*cache = { .network = network, .latitude = parsed.latitude, .longitude = parsed.longitude };
|
||||||
|
}
|
||||||
|
report(provider, parsed.latitude, parsed.longitude);
|
||||||
|
LOG_D(TAG, "Lookup succeeded");
|
||||||
|
delay = MIN_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.rate_remaining == 0 && parsed.rate_reset_seconds > 0) {
|
||||||
|
delay = std::max(delay, static_cast<uint64_t>(parsed.rate_reset_seconds) * 1000);
|
||||||
|
}
|
||||||
|
return delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t threadMain(void* context) {
|
||||||
|
auto& provider = *static_cast<IpProvider*>(context);
|
||||||
|
uint64_t lastAttemptMs = 0;
|
||||||
|
uint64_t nextAttemptMs = 0;
|
||||||
|
bool attempted = false;
|
||||||
|
// Whether the cached result was reported during this run
|
||||||
|
bool reportedCache = false;
|
||||||
|
|
||||||
|
while (provider.running) {
|
||||||
|
const uint64_t now = getUptimeMs();
|
||||||
|
const uint64_t waitMs = std::min<uint64_t>(CHECK_INTERVAL_MS, nextAttemptMs > now ? nextAttemptMs - now : 0);
|
||||||
|
uint32_t flags = 0;
|
||||||
|
task_event_group_wait_any(&provider.group, &flags, millis_to_ticks(static_cast<uint32_t>(waitMs)));
|
||||||
|
if (!provider.running) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The public IP address can change with the network
|
||||||
|
if (system_event_poll(&provider.networkSubscription) == ERROR_NONE && attempted) {
|
||||||
|
nextAttemptMs = lastAttemptMs + MIN_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!location_host_needs_fallback()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t network = trackedNetwork;
|
||||||
|
if (cache != nullptr && cache->network == network) {
|
||||||
|
if (!reportedCache) {
|
||||||
|
report(provider, cache->latitude, cache->longitude);
|
||||||
|
reportedCache = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t attemptMs = getUptimeMs();
|
||||||
|
if (attemptMs < nextAttemptMs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attempted = true;
|
||||||
|
lastAttemptMs = attemptMs;
|
||||||
|
nextAttemptMs = attemptMs + lookup(provider, network);
|
||||||
|
reportedCache = cache != nullptr && cache->network == network;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* create(LocationHost* host) {
|
||||||
|
auto* provider = tt::newObject<IpProvider>();
|
||||||
|
if (provider == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
provider->host = host;
|
||||||
|
provider->running = true;
|
||||||
|
task_event_group_construct(&provider->group);
|
||||||
|
if (task_event_group_claim_bit(&provider->group, &provider->stopBit) != ERROR_NONE) {
|
||||||
|
task_event_group_destruct(&provider->group);
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
provider->networkSubscription.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||||
|
if (system_event_subscribe(&provider->networkSubscription, &provider->group) != ERROR_NONE) {
|
||||||
|
task_event_group_destruct(&provider->group);
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
provider->thread = thread_alloc_full("location_ip", 4096, threadMain, provider, -1);
|
||||||
|
if (provider->thread != nullptr) {
|
||||||
|
// Settings are loaded before providers start, so the thread doesn't access flash
|
||||||
|
thread_set_stack_external(provider->thread, true);
|
||||||
|
}
|
||||||
|
if (provider->thread == nullptr || thread_start(provider->thread) != ERROR_NONE) {
|
||||||
|
if (provider->thread != nullptr) {
|
||||||
|
thread_free(provider->thread);
|
||||||
|
}
|
||||||
|
system_event_unsubscribe(&provider->networkSubscription);
|
||||||
|
task_event_group_destruct(&provider->group);
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
void requestStop(void* context) {
|
||||||
|
auto* provider = static_cast<IpProvider*>(context);
|
||||||
|
provider->running = false;
|
||||||
|
task_event_group_signal(&provider->group, provider->stopBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool waitStopped(void* context, TickType_t timeout) {
|
||||||
|
auto* provider = static_cast<IpProvider*>(context);
|
||||||
|
if (thread_join(provider->thread, timeout, millis_to_ticks(10)) != ERROR_NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
thread_free(provider->thread);
|
||||||
|
system_event_unsubscribe(&provider->networkSubscription);
|
||||||
|
task_event_group_destruct(&provider->group);
|
||||||
|
tt::deleteObject(provider);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view nextLine(std::string_view& text) {
|
||||||
|
const size_t end = text.find('\n');
|
||||||
|
std::string_view line = text.substr(0, end);
|
||||||
|
text = end == std::string_view::npos ? std::string_view {} : text.substr(end + 1);
|
||||||
|
if (!line.empty() && line.back() == '\r') {
|
||||||
|
line.remove_suffix(1);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
int parseHeaderNumber(std::string_view line, const char* name) {
|
||||||
|
const size_t nameLength = strlen(name);
|
||||||
|
if (line.size() <= nameLength || line[nameLength] != ':' || strncasecmp(line.data(), name, nameLength) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return atoi(tt::std::string(line.substr(nameLength + 1)).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
error_t ip_provider_track_network(void) {
|
||||||
|
return system_event_callback_add(KERNEL_EVENT_NETWORK_CONNECTED, onNetworkConnected, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ip_provider_untrack_network(void) {
|
||||||
|
system_event_callback_remove(KERNEL_EVENT_NETWORK_CONNECTED, onNetworkConnected);
|
||||||
|
if (cache != nullptr) {
|
||||||
|
tt::deleteObject(cache);
|
||||||
|
cache = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
||||||
|
*out = IpApiResponse {
|
||||||
|
.status_code = 0,
|
||||||
|
.rate_remaining = -1,
|
||||||
|
.rate_reset_seconds = -1,
|
||||||
|
.success = false,
|
||||||
|
.message = {},
|
||||||
|
.latitude = NAN,
|
||||||
|
.longitude = NAN,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string_view text = response;
|
||||||
|
const std::string_view statusLine = nextLine(text);
|
||||||
|
if (!statusLine.starts_with("HTTP/1.")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t space = statusLine.find(' ');
|
||||||
|
if (space == std::string_view::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->status_code = atoi(tt::std::string(statusLine.substr(space + 1)).c_str());
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (text.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const std::string_view header = nextLine(text);
|
||||||
|
if (header.empty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int remaining = parseHeaderNumber(header, "X-Rl");
|
||||||
|
if (remaining >= 0) {
|
||||||
|
out->rate_remaining = remaining;
|
||||||
|
}
|
||||||
|
const int resetSeconds = parseHeaderNumber(header, "X-Ttl");
|
||||||
|
if (resetSeconds >= 0) {
|
||||||
|
out->rate_reset_seconds = resetSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out->status_code != 200) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields are returned one per line, in the order of the API documentation
|
||||||
|
const std::string_view status = nextLine(text);
|
||||||
|
if (status == "success") {
|
||||||
|
const tt::std::string latitude(nextLine(text));
|
||||||
|
const tt::std::string longitude(nextLine(text));
|
||||||
|
char* latitudeEnd;
|
||||||
|
char* longitudeEnd;
|
||||||
|
out->latitude = strtod(latitude.c_str(), &latitudeEnd);
|
||||||
|
out->longitude = strtod(longitude.c_str(), &longitudeEnd);
|
||||||
|
if (latitude.empty() || *latitudeEnd != '\0' || longitude.empty() || *longitudeEnd != '\0' ||
|
||||||
|
!std::isfinite(out->latitude) || !std::isfinite(out->longitude) ||
|
||||||
|
std::fabs(out->latitude) > 90.0 || std::fabs(out->longitude) > 180.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->success = true;
|
||||||
|
} else if (status == "fail") {
|
||||||
|
const std::string_view message = nextLine(text).substr(0, IP_API_MESSAGE_MAX_LENGTH);
|
||||||
|
memcpy(out->message, message.data(), message.size());
|
||||||
|
out->message[message.size()] = '\0';
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationProviderApi ip_location_provider = {
|
||||||
|
.id = "ip",
|
||||||
|
.fallback = true,
|
||||||
|
.create = create,
|
||||||
|
.request_stop = requestStop,
|
||||||
|
.wait_stopped = waitStopped,
|
||||||
|
};
|
||||||
533
Modules/location-module/source/location_service.cpp
Normal file
533
Modules/location-module/source/location_service.cpp
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include <location/location.h>
|
||||||
|
#include <location/location_service.h>
|
||||||
|
#include <location/location_settings.h>
|
||||||
|
#include <location/provider.h>
|
||||||
|
|
||||||
|
#include <service/manager.h>
|
||||||
|
#include <service/manifest.h>
|
||||||
|
|
||||||
|
#include <tactility/concurrent/mutex.h>
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
|
#include <tactility/concurrent/thread.h>
|
||||||
|
#include <tactility/delay.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/time.h>
|
||||||
|
|
||||||
|
#include <TactilityCpp/Memory.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
#define TAG "location"
|
||||||
|
|
||||||
|
enum class HostState {
|
||||||
|
Stopped,
|
||||||
|
Running,
|
||||||
|
Stopping,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LocationHost {
|
||||||
|
const LocationProviderApi* api;
|
||||||
|
void* provider;
|
||||||
|
HostState state;
|
||||||
|
// The worker is calling the provider without holding the mutex
|
||||||
|
bool busy;
|
||||||
|
// Unregistering, which waits for the provider to stop
|
||||||
|
bool removing;
|
||||||
|
// Not created again until the provider is no longer desired
|
||||||
|
bool createFailed;
|
||||||
|
|
||||||
|
bool fallback;
|
||||||
|
bool hasLocation;
|
||||||
|
Location location;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/** A fix of a provider that isn't a fallback is preferred over fallback fixes for this long */
|
||||||
|
constexpr uint64_t FRESH_MS = 120000;
|
||||||
|
// About 2 km north to south
|
||||||
|
constexpr double APPROXIMATE_GRID_DEGREES = 0.02;
|
||||||
|
// The worker waits this long for a stopping provider before it handles the other providers
|
||||||
|
constexpr uint32_t STOP_WAIT_MS = 100;
|
||||||
|
constexpr uint32_t UNREGISTER_POLL_MS = 10;
|
||||||
|
constexpr size_t WORKER_STACK_SIZE = 4096;
|
||||||
|
|
||||||
|
struct GlobalMutex {
|
||||||
|
Mutex handle {};
|
||||||
|
GlobalMutex() { mutex_construct(&handle); }
|
||||||
|
~GlobalMutex() { mutex_destruct(&handle); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Not held while calling provider functions
|
||||||
|
GlobalMutex mutex;
|
||||||
|
|
||||||
|
struct HostStorage {
|
||||||
|
LocationHost hosts[LOCATION_PROVIDER_MAX_COUNT] {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allocated in external memory when available, while providers are registered
|
||||||
|
HostStorage* hostStorage = nullptr;
|
||||||
|
bool running = false;
|
||||||
|
LocationSubscription* subscriptions = nullptr;
|
||||||
|
// Starts and stops providers. It only exists while a provider isn't in its desired state.
|
||||||
|
Thread* worker = nullptr;
|
||||||
|
bool workerActive = false;
|
||||||
|
|
||||||
|
GlobalMutex settingsMutex;
|
||||||
|
LocationSettings settings {};
|
||||||
|
bool settingsLoaded = false;
|
||||||
|
|
||||||
|
/** Must be called with settingsMutex held */
|
||||||
|
void ensureSettingsLoaded() {
|
||||||
|
if (!settingsLoaded) {
|
||||||
|
location_settings_load(&settings);
|
||||||
|
settingsLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with mutex held */
|
||||||
|
std::span<LocationHost> getHosts() {
|
||||||
|
return hostStorage != nullptr ? std::span<LocationHost>(hostStorage->hosts) : std::span<LocationHost>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Frees the hosts when no provider is registered and the worker doesn't use them. Must be called with mutex held. */
|
||||||
|
void freeUnusedHosts() {
|
||||||
|
if (hostStorage == nullptr || workerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& host : hostStorage->hosts) {
|
||||||
|
if (host.api != nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tt::deleteObject(hostStorage);
|
||||||
|
hostStorage = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t getUptimeMs() {
|
||||||
|
return get_micros_since_boot() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with mutex held */
|
||||||
|
int getRank(const LocationHost& host, uint64_t now) {
|
||||||
|
if (host.fallback) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return now - host.location.uptime_ms < FRESH_MS ? 2 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with mutex held */
|
||||||
|
const LocationHost* findBest() {
|
||||||
|
const uint64_t now = getUptimeMs();
|
||||||
|
const LocationHost* best = nullptr;
|
||||||
|
int bestRank = -1;
|
||||||
|
for (const auto& host : getHosts()) {
|
||||||
|
if (!host.hasLocation) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int rank = getRank(host, now);
|
||||||
|
if (rank > bestRank || (rank == bestRank && host.location.uptime_ms > best->location.uptime_ms)) {
|
||||||
|
best = &host;
|
||||||
|
bestRank = rank;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with mutex held */
|
||||||
|
bool isDesired(const LocationHost& host) {
|
||||||
|
return host.api != nullptr && !host.removing && running && subscriptions != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Must be called with mutex held */
|
||||||
|
bool isSettled(const LocationHost& host) {
|
||||||
|
switch (host.state) {
|
||||||
|
case HostState::Running:
|
||||||
|
return isDesired(host);
|
||||||
|
case HostState::Stopped:
|
||||||
|
return !isDesired(host) || host.createFailed;
|
||||||
|
case HostState::Stopping:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Takes one step towards the desired state. Must be called with mutex held, which is released during the step. */
|
||||||
|
void step(LocationHost& host) {
|
||||||
|
const LocationProviderApi* api = host.api;
|
||||||
|
void* provider = host.provider;
|
||||||
|
const HostState state = host.state;
|
||||||
|
host.busy = true;
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case HostState::Stopped: {
|
||||||
|
void* created = api->create(&host);
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
if (created != nullptr) {
|
||||||
|
host.provider = created;
|
||||||
|
host.state = HostState::Running;
|
||||||
|
} else {
|
||||||
|
LOG_E(TAG, "Failed to start provider %s", api->id);
|
||||||
|
host.createFailed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case HostState::Running:
|
||||||
|
api->request_stop(provider);
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
host.state = HostState::Stopping;
|
||||||
|
break;
|
||||||
|
case HostState::Stopping: {
|
||||||
|
const bool stopped = api->wait_stopped(provider, millis_to_ticks(STOP_WAIT_MS));
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
if (stopped) {
|
||||||
|
host.provider = nullptr;
|
||||||
|
host.state = HostState::Stopped;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host.busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t workerMain(void*) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
bool settled = false;
|
||||||
|
while (!settled) {
|
||||||
|
settled = true;
|
||||||
|
for (auto& host : getHosts()) {
|
||||||
|
if (!host.busy && !isSettled(host)) {
|
||||||
|
settled = false;
|
||||||
|
step(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
workerActive = false;
|
||||||
|
freeUnusedHosts();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Frees the worker after it exited. Must be called with mutex held. */
|
||||||
|
void freeFinishedWorker() {
|
||||||
|
if (worker != nullptr && !workerActive) {
|
||||||
|
// The worker released the mutex for the last time, so it exits right away
|
||||||
|
thread_join(worker, portMAX_DELAY, 1);
|
||||||
|
thread_free(worker);
|
||||||
|
worker = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts the worker when a provider isn't in its desired state. Must be called with mutex held. */
|
||||||
|
void reconcile() {
|
||||||
|
bool settled = true;
|
||||||
|
for (auto& host : getHosts()) {
|
||||||
|
if (!isDesired(host)) {
|
||||||
|
host.createFailed = false;
|
||||||
|
}
|
||||||
|
settled = settled && isSettled(host);
|
||||||
|
}
|
||||||
|
if (settled || workerActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The worker doesn't access flash, so its stack can be in external memory
|
||||||
|
mutex_lock(&settingsMutex.handle);
|
||||||
|
ensureSettingsLoaded();
|
||||||
|
mutex_unlock(&settingsMutex.handle);
|
||||||
|
|
||||||
|
freeFinishedWorker();
|
||||||
|
worker = thread_alloc_full("location", WORKER_STACK_SIZE, workerMain, nullptr, -1);
|
||||||
|
if (worker == nullptr) {
|
||||||
|
LOG_E(TAG, "Failed to allocate worker");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread_set_stack_external(worker, true);
|
||||||
|
if (thread_start(worker) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to start worker");
|
||||||
|
thread_free(worker);
|
||||||
|
worker = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workerActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* createService(const ServiceManifest*) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroyService(const ServiceManifest*, void*) {}
|
||||||
|
|
||||||
|
error_t onStart(ServiceInstance*, void*) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
running = true;
|
||||||
|
reconcile();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onStop(ServiceInstance*, void*) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
running = false;
|
||||||
|
for (auto& host : getHosts()) {
|
||||||
|
host.hasLocation = false;
|
||||||
|
}
|
||||||
|
reconcile();
|
||||||
|
freeFinishedWorker();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
error_t location_set_enabled(bool enabled) {
|
||||||
|
mutex_lock(&settingsMutex.handle);
|
||||||
|
ensureSettingsLoaded();
|
||||||
|
settings.enabled = enabled;
|
||||||
|
const LocationSettings copy = settings;
|
||||||
|
mutex_unlock(&settingsMutex.handle);
|
||||||
|
|
||||||
|
const error_t saved = location_settings_save(©);
|
||||||
|
error_t result;
|
||||||
|
if (enabled) {
|
||||||
|
result = service_manager_start(LOCATION_SERVICE_ID);
|
||||||
|
if (result == ERROR_INVALID_STATE) {
|
||||||
|
result = ERROR_NONE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = service_manager_stop(LOCATION_SERVICE_ID);
|
||||||
|
if (result == ERROR_NOT_FOUND) {
|
||||||
|
result = ERROR_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result != ERROR_NONE ? result : saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool location_is_enabled(void) {
|
||||||
|
mutex_lock(&settingsMutex.handle);
|
||||||
|
ensureSettingsLoaded();
|
||||||
|
const bool enabled = settings.enabled;
|
||||||
|
mutex_unlock(&settingsMutex.handle);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_set_precision(LocationPrecision precision) {
|
||||||
|
mutex_lock(&settingsMutex.handle);
|
||||||
|
ensureSettingsLoaded();
|
||||||
|
settings.precision = precision;
|
||||||
|
const LocationSettings copy = settings;
|
||||||
|
mutex_unlock(&settingsMutex.handle);
|
||||||
|
|
||||||
|
if (precision == LOCATION_PRECISION_APPROXIMATE) {
|
||||||
|
// Precise fixes are dropped until their providers report again. Coarse fixes are rounded.
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
for (auto& host : getHosts()) {
|
||||||
|
if (host.hasLocation) {
|
||||||
|
if (host.location.horizontal_accuracy >= LOCATION_APPROXIMATE_ACCURACY_METERS) {
|
||||||
|
location_make_approximate(&host.location);
|
||||||
|
} else {
|
||||||
|
host.hasLocation = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
}
|
||||||
|
return location_settings_save(©);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationPrecision location_get_precision(void) {
|
||||||
|
mutex_lock(&settingsMutex.handle);
|
||||||
|
ensureSettingsLoaded();
|
||||||
|
const LocationPrecision precision = settings.precision;
|
||||||
|
mutex_unlock(&settingsMutex.handle);
|
||||||
|
return precision;
|
||||||
|
}
|
||||||
|
|
||||||
|
void location_make_approximate(Location* location) {
|
||||||
|
location->latitude = std::round(location->latitude / APPROXIMATE_GRID_DEGREES) * APPROXIMATE_GRID_DEGREES;
|
||||||
|
location->longitude = std::round(location->longitude / APPROXIMATE_GRID_DEGREES) * APPROXIMATE_GRID_DEGREES;
|
||||||
|
if (!(location->horizontal_accuracy >= LOCATION_APPROXIMATE_ACCURACY_METERS)) {
|
||||||
|
location->horizontal_accuracy = LOCATION_APPROXIMATE_ACCURACY_METERS;
|
||||||
|
}
|
||||||
|
location->altitude = NAN;
|
||||||
|
location->ellipsoidal_altitude = NAN;
|
||||||
|
location->vertical_accuracy = NAN;
|
||||||
|
location->speed = NAN;
|
||||||
|
location->speed_accuracy = NAN;
|
||||||
|
location->course = NAN;
|
||||||
|
location->course_accuracy = NAN;
|
||||||
|
location->satellites = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_provider_register(const LocationProviderApi* api) {
|
||||||
|
if (strlen(api->id) > LOCATION_PROVIDER_ID_MAX_LENGTH) {
|
||||||
|
return ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
if (hostStorage == nullptr) {
|
||||||
|
hostStorage = tt::newObject<HostStorage>();
|
||||||
|
if (hostStorage == nullptr) {
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_OUT_OF_MEMORY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LocationHost* freeHost = nullptr;
|
||||||
|
for (auto& host : getHosts()) {
|
||||||
|
if (host.api == nullptr) {
|
||||||
|
if (freeHost == nullptr) {
|
||||||
|
freeHost = &host;
|
||||||
|
}
|
||||||
|
} else if (strcmp(host.api->id, api->id) == 0) {
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_ALREADY_EXISTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (freeHost == nullptr) {
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
freeHost->api = api;
|
||||||
|
freeHost->fallback = api->fallback;
|
||||||
|
reconcile();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_provider_unregister(const LocationProviderApi* api) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
LocationHost* host = nullptr;
|
||||||
|
for (auto& candidate : getHosts()) {
|
||||||
|
if (candidate.api == api && !candidate.removing) {
|
||||||
|
host = &candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (host == nullptr) {
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
host->removing = true;
|
||||||
|
while (host->state != HostState::Stopped || host->busy) {
|
||||||
|
reconcile();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
delay_millis(UNREGISTER_POLL_MS);
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
}
|
||||||
|
host->hasLocation = false;
|
||||||
|
host->api = nullptr;
|
||||||
|
host->removing = false;
|
||||||
|
host->createFailed = false;
|
||||||
|
freeUnusedHosts();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void location_host_report(LocationHost* host, const Location* location) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
// Providers that are being stopped don't report anymore
|
||||||
|
if (host->state != HostState::Running || !isDesired(*host)) {
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
host->location = *location;
|
||||||
|
strcpy(host->location.provider, host->api->id);
|
||||||
|
host->location.uptime_ms = getUptimeMs();
|
||||||
|
host->hasLocation = true;
|
||||||
|
if (findBest() == host) {
|
||||||
|
for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||||
|
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool location_host_needs_fallback(void) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
const uint64_t now = getUptimeMs();
|
||||||
|
bool needed = true;
|
||||||
|
for (const auto& host : getHosts()) {
|
||||||
|
if (host.hasLocation && getRank(host, now) == 2) {
|
||||||
|
needed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return needed;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_get(Location* location) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
const auto* best = findBest();
|
||||||
|
if (best != nullptr) {
|
||||||
|
*location = best->location;
|
||||||
|
}
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return best != nullptr ? ERROR_NONE : ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_subscribe(LocationSubscription* sub, TaskEventGroup* event_group) {
|
||||||
|
uint32_t bit;
|
||||||
|
const error_t claimed = task_event_group_claim_bit(event_group, &bit);
|
||||||
|
if (claimed != ERROR_NONE) {
|
||||||
|
return claimed;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
for (auto* existing = subscriptions; existing != nullptr; existing = existing->internal.next) {
|
||||||
|
if (existing == sub) {
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
task_event_group_release_bit(event_group, bit);
|
||||||
|
return ERROR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sub->bit = bit;
|
||||||
|
sub->internal.event_group = event_group;
|
||||||
|
sub->internal.next = subscriptions;
|
||||||
|
subscriptions = sub;
|
||||||
|
reconcile();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_unsubscribe(LocationSubscription* sub) {
|
||||||
|
error_t result = ERROR_NOT_FOUND;
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
for (auto** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||||
|
if (*link == sub) {
|
||||||
|
*link = sub->internal.next;
|
||||||
|
result = ERROR_NONE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reconcile();
|
||||||
|
freeFinishedWorker();
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
if (result == ERROR_NONE) {
|
||||||
|
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool location_is_in_use(void) {
|
||||||
|
mutex_lock(&mutex.handle);
|
||||||
|
const bool inUse = running && subscriptions != nullptr;
|
||||||
|
mutex_unlock(&mutex.handle);
|
||||||
|
return inUse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern const ServiceManifest location_service_manifest = {
|
||||||
|
.id = LOCATION_SERVICE_ID,
|
||||||
|
.create_service = createService,
|
||||||
|
.destroy_service = destroyService,
|
||||||
|
.on_start = onStart,
|
||||||
|
.on_stop = onStop,
|
||||||
|
};
|
||||||
66
Modules/location-module/source/location_settings.cpp
Normal file
66
Modules/location-module/source/location_settings.cpp
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include <location/location_settings.h>
|
||||||
|
|
||||||
|
#include <service/paths.h>
|
||||||
|
|
||||||
|
#include <tactility/filesystem/fs.h>
|
||||||
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/paths.h>
|
||||||
|
#include <tactility/properties_file.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
constexpr auto* TAG = "location_settings";
|
||||||
|
constexpr auto* SETTINGS_FILE_NAME = "settings.properties";
|
||||||
|
constexpr auto* KEY_ENABLED = "enabled";
|
||||||
|
constexpr auto* KEY_PRECISION = "precision";
|
||||||
|
constexpr auto* PRECISION_APPROXIMATE = "approximate";
|
||||||
|
constexpr auto* PRECISION_PRECISE = "precise";
|
||||||
|
|
||||||
|
void location_settings_load(LocationSettings* settings) {
|
||||||
|
*settings = LocationSettings {
|
||||||
|
.enabled = false,
|
||||||
|
.precision = LOCATION_PRECISION_PRECISE,
|
||||||
|
};
|
||||||
|
|
||||||
|
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||||
|
if (service_paths_get_user_data_path(LOCATION_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertiesFile* file = properties_file_open(path);
|
||||||
|
if (file == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char value[32];
|
||||||
|
if (properties_file_get(file, KEY_ENABLED, value, sizeof(value)) == ERROR_NONE) {
|
||||||
|
settings->enabled = strcmp(value, "true") == 0;
|
||||||
|
}
|
||||||
|
if (properties_file_get(file, KEY_PRECISION, value, sizeof(value)) == ERROR_NONE) {
|
||||||
|
settings->precision = strcmp(value, PRECISION_APPROXIMATE) == 0 ? LOCATION_PRECISION_APPROXIMATE : LOCATION_PRECISION_PRECISE;
|
||||||
|
}
|
||||||
|
properties_file_close(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
error_t location_settings_save(const LocationSettings* settings) {
|
||||||
|
char directory[FILE_MAX_PATH_STRING_LENGTH];
|
||||||
|
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||||
|
if (service_paths_get_user_data_directory(LOCATION_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE ||
|
||||||
|
service_paths_get_user_data_path(LOCATION_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
|
||||||
|
return ERROR_BUFFER_OVERFLOW;
|
||||||
|
}
|
||||||
|
const error_t made = directory_make(directory, true);
|
||||||
|
if (made != ERROR_NONE && made != ERROR_ALREADY_EXISTS) {
|
||||||
|
LOG_E(TAG, "Failed to create %s", directory);
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertiesFile* file = properties_file_open(path);
|
||||||
|
if (file == nullptr) {
|
||||||
|
return ERROR_RESOURCE;
|
||||||
|
}
|
||||||
|
properties_file_set(file, KEY_ENABLED, settings->enabled ? "true" : "false");
|
||||||
|
properties_file_set(file, KEY_PRECISION, settings->precision == LOCATION_PRECISION_APPROXIMATE ? PRECISION_APPROXIMATE : PRECISION_PRECISE);
|
||||||
|
return properties_file_close(file);
|
||||||
|
}
|
||||||
66
Modules/location-module/source/module.cpp
Normal file
66
Modules/location-module/source/module.cpp
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
#include <location/gps_provider.h>
|
||||||
|
#include <location/ip_provider.h>
|
||||||
|
#include <location/location.h>
|
||||||
|
#include <location/location_service.h>
|
||||||
|
#include <location/module.h>
|
||||||
|
|
||||||
|
#include <service/manager.h>
|
||||||
|
|
||||||
|
#include <tactility/error.h>
|
||||||
|
#include <tactility/module.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
static error_t start() {
|
||||||
|
error_t error = ip_provider_track_network();
|
||||||
|
if (error != ERROR_NONE) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = location_provider_register(&gps_location_provider);
|
||||||
|
if (error != ERROR_NONE) {
|
||||||
|
ip_provider_untrack_network();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
error = location_provider_register(&ip_location_provider);
|
||||||
|
if (error != ERROR_NONE) {
|
||||||
|
location_provider_unregister(&gps_location_provider);
|
||||||
|
ip_provider_untrack_network();
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
return service_manager_add(&location_service_manifest, location_is_enabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
static error_t stop() {
|
||||||
|
const error_t error = service_manager_stop(location_service_manifest.id);
|
||||||
|
if (error != ERROR_NONE && error != ERROR_NOT_FOUND) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
location_provider_unregister(&ip_location_provider);
|
||||||
|
location_provider_unregister(&gps_location_provider);
|
||||||
|
ip_provider_untrack_network();
|
||||||
|
return service_manager_remove(location_service_manifest.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const ModuleSymbol location_module_symbols[] = {
|
||||||
|
DEFINE_MODULE_SYMBOL(location_set_enabled),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_is_enabled),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_set_precision),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_get_precision),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_get),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_subscribe),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_unsubscribe),
|
||||||
|
DEFINE_MODULE_SYMBOL(location_is_in_use),
|
||||||
|
MODULE_SYMBOL_TERMINATOR
|
||||||
|
};
|
||||||
|
|
||||||
|
Module location_module = {
|
||||||
|
.name = "location",
|
||||||
|
.start = start,
|
||||||
|
.stop = stop,
|
||||||
|
.drivers = nullptr,
|
||||||
|
.symbols = location_module_symbols,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
20
Modules/location-module/tests/CMakeLists.txt
Normal file
20
Modules/location-module/tests/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
project(LocationModuleTests)
|
||||||
|
|
||||||
|
enable_language(C CXX ASM)
|
||||||
|
|
||||||
|
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||||
|
add_executable(LocationModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||||
|
|
||||||
|
target_include_directories(LocationModuleTests PRIVATE
|
||||||
|
${DOCTESTINC}
|
||||||
|
${PROJECT_SOURCE_DIR}/../private
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME LocationModuleTests COMMAND LocationModuleTests)
|
||||||
|
|
||||||
|
target_link_libraries(LocationModuleTests PUBLIC
|
||||||
|
TactilityKernel
|
||||||
|
platform-posix
|
||||||
|
service-module
|
||||||
|
location-module
|
||||||
|
)
|
||||||
64
Modules/location-module/tests/source/gps_provider_test.cpp
Normal file
64
Modules/location-module/tests/source/gps_provider_test.cpp
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#include "doctest.h"
|
||||||
|
|
||||||
|
#include <location/gps_provider.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto* RMC = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
||||||
|
constexpr auto* GGA = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("gps_location_from_nmea converts RMC and GGA") {
|
||||||
|
minmea_sentence_rmc rmc;
|
||||||
|
minmea_sentence_gga gga;
|
||||||
|
REQUIRE(minmea_parse_rmc(&rmc, RMC));
|
||||||
|
REQUIRE(minmea_parse_gga(&gga, GGA));
|
||||||
|
|
||||||
|
Location location;
|
||||||
|
REQUIRE(gps_location_from_nmea(&rmc, &gga, &location));
|
||||||
|
CHECK(location.latitude == doctest::Approx(48.1173));
|
||||||
|
CHECK(location.longitude == doctest::Approx(11.516667));
|
||||||
|
CHECK(location.speed == doctest::Approx(11.5235).epsilon(0.001));
|
||||||
|
CHECK(location.course == doctest::Approx(84.4));
|
||||||
|
CHECK(location.horizontal_accuracy == doctest::Approx(4.5));
|
||||||
|
CHECK(location.altitude == doctest::Approx(545.4));
|
||||||
|
CHECK(location.ellipsoidal_altitude == doctest::Approx(592.3));
|
||||||
|
CHECK(std::isnan(location.vertical_accuracy));
|
||||||
|
CHECK(location.satellites == 8);
|
||||||
|
CHECK(location.time_ms == 764426119000LL);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("gps_location_from_nmea without GGA has no altitude or accuracy") {
|
||||||
|
minmea_sentence_rmc rmc;
|
||||||
|
REQUIRE(minmea_parse_rmc(&rmc, RMC));
|
||||||
|
|
||||||
|
Location location;
|
||||||
|
REQUIRE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||||
|
CHECK(location.latitude == doctest::Approx(48.1173));
|
||||||
|
CHECK(std::isnan(location.altitude));
|
||||||
|
CHECK(std::isnan(location.horizontal_accuracy));
|
||||||
|
CHECK(location.satellites == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("gps_location_from_nmea handles the southern and western hemispheres") {
|
||||||
|
minmea_sentence_rmc rmc;
|
||||||
|
REQUIRE(minmea_parse_rmc(&rmc, "$GPRMC,123519,A,3352.128,S,15112.558,W,000.0,,230394,,*00"));
|
||||||
|
|
||||||
|
Location location;
|
||||||
|
REQUIRE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||||
|
CHECK(location.latitude == doctest::Approx(-33.8688));
|
||||||
|
CHECK(location.longitude == doctest::Approx(-151.209300));
|
||||||
|
CHECK(location.speed == doctest::Approx(0.0));
|
||||||
|
CHECK(std::isnan(location.course));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("gps_location_from_nmea rejects a fix that is not valid") {
|
||||||
|
minmea_sentence_rmc rmc;
|
||||||
|
REQUIRE(minmea_parse_rmc(&rmc, "$GPRMC,123519,V,,,,,,,230394,,*00"));
|
||||||
|
|
||||||
|
Location location;
|
||||||
|
CHECK_FALSE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||||
|
}
|
||||||
57
Modules/location-module/tests/source/ip_provider_test.cpp
Normal file
57
Modules/location-module/tests/source/ip_provider_test.cpp
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#include "doctest.h"
|
||||||
|
|
||||||
|
#include <location/ip_provider.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
TEST_CASE("ip_api_parse_response parses a successful lookup") {
|
||||||
|
const char* response =
|
||||||
|
"HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||||
|
"X-Ttl: 60\r\n"
|
||||||
|
"X-Rl: 44\r\n"
|
||||||
|
"\r\n"
|
||||||
|
"success\n"
|
||||||
|
"39.03\n"
|
||||||
|
"-77.5\n";
|
||||||
|
IpApiResponse parsed;
|
||||||
|
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||||
|
CHECK(parsed.status_code == 200);
|
||||||
|
CHECK(parsed.success);
|
||||||
|
CHECK(parsed.latitude == doctest::Approx(39.03));
|
||||||
|
CHECK(parsed.longitude == doctest::Approx(-77.5));
|
||||||
|
CHECK(parsed.rate_remaining == 44);
|
||||||
|
CHECK(parsed.rate_reset_seconds == 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ip_api_parse_response parses a failed lookup") {
|
||||||
|
const char* response = "HTTP/1.1 200 OK\r\nx-rl: 0\r\nx-ttl: 12\r\n\r\nfail\nprivate range\n";
|
||||||
|
IpApiResponse parsed;
|
||||||
|
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||||
|
CHECK_FALSE(parsed.success);
|
||||||
|
CHECK(strcmp(parsed.message, "private range") == 0);
|
||||||
|
CHECK(parsed.rate_remaining == 0);
|
||||||
|
CHECK(parsed.rate_reset_seconds == 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ip_api_parse_response handles a throttled request") {
|
||||||
|
const char* response = "HTTP/1.1 429 Too Many Requests\r\nX-Rl: 0\r\nX-Ttl: 30\r\n\r\n";
|
||||||
|
IpApiResponse parsed;
|
||||||
|
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||||
|
CHECK(parsed.status_code == 429);
|
||||||
|
CHECK_FALSE(parsed.success);
|
||||||
|
CHECK(parsed.rate_reset_seconds == 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ip_api_parse_response rejects malformed responses") {
|
||||||
|
IpApiResponse parsed;
|
||||||
|
CHECK_FALSE(ip_api_parse_response("", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("garbage", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\nX-Rl: 1\r\n", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\nabc\n1.0\n", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nunknown\n", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\nnan\n1.0\n", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n1.0\ninf\n", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n90.5\n1.0\n", &parsed));
|
||||||
|
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n1.0\n-180.5\n", &parsed));
|
||||||
|
}
|
||||||
48
Modules/location-module/tests/source/main.cpp
Normal file
48
Modules/location-module/tests/source/main.cpp
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#define DOCTEST_CONFIG_IMPLEMENT
|
||||||
|
#include "doctest.h"
|
||||||
|
#include <tactility/check.h>
|
||||||
|
|
||||||
|
#include <tactility/dts.h>
|
||||||
|
#include <tactility/freertos/task.h>
|
||||||
|
#include <tactility/kernel_init.h>
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int argc;
|
||||||
|
char** argv;
|
||||||
|
int result;
|
||||||
|
} TestTaskData;
|
||||||
|
|
||||||
|
extern "C" struct Module platform_posix_module;
|
||||||
|
|
||||||
|
void test_task(void* parameter) {
|
||||||
|
auto* data = (TestTaskData*)parameter;
|
||||||
|
|
||||||
|
doctest::Context context;
|
||||||
|
context.applyCommandLine(data->argc, data->argv);
|
||||||
|
context.setOption("no-breaks", true);
|
||||||
|
|
||||||
|
Module* dts_modules[] = { &platform_posix_module, nullptr };
|
||||||
|
DtsDevice dts_devices[] = { DTS_DEVICE_TERMINATOR };
|
||||||
|
check(kernel_init(dts_modules, dts_devices) == ERROR_NONE);
|
||||||
|
|
||||||
|
data->result = context.run();
|
||||||
|
|
||||||
|
vTaskEndScheduler();
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
TestTaskData data = {
|
||||||
|
.argc = argc,
|
||||||
|
.argv = argv,
|
||||||
|
.result = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
if (xTaskCreate(test_task, "test_task", 16384, &data, 1, nullptr) != pdPASS) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskStartScheduler();
|
||||||
|
return data.result;
|
||||||
|
}
|
||||||
427
Modules/location-module/tests/source/service_test.cpp
Normal file
427
Modules/location-module/tests/source/service_test.cpp
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
#include "doctest.h"
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
#include <location/gps_provider.h>
|
||||||
|
#include <location/location_service.h>
|
||||||
|
#include <location/provider.h>
|
||||||
|
|
||||||
|
#include <service/manager.h>
|
||||||
|
|
||||||
|
#include <tactility/delay.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Providers start and stop on the service's worker
|
||||||
|
std::atomic<LocationHost*> fakeHost = nullptr;
|
||||||
|
std::atomic<int> createCount = 0;
|
||||||
|
std::atomic<int> destroyCount = 0;
|
||||||
|
|
||||||
|
void* fakeCreate(LocationHost* host) {
|
||||||
|
fakeHost = host;
|
||||||
|
createCount++;
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fakeRequestStop(void*) {}
|
||||||
|
|
||||||
|
bool fakeWaitStopped(void*, TickType_t) {
|
||||||
|
fakeHost = nullptr;
|
||||||
|
destroyCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationProviderApi fakeProvider = {
|
||||||
|
.id = "fake",
|
||||||
|
.fallback = false,
|
||||||
|
.create = fakeCreate,
|
||||||
|
.request_stop = fakeRequestStop,
|
||||||
|
.wait_stopped = fakeWaitStopped,
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Predicate>
|
||||||
|
bool waitUntil(Predicate predicate) {
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
if (predicate()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delay_millis(10);
|
||||||
|
}
|
||||||
|
return predicate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Location makeLocation(double latitude, double longitude) {
|
||||||
|
return Location {
|
||||||
|
.latitude = latitude,
|
||||||
|
.longitude = longitude,
|
||||||
|
.horizontal_accuracy = 3.0f,
|
||||||
|
.altitude = NAN,
|
||||||
|
.ellipsoidal_altitude = NAN,
|
||||||
|
.vertical_accuracy = NAN,
|
||||||
|
.speed = NAN,
|
||||||
|
.speed_accuracy = NAN,
|
||||||
|
.course = NAN,
|
||||||
|
.course_accuracy = NAN,
|
||||||
|
.satellites = 0,
|
||||||
|
.time_ms = 1000,
|
||||||
|
.uptime_ms = 0,
|
||||||
|
.provider = {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("location service reports fixes of its providers") {
|
||||||
|
createCount = 0;
|
||||||
|
destroyCount = 0;
|
||||||
|
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_register(&fakeProvider) == ERROR_ALREADY_EXISTS);
|
||||||
|
|
||||||
|
// Providers don't run without subscriptions
|
||||||
|
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||||
|
CHECK(createCount == 0);
|
||||||
|
CHECK_FALSE(location_is_in_use());
|
||||||
|
|
||||||
|
Location location;
|
||||||
|
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||||
|
|
||||||
|
TaskEventGroup group;
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
LocationSubscription sub {};
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
CHECK(location_subscribe(&sub, &group) == ERROR_INVALID_STATE);
|
||||||
|
CHECK(location_is_in_use());
|
||||||
|
REQUIRE(waitUntil([] { return fakeHost != nullptr; }));
|
||||||
|
CHECK(createCount == 1);
|
||||||
|
|
||||||
|
const Location reported = makeLocation(51.05, 3.72);
|
||||||
|
location_host_report(fakeHost, &reported);
|
||||||
|
|
||||||
|
uint32_t flags = 0;
|
||||||
|
CHECK(task_event_group_wait(&group, sub.bit, false, &flags, 0) == ERROR_NONE);
|
||||||
|
CHECK(flags == sub.bit);
|
||||||
|
|
||||||
|
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||||
|
CHECK(location.latitude == doctest::Approx(51.05));
|
||||||
|
CHECK(location.longitude == doctest::Approx(3.72));
|
||||||
|
CHECK(location.horizontal_accuracy == doctest::Approx(3.0));
|
||||||
|
CHECK(location.time_ms == 1000);
|
||||||
|
CHECK(strcmp(location.provider, "fake") == 0);
|
||||||
|
|
||||||
|
// The last known fix remains available after the last subscription is removed
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NOT_FOUND);
|
||||||
|
CHECK_FALSE(location_is_in_use());
|
||||||
|
CHECK(waitUntil([] { return destroyCount == 1; }));
|
||||||
|
CHECK(location_get(&location) == ERROR_NONE);
|
||||||
|
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
CHECK(waitUntil([] { return createCount == 2; }));
|
||||||
|
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK_FALSE(location_is_in_use());
|
||||||
|
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||||
|
CHECK(waitUntil([] { return destroyCount == 2; }));
|
||||||
|
|
||||||
|
REQUIRE(service_manager_start(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(waitUntil([] { return createCount == 3; }));
|
||||||
|
// Unregistering waits for the provider to stop
|
||||||
|
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||||
|
CHECK(destroyCount == 3);
|
||||||
|
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NOT_FOUND);
|
||||||
|
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(destroyCount == 3);
|
||||||
|
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("location provider registration is limited") {
|
||||||
|
LocationProviderApi providers[LOCATION_PROVIDER_MAX_COUNT + 1];
|
||||||
|
char ids[LOCATION_PROVIDER_MAX_COUNT + 1][8];
|
||||||
|
for (size_t i = 0; i <= LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||||
|
snprintf(ids[i], sizeof(ids[i]), "p%zu", i);
|
||||||
|
providers[i] = { .id = ids[i], .fallback = false, .create = fakeCreate, .request_stop = fakeRequestStop, .wait_stopped = fakeWaitStopped };
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||||
|
REQUIRE(location_provider_register(&providers[i]) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
CHECK(location_provider_register(&providers[LOCATION_PROVIDER_MAX_COUNT]) == ERROR_RESOURCE);
|
||||||
|
for (size_t i = 0; i < LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||||
|
CHECK(location_provider_unregister(&providers[i]) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::atomic<LocationHost*> fallbackHost = nullptr;
|
||||||
|
|
||||||
|
void* fallbackCreate(LocationHost* host) {
|
||||||
|
fallbackHost = host;
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fallbackRequestStop(void*) {}
|
||||||
|
|
||||||
|
bool fallbackWaitStopped(void*, TickType_t) {
|
||||||
|
fallbackHost = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationProviderApi fakeFallbackProvider = {
|
||||||
|
.id = "fakefallback",
|
||||||
|
.fallback = true,
|
||||||
|
.create = fallbackCreate,
|
||||||
|
.request_stop = fallbackRequestStop,
|
||||||
|
.wait_stopped = fallbackWaitStopped,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("location service prefers fixes of providers that aren't a fallback") {
|
||||||
|
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(location_provider_register(&fakeFallbackProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||||
|
|
||||||
|
TaskEventGroup group;
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
LocationSubscription sub {};
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
REQUIRE(waitUntil([] { return fakeHost != nullptr && fallbackHost != nullptr; }));
|
||||||
|
|
||||||
|
CHECK(location_host_needs_fallback());
|
||||||
|
const Location fallback = makeLocation(10.0, 20.0);
|
||||||
|
location_host_report(fallbackHost, &fallback);
|
||||||
|
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_NONE);
|
||||||
|
|
||||||
|
Location location;
|
||||||
|
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||||
|
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||||
|
|
||||||
|
const Location primary = makeLocation(51.05, 3.72);
|
||||||
|
location_host_report(fakeHost, &primary);
|
||||||
|
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_NONE);
|
||||||
|
CHECK_FALSE(location_host_needs_fallback());
|
||||||
|
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||||
|
CHECK(strcmp(location.provider, "fake") == 0);
|
||||||
|
|
||||||
|
// A fallback fix doesn't replace a fresh fix, so subscribers aren't signalled
|
||||||
|
location_host_report(fallbackHost, &fallback);
|
||||||
|
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_TIMEOUT);
|
||||||
|
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||||
|
CHECK(strcmp(location.provider, "fake") == 0);
|
||||||
|
|
||||||
|
// Without the primary provider, the fallback fix is used again
|
||||||
|
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||||
|
CHECK(location_host_needs_fallback());
|
||||||
|
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||||
|
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||||
|
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&fakeFallbackProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("location_make_approximate keeps only a coarse position") {
|
||||||
|
Location location = makeLocation(51.054321, 3.721234);
|
||||||
|
location.altitude = 12.0f;
|
||||||
|
location.speed = 1.5f;
|
||||||
|
location.satellites = 7;
|
||||||
|
location_make_approximate(&location);
|
||||||
|
CHECK(location.latitude == doctest::Approx(51.06));
|
||||||
|
CHECK(location.longitude == doctest::Approx(3.72));
|
||||||
|
CHECK(location.horizontal_accuracy == doctest::Approx(LOCATION_APPROXIMATE_ACCURACY_METERS));
|
||||||
|
CHECK(std::isnan(location.altitude));
|
||||||
|
CHECK(std::isnan(location.speed));
|
||||||
|
CHECK(location.satellites == 0);
|
||||||
|
CHECK(location.time_ms == 1000);
|
||||||
|
|
||||||
|
location.horizontal_accuracy = 25000.0f;
|
||||||
|
location_make_approximate(&location);
|
||||||
|
CHECK(location.horizontal_accuracy == doctest::Approx(25000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::atomic<LocationHost*> stuckHost = nullptr;
|
||||||
|
std::atomic<int> stuckCreateCount = 0;
|
||||||
|
std::atomic<bool> stuck = false;
|
||||||
|
|
||||||
|
void* stuckCreate(LocationHost* host) {
|
||||||
|
stuckHost = host;
|
||||||
|
stuckCreateCount++;
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
void stuckRequestStop(void*) {}
|
||||||
|
|
||||||
|
bool stuckWaitStopped(void*, TickType_t timeout) {
|
||||||
|
if (stuck) {
|
||||||
|
vTaskDelay(timeout);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
stuckHost = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationProviderApi stuckProvider = {
|
||||||
|
.id = "stuck",
|
||||||
|
.fallback = false,
|
||||||
|
.create = stuckCreate,
|
||||||
|
.request_stop = stuckRequestStop,
|
||||||
|
.wait_stopped = stuckWaitStopped,
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("location service doesn't wait for a provider that is slow to stop") {
|
||||||
|
createCount = 0;
|
||||||
|
destroyCount = 0;
|
||||||
|
stuckCreateCount = 0;
|
||||||
|
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(location_provider_register(&stuckProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||||
|
|
||||||
|
TaskEventGroup group;
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
LocationSubscription sub {};
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
REQUIRE(waitUntil([] { return fakeHost != nullptr && stuckHost != nullptr; }));
|
||||||
|
LocationHost* stoppingHost = stuckHost;
|
||||||
|
|
||||||
|
stuck = true;
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
// The other provider stops while the stuck one is still stopping
|
||||||
|
CHECK(waitUntil([] { return destroyCount == 1; }));
|
||||||
|
|
||||||
|
// A provider that is stopping doesn't report anymore
|
||||||
|
const Location reported = makeLocation(51.05, 3.72);
|
||||||
|
location_host_report(stoppingHost, &reported);
|
||||||
|
Location location;
|
||||||
|
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||||
|
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
CHECK(waitUntil([] { return createCount == 2; }));
|
||||||
|
CHECK(stuckCreateCount == 1);
|
||||||
|
|
||||||
|
// Once stopped, the provider starts again because it's desired again
|
||||||
|
stuck = false;
|
||||||
|
CHECK(waitUntil([] { return stuckCreateCount == 2; }));
|
||||||
|
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&stuckProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/** @return the names of the location tasks that weren't deleted, separated by spaces */
|
||||||
|
std::string getLocationTasks() {
|
||||||
|
TaskStatus_t statuses[32];
|
||||||
|
const UBaseType_t count = uxTaskGetSystemState(statuses, 32, nullptr);
|
||||||
|
std::string names;
|
||||||
|
for (UBaseType_t i = 0; i < count; i++) {
|
||||||
|
if (statuses[i].eCurrentState != eDeleted && strncmp(statuses[i].pcTaskName, "location", 8) == 0) {
|
||||||
|
names += statuses[i].pcTaskName;
|
||||||
|
names += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("location service has no background task without subscriptions") {
|
||||||
|
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||||
|
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||||
|
|
||||||
|
TaskEventGroup group;
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
LocationSubscription sub {};
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
REQUIRE(waitUntil([] { return fakeHost != nullptr; }));
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
REQUIRE(waitUntil([] { return fakeHost == nullptr; }));
|
||||||
|
|
||||||
|
// The worker exits once the provider stopped
|
||||||
|
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||||
|
INFO("location tasks: ", getLocationTasks());
|
||||||
|
CHECK(getLocationTasks().empty());
|
||||||
|
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("the gps provider has no task without subscriptions") {
|
||||||
|
REQUIRE(location_provider_register(&gps_location_provider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||||
|
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||||
|
|
||||||
|
TaskEventGroup group;
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
LocationSubscription sub {};
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
CHECK(waitUntil([] { return getLocationTasks().find("location_gps") != std::string::npos; }));
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
|
||||||
|
// Without a GPS device, the provider polls for one every second
|
||||||
|
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||||
|
INFO("location tasks: ", getLocationTasks());
|
||||||
|
CHECK(getLocationTasks().empty());
|
||||||
|
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&gps_location_provider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("selecting approximate precision rounds coarse fixes and drops precise ones") {
|
||||||
|
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(location_provider_register(&fakeFallbackProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||||
|
const LocationPrecision originalPrecision = location_get_precision();
|
||||||
|
// Saving the setting fails without a data partition, but the precision changes regardless
|
||||||
|
location_set_precision(LOCATION_PRECISION_PRECISE);
|
||||||
|
|
||||||
|
TaskEventGroup group;
|
||||||
|
task_event_group_construct(&group);
|
||||||
|
LocationSubscription sub {};
|
||||||
|
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||||
|
REQUIRE(waitUntil([] { return fakeHost != nullptr && fallbackHost != nullptr; }));
|
||||||
|
|
||||||
|
Location coarse = makeLocation(51.054321, 3.721234);
|
||||||
|
coarse.horizontal_accuracy = 25000.0f;
|
||||||
|
location_host_report(fallbackHost, &coarse);
|
||||||
|
const Location precise = makeLocation(10.0, 20.0);
|
||||||
|
location_host_report(fakeHost, &precise);
|
||||||
|
|
||||||
|
location_set_precision(LOCATION_PRECISION_APPROXIMATE);
|
||||||
|
REQUIRE(location_get_precision() == LOCATION_PRECISION_APPROXIMATE);
|
||||||
|
Location location;
|
||||||
|
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||||
|
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||||
|
CHECK(location.latitude == doctest::Approx(51.06));
|
||||||
|
CHECK(location.longitude == doctest::Approx(3.72));
|
||||||
|
|
||||||
|
location_set_precision(originalPrecision);
|
||||||
|
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||||
|
task_event_group_destruct(&group);
|
||||||
|
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||||
|
CHECK(location_provider_unregister(&fakeFallbackProvider) == ERROR_NONE);
|
||||||
|
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||||
|
}
|
||||||
@ -68,14 +68,15 @@ error_t wifi_settings_remove(const char* ssid);
|
|||||||
void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, void* context));
|
void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, void* context));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param[in] enable whether to turn on the radio when booting
|
* Persist whether Wi-Fi is on. The radio is turned on when booting while it's enabled.
|
||||||
|
* @param[in] enable whether Wi-Fi is on
|
||||||
* @retval ERROR_RESOURCE the setting could not be written
|
* @retval ERROR_RESOURCE the setting could not be written
|
||||||
* @retval ERROR_NONE on success
|
* @retval ERROR_NONE on success
|
||||||
*/
|
*/
|
||||||
error_t wifi_settings_set_enable_on_boot(bool enable);
|
error_t wifi_settings_set_enable(bool enable);
|
||||||
|
|
||||||
/** @return true when the radio should be turned on when booting */
|
/** @return true when Wi-Fi is on, see wifi_settings_set_enable() */
|
||||||
bool wifi_settings_get_enable_on_boot(void);
|
bool wifi_settings_get_enable(void);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,7 +160,7 @@ static void on_wifi_event(WifiAutoConnect* data, const WifiEvent& event) {
|
|||||||
static void on_boot_completed(WifiAutoConnect* data) {
|
static void on_boot_completed(WifiAutoConnect* data) {
|
||||||
wifi_provisioning_import();
|
wifi_provisioning_import();
|
||||||
|
|
||||||
if (wifi_settings_get_enable_on_boot()) {
|
if (wifi_settings_get_enable()) {
|
||||||
LOG_I(TAG, "Auto-enabling WiFi");
|
LOG_I(TAG, "Auto-enabling WiFi");
|
||||||
if (wifi_set_radio_on(data->device) != ERROR_NONE) {
|
if (wifi_set_radio_on(data->device) != ERROR_NONE) {
|
||||||
LOG_E(TAG, "Failed to enable WiFi radio");
|
LOG_E(TAG, "Failed to enable WiFi radio");
|
||||||
|
|||||||
@ -156,6 +156,10 @@ static int command_radio(Device* device, int argc, char* argv[]) {
|
|||||||
if (error != ERROR_NONE) {
|
if (error != ERROR_NONE) {
|
||||||
return report_error(on ? "radio on" : "radio off", error);
|
return report_error(on ? "radio on" : "radio off", error);
|
||||||
}
|
}
|
||||||
|
error = wifi_settings_set_enable(on);
|
||||||
|
if (error != ERROR_NONE) {
|
||||||
|
return report_error("save radio state", error);
|
||||||
|
}
|
||||||
printf("radio: %s\n", on ? "on" : "off");
|
printf("radio: %s\n", on ? "on" : "off");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -281,7 +281,7 @@ void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, voi
|
|||||||
closedir(dir);
|
closedir(dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
error_t wifi_settings_set_enable_on_boot(bool enable) {
|
error_t wifi_settings_set_enable(bool enable) {
|
||||||
enable_on_boot = enable;
|
enable_on_boot = enable;
|
||||||
enable_on_boot_cached = true;
|
enable_on_boot_cached = true;
|
||||||
|
|
||||||
@ -306,7 +306,7 @@ error_t wifi_settings_set_enable_on_boot(bool enable) {
|
|||||||
return properties_file_close(file);
|
return properties_file_close(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool wifi_settings_get_enable_on_boot(void) {
|
bool wifi_settings_get_enable(void) {
|
||||||
if (enable_on_boot_cached) {
|
if (enable_on_boot_cached) {
|
||||||
return enable_on_boot;
|
return enable_on_boot;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
#include "freertos_task_hooks.h"
|
#include "freertos_task_hooks.h"
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
static FreeRtosTaskCreateHook create_hook = NULL;
|
static FreeRtosTaskCreateHook create_hook = NULL;
|
||||||
static FreeRtosTaskDeleteHook delete_hook = NULL;
|
static FreeRtosTaskDeleteHook delete_hook = NULL;
|
||||||
|
|
||||||
@ -23,3 +26,12 @@ void vTaskDelete(TaskHandle_t handle) {
|
|||||||
}
|
}
|
||||||
freertos_real_vTaskDelete(handle);
|
freertos_real_vTaskDelete(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void vPortCancelThread(void* task) {
|
||||||
|
sigset_t all_signals;
|
||||||
|
sigset_t previous_signals;
|
||||||
|
sigfillset(&all_signals);
|
||||||
|
pthread_sigmask(SIG_BLOCK, &all_signals, &previous_signals);
|
||||||
|
freertos_real_vPortCancelThread(task);
|
||||||
|
pthread_sigmask(SIG_SETMASK, &previous_signals, NULL);
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,14 @@ BaseType_t freertos_real_xTaskCreate(TaskFunction_t function, const char* name,
|
|||||||
/** FreeRTOS-Kernel's own vTaskDelete() */
|
/** FreeRTOS-Kernel's own vTaskDelete() */
|
||||||
void freertos_real_vTaskDelete(TaskHandle_t handle);
|
void freertos_real_vTaskDelete(TaskHandle_t handle);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The POSIX port's own vPortCancelThread(), which joins the thread of a deleted task.
|
||||||
|
* The simulator's vPortCancelThread() calls it with all signals blocked: glibc frees the joined thread's stack
|
||||||
|
* while holding its stack cache lock. When the tick signal switches tasks while that lock is held,
|
||||||
|
* the next pthread_create() blocks forever, because the port calls it with the tick signal blocked.
|
||||||
|
*/
|
||||||
|
void freertos_real_vPortCancelThread(void* task);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -42,6 +42,7 @@ list(APPEND REQUIRES_LIST
|
|||||||
gps-module
|
gps-module
|
||||||
graphics-module
|
graphics-module
|
||||||
http-module
|
http-module
|
||||||
|
location-module
|
||||||
mbedtls-module
|
mbedtls-module
|
||||||
mesh-module
|
mesh-module
|
||||||
mesh-meshcore-module
|
mesh-meshcore-module
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
namespace tt::app::gpssettings {
|
|
||||||
|
|
||||||
void start();
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace tt::bluetooth::settings {
|
namespace tt::bluetooth::settings {
|
||||||
|
|
||||||
void setEnableOnBoot(bool enable);
|
void setEnable(bool enable);
|
||||||
bool shouldEnableOnBoot();
|
bool isEnabled();
|
||||||
|
|
||||||
void setSppAutoStart(bool enable);
|
void setSppAutoStart(bool enable);
|
||||||
bool shouldSppAutoStart();
|
bool shouldSppAutoStart();
|
||||||
|
|||||||
@ -17,12 +17,10 @@ class View final {
|
|||||||
void* context = nullptr;
|
void* context = nullptr;
|
||||||
lv_obj_t* root = nullptr;
|
lv_obj_t* root = nullptr;
|
||||||
lv_obj_t* enable_switch = nullptr;
|
lv_obj_t* enable_switch = nullptr;
|
||||||
lv_obj_t* enable_on_boot_switch = nullptr;
|
|
||||||
lv_obj_t* scanning_spinner = nullptr;
|
lv_obj_t* scanning_spinner = nullptr;
|
||||||
lv_obj_t* peers_list = nullptr;
|
lv_obj_t* peers_list = nullptr;
|
||||||
|
|
||||||
void updateBtToggle();
|
void updateBtToggle();
|
||||||
void updateEnableOnBootToggle();
|
|
||||||
void updateScanning();
|
void updateScanning();
|
||||||
void updatePeerList();
|
void updatePeerList();
|
||||||
|
|
||||||
|
|||||||
@ -15,14 +15,12 @@ class View final {
|
|||||||
uint32_t appInstanceId = 0;
|
uint32_t appInstanceId = 0;
|
||||||
lv_obj_t* root = nullptr;
|
lv_obj_t* root = nullptr;
|
||||||
lv_obj_t* enable_switch = nullptr;
|
lv_obj_t* enable_switch = nullptr;
|
||||||
lv_obj_t* enable_on_boot_switch = nullptr;
|
|
||||||
lv_obj_t* scanning_spinner = nullptr;
|
lv_obj_t* scanning_spinner = nullptr;
|
||||||
lv_obj_t* refresh_button = nullptr;
|
lv_obj_t* refresh_button = nullptr;
|
||||||
lv_obj_t* networks_list = nullptr;
|
lv_obj_t* networks_list = nullptr;
|
||||||
lv_obj_t* connect_to_hidden = nullptr;
|
lv_obj_t* connect_to_hidden = nullptr;
|
||||||
|
|
||||||
void updateWifiToggle();
|
void updateWifiToggle();
|
||||||
void updateEnableOnBootToggle();
|
|
||||||
void updateScanning();
|
void updateScanning();
|
||||||
void updateRefreshButton();
|
void updateRefreshButton();
|
||||||
void updateNetworkList();
|
void updateNetworkList();
|
||||||
|
|||||||
@ -48,7 +48,6 @@ namespace app {
|
|||||||
namespace display { extern const ::AppManifest manifest; }
|
namespace display { extern const ::AppManifest manifest; }
|
||||||
namespace files { extern const ::AppManifest manifest; }
|
namespace files { extern const ::AppManifest manifest; }
|
||||||
namespace fileselection { extern const ::AppManifest manifest; }
|
namespace fileselection { extern const ::AppManifest manifest; }
|
||||||
namespace gpssettings { extern const ::AppManifest manifest; }
|
|
||||||
namespace grovesettings { extern const ::AppManifest manifest; }
|
namespace grovesettings { extern const ::AppManifest manifest; }
|
||||||
namespace i2cscanner { extern const ::AppManifest manifest; }
|
namespace i2cscanner { extern const ::AppManifest manifest; }
|
||||||
namespace imageviewer { extern const ::AppManifest manifest; }
|
namespace imageviewer { extern const ::AppManifest manifest; }
|
||||||
@ -56,6 +55,7 @@ namespace app {
|
|||||||
namespace launcher { extern const ::AppManifest manifest; }
|
namespace launcher { extern const ::AppManifest manifest; }
|
||||||
namespace ledstripsettings { extern const ::AppManifest manifest; }
|
namespace ledstripsettings { extern const ::AppManifest manifest; }
|
||||||
namespace localesettings { extern const ::AppManifest manifest; }
|
namespace localesettings { extern const ::AppManifest manifest; }
|
||||||
|
namespace locationsettings { extern const ::AppManifest manifest; }
|
||||||
namespace notes { extern const ::AppManifest manifest; }
|
namespace notes { extern const ::AppManifest manifest; }
|
||||||
namespace power { extern const ::AppManifest manifest; }
|
namespace power { extern const ::AppManifest manifest; }
|
||||||
namespace poweroff { extern const ::AppManifest manifest; }
|
namespace poweroff { extern const ::AppManifest manifest; }
|
||||||
@ -126,6 +126,7 @@ static void registerInternalApps() {
|
|||||||
app_manager_add(&app::inputdialog::manifest);
|
app_manager_add(&app::inputdialog::manifest);
|
||||||
app_manager_add(&app::launcher::manifest);
|
app_manager_add(&app::launcher::manifest);
|
||||||
app_manager_add(&app::localesettings::manifest);
|
app_manager_add(&app::localesettings::manifest);
|
||||||
|
app_manager_add(&app::locationsettings::manifest);
|
||||||
app_manager_add(&app::notes::manifest);
|
app_manager_add(&app::notes::manifest);
|
||||||
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
||||||
app_manager_add(&app::poweroff::manifest);
|
app_manager_add(&app::poweroff::manifest);
|
||||||
@ -193,7 +194,6 @@ static void registerInternalApps() {
|
|||||||
|
|
||||||
if (enable_gps) {
|
if (enable_gps) {
|
||||||
app_manager_add(&app::addgps::manifest);
|
app_manager_add(&app::addgps::manifest);
|
||||||
app_manager_add(&app::gpssettings::manifest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <Tactility/service/ServiceRegistration.h>
|
#include <Tactility/service/ServiceRegistration.h>
|
||||||
#include <Tactility/settings/RadioSettings.h>
|
#include <Tactility/settings/RadioSettings.h>
|
||||||
|
|
||||||
|
#include <location/module.h>
|
||||||
#include <app_hub/module.h>
|
#include <app_hub/module.h>
|
||||||
#include <wifi/module.h>
|
#include <wifi/module.h>
|
||||||
#if TT_FEATURE_MESH
|
#if TT_FEATURE_MESH
|
||||||
@ -50,6 +51,7 @@ void registerAndStartServices() {
|
|||||||
addService(service::audio::manifest);
|
addService(service::audio::manifest);
|
||||||
}
|
}
|
||||||
check(module_ensure_started(&wifi_module) == ERROR_NONE);
|
check(module_ensure_started(&wifi_module) == ERROR_NONE);
|
||||||
|
check(module_ensure_started(&location_module) == ERROR_NONE);
|
||||||
check(module_ensure_started(&app_hub_module) == ERROR_NONE);
|
check(module_ensure_started(&app_hub_module) == ERROR_NONE);
|
||||||
// Before the mesh service starts, as it needs the region to turn on the radio
|
// Before the mesh service starts, as it needs the region to turn on the radio
|
||||||
settings::radio::applyRegion();
|
settings::radio::applyRegion();
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <Tactility/app/btmanage/BtManagePrivate.h>
|
#include <Tactility/app/btmanage/BtManagePrivate.h>
|
||||||
#include <Tactility/app/btmanage/View.h>
|
#include <Tactility/app/btmanage/View.h>
|
||||||
|
#include <Tactility/bluetooth/BluetoothSettings.h>
|
||||||
|
#include <Tactility/Tactility.h>
|
||||||
|
|
||||||
#include <app/event.h>
|
#include <app/event.h>
|
||||||
#include <app/manager.h>
|
#include <app/manager.h>
|
||||||
@ -24,6 +26,10 @@ extern const ::AppManifest manifest;
|
|||||||
|
|
||||||
static void onBtToggled(void* /*context*/, bool requestOn) {
|
static void onBtToggled(void* /*context*/, bool requestOn) {
|
||||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||||
|
// Dispatched, so file IO doesn't block the UI
|
||||||
|
getMainDispatcher().dispatch([requestOn] {
|
||||||
|
bluetooth::settings::setEnable(requestOn);
|
||||||
|
});
|
||||||
Device* dev;
|
Device* dev;
|
||||||
if (device_get_first_by_type(&BLUETOOTH_TYPE, &dev) == ERROR_NONE) {
|
if (device_get_first_by_type(&BLUETOOTH_TYPE, &dev) == ERROR_NONE) {
|
||||||
bool radio_on = bluetooth::isRadioOnOrPending(dev);
|
bool radio_on = bluetooth::isRadioOnOrPending(dev);
|
||||||
|
|||||||
@ -30,28 +30,6 @@ static void onEnableSwitchChanged(lv_event_t* event) {
|
|||||||
ctx->bindings.onBtToggled(ctx, is_on);
|
ctx->bindings.onBtToggled(ctx, is_on);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onEnableOnBootSwitchChanged(lv_event_t* event) {
|
|
||||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
|
||||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
getMainDispatcher().dispatch([is_on] {
|
|
||||||
bluetooth::settings::setEnableOnBoot(is_on);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onEnableOnBootParentClicked(lv_event_t* event) {
|
|
||||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
|
||||||
bool new_state = !lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
if (new_state) {
|
|
||||||
lv_obj_add_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
} else {
|
|
||||||
lv_obj_remove_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
}
|
|
||||||
// add/remove_state does not fire LV_EVENT_VALUE_CHANGED, so persist here directly.
|
|
||||||
getMainDispatcher().dispatch([new_state] {
|
|
||||||
bluetooth::settings::setEnableOnBoot(new_state);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onScanButtonClicked(lv_event_t* event) {
|
static void onScanButtonClicked(lv_event_t* event) {
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
Device* dev = nullptr;
|
Device* dev = nullptr;
|
||||||
@ -162,17 +140,6 @@ void View::updateBtToggle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void View::updateEnableOnBootToggle() {
|
|
||||||
if (enable_on_boot_switch != nullptr) {
|
|
||||||
lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY);
|
|
||||||
if (bluetooth::settings::shouldEnableOnBoot()) {
|
|
||||||
lv_obj_add_state(enable_on_boot_switch, LV_STATE_CHECKED);
|
|
||||||
} else {
|
|
||||||
lv_obj_remove_state(enable_on_boot_switch, LV_STATE_CHECKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void View::updateScanning() {
|
void View::updateScanning() {
|
||||||
if (state->getRadioState() == bluetooth::RadioState::On && state->isScanning()) {
|
if (state->getRadioState() == bluetooth::RadioState::On && state->isScanning()) {
|
||||||
lv_obj_set_hidden(scanning_spinner, false);
|
lv_obj_set_hidden(scanning_spinner, false);
|
||||||
@ -184,27 +151,6 @@ void View::updateScanning() {
|
|||||||
void View::updatePeerList() {
|
void View::updatePeerList() {
|
||||||
lv_obj_clean(peers_list);
|
lv_obj_clean(peers_list);
|
||||||
|
|
||||||
// Enable on boot
|
|
||||||
|
|
||||||
auto* enable_on_boot_wrapper = lv_obj_create(peers_list);
|
|
||||||
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
|
||||||
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_bg_opa(enable_on_boot_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_flex_flow(enable_on_boot_wrapper, LV_FLEX_FLOW_ROW);
|
|
||||||
lv_obj_set_flex_align(enable_on_boot_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
||||||
lv_obj_set_scrollable(enable_on_boot_wrapper, false);
|
|
||||||
|
|
||||||
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
|
|
||||||
lv_label_set_text(enable_label, "Enable on boot");
|
|
||||||
lv_obj_set_flex_grow(enable_label, 1);
|
|
||||||
|
|
||||||
enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper);
|
|
||||||
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
|
||||||
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
|
|
||||||
|
|
||||||
updateEnableOnBootToggle();
|
|
||||||
|
|
||||||
using enum bluetooth::RadioState;
|
using enum bluetooth::RadioState;
|
||||||
if (state->getRadioState() == On) {
|
if (state->getRadioState() == On) {
|
||||||
// Paired peers section
|
// Paired peers section
|
||||||
|
|||||||
@ -1,388 +0,0 @@
|
|||||||
#include <lvgl/fonts.h>
|
|
||||||
#include <lvgl/icons/shared.h>
|
|
||||||
#include <lvgl/lvgl.h>
|
|
||||||
#include <lvgl/widgets/card.h>
|
|
||||||
#include <lvgl/widgets/toolbar.h>
|
|
||||||
|
|
||||||
#include <Tactility/Tactility.h>
|
|
||||||
#include <Tactility/Timer.h>
|
|
||||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
|
||||||
|
|
||||||
#include <app/event.h>
|
|
||||||
#include <app/manager.h>
|
|
||||||
#include <app/manifest.h>
|
|
||||||
#include <app/scheduler.h>
|
|
||||||
#include <app/start.h>
|
|
||||||
|
|
||||||
#include <lvgl_window_manager/window_manager.h>
|
|
||||||
|
|
||||||
#include <tactility/check.h>
|
|
||||||
#include <tactility/device.h>
|
|
||||||
#include <tactility/time.h>
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <cstring>
|
|
||||||
#include <lvgl.h>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <gps/gps.h>
|
|
||||||
#include <gps/gps_settings.h>
|
|
||||||
#include <gps_generic/gps_generic.h>
|
|
||||||
|
|
||||||
namespace tt::app::addgps {
|
|
||||||
extern const ::AppManifest manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace tt::app::gpssettings {
|
|
||||||
|
|
||||||
extern const ::AppManifest manifest;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
struct DeviceRow {
|
|
||||||
Device* device;
|
|
||||||
lv_obj_t* button;
|
|
||||||
lv_obj_t* buttonLabel;
|
|
||||||
bool hasConfiguration = false;
|
|
||||||
size_t configurationIndex = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Context {
|
|
||||||
uint32_t appInstanceId;
|
|
||||||
lv_obj_t* deviceListWrapper = nullptr;
|
|
||||||
std::vector<DeviceRow> deviceRows;
|
|
||||||
std::unique_ptr<Timer> timer;
|
|
||||||
|
|
||||||
// Set when a delete confirmation is pending; read/cleared on this app's own thread when
|
|
||||||
// the dialog's result arrives.
|
|
||||||
bool hasPendingDelete = false;
|
|
||||||
Device* pendingDeleteDevice = nullptr;
|
|
||||||
size_t pendingDeleteIndex = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
void rebuildDeviceList(Context* ctx);
|
|
||||||
void updateDeviceStates(Context* ctx);
|
|
||||||
void createWidgets(lv_obj_t* parent, void* userData);
|
|
||||||
void destroyWidgets(void* userData);
|
|
||||||
|
|
||||||
void onBackPressed(lv_event_t* event) {
|
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
||||||
app_event_emit_close(ctx->appInstanceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAddGpsPressed(lv_event_t* event) {
|
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
||||||
// Fire-and-forget top-level launch, matching the original (its result never fed back into
|
|
||||||
// this app; rebuildDeviceList() runs fresh whenever this app is resumed regardless).
|
|
||||||
(void)ctx;
|
|
||||||
uint32_t instanceId = 0;
|
|
||||||
AppStartContext context = app_start_context_for_manifest(&addgps::manifest);
|
|
||||||
app_start_with_context(&context, &instanceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDeviceButtonPressed(lv_event_t* event) {
|
|
||||||
auto* button = lv_event_get_target_obj(event);
|
|
||||||
auto* device = static_cast<Device*>(lv_obj_get_user_data(button));
|
|
||||||
|
|
||||||
bool running = device_is_ready(device);
|
|
||||||
// device_start()/device_stop() are potentially blocking calls, so use a dispatcher to not block the UI
|
|
||||||
getMainDispatcher().dispatch([device, running] {
|
|
||||||
if (running) {
|
|
||||||
device_stop(device);
|
|
||||||
} else {
|
|
||||||
device_start(device);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds the persisted configuration backing `device` (matched by its parent UART's name)
|
|
||||||
// and returns its index into gps_settings_for_each_configuration()'s ordering - the handle
|
|
||||||
// gps_settings_remove_configuration_at() needs to delete exactly this entry, even if another
|
|
||||||
// entry happens to have identical field values.
|
|
||||||
// Devicetree-declared GPS_TYPE devices have no such configuration and never match.
|
|
||||||
bool findConfigurationIndexForDevice(Device* device, size_t& outIndex) {
|
|
||||||
auto* parent = device_get_parent(device);
|
|
||||||
if (parent == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FindContext {
|
|
||||||
const char* uartName;
|
|
||||||
size_t* outIndex;
|
|
||||||
bool found;
|
|
||||||
} findContext = { parent->name, &outIndex, false };
|
|
||||||
|
|
||||||
gps_settings_for_each_configuration(&findContext, [](const GpsConfiguration* configuration, size_t index, void* untyped_context) {
|
|
||||||
auto* ctx = static_cast<FindContext*>(untyped_context);
|
|
||||||
if (!ctx->found && strcmp(configuration->uart_name, ctx->uartName) == 0) {
|
|
||||||
*ctx->outIndex = index;
|
|
||||||
ctx->found = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return findContext.found;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDeleteButtonPressed(lv_event_t* event) {
|
|
||||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
|
||||||
auto* button = lv_event_get_target_obj(event);
|
|
||||||
auto* device = static_cast<Device*>(lv_obj_get_user_data(button));
|
|
||||||
|
|
||||||
for (auto& row : ctx->deviceRows) {
|
|
||||||
if (row.device == device && row.hasConfiguration) {
|
|
||||||
ctx->pendingDeleteDevice = device;
|
|
||||||
ctx->pendingDeleteIndex = row.configurationIndex;
|
|
||||||
ctx->hasPendingDelete = true;
|
|
||||||
alertdialog::start(ctx->appInstanceId, "Confirmation", std::string("Do you want to delete ") + device->name + "?", std::vector<std::string> { "Yes", "No" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return the bus name and speed, e.g. "ttyACM0 9600 bps" */
|
|
||||||
std::string getConnectionDescription(Device* device) {
|
|
||||||
auto* parent = device_get_parent(device);
|
|
||||||
std::string result = parent != nullptr ? parent->name : "";
|
|
||||||
if (device_is_compatible(device, "tactility,gps-generic")) {
|
|
||||||
const auto* config = static_cast<const GpsConfig*>(device->config);
|
|
||||||
result += " " + std::to_string(config->baud_rate) + " bps";
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void createDeviceRow(Context* ctx, Device* device) {
|
|
||||||
auto* card = lvgl_card_create(ctx->deviceListWrapper);
|
|
||||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
||||||
|
|
||||||
// Transparent, so the card provides the background
|
|
||||||
auto* wrapper = lv_obj_create(card);
|
|
||||||
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
|
||||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
|
|
||||||
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
||||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
|
||||||
lv_obj_set_style_pad_all(wrapper, 0, 0);
|
|
||||||
lv_obj_set_style_bg_opa(wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_scrollable(wrapper, false);
|
|
||||||
|
|
||||||
// The texts take the width that the buttons leave. Texts that don't fit scroll.
|
|
||||||
auto* texts = lv_obj_create(wrapper);
|
|
||||||
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
|
||||||
lv_obj_set_flex_grow(texts, 1);
|
|
||||||
lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN);
|
|
||||||
lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_scrollable(texts, false);
|
|
||||||
|
|
||||||
auto* name_label = lv_label_create(texts);
|
|
||||||
lv_obj_set_width(name_label, LV_PCT(100));
|
|
||||||
lv_label_set_long_mode(name_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
|
||||||
char model_name[64];
|
|
||||||
if (gps_get_model_name(device, model_name, sizeof(model_name)) == ERROR_NONE) {
|
|
||||||
lv_label_set_text(name_label, model_name);
|
|
||||||
} else {
|
|
||||||
lv_label_set_text(name_label, device->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* connection_label = lv_label_create(texts);
|
|
||||||
lv_obj_set_width(connection_label, LV_PCT(100));
|
|
||||||
lv_label_set_long_mode(connection_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
|
||||||
lv_obj_set_style_text_font(connection_label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
|
||||||
lv_label_set_text(connection_label, getConnectionDescription(device).c_str());
|
|
||||||
|
|
||||||
auto* actions_wrapper = lv_obj_create(wrapper);
|
|
||||||
lv_obj_set_size(actions_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
|
||||||
lv_obj_set_flex_flow(actions_wrapper, LV_FLEX_FLOW_ROW);
|
|
||||||
lv_obj_set_style_border_width(actions_wrapper, 0, 0);
|
|
||||||
lv_obj_set_style_pad_all(actions_wrapper, 0, 0);
|
|
||||||
lv_obj_set_style_pad_column(actions_wrapper, 4, 0);
|
|
||||||
lv_obj_set_style_bg_opa(actions_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
||||||
|
|
||||||
auto* button = lv_button_create(actions_wrapper);
|
|
||||||
lv_obj_add_event_cb(button, onDeviceButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
|
||||||
lv_obj_set_user_data(button, device);
|
|
||||||
auto* button_label = lv_label_create(button);
|
|
||||||
lv_label_set_text(button_label, "Start");
|
|
||||||
|
|
||||||
DeviceRow row { .device = device, .button = button, .buttonLabel = button_label };
|
|
||||||
|
|
||||||
// Only devices backed by a persisted configuration (not devicetree-declared ones) can be deleted.
|
|
||||||
size_t configurationIndex;
|
|
||||||
if ((device->flags & DEVICE_FLAG_DYNAMIC) && findConfigurationIndexForDevice(device, configurationIndex)) {
|
|
||||||
auto* delete_button = lv_button_create(actions_wrapper);
|
|
||||||
lv_obj_add_event_cb(delete_button, onDeleteButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
|
||||||
lv_obj_set_user_data(delete_button, device);
|
|
||||||
auto* delete_label = lv_label_create(delete_button);
|
|
||||||
lv_obj_set_style_text_font(delete_label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
|
||||||
lv_label_set_text(delete_label, LVGL_ICON_SHARED_DELETE);
|
|
||||||
|
|
||||||
row.hasConfiguration = true;
|
|
||||||
row.configurationIndex = configurationIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx->deviceRows.push_back(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuilds the device list. Only needs to run when the set of devices could've changed (on
|
|
||||||
// creation, and after returning from AddGps) - button state itself is refreshed by the timer.
|
|
||||||
void rebuildDeviceList(Context* ctx) {
|
|
||||||
ctx->deviceRows.clear();
|
|
||||||
// Rebuilt by createWidgets() when the window resurfaces
|
|
||||||
if (ctx->deviceListWrapper == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lv_obj_clean(ctx->deviceListWrapper);
|
|
||||||
|
|
||||||
device_for_each_of_type(&GPS_TYPE, ctx, [](Device* device, void* context) {
|
|
||||||
createDeviceRow(static_cast<Context*>(context), device);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ctx->deviceRows.empty()) {
|
|
||||||
auto* card = lvgl_card_create(ctx->deviceListWrapper);
|
|
||||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
|
||||||
auto* label = lv_label_create(card);
|
|
||||||
lv_label_set_text(label, "No devices configured");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateDeviceStates(Context* ctx) {
|
|
||||||
lvgl_lock();
|
|
||||||
for (const auto& row : ctx->deviceRows) {
|
|
||||||
const char* text = "Start";
|
|
||||||
bool enabled = true;
|
|
||||||
|
|
||||||
if (device_is_ready(row.device)) {
|
|
||||||
switch (gps_get_state(row.device)) {
|
|
||||||
case GPS_STATE_PENDING_ON:
|
|
||||||
text = "Starting...";
|
|
||||||
enabled = false;
|
|
||||||
break;
|
|
||||||
case GPS_STATE_PENDING_OFF:
|
|
||||||
text = "Stopping...";
|
|
||||||
enabled = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
text = "Stop";
|
|
||||||
enabled = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lv_label_set_text(row.buttonLabel, text);
|
|
||||||
if (enabled) {
|
|
||||||
lv_obj_remove_state(row.button, LV_STATE_DISABLED);
|
|
||||||
} else {
|
|
||||||
lv_obj_add_state(row.button, LV_STATE_DISABLED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lvgl_unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
|
||||||
auto* ctx = static_cast<Context*>(userData);
|
|
||||||
|
|
||||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
|
||||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
|
||||||
|
|
||||||
auto* toolbar = lvgl_toolbar_create(parent, "GPS");
|
|
||||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
|
||||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
|
||||||
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_PLUS, onAddGpsPressed, ctx);
|
|
||||||
|
|
||||||
ctx->deviceListWrapper = lv_obj_create(parent);
|
|
||||||
lv_obj_set_size(ctx->deviceListWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
|
||||||
lv_obj_set_flex_flow(ctx->deviceListWrapper, LV_FLEX_FLOW_COLUMN);
|
|
||||||
lv_obj_set_flex_grow(ctx->deviceListWrapper, 1);
|
|
||||||
lv_obj_set_style_border_width(ctx->deviceListWrapper, 0, 0);
|
|
||||||
|
|
||||||
rebuildDeviceList(ctx);
|
|
||||||
updateDeviceStates(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The window's widgets are deleted while another window is on top, so the timer must not reach them
|
|
||||||
void destroyWidgets(void* userData) {
|
|
||||||
auto* ctx = static_cast<Context*>(userData);
|
|
||||||
ctx->deviceListWrapper = nullptr;
|
|
||||||
ctx->deviceRows.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t appMain(int argc, char* argv[]) {
|
|
||||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
|
||||||
Context ctx {};
|
|
||||||
ctx.appInstanceId = appInstanceId;
|
|
||||||
|
|
||||||
// Runs for this app instance's whole lifetime - there's no push notification for GPS
|
|
||||||
// device state changes, so this is the only way this screen finds out about them.
|
|
||||||
ctx.timer = std::make_unique<Timer>(Timer::Type::Periodic, seconds_to_ticks(1), [&ctx] {
|
|
||||||
updateDeviceStates(&ctx);
|
|
||||||
});
|
|
||||||
|
|
||||||
TaskEventGroup event_group {};
|
|
||||||
task_event_group_construct(&event_group);
|
|
||||||
|
|
||||||
AppEventSubscription sub {};
|
|
||||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
|
||||||
|
|
||||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
|
||||||
ctx.timer->start();
|
|
||||||
|
|
||||||
bool shouldClose = false;
|
|
||||||
while (!shouldClose) {
|
|
||||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
|
||||||
|
|
||||||
AppEvent event {};
|
|
||||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
|
||||||
switch (event.type) {
|
|
||||||
case APP_EVENT_CLOSE:
|
|
||||||
shouldClose = true;
|
|
||||||
break;
|
|
||||||
case APP_EVENT_RESULT:
|
|
||||||
if (ctx.hasPendingDelete) {
|
|
||||||
ctx.hasPendingDelete = false;
|
|
||||||
if (event.result.result == 0) { // 0 = Yes
|
|
||||||
lvgl_lock();
|
|
||||||
std::erase_if(ctx.deviceRows, [&ctx](const DeviceRow& row) {
|
|
||||||
return row.device == ctx.pendingDeleteDevice;
|
|
||||||
});
|
|
||||||
lvgl_unlock();
|
|
||||||
|
|
||||||
gps_settings_remove_configuration_at(ctx.pendingDeleteIndex);
|
|
||||||
ctx.pendingDeleteDevice = nullptr;
|
|
||||||
|
|
||||||
lvgl_lock();
|
|
||||||
rebuildDeviceList(&ctx);
|
|
||||||
lvgl_unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
app_manager_stop(event.result.launch_id);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (shouldClose) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.timer->stop();
|
|
||||||
window_manager_remove(window);
|
|
||||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
|
||||||
task_event_group_destruct(&event_group);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
extern const ::AppManifest manifest = {
|
|
||||||
.id = "tactility.gpssettings",
|
|
||||||
.name = "GPS",
|
|
||||||
.category = APP_CATEGORY_SETTINGS,
|
|
||||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
|
||||||
.flags = 0,
|
|
||||||
.stack = {}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
@ -23,11 +23,11 @@ constexpr IconEntry ICONS[] = {
|
|||||||
{"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH},
|
{"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH},
|
||||||
{"tactility.development", LVGL_ICON_SHARED_LOGO_DEV},
|
{"tactility.development", LVGL_ICON_SHARED_LOGO_DEV},
|
||||||
{"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS},
|
{"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS},
|
||||||
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
|
|
||||||
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
|
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
|
||||||
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
|
||||||
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
|
||||||
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
|
||||||
|
{"tactility.locationsettings", LVGL_ICON_SHARED_LOCATION_ON},
|
||||||
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
|
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
|
||||||
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
|
||||||
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},
|
||||||
|
|||||||
441
Tactility/Source/app/locationsettings/LocationSettings.cpp
Normal file
441
Tactility/Source/app/locationsettings/LocationSettings.cpp
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||||
|
#include <Tactility/lvgl/Style.h>
|
||||||
|
|
||||||
|
#include <app/event.h>
|
||||||
|
#include <app/manager.h>
|
||||||
|
#include <app/manifest.h>
|
||||||
|
#include <app/scheduler.h>
|
||||||
|
#include <app/start.h>
|
||||||
|
|
||||||
|
#include <gps/gps.h>
|
||||||
|
#include <gps/gps_settings.h>
|
||||||
|
#include <gps_generic/gps_generic.h>
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
|
||||||
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
|
#include <tactility/check.h>
|
||||||
|
#include <tactility/concurrent/task_event_group.h>
|
||||||
|
#include <tactility/device.h>
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <lvgl/fonts.h>
|
||||||
|
#include <lvgl/grid_navigation.h>
|
||||||
|
#include <lvgl/icons/shared.h>
|
||||||
|
#include <lvgl/lvgl.h>
|
||||||
|
#include <lvgl/widgets/card.h>
|
||||||
|
#include <lvgl/widgets/chip.h>
|
||||||
|
#include <lvgl/widgets/toolbar.h>
|
||||||
|
|
||||||
|
#include <TactilityCpp/std/string.h>
|
||||||
|
#include <TactilityCpp/std/vector.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <format>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
namespace tt::app::addgps {
|
||||||
|
extern const ::AppManifest manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace tt::app::locationsettings {
|
||||||
|
|
||||||
|
extern const ::AppManifest manifest;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct PrecisionOption {
|
||||||
|
LocationPrecision precision;
|
||||||
|
const char* name;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr PrecisionOption PRECISION_OPTIONS[] = {
|
||||||
|
{ LOCATION_PRECISION_APPROXIMATE, "Approximate" },
|
||||||
|
{ LOCATION_PRECISION_PRECISE, "Precise" },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A GPS device that is backed by a persisted configuration */
|
||||||
|
struct GpsConfigurationRow {
|
||||||
|
Device* device;
|
||||||
|
size_t configurationIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Context {
|
||||||
|
uint32_t appInstanceId;
|
||||||
|
// GPS devices are only configurable on hardware that can connect them
|
||||||
|
bool showGpsDevices = false;
|
||||||
|
TaskEventGroup* eventGroup = nullptr;
|
||||||
|
uint32_t enableBit = 0;
|
||||||
|
uint32_t precisionBit = 0;
|
||||||
|
::std::atomic<bool> requestedEnabled = false;
|
||||||
|
::std::atomic<LocationPrecision> requestedPrecision = LOCATION_PRECISION_PRECISE;
|
||||||
|
// Valid while the window is shown
|
||||||
|
lv_obj_t* enableSwitch = nullptr;
|
||||||
|
lv_obj_t* precisionChips = nullptr;
|
||||||
|
lv_obj_t* statusLabel = nullptr;
|
||||||
|
lv_obj_t* gpsDeviceList = nullptr;
|
||||||
|
std::vector<GpsConfigurationRow> gpsConfigurationRows;
|
||||||
|
// Set while a delete confirmation is pending
|
||||||
|
bool hasPendingDelete = false;
|
||||||
|
size_t pendingDeleteIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
LocationPrecision getChipPrecision(lv_obj_t* chip) {
|
||||||
|
return static_cast<LocationPrecision>(reinterpret_cast<intptr_t>(lv_obj_get_user_data(chip)));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getStatusText() {
|
||||||
|
if (!location_is_enabled()) {
|
||||||
|
return "Location services are off";
|
||||||
|
}
|
||||||
|
Location location;
|
||||||
|
if (location_get(&location) != ERROR_NONE) {
|
||||||
|
return "Waiting for location...";
|
||||||
|
}
|
||||||
|
std::string text;
|
||||||
|
auto output = ::std::back_inserter(text);
|
||||||
|
::std::format_to(output, "{:.5f}, {:.5f}", location.latitude, location.longitude);
|
||||||
|
if (!::std::isnan(location.horizontal_accuracy)) {
|
||||||
|
::std::format_to(output, " (±{:.0f} m)", location.horizontal_accuracy);
|
||||||
|
}
|
||||||
|
::std::format_to(output, "\nSource: {}", location.provider);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateWidgets(Context* ctx) {
|
||||||
|
if (ctx->statusLabel == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lv_obj_set_state(ctx->enableSwitch, LV_STATE_CHECKED, location_is_enabled());
|
||||||
|
|
||||||
|
const LocationPrecision precision = location_get_precision();
|
||||||
|
const uint32_t count = lv_obj_get_child_count(ctx->precisionChips);
|
||||||
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
|
auto* chip = lv_obj_get_child(ctx->precisionChips, static_cast<int32_t>(i));
|
||||||
|
lv_obj_set_state(chip, LV_STATE_CHECKED, getChipPrecision(chip) == precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_label_set_text(ctx->statusLabel, getStatusText().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBackPressed(lv_event_t* event) {
|
||||||
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
|
app_event_emit_close(ctx->appInstanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starting and stopping the service blocks, so it happens on the app's own task
|
||||||
|
void onEnableSwitchChanged(lv_event_t* event) {
|
||||||
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
|
ctx->requestedEnabled = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED);
|
||||||
|
task_event_group_signal(ctx->eventGroup, ctx->enableBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPrecisionChipPressed(lv_event_t* event) {
|
||||||
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
|
ctx->requestedPrecision = getChipPrecision(lv_event_get_target_obj(event));
|
||||||
|
task_event_group_signal(ctx->eventGroup, ctx->precisionBit);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_t* createContainer(lv_obj_t* parent, lv_flex_flow_t flow) {
|
||||||
|
auto* container = lv_obj_create(parent);
|
||||||
|
lv_obj_set_flex_flow(container, flow);
|
||||||
|
lv_obj_set_style_border_width(container, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_all(container, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_scrollable(container, false);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAddGpsPressed(lv_event_t*) {
|
||||||
|
AppStartContext context = app_start_context_for_manifest(&addgps::manifest);
|
||||||
|
uint32_t instanceId = 0;
|
||||||
|
app_start_with_context(&context, &instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDeleteGpsPressed(lv_event_t* event) {
|
||||||
|
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||||
|
auto* device = static_cast<Device*>(lv_obj_get_user_data(lv_event_get_target_obj(event)));
|
||||||
|
for (const auto& row : ctx->gpsConfigurationRows) {
|
||||||
|
if (row.device == device) {
|
||||||
|
ctx->pendingDeleteIndex = row.configurationIndex;
|
||||||
|
ctx->hasPendingDelete = true;
|
||||||
|
alertdialog::start(ctx->appInstanceId, "Confirmation", ::std::string("Do you want to delete ") + device->name + "?", ::std::vector<::std::string> { "Yes", "No" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the persisted configuration of a device by the name of its UART.
|
||||||
|
* The index identifies the entry for gps_settings_remove_configuration_at(), even when entries are identical.
|
||||||
|
*/
|
||||||
|
bool findConfigurationIndex(Device* device, size_t& outIndex) {
|
||||||
|
auto* parent = device_get_parent(device);
|
||||||
|
if (parent == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FindContext {
|
||||||
|
const char* uartName;
|
||||||
|
size_t* outIndex;
|
||||||
|
bool found;
|
||||||
|
} findContext = { parent->name, &outIndex, false };
|
||||||
|
|
||||||
|
gps_settings_for_each_configuration(&findContext, [](const GpsConfiguration* configuration, size_t index, void* untypedContext) {
|
||||||
|
auto* context = static_cast<FindContext*>(untypedContext);
|
||||||
|
if (!context->found && strcmp(configuration->uart_name, context->uartName) == 0) {
|
||||||
|
*context->outIndex = index;
|
||||||
|
context->found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return findContext.found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the bus name and speed, e.g. "ttyACM0 9600 bps" */
|
||||||
|
std::string getConnectionDescription(Device* device) {
|
||||||
|
auto* parent = device_get_parent(device);
|
||||||
|
std::string result = parent != nullptr ? parent->name : "";
|
||||||
|
if (device_is_compatible(device, "tactility,gps-generic")) {
|
||||||
|
const auto* config = static_cast<const GpsConfig*>(device->config);
|
||||||
|
::std::format_to(::std::back_inserter(result), " {} bps", config->baud_rate);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void createGpsDeviceRow(Context* ctx, Device* device) {
|
||||||
|
auto* row = createContainer(ctx->gpsDeviceList, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
// The texts take the width that the button leaves. Texts that don't fit scroll.
|
||||||
|
auto* texts = createContainer(row, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_grow(texts, 1);
|
||||||
|
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
auto* nameLabel = lv_label_create(texts);
|
||||||
|
lv_obj_set_width(nameLabel, LV_PCT(100));
|
||||||
|
lv_label_set_long_mode(nameLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||||
|
char modelName[64];
|
||||||
|
lv_label_set_text(nameLabel, gps_get_model_name(device, modelName, sizeof(modelName)) == ERROR_NONE ? modelName : device->name);
|
||||||
|
|
||||||
|
auto* connectionLabel = lv_label_create(texts);
|
||||||
|
lv_obj_set_width(connectionLabel, LV_PCT(100));
|
||||||
|
lv_label_set_long_mode(connectionLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||||
|
lv_obj_set_style_text_font(connectionLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||||
|
lv_label_set_text(connectionLabel, getConnectionDescription(device).c_str());
|
||||||
|
|
||||||
|
// Devicetree devices have no persisted configuration and can't be deleted
|
||||||
|
size_t configurationIndex;
|
||||||
|
if ((device->flags & DEVICE_FLAG_DYNAMIC) && findConfigurationIndex(device, configurationIndex)) {
|
||||||
|
auto* deleteButton = lv_button_create(row);
|
||||||
|
lv_obj_set_user_data(deleteButton, device);
|
||||||
|
lv_obj_add_event_cb(deleteButton, onDeleteGpsPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
|
auto* deleteLabel = lv_label_create(deleteButton);
|
||||||
|
lv_obj_set_style_text_font(deleteLabel, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||||
|
lv_label_set_text(deleteLabel, LVGL_ICON_SHARED_DELETE);
|
||||||
|
ctx->gpsConfigurationRows.push_back({ .device = device, .configurationIndex = configurationIndex });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void rebuildGpsDeviceList(Context* ctx) {
|
||||||
|
ctx->gpsConfigurationRows.clear();
|
||||||
|
if (ctx->gpsDeviceList == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lv_obj_clean(ctx->gpsDeviceList);
|
||||||
|
|
||||||
|
bool hasDevices = false;
|
||||||
|
struct ForEachContext {
|
||||||
|
Context* ctx;
|
||||||
|
bool* hasDevices;
|
||||||
|
} forEachContext = { ctx, &hasDevices };
|
||||||
|
device_for_each_of_type(&GPS_TYPE, &forEachContext, [](Device* device, void* context) {
|
||||||
|
auto* forEach = static_cast<ForEachContext*>(context);
|
||||||
|
createGpsDeviceRow(forEach->ctx, device);
|
||||||
|
*forEach->hasDevices = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasDevices) {
|
||||||
|
lv_label_set_text(lv_label_create(ctx->gpsDeviceList), "No devices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void createGpsDevices(Context* ctx, lv_obj_t* parent) {
|
||||||
|
auto* header = createContainer(parent, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_size(header, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_align(header, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
auto* title = lv_label_create(header);
|
||||||
|
lv_obj_set_flex_grow(title, 1);
|
||||||
|
lv_label_set_text(title, "GPS devices");
|
||||||
|
|
||||||
|
auto* addButton = lv_button_create(header);
|
||||||
|
lv_obj_add_event_cb(addButton, onAddGpsPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
|
lv_label_set_text(lv_label_create(addButton), LV_SYMBOL_PLUS);
|
||||||
|
|
||||||
|
auto* card = lvgl_card_create(parent);
|
||||||
|
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_left(title, lv_obj_get_style_pad_left(card, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
ctx->gpsDeviceList = createContainer(card, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_size(ctx->gpsDeviceList, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
rebuildGpsDeviceList(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||||
|
auto* ctx = static_cast<Context*>(userData);
|
||||||
|
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
auto* toolbar = lvgl_toolbar_create(parent, "Location");
|
||||||
|
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||||
|
ctx->enableSwitch = lvgl_toolbar_add_switch_action(toolbar);
|
||||||
|
lv_obj_add_event_cb(ctx->enableSwitch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||||
|
|
||||||
|
auto* wrapper = lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(wrapper, 1);
|
||||||
|
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||||
|
|
||||||
|
auto* precisionLabel = lv_label_create(wrapper);
|
||||||
|
lv_label_set_text(precisionLabel, "Precision");
|
||||||
|
|
||||||
|
// The chips are centered in the card while they fit, and scroll when they don't
|
||||||
|
auto* precisionCard = lvgl_card_create(wrapper);
|
||||||
|
lv_obj_set_size(precisionCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(precisionCard, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(precisionCard, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_left(precisionLabel, lv_obj_get_style_pad_left(precisionCard, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
ctx->precisionChips = lv_obj_create(precisionCard);
|
||||||
|
lv_obj_set_size(ctx->precisionChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_max_width(ctx->precisionChips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_flex_flow(ctx->precisionChips, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_scroll_dir(ctx->precisionChips, LV_DIR_HOR);
|
||||||
|
lv_obj_set_style_bg_opa(ctx->precisionChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||||
|
// The chips' margins leave room for their focus rings and space them apart
|
||||||
|
lv_obj_set_style_pad_all(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_column(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
for (const auto& option : PRECISION_OPTIONS) {
|
||||||
|
auto* chip = lvgl_chip_create(ctx->precisionChips);
|
||||||
|
lv_label_set_text(lv_label_create(chip), option.name);
|
||||||
|
lv_obj_set_user_data(chip, reinterpret_cast<void*>(static_cast<intptr_t>(option.precision)));
|
||||||
|
lv_obj_add_event_cb(chip, onPrecisionChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||||
|
}
|
||||||
|
lvgl_grid_navigation_add(ctx->precisionChips);
|
||||||
|
|
||||||
|
auto* precisionHint = lv_label_create(precisionCard);
|
||||||
|
lv_obj_set_width(precisionHint, LV_PCT(100));
|
||||||
|
lv_label_set_long_mode(precisionHint, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_style_text_font(precisionHint, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||||
|
lv_label_set_text(precisionHint, "Approximate location is accurate to a few kilometers. Location based on the IP address is never precise.");
|
||||||
|
|
||||||
|
auto* statusCard = lvgl_card_create(wrapper);
|
||||||
|
lv_obj_set_size(statusCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
ctx->statusLabel = lv_label_create(statusCard);
|
||||||
|
lv_obj_set_width(ctx->statusLabel, LV_PCT(100));
|
||||||
|
lv_label_set_long_mode(ctx->statusLabel, LV_LABEL_LONG_WRAP);
|
||||||
|
|
||||||
|
if (ctx->showGpsDevices) {
|
||||||
|
createGpsDevices(ctx, wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidgets(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroyWidgets(void* userData) {
|
||||||
|
auto* ctx = static_cast<Context*>(userData);
|
||||||
|
ctx->enableSwitch = nullptr;
|
||||||
|
ctx->precisionChips = nullptr;
|
||||||
|
ctx->statusLabel = nullptr;
|
||||||
|
ctx->gpsDeviceList = nullptr;
|
||||||
|
ctx->gpsConfigurationRows.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t appMain(int argc, char* argv[]) {
|
||||||
|
Context ctx {};
|
||||||
|
ctx.appInstanceId = app_scheduler_current_app_id();
|
||||||
|
::AppManifest addGpsManifest;
|
||||||
|
ctx.showGpsDevices = app_manager_find_manifest(addgps::manifest.id, &addGpsManifest) == ERROR_NONE;
|
||||||
|
|
||||||
|
TaskEventGroup eventGroup {};
|
||||||
|
task_event_group_construct(&eventGroup);
|
||||||
|
ctx.eventGroup = &eventGroup;
|
||||||
|
check(task_event_group_claim_bit(&eventGroup, &ctx.enableBit) == ERROR_NONE);
|
||||||
|
check(task_event_group_claim_bit(&eventGroup, &ctx.precisionBit) == ERROR_NONE);
|
||||||
|
|
||||||
|
AppEventSubscription appEvents {};
|
||||||
|
check(app_event_subscribe(&appEvents, &eventGroup) == ERROR_NONE);
|
||||||
|
LocationSubscription locationEvents {};
|
||||||
|
check(location_subscribe(&locationEvents, &eventGroup) == ERROR_NONE);
|
||||||
|
|
||||||
|
WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||||
|
|
||||||
|
bool shouldClose = false;
|
||||||
|
while (!shouldClose) {
|
||||||
|
uint32_t flags = 0;
|
||||||
|
task_event_group_wait_any(&eventGroup, &flags, portMAX_DELAY);
|
||||||
|
|
||||||
|
if (flags & ctx.enableBit) {
|
||||||
|
location_set_enabled(ctx.requestedEnabled);
|
||||||
|
}
|
||||||
|
if (flags & ctx.precisionBit) {
|
||||||
|
location_set_precision(ctx.requestedPrecision);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppEvent event {};
|
||||||
|
while (app_event_poll(&appEvents, &event) == ERROR_NONE) {
|
||||||
|
if (event.type == APP_EVENT_CLOSE) {
|
||||||
|
shouldClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (event.type == APP_EVENT_RESULT) {
|
||||||
|
if (ctx.hasPendingDelete) {
|
||||||
|
ctx.hasPendingDelete = false;
|
||||||
|
// The first button is "Yes"
|
||||||
|
if (event.result.result == 0) {
|
||||||
|
gps_settings_remove_configuration_at(ctx.pendingDeleteIndex);
|
||||||
|
lvgl_lock();
|
||||||
|
rebuildGpsDeviceList(&ctx);
|
||||||
|
lvgl_unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app_manager_stop(event.result.launch_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lvgl_lock();
|
||||||
|
updateWidgets(&ctx);
|
||||||
|
lvgl_unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
window_manager_remove(window);
|
||||||
|
check(location_unsubscribe(&locationEvents) == ERROR_NONE);
|
||||||
|
check(app_event_unsubscribe(&appEvents) == ERROR_NONE);
|
||||||
|
task_event_group_destruct(&eventGroup);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern const ::AppManifest manifest = {
|
||||||
|
.id = "tactility.locationsettings",
|
||||||
|
.name = "Location",
|
||||||
|
.category = APP_CATEGORY_SETTINGS,
|
||||||
|
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||||
|
.flags = 0,
|
||||||
|
.stack = {}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@ -16,6 +16,10 @@
|
|||||||
#include <app/scheduler.h>
|
#include <app/scheduler.h>
|
||||||
#include <app/start.h>
|
#include <app/start.h>
|
||||||
|
|
||||||
|
#include <location/location.h>
|
||||||
|
|
||||||
|
#include <wifi/wifi_settings.h>
|
||||||
|
|
||||||
#include <lvgl_window_manager/window_manager.h>
|
#include <lvgl_window_manager/window_manager.h>
|
||||||
|
|
||||||
#include <tactility/check.h>
|
#include <tactility/check.h>
|
||||||
@ -230,7 +234,9 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
getMainDispatcher().dispatch([] {
|
getMainDispatcher().dispatch([] {
|
||||||
Device* wifi_device = nullptr;
|
Device* wifi_device = nullptr;
|
||||||
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) {
|
if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) {
|
||||||
wifi_set_radio_on(wifi_device);
|
if (wifi_set_radio_on(wifi_device) == ERROR_NONE && wifi_settings_set_enable(true) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to save the Wi-Fi state");
|
||||||
|
}
|
||||||
device_put(wifi_device);
|
device_put(wifi_device);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -259,6 +265,23 @@ int32_t appMain(int argc, char* argv[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.steps.push_back({
|
||||||
|
.title = "Location Services",
|
||||||
|
.description = "Location services let apps use your location. It comes from GPS, or from your IP address via ip-api.com when there is no GPS fix.",
|
||||||
|
.run = [&ctx] {
|
||||||
|
const std::vector<std::string> items = { "Allow precise location", "Allow approximate location", "Don't allow" };
|
||||||
|
ctx.pendingStepDialogId = selectiondialog::start(ctx.appInstanceId, "Location", items);
|
||||||
|
},
|
||||||
|
.onResult = [](int32_t selected) {
|
||||||
|
if (selected == 0 || selected == 1) {
|
||||||
|
location_set_precision(selected == 0 ? LOCATION_PRECISION_PRECISE : LOCATION_PRECISION_APPROXIMATE);
|
||||||
|
location_set_enabled(true);
|
||||||
|
} else if (selected == 2) {
|
||||||
|
location_set_enabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
TaskEventGroup event_group {};
|
TaskEventGroup event_group {};
|
||||||
task_event_group_construct(&event_group);
|
task_event_group_construct(&event_group);
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,9 @@ void connectToAp(const WifiApSettings& ap) {
|
|||||||
}
|
}
|
||||||
error_t result = wifi_set_radio_on(wifi_device);
|
error_t result = wifi_set_radio_on(wifi_device);
|
||||||
if (result == ERROR_NONE) {
|
if (result == ERROR_NONE) {
|
||||||
|
if (wifi_settings_set_enable(true) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to save the Wi-Fi state");
|
||||||
|
}
|
||||||
result = wifi_station_connect(wifi_device, ap.ssid, ap.password, ap.channel);
|
result = wifi_station_connect(wifi_device, ap.ssid, ap.password, ap.channel);
|
||||||
}
|
}
|
||||||
if (result != ERROR_NONE) {
|
if (result != ERROR_NONE) {
|
||||||
|
|||||||
@ -186,6 +186,9 @@ void onConnectPressed(lv_event_t* event) {
|
|||||||
}
|
}
|
||||||
error_t result = wifi_set_radio_on(wifi_device);
|
error_t result = wifi_set_radio_on(wifi_device);
|
||||||
if (result == ERROR_NONE) {
|
if (result == ERROR_NONE) {
|
||||||
|
if (wifi_settings_set_enable(true) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to save the Wi-Fi state");
|
||||||
|
}
|
||||||
result = wifi_station_connect(wifi_device, settings.ssid, settings.password, settings.channel);
|
result = wifi_station_connect(wifi_device, settings.ssid, settings.password, settings.channel);
|
||||||
}
|
}
|
||||||
if (result != ERROR_NONE) {
|
if (result != ERROR_NONE) {
|
||||||
|
|||||||
@ -46,24 +46,6 @@ static void onEnableSwitchChanged(lv_event_t* event) {
|
|||||||
bindings->onWifiToggled(is_on);
|
bindings->onWifiToggled(is_on);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onEnableOnBootSwitchChanged(lv_event_t* event) {
|
|
||||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
|
||||||
bool is_on = lv_obj_has_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
// Dispatch it, so file IO doesn't block the UI
|
|
||||||
getMainDispatcher().dispatch([is_on] {
|
|
||||||
wifi_settings_set_enable_on_boot(is_on);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onEnableOnBootParentClicked(lv_event_t* event) {
|
|
||||||
auto* enable_switch = static_cast<lv_obj_t*>(lv_event_get_user_data(event));
|
|
||||||
if (lv_obj_has_state(enable_switch, LV_STATE_CHECKED)) {
|
|
||||||
lv_obj_remove_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
} else {
|
|
||||||
lv_obj_add_state(enable_switch, LV_STATE_CHECKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onConnectToHiddenClicked(lv_event_t* event) {
|
static void onConnectToHiddenClicked(lv_event_t* event) {
|
||||||
auto* bindings = (Bindings*)lv_event_get_user_data(event);
|
auto* bindings = (Bindings*)lv_event_get_user_data(event);
|
||||||
bindings->onConnectToHidden();
|
bindings->onConnectToHidden();
|
||||||
@ -167,27 +149,6 @@ void View::updateConnectToHidden() {
|
|||||||
void View::updateNetworkList() {
|
void View::updateNetworkList() {
|
||||||
lv_obj_clean(networks_list);
|
lv_obj_clean(networks_list);
|
||||||
|
|
||||||
// Enable on boot
|
|
||||||
|
|
||||||
auto* enable_on_boot_wrapper = lv_obj_create(networks_list);
|
|
||||||
lv_obj_set_size(enable_on_boot_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
|
||||||
lv_obj_set_style_pad_all(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_border_width(enable_on_boot_wrapper, 0, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_style_bg_opa(enable_on_boot_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
|
||||||
lv_obj_set_flex_flow(enable_on_boot_wrapper, LV_FLEX_FLOW_ROW);
|
|
||||||
lv_obj_set_flex_align(enable_on_boot_wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
||||||
lv_obj_set_scrollable(enable_on_boot_wrapper, false);
|
|
||||||
|
|
||||||
auto* enable_label = lv_label_create(enable_on_boot_wrapper);
|
|
||||||
lv_label_set_text(enable_label, "Enable on boot");
|
|
||||||
lv_obj_set_flex_grow(enable_label, 1);
|
|
||||||
|
|
||||||
enable_on_boot_switch = lv_switch_create(enable_on_boot_wrapper);
|
|
||||||
lv_obj_add_event_cb(enable_on_boot_switch, onEnableOnBootSwitchChanged, LV_EVENT_VALUE_CHANGED, bindings);
|
|
||||||
lv_obj_add_event_cb(enable_on_boot_wrapper, onEnableOnBootParentClicked, LV_EVENT_SHORT_CLICKED, enable_on_boot_switch);
|
|
||||||
|
|
||||||
updateEnableOnBootToggle();
|
|
||||||
|
|
||||||
switch (state->getRadioState()) {
|
switch (state->getRadioState()) {
|
||||||
case WIFI_RADIO_STATE_ON_PENDING:
|
case WIFI_RADIO_STATE_ON_PENDING:
|
||||||
case WIFI_RADIO_STATE_ON: {
|
case WIFI_RADIO_STATE_ON: {
|
||||||
@ -288,17 +249,6 @@ void View::updateWifiToggle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void View::updateEnableOnBootToggle() {
|
|
||||||
if (enable_on_boot_switch != nullptr) {
|
|
||||||
lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY);
|
|
||||||
if (wifi_settings_get_enable_on_boot()) {
|
|
||||||
lv_obj_add_state(enable_on_boot_switch, LV_STATE_CHECKED);
|
|
||||||
} else {
|
|
||||||
lv_obj_remove_state(enable_on_boot_switch, LV_STATE_CHECKED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion Secondary updates
|
// endregion Secondary updates
|
||||||
|
|
||||||
// region Main
|
// region Main
|
||||||
@ -349,7 +299,6 @@ void View::update(bool rebuildList) {
|
|||||||
void View::reset() {
|
void View::reset() {
|
||||||
root = nullptr;
|
root = nullptr;
|
||||||
enable_switch = nullptr;
|
enable_switch = nullptr;
|
||||||
enable_on_boot_switch = nullptr;
|
|
||||||
scanning_spinner = nullptr;
|
scanning_spinner = nullptr;
|
||||||
refresh_button = nullptr;
|
refresh_button = nullptr;
|
||||||
networks_list = nullptr;
|
networks_list = nullptr;
|
||||||
|
|||||||
@ -61,6 +61,9 @@ static void connectToAp(const WifiApSettings& ap) {
|
|||||||
}
|
}
|
||||||
error_t result = wifi_set_radio_on(wifi_device);
|
error_t result = wifi_set_radio_on(wifi_device);
|
||||||
if (result == ERROR_NONE) {
|
if (result == ERROR_NONE) {
|
||||||
|
if (wifi_settings_set_enable(true) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to save the Wi-Fi state");
|
||||||
|
}
|
||||||
result = wifi_station_connect(wifi_device, ap.ssid, ap.password, ap.channel);
|
result = wifi_station_connect(wifi_device, ap.ssid, ap.password, ap.channel);
|
||||||
}
|
}
|
||||||
if (result != ERROR_NONE) {
|
if (result != ERROR_NONE) {
|
||||||
@ -114,7 +117,11 @@ static void onWifiToggled(bool enabled) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
error_t result = enabled ? wifi_set_radio_on(wifi_device) : wifi_set_radio_off(wifi_device);
|
error_t result = enabled ? wifi_set_radio_on(wifi_device) : wifi_set_radio_off(wifi_device);
|
||||||
if (result != ERROR_NONE) {
|
if (result == ERROR_NONE) {
|
||||||
|
if (wifi_settings_set_enable(enabled) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to save the Wi-Fi state");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LOG_E(TAG, "Failed to set radio state (%s)", error_to_string(result));
|
LOG_E(TAG, "Failed to set radio state (%s)", error_to_string(result));
|
||||||
}
|
}
|
||||||
device_put(wifi_device);
|
device_put(wifi_device);
|
||||||
|
|||||||
@ -320,7 +320,7 @@ void systemStart() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings::shouldEnableOnBoot()) {
|
if (settings::isEnabled()) {
|
||||||
start(dev);
|
start(dev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,7 +79,7 @@ static BluetoothSettings getCachedOrLoad() {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setEnableOnBoot(bool enable) {
|
void setEnable(bool enable) {
|
||||||
settings_mutex.lock();
|
settings_mutex.lock();
|
||||||
cached.enableOnBoot = enable;
|
cached.enableOnBoot = enable;
|
||||||
cached_valid = true;
|
cached_valid = true;
|
||||||
@ -87,7 +87,7 @@ void setEnableOnBoot(bool enable) {
|
|||||||
if (!save(cached)) LOG_E(TAG, "Failed to save");
|
if (!save(cached)) LOG_E(TAG, "Failed to save");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldEnableOnBoot() {
|
bool isEnabled() {
|
||||||
return getCachedOrLoad().enableOnBoot;
|
return getCachedOrLoad().enableOnBoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
#include <Tactility/TactilityPrivate.h>
|
#include <Tactility/TactilityPrivate.h>
|
||||||
#include <Tactility/bluetooth/Bluetooth.h>
|
#include <Tactility/bluetooth/Bluetooth.h>
|
||||||
|
#include <Tactility/bluetooth/BluetoothSettings.h>
|
||||||
#include <Tactility/settings/RadioSettings.h>
|
#include <Tactility/settings/RadioSettings.h>
|
||||||
|
|
||||||
#include <app/manager.h>
|
#include <app/manager.h>
|
||||||
#include <app/start.h>
|
#include <app/start.h>
|
||||||
|
|
||||||
#include <gps/gps.h>
|
#include <location/location.h>
|
||||||
|
|
||||||
|
#include <wifi/wifi_settings.h>
|
||||||
|
|
||||||
#include <lvgl/devices/indev.h>
|
#include <lvgl/devices/indev.h>
|
||||||
#include <lvgl/fonts.h>
|
#include <lvgl/fonts.h>
|
||||||
@ -55,10 +58,6 @@ static bool hasWifi() {
|
|||||||
return device_exists_of_type(&WIFI_TYPE);
|
return device_exists_of_type(&WIFI_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool hasGps() {
|
|
||||||
return device_exists_of_type(&GPS_TYPE);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool hasBluetooth() {
|
static bool hasBluetooth() {
|
||||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||||
return device_exists_of_type(&BLUETOOTH_TYPE);
|
return device_exists_of_type(&BLUETOOTH_TYPE);
|
||||||
@ -95,35 +94,27 @@ static void setWifiEnabled(void* context) {
|
|||||||
if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) {
|
if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
error_t result = fromContext(context) ? wifi_set_radio_on(device) : wifi_set_radio_off(device);
|
const bool enabled = fromContext(context);
|
||||||
if (result != ERROR_NONE) {
|
error_t result = enabled ? wifi_set_radio_on(device) : wifi_set_radio_off(device);
|
||||||
|
if (result == ERROR_NONE) {
|
||||||
|
if (wifi_settings_set_enable(enabled) != ERROR_NONE) {
|
||||||
|
LOG_E(TAG, "Failed to save the Wi-Fi state");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
LOG_E(TAG, "Failed to set Wi-Fi radio state (%s)", error_to_string(result));
|
LOG_E(TAG, "Failed to set Wi-Fi radio state (%s)", error_to_string(result));
|
||||||
}
|
}
|
||||||
device_put(device);
|
device_put(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isGpsEnabled() {
|
static bool isLocationEnabled() {
|
||||||
bool enabled = false;
|
return location_is_enabled();
|
||||||
device_for_each_of_type(&GPS_TYPE, &enabled, [](Device* device, void* context) {
|
|
||||||
if (device_is_ready(device)) {
|
|
||||||
*static_cast<bool*>(context) = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
return enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setGpsEnabled(void* context) {
|
static void setLocationEnabled(void* context) {
|
||||||
device_for_each_of_type(&GPS_TYPE, context, [](Device* device, void* context) {
|
const error_t result = location_set_enabled(fromContext(context));
|
||||||
const bool enabled = fromContext(context);
|
if (result != ERROR_NONE) {
|
||||||
if (enabled && !device_is_ready(device)) {
|
LOG_E(TAG, "Failed to set location services state (%s)", error_to_string(result));
|
||||||
device_start(device);
|
|
||||||
} else if (!enabled && device_is_ready(device)) {
|
|
||||||
device_stop(device);
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||||
@ -144,6 +135,7 @@ static void setBluetoothEnabled(void* context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bool enabled = fromContext(context);
|
const bool enabled = fromContext(context);
|
||||||
|
bluetooth::settings::setEnable(enabled);
|
||||||
if (enabled != bluetooth::isRadioOnOrPending(device)) {
|
if (enabled != bluetooth::isRadioOnOrPending(device)) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
bluetooth::start(device);
|
bluetooth::start(device);
|
||||||
@ -290,10 +282,8 @@ static void createWidgets(lv_obj_t* parent, void* /*userData*/) {
|
|||||||
if (hasWifi()) {
|
if (hasWifi()) {
|
||||||
createOption(grid, &wifi_option);
|
createOption(grid, &wifi_option);
|
||||||
}
|
}
|
||||||
static constexpr ToggleOption gps_option = { LVGL_ICON_SHARED_LOCATION_ON, LVGL_ICON_SHARED_LOCATION_OFF, isGpsEnabled, setGpsEnabled };
|
static constexpr ToggleOption location_option = { LVGL_ICON_SHARED_LOCATION_ON, LVGL_ICON_SHARED_LOCATION_OFF, isLocationEnabled, setLocationEnabled };
|
||||||
if (hasGps()) {
|
createOption(grid, &location_option);
|
||||||
createOption(grid, &gps_option);
|
|
||||||
}
|
|
||||||
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
#if defined(CONFIG_BT_NIMBLE_ENABLED)
|
||||||
static constexpr ToggleOption bluetooth_option = { LVGL_ICON_SHARED_BLUETOOTH, LVGL_ICON_SHARED_BLUETOOTH_DISABLED, isBluetoothEnabled, setBluetoothEnabled };
|
static constexpr ToggleOption bluetooth_option = { LVGL_ICON_SHARED_BLUETOOTH, LVGL_ICON_SHARED_BLUETOOTH_DISABLED, isBluetoothEnabled, setBluetoothEnabled };
|
||||||
if (hasBluetooth()) {
|
if (hasBluetooth()) {
|
||||||
@ -324,9 +314,6 @@ static void createWidgets(lv_obj_t* parent, void* /*userData*/) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void systemMenuShow() {
|
void systemMenuShow() {
|
||||||
if (!hasWifi() && !hasGps() && !hasBluetooth() && !hasMesh() && !supportsPowerOff()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window_manager_overlay_show(createWidgets, nullptr, nullptr);
|
window_manager_overlay_show(createWidgets, nullptr, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <gps/gps.h>
|
#include <location/location.h>
|
||||||
|
|
||||||
#if TT_FEATURE_MESH
|
#if TT_FEATURE_MESH
|
||||||
#include <mesh/mesh.h>
|
#include <mesh/mesh.h>
|
||||||
@ -183,8 +183,8 @@ class StatusbarService final : public Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void updateGpsIcon() {
|
void updateGpsIcon() {
|
||||||
// Only shown while a GPS device is on
|
// Only shown while location is being used
|
||||||
const char* icon = device_has_active_by_type(&GPS_TYPE) ? LVGL_ICON_STATUSBAR_LOCATION_ON : nullptr;
|
const char* icon = location_is_in_use() ? LVGL_ICON_STATUSBAR_LOCATION_ON : nullptr;
|
||||||
if (gps_last_icon != icon) {
|
if (gps_last_icon != icon) {
|
||||||
if (icon != nullptr) {
|
if (icon != nullptr) {
|
||||||
lvgl_statusbar_icon_set_image(gps_icon_id, icon);
|
lvgl_statusbar_icon_set_image(gps_icon_id, icon);
|
||||||
|
|||||||
@ -52,6 +52,7 @@ target_link_libraries(TactilityTests PRIVATE
|
|||||||
gps-module
|
gps-module
|
||||||
graphics-module
|
graphics-module
|
||||||
http-module
|
http-module
|
||||||
|
location-module
|
||||||
mbedtls-module
|
mbedtls-module
|
||||||
posix-symbols-module
|
posix-symbols-module
|
||||||
pthread-module
|
pthread-module
|
||||||
|
|||||||
@ -87,6 +87,16 @@ void thread_set_name(Thread* thread, const char* name);
|
|||||||
*/
|
*/
|
||||||
void thread_set_stack_size(Thread* thread, size_t stack_size);
|
void thread_set_stack_size(Thread* thread, size_t stack_size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets whether the stack should be allocated in external memory (PSRAM) when available.
|
||||||
|
* The stack is allocated in internal memory when there is no external memory.
|
||||||
|
* @warning A thread with its stack in external memory must not access flash (e.g. write to the internal filesystem or NVS).
|
||||||
|
* @param[in] thread The thread instance.
|
||||||
|
* @param[in] external true to prefer external memory
|
||||||
|
* @note Can only be called when the thread is in the STOPPED state.
|
||||||
|
*/
|
||||||
|
void thread_set_stack_external(Thread* thread, bool external);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Sets the CPU core affinity for the thread.
|
* @brief Sets the CPU core affinity for the thread.
|
||||||
* @param[in] thread The thread instance.
|
* @param[in] thread The thread instance.
|
||||||
|
|||||||
@ -75,7 +75,7 @@ error_t device_construct(struct Device* device);
|
|||||||
* This fails when a device is busy or has children.
|
* This fails when a device is busy or has children.
|
||||||
*
|
*
|
||||||
* @param[in,out] device non-null device pointer
|
* @param[in,out] device non-null device pointer
|
||||||
* @retval ERROR_INVALID_STATE if the device is started, added, or has children
|
* @retval ERROR_INVALID_STATE if the device is started, added, being removed, or has children
|
||||||
* @retval ERROR_RESOURCE_BUSY if the device has outstanding device_get() references
|
* @retval ERROR_RESOURCE_BUSY if the device has outstanding device_get() references
|
||||||
* @retval ERROR_NONE on success
|
* @retval ERROR_NONE on success
|
||||||
*/
|
*/
|
||||||
@ -87,7 +87,7 @@ error_t device_destruct(struct Device* device);
|
|||||||
* - its parent (if any)
|
* - its parent (if any)
|
||||||
*
|
*
|
||||||
* @param[in,out] device non-null device pointer
|
* @param[in,out] device non-null device pointer
|
||||||
* @retval ERROR_INVALID_STATE if the device is already added
|
* @retval ERROR_INVALID_STATE if the device is already added or being removed
|
||||||
* @retval ERROR_NONE on success
|
* @retval ERROR_NONE on success
|
||||||
*/
|
*/
|
||||||
error_t device_add(struct Device* device);
|
error_t device_add(struct Device* device);
|
||||||
@ -96,9 +96,10 @@ error_t device_add(struct Device* device);
|
|||||||
* Deregister a device. Remove it from all relevant systems:
|
* Deregister a device. Remove it from all relevant systems:
|
||||||
* - the global ledger
|
* - the global ledger
|
||||||
* - its parent (if any)
|
* - its parent (if any)
|
||||||
|
* Afterwards, device_start() fails for this device, also when called with a reference that was taken before.
|
||||||
*
|
*
|
||||||
* @param[in,out] device non-null device pointer
|
* @param[in,out] device non-null device pointer
|
||||||
* @retval ERROR_INVALID_STATE if the device is still started
|
* @retval ERROR_INVALID_STATE if the device is started, being started, or not added
|
||||||
* @retval ERROR_NOT_FOUND if the device was not found in the system
|
* @retval ERROR_NOT_FOUND if the device was not found in the system
|
||||||
* @retval ERROR_NONE on success
|
* @retval ERROR_NONE on success
|
||||||
*/
|
*/
|
||||||
@ -126,8 +127,8 @@ error_t device_start(struct Device* device);
|
|||||||
* device_start()/device_stop() on this same device - device_stop() holds this
|
* device_start()/device_stop() on this same device - device_stop() holds this
|
||||||
* device's lock for its entire body, so doing so would deadlock.
|
* device's lock for its entire body, so doing so would deadlock.
|
||||||
* @param[in,out] device non-null device pointer
|
* @param[in,out] device non-null device pointer
|
||||||
* @retval ERROR_INVALID_STATE if the device is not started
|
* @retval ERROR_INVALID_STATE if the device is not added
|
||||||
* @retval ERROR_RESOURCE_BUSY if the device has outstanding device_get() references
|
* @retval ERROR_RESOURCE_BUSY if the device is being stopped by another task
|
||||||
* @retval ERROR_RESOURCE when driver unbinding fails
|
* @retval ERROR_RESOURCE when driver unbinding fails
|
||||||
* @retval ERROR_NONE on success
|
* @retval ERROR_NONE on success
|
||||||
*/
|
*/
|
||||||
@ -260,18 +261,19 @@ void device_unlock(struct Device* device);
|
|||||||
bool device_is_constructed(const struct Device* device);
|
bool device_is_constructed(const struct Device* device);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Take a reference on a device, guaranteeing its driver data remains valid until a matching
|
* Take a reference on a device, guaranteeing that the device stays constructed until a matching
|
||||||
* device_put(). Named after the Linux kernel's get_device()/put_device(). Intended to bracket
|
* device_put(). Named after the Linux kernel's get_device()/put_device(). The reference doesn't depend
|
||||||
* a single hardware operation (one API call or a short sequence of them), not to be held across
|
* on the device's state: the device can be started, stopped and removed while it's held, and only
|
||||||
* a blocking wait or unrelated later work - device_stop() fails with ERROR_RESOURCE_BUSY while
|
* device_destruct() fails with ERROR_RESOURCE_BUSY while a reference is outstanding.
|
||||||
* any reference is outstanding.
|
* Hold it for a short sequence of operations (e.g. device_start() on a device found with a lookup),
|
||||||
|
* not across unrelated later work.
|
||||||
*
|
*
|
||||||
* @param[in,out] device non-null device pointer, already known to be valid (e.g. a static
|
* @param[in,out] device non-null device pointer, already known to be valid (e.g. a static
|
||||||
* devicetree pointer, or one captured earlier under a lock) - device_get() does
|
* devicetree pointer, or one captured earlier under a lock) - device_get() does
|
||||||
* not itself protect against a dangling/freed Device*. For a fresh lookup that
|
* not itself protect against a dangling/freed Device*. For a fresh lookup that
|
||||||
* might race a concurrent teardown of a dynamically-allocated Device, use one of
|
* might race a concurrent teardown of a dynamically-allocated Device, use one of
|
||||||
* the device_get_*() lookup functions below instead.
|
* the device_get_*() lookup functions below instead.
|
||||||
* @retval ERROR_INVALID_STATE if the device is not started (or has been destructed)
|
* @retval ERROR_INVALID_STATE if the device has been destructed
|
||||||
* @retval ERROR_NONE on success; caller must call device_put(device) exactly once
|
* @retval ERROR_NONE on success; caller must call device_put(device) exactly once
|
||||||
*/
|
*/
|
||||||
error_t device_get(struct Device* device);
|
error_t device_get(struct Device* device);
|
||||||
@ -350,12 +352,11 @@ bool device_exists_of_type(const struct DeviceType* type) ;
|
|||||||
error_t device_get_by_name(const char* name, struct Device** out_device);
|
error_t device_get_by_name(const char* name, struct Device** out_device);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the first device of the given type and atomically take a reference on it.
|
* Find the first device of the given type, started or not, and atomically take a reference on it.
|
||||||
*
|
*
|
||||||
* @param[in] type non-null device type pointer
|
* @param[in] type non-null device type pointer
|
||||||
* @param[out] out_device receives the found device on success; untouched on failure
|
* @param[out] out_device receives the found device on success; untouched on failure
|
||||||
* @retval ERROR_NOT_FOUND if no device of that type exists
|
* @retval ERROR_NOT_FOUND if no device of that type exists
|
||||||
* @retval ERROR_INVALID_STATE if found but not started (no reference taken)
|
|
||||||
* @retval ERROR_NONE on success; caller must call device_put(*out_device) exactly once
|
* @retval ERROR_NONE on success; caller must call device_put(*out_device) exactly once
|
||||||
*/
|
*/
|
||||||
error_t device_get_first_by_type(const struct DeviceType* type, struct Device** out_device);
|
error_t device_get_first_by_type(const struct DeviceType* type, struct Device** out_device);
|
||||||
|
|||||||
@ -28,6 +28,9 @@ struct Thread {
|
|||||||
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
|
||||||
Mutex mutex = { 0 };
|
Mutex mutex = { 0 };
|
||||||
configSTACK_DEPTH_TYPE stackDepth = 4096;
|
configSTACK_DEPTH_TYPE stackDepth = 4096;
|
||||||
|
bool stackExternal = false;
|
||||||
|
// Whether the task was created with xTaskCreatePinnedToCoreWithCaps()
|
||||||
|
bool createdWithCaps = false;
|
||||||
portBASE_TYPE affinity = -1;
|
portBASE_TYPE affinity = -1;
|
||||||
|
|
||||||
Thread() {
|
Thread() {
|
||||||
@ -77,10 +80,17 @@ static void thread_main_body(void* context) {
|
|||||||
|
|
||||||
vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr);
|
vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr);
|
||||||
|
|
||||||
|
// The thread can be freed once taskHandle is cleared
|
||||||
thread->lock();
|
thread->lock();
|
||||||
|
const bool createdWithCaps = thread->createdWithCaps;
|
||||||
thread->taskHandle = nullptr;
|
thread->taskHandle = nullptr;
|
||||||
thread->unlock();
|
thread->unlock();
|
||||||
|
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
if (createdWithCaps) {
|
||||||
|
vTaskDeleteWithCaps(nullptr);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
vTaskDelete(nullptr);
|
vTaskDelete(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +145,13 @@ void thread_set_stack_size(Thread* thread, size_t stack_size) {
|
|||||||
thread->unlock();
|
thread->unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void thread_set_stack_external(Thread* thread, bool external) {
|
||||||
|
thread->lock();
|
||||||
|
check(thread->state == THREAD_STATE_STOPPED);
|
||||||
|
thread->stackExternal = external;
|
||||||
|
thread->unlock();
|
||||||
|
}
|
||||||
|
|
||||||
void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) {
|
void thread_set_affinity(Thread* thread, portBASE_TYPE affinity) {
|
||||||
thread->lock();
|
thread->lock();
|
||||||
check(thread->state == THREAD_STATE_STOPPED);
|
check(thread->state == THREAD_STATE_STOPPED);
|
||||||
@ -187,10 +204,33 @@ error_t thread_start(Thread* thread) {
|
|||||||
thread->lock();
|
thread->lock();
|
||||||
auto priority = static_cast<UBaseType_t>(thread->priority);
|
auto priority = static_cast<UBaseType_t>(thread->priority);
|
||||||
portBASE_TYPE affinity = thread->affinity;
|
portBASE_TYPE affinity = thread->affinity;
|
||||||
|
bool stackExternal = thread->stackExternal;
|
||||||
|
thread->createdWithCaps = false;
|
||||||
thread->unlock();
|
thread->unlock();
|
||||||
|
|
||||||
BaseType_t result;
|
BaseType_t result = pdFAIL;
|
||||||
if (affinity != -1) {
|
#ifdef ESP_PLATFORM
|
||||||
|
if (stackExternal) {
|
||||||
|
// Set before the task can read it, as the task can run before the creation call returns
|
||||||
|
thread->createdWithCaps = true;
|
||||||
|
result = xTaskCreatePinnedToCoreWithCaps(
|
||||||
|
thread_main_body,
|
||||||
|
thread->name.c_str(),
|
||||||
|
thread->stackDepth,
|
||||||
|
thread,
|
||||||
|
priority,
|
||||||
|
&thread->taskHandle,
|
||||||
|
affinity != -1 ? affinity : tskNO_AFFINITY,
|
||||||
|
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
|
||||||
|
);
|
||||||
|
if (result != pdPASS) {
|
||||||
|
thread->createdWithCaps = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (result == pdPASS) {
|
||||||
|
// Stack is in external memory
|
||||||
|
} else if (affinity != -1) {
|
||||||
#ifdef ESP_PLATFORM
|
#ifdef ESP_PLATFORM
|
||||||
result = xTaskCreatePinnedToCore(
|
result = xTaskCreatePinnedToCore(
|
||||||
thread_main_body,
|
thread_main_body,
|
||||||
|
|||||||
@ -45,6 +45,9 @@ struct DeviceInternal {
|
|||||||
// also checked by device_get() so no new ref can be acquired while a stop is in flight.
|
// also checked by device_get() so no new ref can be acquired while a stop is in flight.
|
||||||
bool starting : 1 = false;
|
bool starting : 1 = false;
|
||||||
bool stopping : 1 = false;
|
bool stopping : 1 = false;
|
||||||
|
// Set while device_remove() updates the parent and the ledger, with `mutex` released.
|
||||||
|
// Blocks device_destruct() and device_add() until the removal or its rollback finishes.
|
||||||
|
bool removing : 1 = false;
|
||||||
} state;
|
} state;
|
||||||
/** Attached child devices */
|
/** Attached child devices */
|
||||||
std::vector<Device*> children {};
|
std::vector<Device*> children {};
|
||||||
@ -107,7 +110,7 @@ error_t device_destruct(Device* device) {
|
|||||||
unlock_internal(device->internal);
|
unlock_internal(device->internal);
|
||||||
return ERROR_RESOURCE_BUSY;
|
return ERROR_RESOURCE_BUSY;
|
||||||
}
|
}
|
||||||
if (internal->state.started || internal->state.added) {
|
if (internal->state.started || internal->state.added || internal->state.removing) {
|
||||||
unlock_internal(device->internal);
|
unlock_internal(device->internal);
|
||||||
return ERROR_INVALID_STATE;
|
return ERROR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
@ -146,8 +149,10 @@ static void device_remove_child(Device* device, Device* child) {
|
|||||||
error_t device_add(Device* device) {
|
error_t device_add(Device* device) {
|
||||||
LOG_D(TAG, "add %s", device->name);
|
LOG_D(TAG, "add %s", device->name);
|
||||||
|
|
||||||
// Already added
|
lock_internal(device->internal);
|
||||||
if (device->internal->state.started || device->internal->state.added) {
|
const bool invalid = device->internal->state.started || device->internal->state.added || device->internal->state.removing;
|
||||||
|
unlock_internal(device->internal);
|
||||||
|
if (invalid) {
|
||||||
return ERROR_INVALID_STATE;
|
return ERROR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,10 +173,18 @@ error_t device_add(Device* device) {
|
|||||||
|
|
||||||
error_t device_remove(Device* device) {
|
error_t device_remove(Device* device) {
|
||||||
LOG_D(TAG, "remove %s", device->name);
|
LOG_D(TAG, "remove %s", device->name);
|
||||||
|
auto* internal = device->internal;
|
||||||
|
|
||||||
if (device->internal->state.started || !device->internal->state.added) {
|
// Cleared under the lock that device_start() checks it with, so the device can't be started anymore.
|
||||||
|
// The lock is released before taking ledger_lock, as lookups take ledger_lock first.
|
||||||
|
lock_internal(internal);
|
||||||
|
if (internal->state.started || internal->state.starting || !internal->state.added) {
|
||||||
|
unlock_internal(internal);
|
||||||
return ERROR_INVALID_STATE;
|
return ERROR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
|
internal->state.added = false;
|
||||||
|
internal->state.removing = true;
|
||||||
|
unlock_internal(internal);
|
||||||
|
|
||||||
// Remove self from parent's children list
|
// Remove self from parent's children list
|
||||||
auto* parent = device->parent;
|
auto* parent = device->parent;
|
||||||
@ -188,7 +201,9 @@ error_t device_remove(Device* device) {
|
|||||||
ledger.devices.erase(iterator);
|
ledger.devices.erase(iterator);
|
||||||
ledger_unlock();
|
ledger_unlock();
|
||||||
|
|
||||||
device->internal->state.added = false;
|
lock_internal(internal);
|
||||||
|
internal->state.removing = false;
|
||||||
|
unlock_internal(internal);
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
|
|
||||||
failed_ledger_lookup:
|
failed_ledger_lookup:
|
||||||
@ -198,6 +213,10 @@ failed_ledger_lookup:
|
|||||||
device_add_child(parent, device);
|
device_add_child(parent, device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock_internal(internal);
|
||||||
|
internal->state.added = true;
|
||||||
|
internal->state.removing = false;
|
||||||
|
unlock_internal(internal);
|
||||||
return ERROR_NOT_FOUND;
|
return ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -153,3 +153,113 @@ TEST_CASE("device_get_by_name should find and reference an added device regardle
|
|||||||
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
||||||
CHECK_EQ(driver_remove_destruct(&test_driver), ERROR_NONE);
|
CHECK_EQ(driver_remove_destruct(&test_driver), ERROR_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const DeviceType typed_test_type = { .name = "device_get_put_test_type" };
|
||||||
|
|
||||||
|
Driver typed_test_driver = {
|
||||||
|
.name = "device_get_put_test_typed_driver",
|
||||||
|
.compatible = (const char*[]) { "device_get_put_test_typed", nullptr },
|
||||||
|
.start_device = start,
|
||||||
|
.stop_device = stop,
|
||||||
|
.api = nullptr,
|
||||||
|
.device_type = &typed_test_type,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::atomic<bool> blocking_start_entered { false };
|
||||||
|
std::atomic<bool> blocking_start_release { false };
|
||||||
|
|
||||||
|
int blocking_start(Device*) {
|
||||||
|
blocking_start_entered = true;
|
||||||
|
while (!blocking_start_release.load()) {
|
||||||
|
delay_millis(1);
|
||||||
|
}
|
||||||
|
return ERROR_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Driver blocking_test_driver = {
|
||||||
|
.name = "device_get_put_test_blocking_driver",
|
||||||
|
.compatible = (const char*[]) { "device_get_put_test_blocking", nullptr },
|
||||||
|
.start_device = blocking_start,
|
||||||
|
.stop_device = stop,
|
||||||
|
.api = nullptr,
|
||||||
|
.device_type = nullptr,
|
||||||
|
.internal = nullptr,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("device_get_first_by_type should reference a device that is not started") {
|
||||||
|
Device device = { .name = "get_first_by_type_device", .config = nullptr, .parent = nullptr };
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&typed_test_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct(&device), ERROR_NONE);
|
||||||
|
device_set_driver(&device, &typed_test_driver);
|
||||||
|
CHECK_EQ(device_add(&device), ERROR_NONE);
|
||||||
|
|
||||||
|
Device* out = nullptr;
|
||||||
|
CHECK_EQ(device_get_first_by_type(&typed_test_type, &out), ERROR_NONE);
|
||||||
|
CHECK_EQ(out, &device);
|
||||||
|
CHECK_EQ(device_start(out), ERROR_NONE);
|
||||||
|
device_put(out);
|
||||||
|
|
||||||
|
CHECK_EQ(device_stop(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&typed_test_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("device_start should fail for a referenced device after device_remove") {
|
||||||
|
Device device = { .name = "start_after_remove", .config = nullptr, .parent = nullptr };
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&test_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct(&device), ERROR_NONE);
|
||||||
|
device_set_driver(&device, &test_driver);
|
||||||
|
CHECK_EQ(device_add(&device), ERROR_NONE);
|
||||||
|
|
||||||
|
CHECK_EQ(device_get(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_start(&device), ERROR_INVALID_STATE);
|
||||||
|
CHECK_EQ(device_destruct(&device), ERROR_RESOURCE_BUSY);
|
||||||
|
device_put(&device);
|
||||||
|
|
||||||
|
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&test_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("device_remove should fail while the device is being started") {
|
||||||
|
static Device device = { .name = "remove_while_starting", .config = nullptr, .parent = nullptr };
|
||||||
|
|
||||||
|
CHECK_EQ(driver_construct_add(&blocking_test_driver), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_construct(&device), ERROR_NONE);
|
||||||
|
device_set_driver(&device, &blocking_test_driver);
|
||||||
|
CHECK_EQ(device_add(&device), ERROR_NONE);
|
||||||
|
|
||||||
|
blocking_start_entered = false;
|
||||||
|
blocking_start_release = false;
|
||||||
|
auto* thread = thread_alloc_full(
|
||||||
|
"device_remove_starter",
|
||||||
|
4096,
|
||||||
|
[](void*) -> int32_t { return device_start(&device) == ERROR_NONE ? 0 : 1; },
|
||||||
|
nullptr,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
CHECK_EQ(thread_start(thread), ERROR_NONE);
|
||||||
|
while (!blocking_start_entered.load()) {
|
||||||
|
delay_millis(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK_EQ(device_remove(&device), ERROR_INVALID_STATE);
|
||||||
|
|
||||||
|
blocking_start_release = true;
|
||||||
|
CHECK_EQ(thread_join(thread, 200, 1), ERROR_NONE);
|
||||||
|
CHECK_EQ(thread_get_return_code(thread), 0);
|
||||||
|
thread_free(thread);
|
||||||
|
|
||||||
|
CHECK_EQ(device_stop(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_remove(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(device_destruct(&device), ERROR_NONE);
|
||||||
|
CHECK_EQ(driver_remove_destruct(&blocking_test_driver), ERROR_NONE);
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/http-module/tests ${CMAKE_CURRENT_B
|
|||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/binfont-module/tests ${CMAKE_CURRENT_BINARY_DIR}/binfont-module)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/binfont-module/tests ${CMAKE_CURRENT_BINARY_DIR}/binfont-module)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lodepng-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lodepng-module)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lodepng-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lodepng-module)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lvgl-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lvgl-module)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/lvgl-module/tests ${CMAKE_CURRENT_BINARY_DIR}/lvgl-module)
|
||||||
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/location-module/tests ${CMAKE_CURRENT_BINARY_DIR}/location-module)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-module)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-module)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-meshcore-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-meshcore-module)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-meshcore-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-meshcore-module)
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-meshtastic-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-meshtastic-module)
|
add_subdirectory(${CMAKE_SOURCE_DIR}/Modules/mesh-meshtastic-module/tests ${CMAKE_CURRENT_BINARY_DIR}/mesh-meshtastic-module)
|
||||||
@ -34,6 +35,7 @@ add_dependencies(build-tests HttpModuleTests)
|
|||||||
add_dependencies(build-tests BinFontModuleTests)
|
add_dependencies(build-tests BinFontModuleTests)
|
||||||
add_dependencies(build-tests LodePngModuleTests)
|
add_dependencies(build-tests LodePngModuleTests)
|
||||||
add_dependencies(build-tests LvglModuleTests)
|
add_dependencies(build-tests LvglModuleTests)
|
||||||
|
add_dependencies(build-tests LocationModuleTests)
|
||||||
add_dependencies(build-tests MeshModuleTests)
|
add_dependencies(build-tests MeshModuleTests)
|
||||||
add_dependencies(build-tests MeshMeshCoreModuleTests)
|
add_dependencies(build-tests MeshMeshCoreModuleTests)
|
||||||
add_dependencies(build-tests MeshMeshtasticModuleTests)
|
add_dependencies(build-tests MeshMeshtasticModuleTests)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user