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:
Ken Van Hoeylandt 2026-10-13 23:13:30 +02:00 committed by GitHub
parent 3a7db7a801
commit 6e437d3288
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 3809 additions and 850 deletions

View File

@ -173,6 +173,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Modules/gps-module)
add_subdirectory(Modules/graphics-module)
add_subdirectory(Modules/http-module)
add_subdirectory(Modules/location-module)
add_subdirectory(Modules/mbedtls-module)
add_subdirectory(Modules/mesh-module)
add_subdirectory(Modules/mesh-meshcore-module)
@ -203,6 +204,11 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
TARGET_DIRECTORY freertos_kernel
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_include_directories(freertos_kernel PUBLIC Platforms/platform-posix/freertos)

View File

@ -1,6 +1,6 @@
general.vendor=LilyGO
general.name=T5 4.7 Inch E-Paper S3
general.incubating=true
general.incubating=false
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=LilyGO
general.name=T-Deck Max
# 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.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=LilyGO
general.name=T-Deck Pro
# The display driver is not reliable yet
general.incubating=true
general.incubating=false
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,6 +1,6 @@
general.vendor=LilyGO
general.name=T-Display
general.incubating=true
general.incubating=false
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32

View File

@ -1,6 +1,6 @@
general.vendor=LilyGO
general.name=T-Dongle S3
general.incubating=true
general.incubating=false
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3

View File

@ -1,6 +1,6 @@
general.vendor=M5Stack
general.name=PaperS3
general.incubating=true
general.incubating=false
hardware.target=esp32s3
hardware.flashSize=16MB

View File

@ -1,6 +1,6 @@
general.vendor=Waveshare
general.name=ESP32 S3 GEEK
general.incubating=true
general.incubating=false
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3

View File

@ -1,6 +1,6 @@
general.vendor=WaveShare
general.name=S3 LCD 1.3"
general.incubating=true
general.incubating=false
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3

View File

@ -1,6 +1,6 @@
general.vendor=WaveShare
general.name=S3 Touch LCD 1.28"
general.incubating=true
general.incubating=false
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3

View File

@ -1,6 +1,6 @@
general.vendor=WaveShare
general.name=S3 Touch LCD 1.47"
general.incubating=true
general.incubating=false
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -7,11 +7,13 @@
## 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)
- 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.
- 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.
- 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
- 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.
- 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::*`
@ -30,6 +32,8 @@
## 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)
- 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.

View File

@ -3,9 +3,12 @@
#include <gps/gps.h>
#include <atomic>
struct Device;
/**
* 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);

View File

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

View File

@ -3,10 +3,12 @@
#include <gps/gps.h>
#include <atomic>
struct Device;
/**
* 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);

View File

@ -3,6 +3,7 @@
#include <gps/gps.h>
#include <atomic>
#include <cstddef>
#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
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);
}

View File

@ -19,6 +19,7 @@
#include <minmea.h>
#include <atomic>
#include <cstdio>
#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 {
RecursiveMutex mutex;
Thread* thread;
volatile bool interrupt_requested;
std::atomic<bool> interrupt_requested;
GpsState state;
// Mirrors GpsConfig::model, but overwritten with the autodetected model once probing succeeds.
GpsModel model;
@ -105,6 +106,31 @@ static bool is_interrupted(GpsInternal* internal) {
// 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) {
auto* device = static_cast<Device*>(context);
auto* internal = static_cast<GpsInternal*>(device_get_driver_data(device));
@ -132,27 +158,13 @@ static int32_t gps_thread_main(void* context) {
return -1;
}
GpsModel model = internal->model;
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
model = gps_probe(uart);
if (model == GpsModel::GPS_MODEL_UNKNOWN) {
LOG_E(TAG, "Probe failed");
set_state(internal, GpsState::GPS_STATE_ERROR);
return -1;
}
recursive_mutex_lock(&internal->mutex);
internal->model = model;
recursive_mutex_unlock(&internal->mutex);
}
if (!gps_init(uart, model)) {
LOG_E(TAG, "Init failed");
if (setup_receiver(internal, uart)) {
set_state(internal, GpsState::GPS_STATE_ON);
} else if (!is_interrupted(internal)) {
set_state(internal, GpsState::GPS_STATE_ERROR);
return -1;
}
set_state(internal, GpsState::GPS_STATE_ON);
// Reference: https://gpsd.gitlab.io/gpsd/NMEA.html
uint8_t buffer[GPS_UART_BUFFER_SIZE];
while (!is_interrupted(internal)) {

View File

@ -1,11 +1,11 @@
// SPDX-License-Identifier: GPL-3.0
#include <gps_generic/private/cas_messages.h>
#include <gps_generic/private/init.h>
#include <gps_generic/private/interrupt.h>
#include <gps_generic/private/ublox.h>
#include <gps_generic/private/gps_response.h>
#include <tactility/check.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
@ -15,12 +15,12 @@
constexpr auto* TAG = "gps-meshtastic";
bool init_mtk(Device* uart);
bool init_mtk_l76b(Device* uart);
bool init_mtk_pa1616s(Device* uart);
bool init_atgm336h(Device* uart);
bool init_uc6580(Device* uart);
bool init_ag33xx(Device* uart);
bool init_mtk(Device* uart, const std::atomic<bool>& interrupted);
bool init_mtk_l76b(Device* uart, const std::atomic<bool>& interrupted);
bool init_mtk_pa1616s(Device* uart, const std::atomic<bool>& interrupted);
bool init_atgm336h(Device* uart, const std::atomic<bool>& interrupted);
bool init_uc6580(Device* uart, const std::atomic<bool>& interrupted);
bool init_ag33xx(Device* uart, const std::atomic<bool>& interrupted);
// 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);
}
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();
uint8_t buffer[CAS_MESSAGE_ACK_NACK_SIZE] = {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-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;
uart_controller_get_available(uart, &available);
if (available > 0) {
@ -134,38 +134,38 @@ static GpsResponse get_ack_cas(Device* uart, uint8_t class_id, uint8_t msg_id, u
// endregion
bool gps_init(Device* uart, GpsModel type) {
bool gps_init(Device* uart, GpsModel type, const std::atomic<bool>& interrupted) {
switch (type) {
case GPS_MODEL_UNKNOWN:
check(false);
case GPS_MODEL_AG3335:
case GPS_MODEL_AG3352:
return init_ag33xx(uart);
return init_ag33xx(uart, interrupted);
case GPS_MODEL_ATGM336H:
return init_atgm336h(uart);
return init_atgm336h(uart, interrupted);
case GPS_MODEL_LS20031:
return true;
case GPS_MODEL_MTK:
return init_mtk(uart);
return init_mtk(uart, interrupted);
case GPS_MODEL_MTK_L76B:
return init_mtk_l76b(uart);
return init_mtk_l76b(uart, interrupted);
case GPS_MODEL_MTK_PA1616S:
return init_mtk_pa1616s(uart);
return init_mtk_pa1616s(uart, interrupted);
case GPS_MODEL_UBLOX6:
case GPS_MODEL_UBLOX7:
case GPS_MODEL_UBLOX8:
case GPS_MODEL_UBLOX9:
case GPS_MODEL_UBLOX10:
return gps_ublox::init(uart, type);
return gps_ublox::init(uart, type, interrupted);
case GPS_MODEL_UC6580:
return init_uc6580(uart);
return init_uc6580(uart, interrupted);
}
LOG_I(TAG, "Init not implemented %d", static_cast<int>(type));
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
// 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,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
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
// use GPS L1 & L5 + BDS B1I & B2a + GLONASS L1 + GALILEO E1 & E5a + SBAS + QZSS
// This will reset the receiver, so wait a bit afterwards
// The paranoid will wait for the OK*04 confirmation response after each command.
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGSYS,h35155\r\n", 16, 250);
delay_millis(750);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
// Must be done after the CFGSYS command
// Turn off GSV messages, we don't really care about which and where the sats are, maybe someday.
uart_controller_write_bytes(uart, (const uint8_t*)"$CFGMSG,0,3,0\r\n", 15, 250);
delay_millis(250);
if (!gps_delay_millis(interrupted, 250)) {
return false;
}
// 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);
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.
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);
delay_millis(250);
if (!gps_delay_millis(interrupted, 250)) {
return false;
}
return true;
}
bool init_atgm336h(Device* uart) {
bool init_atgm336h(Device* uart, const std::atomic<bool>& interrupted) {
uint8_t buffer[256];
// 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);
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");
}
// Set the update frequence to 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);
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");
}
@ -229,60 +249,85 @@ bool init_atgm336h(Device* uart) {
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);
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]);
}
}
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 does not have GLONASS capability. PA1616D does, but is not implemented here.
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK353,1,0,0,0,0*2A\r\n", 23, 250);
// Above command will reset the GPS and takes longer before it will accept new commands
delay_millis(1000);
if (!gps_delay_millis(interrupted, 1000)) {
return false;
}
// 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);
delay_millis(250);
if (!gps_delay_millis(interrupted, 250)) {
return false;
}
// Enable SBAS / WAAS
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;
}
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
// Initialize the L76B Chip, use GPS + GLONASS
// See note in L76_Series_GNSS_Protocol_Specification, chapter 3.29
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK353,1,1,0,0,0*2B\r\n", 23, 250);
// Above command will reset the GPS and takes longer before it will accept new commands
delay_millis(1000);
if (!gps_delay_millis(interrupted, 1000)) {
return false;
}
// only ask for RMC and GGA (GNRMC and GNGGA)
// See note in L76_Series_GNSS_Protocol_Specification, chapter 2.1
uart_controller_write_bytes(uart, (const uint8_t*)"$PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0*28\r\n", 51, 250);
delay_millis(250);
if (!gps_delay_millis(interrupted, 250)) {
return false;
}
// Enable SBAS
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
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)
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;
}
bool init_mtk(Device* uart) {
bool init_mtk(Device* uart, const std::atomic<bool>& interrupted) {
// Initialize the L76K Chip, use GPS + GLONASS + BEIDOU
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
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
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;
}

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

View File

@ -1,5 +1,6 @@
// SPDX-License-Identifier: GPL-3.0
#include <gps_generic/private/gps_response.h>
#include <gps_generic/private/interrupt.h>
#include <gps_generic/private/probe.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);
}
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 b;
int bytes_read = 0;
uint32_t start_timeout = get_millis() + wait_millis;
while (get_millis() < start_timeout) {
while (get_millis() < start_timeout && !interrupted) {
size_t available = 0;
uart_controller_get_available(uart, &available);
if (available > 0) {
@ -58,18 +59,21 @@ static GpsResponse get_ack(Device* uart, const char* message, uint32_t wait_mill
return GpsResponse::None;
}
#define PROBE_SIMPLE(UART, CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \
#define PROBE_SIMPLE(UART, INTERRUPTED, CHIP, TOWRITE, RESPONSE, DRIVER, TIMEOUT, ...) \
do { \
if (INTERRUPTED) { \
return GPS_MODEL_UNKNOWN; \
} \
LOG_I(TAG, "Probing for %s (%s)", CHIP, TOWRITE); \
uart_controller_flush_input(UART); \
uart_controller_write_bytes(UART, (const uint8_t*)(TOWRITE "\r\n"), strlen(TOWRITE "\r\n"), TIMEOUT); \
if (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); \
return DRIVER; \
} \
} while (0)
GpsModel gps_probe(Device* uart) {
GpsModel gps_probe(Device* uart, const std::atomic<bool>& interrupted) {
// Close all NMEA sentences
// 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);
@ -82,12 +86,12 @@ GpsModel gps_probe(Device* uart) {
delay_millis(20);
// Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A
PROBE_SIMPLE(uart, "UC6580", "$PDTINFO", "UC6580", GpsModel::GPS_MODEL_UC6580, 500);
PROBE_SIMPLE(uart, "UM600", "$PDTINFO", "UM600", GpsModel::GPS_MODEL_UC6580, 500);
PROBE_SIMPLE(uart, "ATGM336H", "$PCAS06,1*1A", "$GPTXT,01,01,02,HW=ATGM336H", GpsModel::GPS_MODEL_ATGM336H, 500);
PROBE_SIMPLE(uart, interrupted, "UC6580", "$PDTINFO", "UC6580", GpsModel::GPS_MODEL_UC6580, 500);
PROBE_SIMPLE(uart, interrupted, "UM600", "$PDTINFO", "UM600", GpsModel::GPS_MODEL_UC6580, 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
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
// 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);
// Save configuration
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, "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, "AG3335", "$PAIR021*39", "$PAIR021,AG3335", GpsModel::GPS_MODEL_AG3335, 500);
PROBE_SIMPLE(uart, interrupted, "AG3352", "$PAIR021*39", "$PAIR021,AG3352", 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
// 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);
delay_millis(20);
PROBE_SIMPLE(uart, "L76B", "$PMTK605*31", "Quectel-L76B", GpsModel::GPS_MODEL_MTK_L76B, 500);
PROBE_SIMPLE(uart, "PA1616S", "$PMTK605*31", "1616S", GpsModel::GPS_MODEL_MTK_PA1616S, 500);
PROBE_SIMPLE(uart, interrupted, "L76B", "$PMTK605*31", "Quectel-L76B", GpsModel::GPS_MODEL_MTK_L76B, 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) {
return ublox_result;
} else {

View File

@ -1,14 +1,15 @@
// SPDX-License-Identifier: GPL-3.0
#include <gps_generic/private/ublox.h>
#include <gps_generic/private/gps_response.h>
#include <gps_generic/private/interrupt.h>
#include <gps_generic/private/ublox_messages.h>
#include <gps/gps.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
#include <tactility/memory.h>
#include <tactility/time.h>
#include <cstring>
@ -18,15 +19,18 @@ namespace gps_ublox {
constexpr auto* TAG = "ublox";
bool init_ublox_6(Device* uart);
bool init_ublox_789(Device* uart, GpsModel model);
bool init_ublox_10(Device* uart);
bool init_ublox_6(Device* uart, const std::atomic<bool>& interrupted);
bool init_ublox_789(Device* uart, GpsModel model, const std::atomic<bool>& interrupted);
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 { \
auto msglen = make_packet(TYPE, ID, DATA, sizeof(DATA), BUFFER); \
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); \
} \
} 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);
}
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 ack = 0;
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];
}
while (get_ticks() - start_time < wait_ticks) {
while (get_ticks() - start_time < wait_ticks && !interrupted) {
if (ack > 9) {
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
}
}
}
LOG_W(TAG, "No response for class %02X message %02X", class_id, msg_id);
if (!interrupted) {
LOG_W(TAG, "No response for class %02X message %02X", class_id, msg_id);
}
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;
TickType_t start_time = get_ticks();
TickType_t timeout_ticks = pdMS_TO_TICKS(timeout_millis);
uint16_t need_read = 0;
while ((get_ticks() - start_time) < timeout_ticks) {
while ((get_ticks() - start_time) < timeout_ticks && !interrupted) {
size_t available = 0;
uart_controller_get_available(uart, &available);
while (available > 0) {
@ -189,15 +195,88 @@ static int get_ack(Device* uart, uint8_t* buffer, uint16_t size, uint8_t request
return 0;
}
static struct UbloxGnssModelInfo {
struct UbloxGnssModelInfo {
char swVersion[30];
char hwVersion[10];
uint8_t extensionNo;
char extension[10][30];
uint8_t protocol_version;
} ublox_info;
};
GpsModel probe(Device* uart) {
// The protocol version of the last probed module, used by init
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;
for (char& i: ublox_info.swVersion) {
i = buffer[position];
position++;
}
for (char& i: ublox_info.hwVersion) {
i = buffer[position];
position++;
}
while (ack_response_len >= position + 30) {
for (int i = 0; i < 30; i++) {
ublox_info.extension[ublox_info.extensionNo][i] = buffer[position];
position++;
}
ublox_info.extensionNo++;
if (ublox_info.extensionNo > 9)
break;
}
LOG_I(TAG, "Module Info:");
LOG_I(TAG, "Soft version: %s", ublox_info.swVersion);
LOG_I(TAG, "Hard version: %s", ublox_info.hwVersion);
LOG_I(TAG, "Extensions: %u", ublox_info.extensionNo);
for (int i = 0; i < ublox_info.extensionNo; i++) {
LOG_I(TAG, " %s", ublox_info.extension[i]);
}
memset(buffer, 0, sizeof(buffer));
// tips: extensionNo field is 0 on some 6M GNSS modules
for (int i = 0; i < ublox_info.extensionNo; ++i) {
if (!strncmp(ublox_info.extension[i], "MOD=", 4)) {
strncpy((char*)buffer, &(ublox_info.extension[i][4]), sizeof(buffer));
} else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) {
char* ptr = nullptr;
memset(buffer, 0, sizeof(buffer));
strncpy((char*)buffer, &(ublox_info.extension[i][8]), sizeof(buffer));
LOG_I(TAG, "Protocol Version: %s", (char*)buffer);
if (strlen((char*)buffer)) {
ublox_info.protocol_version = strtoul((char*)buffer, &ptr, 10);
LOG_I(TAG, "ProtVer=%u", ublox_info.protocol_version);
} else {
ublox_info.protocol_version = 0;
}
}
}
#define DETECTED_MESSAGE "%s detected, using %s Module"
if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 6", "6");
return GPS_MODEL_UBLOX6;
} else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 7", "7");
return GPS_MODEL_UBLOX7;
} else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 8", "8");
return GPS_MODEL_UBLOX8;
} else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 9", "9");
return GPS_MODEL_UBLOX9;
} else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 10", "10");
return GPS_MODEL_UBLOX10;
}
return GPS_MODEL_UNKNOWN;
}
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};
@ -205,8 +284,10 @@ GpsModel probe(Device* uart) {
uart_controller_flush_input(uart);
uart_controller_write_bytes(uart, cfg_rate, sizeof(cfg_rate), 500 / portTICK_PERIOD_MS);
// Check that the returned response class and message ID are correct
GpsResponse response = get_ack(uart, 0x06, 0x08, 750);
if (response == GpsResponse::None) {
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) {
@ -226,138 +307,108 @@ GpsModel probe(Device* uart) {
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);
if (ack_response_len) {
uint16_t position = 0;
for (char& i: ublox_info.swVersion) {
i = buffer[position];
position++;
}
for (char& i: ublox_info.hwVersion) {
i = buffer[position];
position++;
}
while (ack_response_len >= position + 30) {
for (int i = 0; i < 30; i++) {
ublox_info.extension[ublox_info.extensionNo][i] = buffer[position];
position++;
}
ublox_info.extensionNo++;
if (ublox_info.extensionNo > 9)
break;
}
LOG_I(TAG, "Module Info:");
LOG_I(TAG, "Soft version: %s", ublox_info.swVersion);
LOG_I(TAG, "Hard version: %s", ublox_info.hwVersion);
LOG_I(TAG, "Extensions: %u", ublox_info.extensionNo);
for (int i = 0; i < ublox_info.extensionNo; i++) {
LOG_I(TAG, " %s", ublox_info.extension[i]);
}
memset(buffer, 0, sizeof(buffer));
// tips: extensionNo field is 0 on some 6M GNSS modules
for (int i = 0; i < ublox_info.extensionNo; ++i) {
if (!strncmp(ublox_info.extension[i], "MOD=", 4)) {
strncpy((char*)buffer, &(ublox_info.extension[i][4]), sizeof(buffer));
} else if (!strncmp(ublox_info.extension[i], "PROTVER", 7)) {
char* ptr = nullptr;
memset(buffer, 0, sizeof(buffer));
strncpy((char*)buffer, &(ublox_info.extension[i][8]), sizeof(buffer));
LOG_I(TAG, "Protocol Version: %s", (char*)buffer);
if (strlen((char*)buffer)) {
ublox_info.protocol_version = strtoul((char*)buffer, &ptr, 10);
LOG_I(TAG, "ProtVer=%u", ublox_info.protocol_version);
} else {
ublox_info.protocol_version = 0;
}
}
}
#define DETECTED_MESSAGE "%s detected, using %s Module"
if (strncmp(ublox_info.hwVersion, "00040007", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 6", "6");
return GPS_MODEL_UBLOX6;
} else if (strncmp(ublox_info.hwVersion, "00070000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 7", "7");
return GPS_MODEL_UBLOX7;
} else if (strncmp(ublox_info.hwVersion, "00080000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 8", "8");
return GPS_MODEL_UBLOX8;
} else if (strncmp(ublox_info.hwVersion, "00190000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 9", "9");
return GPS_MODEL_UBLOX9;
} else if (strncmp(ublox_info.hwVersion, "000A0000", 8) == 0) {
LOG_I(TAG, DETECTED_MESSAGE, "U-blox 10", "10");
return GPS_MODEL_UBLOX10;
}
uint16_t ack_response_len = get_ack(uart, interrupted, buffer, sizeof(buffer), 0x0A, 0x04, 1200);
if (ack_response_len == 0) {
return GPS_MODEL_UNKNOWN;
}
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) {
bool init(Device* uart, GpsModel model, const std::atomic<bool>& interrupted) {
LOG_I(TAG, "U-blox init");
switch (model) {
case GPS_MODEL_UBLOX6:
return init_ublox_6(uart);
return init_ublox_6(uart, interrupted);
case GPS_MODEL_UBLOX7:
case GPS_MODEL_UBLOX8:
case GPS_MODEL_UBLOX9:
return init_ublox_789(uart, model);
return init_ublox_789(uart, model, interrupted);
case GPS_MODEL_UBLOX10:
return init_ublox_10(uart);
return init_ublox_10(uart, interrupted);
default:
LOG_E(TAG, "Unknown or unsupported U-blox model");
return false;
}
}
bool init_ublox_10(Device* uart) {
bool init_ublox_10(Device* uart, const std::atomic<bool>& interrupted) {
uint8_t buffer[256];
delay_millis(1000);
if (!gps_delay_millis(interrupted, 1000)) {
return false;
}
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
delay_millis(750);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_RAM, "disable NMEA messages in M10 RAM", 300);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
delay_millis(750);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_NMEA_BBR, "disable NMEA messages in M10 BBR", 300);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
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);
delay_millis(750);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_RAM, "disable Info messages for M10 GPS RAM", 300);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
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);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ITFM_RAM, "enable jam detection M10 GPS RAM", 300);
delay_millis(750);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_ITFM_BBR, "enable jam detection M10 GPS BBR", 300);
delay_millis(750);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_TXT_INFO_BBR, "disable Info messages for M10 GPS BBR", 300);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_PM_RAM, "enable powersave for M10 GPS RAM", 300);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_PM_BBR, "enable powersave for M10 GPS BBR", 300);
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.
SEND_UBX_PACKET(uart, 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
SEND_UBX_PACKET(uart, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_SBAS_BBR, "disable SBAS M10 GPS BBR", 300);
delay_millis(750); // will cause a receiver restart so wait a bit
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_DISABLE_SBAS_RAM, "disable SBAS M10 GPS RAM", 300);
if (!gps_delay_millis(interrupted, 750)) { // will cause a receiver restart so wait a bit
return false;
}
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
// 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);
delay_millis(750);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_ENABLE_NMEA_BBR, "enable messages for M10 GPS BBR", 300);
if (!gps_delay_millis(interrupted, 750)) {
return false;
}
// 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);
delay_millis(750);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x8A, _message_VALSET_ENABLE_NMEA_RAM, "enable messages for M10 GPS RAM", 500);
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.
// BBR will survive a restart, and power off for a while, but modules with small backup
// batteries or super caps will not retain the config for a long power off time.
auto packet_size = make_packet(0x06, 0x09, _message_SAVE_10, sizeof(_message_SAVE_10), buffer);
uart_controller_write_bytes(uart, buffer, packet_size, 2000 / portTICK_PERIOD_MS);
if (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");
} else {
LOG_I(TAG, "GNSS module configuration saved!");
@ -365,7 +416,7 @@ bool init_ublox_10(Device* uart) {
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];
if (model == GpsModel::GPS_MODEL_UBLOX7) {
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);
}
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.
LOG_D(TAG, "reconfigure GNSS - defaults maintained. Is this module GPS-only?");
} 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
// 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);
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
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);
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
SEND_UBX_PACKET(uart, 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, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
}
// 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, 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, 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, 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, 0x08, _message_1HZ, "set GPS update rate", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_RMC, "enable NMEA RMC", 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);
SEND_UBX_PACKET(uart, 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, 0x86, _message_PMS, "enable powersave 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.
if (model == GpsModel::GPS_MODEL_UBLOX8) {
uart_controller_flush_input(uart);
SEND_UBX_PACKET(uart, buffer, 0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x17, _message_NMEA, "enable NMEA 4.10", 500);
}
} else {
SEND_UBX_PACKET(uart, 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, 0x11, _message_CFG_RXM_PSM, "enable powersave mode 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);
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");
} else {
LOG_I(TAG, "GNSS module configuration saved!");
@ -439,33 +496,33 @@ bool init_ublox_789(Device* uart, GpsModel model) {
return true;
}
bool init_ublox_6(Device* uart) {
bool init_ublox_6(Device* uart, const std::atomic<bool>& interrupted) {
uint8_t buffer[256];
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, 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, 0x02, _message_DISABLE_TXT_INFO, "disable text info messages", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x39, _message_JAM_6_7, "enable interference resistance", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x23, _message_NAVX5, "configure NAVX5 settings", 500);
// 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, 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, 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, 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, 0x08, _message_1HZ, "set GPS update rate", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GLL, "disable NMEA GLL", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSA, "enable NMEA GSA", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GSV, "disable NMEA GSV", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_VTG, "disable NMEA VTG", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_RMC, "enable NMEA RMC", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x01, _message_GGA, "enable NMEA GGA", 500);
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, 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, 0x11, _message_CFG_RXM_ECO, "enable powersave ECO mode for Neo-6", 500);
SEND_UBX_PACKET(uart, interrupted, buffer, 0x06, 0x3B, _message_CFG_PM2, "enable powersave details for GPS", 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);
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");
} else {
LOG_I(TAG, "GNSS module config saved!");

View File

@ -1,5 +1,30 @@
# 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
"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)
- 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
The Tactility logo copyrights are owned by Ken Van Hoeylandt.

View File

@ -4,9 +4,11 @@
#include <gps/gps.h>
#include <gps/gps_settings.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/delay.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <cstdio>
#include <cstring>
@ -14,6 +16,8 @@
#include <vector>
constexpr auto* TAG = "gps_ledger";
constexpr uint32_t DESTROY_RETRY_INTERVAL_MS = 10;
constexpr uint32_t DESTROY_TIMEOUT_MS = 10 * 1000;
struct GpsConfig {
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.
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) {
auto* parent = device_get_parent(device);
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.
// 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) {
if (device_is_ready(device)) {
device_stop(device);
const uint64_t deadline_us = get_micros_since_boot() + DESTROY_TIMEOUT_MS * 1000ULL;
while (get_micros_since_boot() < deadline_us) {
if (device_is_added(device) && device_stop(device) == ERROR_NONE) {
device_remove(device);
}
if (!device_is_added(device) && device_destruct(device) == ERROR_NONE) {
delete reinterpret_cast<GpsLedgerEntry*>(device);
return;
}
delay_millis(DESTROY_RETRY_INTERVAL_MS);
}
device_remove(device);
device_destruct(device);
delete reinterpret_cast<GpsLedgerEntry*>(device);
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) {
@ -136,6 +170,7 @@ static std::vector<Device*> collect_owned_devices() {
void gps_ledger_sync() {
mutex_lock(&ledger_mutex.mutex);
destroy_pending_devices();
std::vector<GpsConfiguration> configurations;
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() {
mutex_lock(&ledger_mutex.mutex);
destroy_pending_devices();
for (auto* device : collect_owned_devices()) {
destroy_device(device);
}
@ -184,6 +220,7 @@ void gps_ledger_clear() {
void gps_ledger_remove_children_of(Device* parent) {
mutex_lock(&ledger_mutex.mutex);
destroy_pending_devices();
for (auto* device : collect_owned_devices()) {
if (device_get_parent(device) == parent) {
destroy_device(device);

View 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}
)

View File

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

View File

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

View 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

View 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

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

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

View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <service/manifest.h>
extern const ServiceManifest location_service_manifest;

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

View 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,
};

View 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,
};

View 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(&copy);
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(&copy);
}
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,
};

View 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);
}

View 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,
};
}

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

View 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));
}

View 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));
}

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

View 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);
}

View File

@ -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));
/**
* @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_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 */
bool wifi_settings_get_enable_on_boot(void);
/** @return true when Wi-Fi is on, see wifi_settings_set_enable() */
bool wifi_settings_get_enable(void);
#ifdef __cplusplus
}

View File

@ -160,7 +160,7 @@ static void on_wifi_event(WifiAutoConnect* data, const WifiEvent& event) {
static void on_boot_completed(WifiAutoConnect* data) {
wifi_provisioning_import();
if (wifi_settings_get_enable_on_boot()) {
if (wifi_settings_get_enable()) {
LOG_I(TAG, "Auto-enabling WiFi");
if (wifi_set_radio_on(data->device) != ERROR_NONE) {
LOG_E(TAG, "Failed to enable WiFi radio");

View File

@ -156,6 +156,10 @@ static int command_radio(Device* device, int argc, char* argv[]) {
if (error != ERROR_NONE) {
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");
return 0;
}

View File

@ -281,7 +281,7 @@ void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, voi
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_cached = true;
@ -306,7 +306,7 @@ error_t wifi_settings_set_enable_on_boot(bool enable) {
return properties_file_close(file);
}
bool wifi_settings_get_enable_on_boot(void) {
bool wifi_settings_get_enable(void) {
if (enable_on_boot_cached) {
return enable_on_boot;
}

View File

@ -1,6 +1,9 @@
// SPDX-License-Identifier: Apache-2.0
#include "freertos_task_hooks.h"
#include <pthread.h>
#include <signal.h>
static FreeRtosTaskCreateHook create_hook = NULL;
static FreeRtosTaskDeleteHook delete_hook = NULL;
@ -23,3 +26,12 @@ void vTaskDelete(TaskHandle_t 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);
}

View File

@ -25,6 +25,14 @@ BaseType_t freertos_real_xTaskCreate(TaskFunction_t function, const char* name,
/** FreeRTOS-Kernel's own vTaskDelete() */
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
}
#endif

View File

@ -42,6 +42,7 @@ list(APPEND REQUIRES_LIST
gps-module
graphics-module
http-module
location-module
mbedtls-module
mesh-module
mesh-meshcore-module

View File

@ -1,7 +0,0 @@
#pragma once
namespace tt::app::gpssettings {
void start();
}

View File

@ -2,8 +2,8 @@
namespace tt::bluetooth::settings {
void setEnableOnBoot(bool enable);
bool shouldEnableOnBoot();
void setEnable(bool enable);
bool isEnabled();
void setSppAutoStart(bool enable);
bool shouldSppAutoStart();

View File

@ -17,12 +17,10 @@ class View final {
void* context = nullptr;
lv_obj_t* root = nullptr;
lv_obj_t* enable_switch = nullptr;
lv_obj_t* enable_on_boot_switch = nullptr;
lv_obj_t* scanning_spinner = nullptr;
lv_obj_t* peers_list = nullptr;
void updateBtToggle();
void updateEnableOnBootToggle();
void updateScanning();
void updatePeerList();

View File

@ -15,14 +15,12 @@ class View final {
uint32_t appInstanceId = 0;
lv_obj_t* root = nullptr;
lv_obj_t* enable_switch = nullptr;
lv_obj_t* enable_on_boot_switch = nullptr;
lv_obj_t* scanning_spinner = nullptr;
lv_obj_t* refresh_button = nullptr;
lv_obj_t* networks_list = nullptr;
lv_obj_t* connect_to_hidden = nullptr;
void updateWifiToggle();
void updateEnableOnBootToggle();
void updateScanning();
void updateRefreshButton();
void updateNetworkList();

View File

@ -48,7 +48,6 @@ namespace app {
namespace display { extern const ::AppManifest manifest; }
namespace files { extern const ::AppManifest manifest; }
namespace fileselection { extern const ::AppManifest manifest; }
namespace gpssettings { extern const ::AppManifest manifest; }
namespace grovesettings { extern const ::AppManifest manifest; }
namespace i2cscanner { extern const ::AppManifest manifest; }
namespace imageviewer { extern const ::AppManifest manifest; }
@ -56,6 +55,7 @@ namespace app {
namespace launcher { extern const ::AppManifest manifest; }
namespace ledstripsettings { extern const ::AppManifest manifest; }
namespace localesettings { extern const ::AppManifest manifest; }
namespace locationsettings { extern const ::AppManifest manifest; }
namespace notes { extern const ::AppManifest manifest; }
namespace power { 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::launcher::manifest);
app_manager_add(&app::localesettings::manifest);
app_manager_add(&app::locationsettings::manifest);
app_manager_add(&app::notes::manifest);
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {
app_manager_add(&app::poweroff::manifest);
@ -193,7 +194,6 @@ static void registerInternalApps() {
if (enable_gps) {
app_manager_add(&app::addgps::manifest);
app_manager_add(&app::gpssettings::manifest);
}
if (device_exists_of_type(&POWER_SUPPLY_TYPE)) {

View File

@ -8,6 +8,7 @@
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/settings/RadioSettings.h>
#include <location/module.h>
#include <app_hub/module.h>
#include <wifi/module.h>
#if TT_FEATURE_MESH
@ -50,6 +51,7 @@ void registerAndStartServices() {
addService(service::audio::manifest);
}
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);
// Before the mesh service starts, as it needs the region to turn on the radio
settings::radio::applyRegion();

View File

@ -2,6 +2,8 @@
#include <Tactility/app/btmanage/BtManagePrivate.h>
#include <Tactility/app/btmanage/View.h>
#include <Tactility/bluetooth/BluetoothSettings.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
@ -24,6 +26,10 @@ extern const ::AppManifest manifest;
static void onBtToggled(void* /*context*/, bool requestOn) {
#if defined(CONFIG_BT_NIMBLE_ENABLED)
// Dispatched, so file IO doesn't block the UI
getMainDispatcher().dispatch([requestOn] {
bluetooth::settings::setEnable(requestOn);
});
Device* dev;
if (device_get_first_by_type(&BLUETOOTH_TYPE, &dev) == ERROR_NONE) {
bool radio_on = bluetooth::isRadioOnOrPending(dev);

View File

@ -30,28 +30,6 @@ static void onEnableSwitchChanged(lv_event_t* event) {
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) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
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() {
if (state->getRadioState() == bluetooth::RadioState::On && state->isScanning()) {
lv_obj_set_hidden(scanning_spinner, false);
@ -184,27 +151,6 @@ void View::updateScanning() {
void View::updatePeerList() {
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;
if (state->getRadioState() == On) {
// Paired peers section

View File

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

View File

@ -23,11 +23,11 @@ constexpr IconEntry ICONS[] = {
{"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH},
{"tactility.development", LVGL_ICON_SHARED_LOGO_DEV},
{"tactility.display", LVGL_ICON_SHARED_DISPLAY_SETTINGS},
{"tactility.gpssettings", LVGL_ICON_SHARED_NAVIGATION},
{"tactility.grovesettings", LVGL_ICON_SHARED_CABLE},
{"tactility.keyboardsettings", LVGL_ICON_SHARED_KEYBOARD_ALT},
{"tactility.ledstripsettings", LVGL_ICON_SHARED_LIGHTSTRIP},
{"tactility.localesettings", LVGL_ICON_SHARED_LANGUAGE},
{"tactility.locationsettings", LVGL_ICON_SHARED_LOCATION_ON},
{"tactility.meshsettings", LVGL_ICON_SHARED_HUB},
{"tactility.power", LVGL_ICON_SHARED_POWER_SETTINGS_NEW},
{"tactility.timedatesettings", LVGL_ICON_SHARED_CALENDAR_MONTH},

View 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 = {}
};
}

View File

@ -16,6 +16,10 @@
#include <app/scheduler.h>
#include <app/start.h>
#include <location/location.h>
#include <wifi/wifi_settings.h>
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
@ -230,7 +234,9 @@ int32_t appMain(int argc, char* argv[]) {
getMainDispatcher().dispatch([] {
Device* wifi_device = nullptr;
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);
}
});
@ -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 {};
task_event_group_construct(&event_group);

View File

@ -59,6 +59,9 @@ void connectToAp(const WifiApSettings& ap) {
}
error_t result = wifi_set_radio_on(wifi_device);
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);
}
if (result != ERROR_NONE) {

View File

@ -186,6 +186,9 @@ void onConnectPressed(lv_event_t* event) {
}
error_t result = wifi_set_radio_on(wifi_device);
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);
}
if (result != ERROR_NONE) {

View File

@ -46,24 +46,6 @@ static void onEnableSwitchChanged(lv_event_t* event) {
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) {
auto* bindings = (Bindings*)lv_event_get_user_data(event);
bindings->onConnectToHidden();
@ -167,27 +149,6 @@ void View::updateConnectToHidden() {
void View::updateNetworkList() {
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()) {
case WIFI_RADIO_STATE_ON_PENDING:
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
// region Main
@ -349,7 +299,6 @@ void View::update(bool rebuildList) {
void View::reset() {
root = nullptr;
enable_switch = nullptr;
enable_on_boot_switch = nullptr;
scanning_spinner = nullptr;
refresh_button = nullptr;
networks_list = nullptr;

View File

@ -61,6 +61,9 @@ static void connectToAp(const WifiApSettings& ap) {
}
error_t result = wifi_set_radio_on(wifi_device);
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);
}
if (result != ERROR_NONE) {
@ -114,7 +117,11 @@ static void onWifiToggled(bool enabled) {
return;
}
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));
}
device_put(wifi_device);

View File

@ -320,7 +320,7 @@ void systemStart() {
return;
}
if (settings::shouldEnableOnBoot()) {
if (settings::isEnabled()) {
start(dev);
}
}

View File

@ -79,7 +79,7 @@ static BluetoothSettings getCachedOrLoad() {
return result;
}
void setEnableOnBoot(bool enable) {
void setEnable(bool enable) {
settings_mutex.lock();
cached.enableOnBoot = enable;
cached_valid = true;
@ -87,7 +87,7 @@ void setEnableOnBoot(bool enable) {
if (!save(cached)) LOG_E(TAG, "Failed to save");
}
bool shouldEnableOnBoot() {
bool isEnabled() {
return getCachedOrLoad().enableOnBoot;
}

View File

@ -2,12 +2,15 @@
#include <Tactility/TactilityPrivate.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/bluetooth/BluetoothSettings.h>
#include <Tactility/settings/RadioSettings.h>
#include <app/manager.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/fonts.h>
@ -55,10 +58,6 @@ static bool hasWifi() {
return device_exists_of_type(&WIFI_TYPE);
}
static bool hasGps() {
return device_exists_of_type(&GPS_TYPE);
}
static bool hasBluetooth() {
#if defined(CONFIG_BT_NIMBLE_ENABLED)
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) {
return;
}
error_t result = fromContext(context) ? wifi_set_radio_on(device) : wifi_set_radio_off(device);
if (result != ERROR_NONE) {
const bool enabled = fromContext(context);
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));
}
device_put(device);
}
static bool isGpsEnabled() {
bool enabled = false;
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 bool isLocationEnabled() {
return location_is_enabled();
}
static void setGpsEnabled(void* context) {
device_for_each_of_type(&GPS_TYPE, context, [](Device* device, void* context) {
const bool enabled = fromContext(context);
if (enabled && !device_is_ready(device)) {
device_start(device);
} else if (!enabled && device_is_ready(device)) {
device_stop(device);
}
return true;
});
static void setLocationEnabled(void* context) {
const error_t result = location_set_enabled(fromContext(context));
if (result != ERROR_NONE) {
LOG_E(TAG, "Failed to set location services state (%s)", error_to_string(result));
}
}
#if defined(CONFIG_BT_NIMBLE_ENABLED)
@ -144,6 +135,7 @@ static void setBluetoothEnabled(void* context) {
return;
}
const bool enabled = fromContext(context);
bluetooth::settings::setEnable(enabled);
if (enabled != bluetooth::isRadioOnOrPending(device)) {
if (enabled) {
bluetooth::start(device);
@ -290,10 +282,8 @@ static void createWidgets(lv_obj_t* parent, void* /*userData*/) {
if (hasWifi()) {
createOption(grid, &wifi_option);
}
static constexpr ToggleOption gps_option = { LVGL_ICON_SHARED_LOCATION_ON, LVGL_ICON_SHARED_LOCATION_OFF, isGpsEnabled, setGpsEnabled };
if (hasGps()) {
createOption(grid, &gps_option);
}
static constexpr ToggleOption location_option = { LVGL_ICON_SHARED_LOCATION_ON, LVGL_ICON_SHARED_LOCATION_OFF, isLocationEnabled, setLocationEnabled };
createOption(grid, &location_option);
#if defined(CONFIG_BT_NIMBLE_ENABLED)
static constexpr ToggleOption bluetooth_option = { LVGL_ICON_SHARED_BLUETOOTH, LVGL_ICON_SHARED_BLUETOOTH_DISABLED, isBluetoothEnabled, setBluetoothEnabled };
if (hasBluetooth()) {
@ -324,9 +314,6 @@ static void createWidgets(lv_obj_t* parent, void* /*userData*/) {
}
void systemMenuShow() {
if (!hasWifi() && !hasGps() && !hasBluetooth() && !hasMesh() && !supportsPowerOff()) {
return;
}
window_manager_overlay_show(createWidgets, nullptr, nullptr);
}

View File

@ -30,7 +30,7 @@
#include <set>
#include <string>
#include <gps/gps.h>
#include <location/location.h>
#if TT_FEATURE_MESH
#include <mesh/mesh.h>
@ -183,8 +183,8 @@ class StatusbarService final : public Service {
}
void updateGpsIcon() {
// Only shown while a GPS device is on
const char* icon = device_has_active_by_type(&GPS_TYPE) ? LVGL_ICON_STATUSBAR_LOCATION_ON : nullptr;
// Only shown while location is being used
const char* icon = location_is_in_use() ? LVGL_ICON_STATUSBAR_LOCATION_ON : nullptr;
if (gps_last_icon != icon) {
if (icon != nullptr) {
lvgl_statusbar_icon_set_image(gps_icon_id, icon);

View File

@ -52,6 +52,7 @@ target_link_libraries(TactilityTests PRIVATE
gps-module
graphics-module
http-module
location-module
mbedtls-module
posix-symbols-module
pthread-module

View File

@ -87,6 +87,16 @@ void thread_set_name(Thread* thread, const char* name);
*/
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.
* @param[in] thread The thread instance.

View File

@ -75,7 +75,7 @@ error_t device_construct(struct Device* device);
* This fails when a device is busy or has children.
*
* @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_NONE on success
*/
@ -87,7 +87,7 @@ error_t device_destruct(struct Device* device);
* - its parent (if any)
*
* @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
*/
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:
* - the global ledger
* - 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
* @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_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's lock for its entire body, so doing so would deadlock.
* @param[in,out] device non-null device pointer
* @retval ERROR_INVALID_STATE if the device is not started
* @retval ERROR_RESOURCE_BUSY if the device has outstanding device_get() references
* @retval ERROR_INVALID_STATE if the device is not added
* @retval ERROR_RESOURCE_BUSY if the device is being stopped by another task
* @retval ERROR_RESOURCE when driver unbinding fails
* @retval ERROR_NONE on success
*/
@ -260,18 +261,19 @@ void device_unlock(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
* device_put(). Named after the Linux kernel's get_device()/put_device(). Intended to bracket
* a single hardware operation (one API call or a short sequence of them), not to be held across
* a blocking wait or unrelated later work - device_stop() fails with ERROR_RESOURCE_BUSY while
* any reference is outstanding.
* 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(). The reference doesn't depend
* on the device's state: the device can be started, stopped and removed while it's held, and only
* device_destruct() fails with ERROR_RESOURCE_BUSY while a 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
* 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
* might race a concurrent teardown of a dynamically-allocated Device, use one of
* 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
*/
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);
/**
* 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[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_INVALID_STATE if found but not started (no reference taken)
* @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);

View File

@ -28,6 +28,9 @@ struct Thread {
ThreadPriority priority = THREAD_PRIORITY_NORMAL;
Mutex mutex = { 0 };
configSTACK_DEPTH_TYPE stackDepth = 4096;
bool stackExternal = false;
// Whether the task was created with xTaskCreatePinnedToCoreWithCaps()
bool createdWithCaps = false;
portBASE_TYPE affinity = -1;
Thread() {
@ -77,10 +80,17 @@ static void thread_main_body(void* context) {
vTaskSetThreadLocalStoragePointer(nullptr, LOCAL_STORAGE_SELF_POINTER_INDEX, nullptr);
// The thread can be freed once taskHandle is cleared
thread->lock();
const bool createdWithCaps = thread->createdWithCaps;
thread->taskHandle = nullptr;
thread->unlock();
#ifdef ESP_PLATFORM
if (createdWithCaps) {
vTaskDeleteWithCaps(nullptr);
}
#endif
vTaskDelete(nullptr);
}
@ -135,6 +145,13 @@ void thread_set_stack_size(Thread* thread, size_t stack_size) {
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) {
thread->lock();
check(thread->state == THREAD_STATE_STOPPED);
@ -187,10 +204,33 @@ error_t thread_start(Thread* thread) {
thread->lock();
auto priority = static_cast<UBaseType_t>(thread->priority);
portBASE_TYPE affinity = thread->affinity;
bool stackExternal = thread->stackExternal;
thread->createdWithCaps = false;
thread->unlock();
BaseType_t result;
if (affinity != -1) {
BaseType_t result = pdFAIL;
#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
result = xTaskCreatePinnedToCore(
thread_main_body,

View File

@ -45,6 +45,9 @@ struct DeviceInternal {
// also checked by device_get() so no new ref can be acquired while a stop is in flight.
bool starting : 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;
/** Attached child devices */
std::vector<Device*> children {};
@ -107,7 +110,7 @@ error_t device_destruct(Device* device) {
unlock_internal(device->internal);
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);
return ERROR_INVALID_STATE;
}
@ -146,8 +149,10 @@ static void device_remove_child(Device* device, Device* child) {
error_t device_add(Device* device) {
LOG_D(TAG, "add %s", device->name);
// Already added
if (device->internal->state.started || device->internal->state.added) {
lock_internal(device->internal);
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;
}
@ -168,10 +173,18 @@ error_t device_add(Device* device) {
error_t device_remove(Device* device) {
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;
}
internal->state.added = false;
internal->state.removing = true;
unlock_internal(internal);
// Remove self from parent's children list
auto* parent = device->parent;
@ -188,7 +201,9 @@ error_t device_remove(Device* device) {
ledger.devices.erase(iterator);
ledger_unlock();
device->internal->state.added = false;
lock_internal(internal);
internal->state.removing = false;
unlock_internal(internal);
return ERROR_NONE;
failed_ledger_lookup:
@ -198,6 +213,10 @@ failed_ledger_lookup:
device_add_child(parent, device);
}
lock_internal(internal);
internal->state.added = true;
internal->state.removing = false;
unlock_internal(internal);
return ERROR_NOT_FOUND;
}

View File

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

View File

@ -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/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/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-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)
@ -34,6 +35,7 @@ add_dependencies(build-tests HttpModuleTests)
add_dependencies(build-tests BinFontModuleTests)
add_dependencies(build-tests LodePngModuleTests)
add_dependencies(build-tests LvglModuleTests)
add_dependencies(build-tests LocationModuleTests)
add_dependencies(build-tests MeshModuleTests)
add_dependencies(build-tests MeshMeshCoreModuleTests)
add_dependencies(build-tests MeshMeshtasticModuleTests)