mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Compare commits
2 Commits
3a7db7a801
...
a65a44de6b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a65a44de6b | ||
|
|
6e437d3288 |
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
general.vendor=LilyGO
|
||||
general.name=T-Display
|
||||
general.incubating=true
|
||||
general.incubating=false
|
||||
apps.autoStartAppId=tactility.apwebserver
|
||||
|
||||
hardware.target=ESP32
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
general.vendor=M5Stack
|
||||
general.name=PaperS3
|
||||
general.incubating=true
|
||||
general.incubating=false
|
||||
|
||||
hardware.target=esp32s3
|
||||
hardware.flashSize=16MB
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
/**
|
||||
* Delays unless the GPS thread is interrupted.
|
||||
* @return false when interrupted
|
||||
*/
|
||||
bool gps_delay_millis(const std::atomic<bool>& interrupted, uint32_t millis);
|
||||
@ -3,10 +3,12 @@
|
||||
|
||||
#include <gps/gps.h>
|
||||
|
||||
#include <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);
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
20
Drivers/gps-meshtastic-module/source/interrupt.cpp
Normal file
20
Drivers/gps-meshtastic-module/source/interrupt.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#include <gps_generic/private/interrupt.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
constexpr uint32_t DELAY_SLICE_MILLIS = 50;
|
||||
|
||||
bool gps_delay_millis(const std::atomic<bool>& interrupted, uint32_t millis) {
|
||||
while (millis > 0) {
|
||||
if (interrupted) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t slice = std::min(millis, DELAY_SLICE_MILLIS);
|
||||
delay_millis(slice);
|
||||
millis -= slice;
|
||||
}
|
||||
return !interrupted;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
#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 {
|
||||
|
||||
@ -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!");
|
||||
|
||||
45
LICENSE.md
45
LICENSE.md
@ -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.
|
||||
|
||||
@ -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);
|
||||
|
||||
24
Modules/location-module/CMakeLists.txt
Normal file
24
Modules/location-module/CMakeLists.txt
Normal file
@ -0,0 +1,24 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
list(APPEND REQUIRES_LIST
|
||||
TactilityKernel
|
||||
TactilityKernelCpp
|
||||
service-module
|
||||
gps-module
|
||||
minmea
|
||||
)
|
||||
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
list(APPEND REQUIRES_LIST lwip)
|
||||
endif ()
|
||||
|
||||
tactility_add_module(location-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES ${REQUIRES_LIST}
|
||||
)
|
||||
195
Modules/location-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/location-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
120
Modules/location-module/include/location/location.h
Normal file
120
Modules/location-module/include/location/location.h
Normal file
@ -0,0 +1,120 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Location service. Location providers (e.g. GPS) report fixes to the service.
|
||||
* The most recent fix of a regular provider is used while it's less than 2 minutes old.
|
||||
* Otherwise, the most recent fix of a fallback provider (e.g. IP geolocation) is used, if there is one.
|
||||
* Providers only run while there is at least one subscription.
|
||||
*/
|
||||
|
||||
#define LOCATION_PROVIDER_ID_MAX_LENGTH 15
|
||||
|
||||
/** Horizontal accuracy in meters of a fix with LOCATION_PRECISION_APPROXIMATE */
|
||||
#define LOCATION_APPROXIMATE_ACCURACY_METERS 3000.0f
|
||||
|
||||
enum LocationPrecision {
|
||||
/** Providers only report fixes with a horizontal accuracy of LOCATION_APPROXIMATE_ACCURACY_METERS or worse */
|
||||
LOCATION_PRECISION_APPROXIMATE,
|
||||
/** Providers report fixes as accurate as they can */
|
||||
LOCATION_PRECISION_PRECISE,
|
||||
};
|
||||
|
||||
/**
|
||||
* A location fix. Values the provider doesn't know are NAN for floating point fields and 0 for the others.
|
||||
* Accuracies are the radius of 68% confidence, in the unit of the value they belong to.
|
||||
*/
|
||||
struct Location {
|
||||
/** Degrees in WGS84, positive is north */
|
||||
double latitude;
|
||||
/** Degrees in WGS84, positive is east */
|
||||
double longitude;
|
||||
/** Meters */
|
||||
float horizontal_accuracy;
|
||||
/** Meters above mean sea level */
|
||||
float altitude;
|
||||
/** Meters above the WGS84 ellipsoid */
|
||||
float ellipsoidal_altitude;
|
||||
/** Meters */
|
||||
float vertical_accuracy;
|
||||
/** Speed over ground in meters per second */
|
||||
float speed;
|
||||
/** Meters per second */
|
||||
float speed_accuracy;
|
||||
/** Direction of travel in degrees clockwise from true north, in range [0, 360) */
|
||||
float course;
|
||||
/** Degrees */
|
||||
float course_accuracy;
|
||||
/** Number of satellites used for the fix */
|
||||
uint8_t satellites;
|
||||
/** Time of the fix according to the provider, in milliseconds since the Unix epoch */
|
||||
int64_t time_ms;
|
||||
/** Milliseconds since boot when the service received the fix */
|
||||
uint64_t uptime_ms;
|
||||
/** Id of the provider that reported the fix */
|
||||
char provider[LOCATION_PROVIDER_ID_MAX_LENGTH + 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow or disallow the use of location services. The service only runs while allowed. The choice is persisted.
|
||||
* Location services are disallowed by default.
|
||||
*/
|
||||
error_t location_set_enabled(bool enabled);
|
||||
|
||||
bool location_is_enabled(void);
|
||||
|
||||
/** Set the precision that providers should report. The choice is persisted. */
|
||||
error_t location_set_precision(enum LocationPrecision precision);
|
||||
|
||||
enum LocationPrecision location_get_precision(void);
|
||||
|
||||
/**
|
||||
* Get the current fix. Without subscriptions, this is the last known fix.
|
||||
* @param[out] location the fix
|
||||
* @retval ERROR_NOT_FOUND when no fix was reported since the service started, or location services are disabled
|
||||
*/
|
||||
error_t location_get(struct Location* location);
|
||||
|
||||
/**
|
||||
* Caller-owned subscription: the service signals @a bit on the event group whenever a new current fix is reported.
|
||||
* Read the fix with location_get().
|
||||
* @warning Fields other than `bit` are for internal use only.
|
||||
*/
|
||||
struct LocationSubscription {
|
||||
uint32_t bit;
|
||||
|
||||
struct {
|
||||
struct TaskEventGroup* event_group;
|
||||
struct LocationSubscription* next;
|
||||
} internal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribing starts the providers when location services are enabled. They stop when the last subscription is removed.
|
||||
* Providers start and stop in the background: subscribing and unsubscribing don't wait for them.
|
||||
* @param[in,out] sub subscription to register, must stay alive until unsubscribed
|
||||
* @param[in] event_group caller-owned group to wait on, must outlive @a sub
|
||||
* @retval ERROR_RESOURCE @a event_group has no free bits left
|
||||
* @retval ERROR_INVALID_STATE @a sub is already registered
|
||||
*/
|
||||
error_t location_subscribe(struct LocationSubscription* sub, struct TaskEventGroup* event_group);
|
||||
|
||||
/** @retval ERROR_NOT_FOUND when @a sub is not registered */
|
||||
error_t location_unsubscribe(struct LocationSubscription* sub);
|
||||
|
||||
/** @return true when location services are enabled and there is at least one subscription */
|
||||
bool location_is_in_use(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
14
Modules/location-module/include/location/module.h
Normal file
14
Modules/location-module/include/location/module.h
Normal file
@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module location_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
79
Modules/location-module/include/location/provider.h
Normal file
79
Modules/location-module/include/location/provider.h
Normal file
@ -0,0 +1,79 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#include <tactility/freertos/freertos.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Interface between the location service and a location provider. Not part of the app SDK.
|
||||
*/
|
||||
|
||||
/** The service, as seen by a provider */
|
||||
struct LocationHost;
|
||||
|
||||
struct LocationProviderApi {
|
||||
/** Unique id, at most LOCATION_PROVIDER_ID_MAX_LENGTH characters */
|
||||
const char* id;
|
||||
|
||||
/**
|
||||
* A fallback provider (e.g. IP geolocation) is only used while no other provider reported a fix in the last
|
||||
* 2 minutes. It should only work when location_host_needs_fallback() returns true.
|
||||
*/
|
||||
bool fallback;
|
||||
|
||||
/**
|
||||
* Start the provider. Called on the service's worker task while the service runs and has subscriptions.
|
||||
* It must not block: the work happens on the provider's own task.
|
||||
* @return the provider state, NULL on failure
|
||||
*/
|
||||
void* (*create)(struct LocationHost* host);
|
||||
|
||||
/** Ask the provider to stop. It must not block. */
|
||||
void (*request_stop)(void* provider);
|
||||
|
||||
/**
|
||||
* Wait at most @a timeout for the provider to stop and release its resources.
|
||||
* It must not report anymore once this returned true. The provider is only created again after that.
|
||||
* @return true when stopped
|
||||
*/
|
||||
bool (*wait_stopped)(void* provider, TickType_t timeout);
|
||||
};
|
||||
|
||||
/** Maximum number of registered providers */
|
||||
#define LOCATION_PROVIDER_MAX_COUNT 4
|
||||
|
||||
/**
|
||||
* Register a provider. Usually called from the provider module's start().
|
||||
* @retval ERROR_ALREADY_EXISTS when a provider with the same id is registered
|
||||
* @retval ERROR_RESOURCE when LOCATION_PROVIDER_MAX_COUNT providers are registered
|
||||
*/
|
||||
error_t location_provider_register(const struct LocationProviderApi* api);
|
||||
|
||||
/** @retval ERROR_NOT_FOUND when @a api is not registered */
|
||||
error_t location_provider_unregister(const struct LocationProviderApi* api);
|
||||
|
||||
/**
|
||||
* Report a new fix. Can be called from any task.
|
||||
* The service sets the `provider` and `uptime_ms` fields.
|
||||
*/
|
||||
void location_host_report(struct LocationHost* host, const struct Location* location);
|
||||
|
||||
/**
|
||||
* Reduce a fix to LOCATION_PRECISION_APPROXIMATE: the position snaps to a grid and only position and time remain.
|
||||
* Providers that can report precise fixes apply this when location_get_precision() is LOCATION_PRECISION_APPROXIMATE.
|
||||
*/
|
||||
void location_make_approximate(struct Location* location);
|
||||
|
||||
/** @return true when no provider that isn't a fallback reported a fix in the last 2 minutes */
|
||||
bool location_host_needs_fallback(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
17
Modules/location-module/private/location/gps_provider.h
Normal file
17
Modules/location-module/private/location/gps_provider.h
Normal file
@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/location.h>
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <minmea.h>
|
||||
|
||||
/** Uses the first started GPS device */
|
||||
extern const LocationProviderApi gps_location_provider;
|
||||
|
||||
/**
|
||||
* Convert the sentences of one GPS epoch into a fix.
|
||||
* @param[in] gga the GGA sentence of the same epoch, NULL when there is none
|
||||
* @return false when @a rmc has no valid fix
|
||||
*/
|
||||
bool gps_location_from_nmea(const minmea_sentence_rmc* rmc, const minmea_sentence_gga* gga, Location* location);
|
||||
41
Modules/location-module/private/location/ip_provider.h
Normal file
41
Modules/location-module/private/location/ip_provider.h
Normal file
@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* Fallback provider that looks up the location of the public IP address with ip-api.com.
|
||||
* The result is reused while the network (local IP address and gateway) is unchanged.
|
||||
*/
|
||||
extern const LocationProviderApi ip_location_provider;
|
||||
|
||||
/** Start tracking the network that the device is connected to, also while the provider doesn't run */
|
||||
error_t ip_provider_track_network(void);
|
||||
|
||||
/** Stop tracking the network and release the cached result. The provider must not run. */
|
||||
void ip_provider_untrack_network(void);
|
||||
|
||||
#define IP_API_MESSAGE_MAX_LENGTH 63
|
||||
|
||||
struct IpApiResponse {
|
||||
/** HTTP status code */
|
||||
int status_code;
|
||||
/** Requests left in the rate limit window, -1 when unknown */
|
||||
int rate_remaining;
|
||||
/** Seconds until the rate limit window resets, -1 when unknown */
|
||||
int rate_reset_seconds;
|
||||
bool success;
|
||||
/** Reason of failure, empty on success */
|
||||
char message[IP_API_MESSAGE_MAX_LENGTH + 1];
|
||||
double latitude;
|
||||
double longitude;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an HTTP response of the newline separated ip-api endpoint, requested with fields "status,message,lat,lon"
|
||||
* @param[in] response the NUL-terminated response, including the status line and headers
|
||||
* @return false when the response is malformed
|
||||
*/
|
||||
bool ip_api_parse_response(const char* response, IpApiResponse* out);
|
||||
@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <service/manifest.h>
|
||||
|
||||
extern const ServiceManifest location_service_manifest;
|
||||
16
Modules/location-module/private/location/location_settings.h
Normal file
16
Modules/location-module/private/location/location_settings.h
Normal file
@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#define LOCATION_SERVICE_ID "tactility.location"
|
||||
|
||||
struct LocationSettings {
|
||||
bool enabled;
|
||||
enum LocationPrecision precision;
|
||||
};
|
||||
|
||||
/** Loads the settings, or defaults when there are none */
|
||||
void location_settings_load(struct LocationSettings* settings);
|
||||
|
||||
error_t location_settings_save(const struct LocationSettings* settings);
|
||||
337
Modules/location-module/source/gps_provider.cpp
Normal file
337
Modules/location-module/source/gps_provider.cpp
Normal file
@ -0,0 +1,337 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/gps_provider.h>
|
||||
|
||||
#include <gps/gps.h>
|
||||
|
||||
#include <tactility/concurrent/recursive_mutex.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/device_listener.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <TactilityCpp/Memory.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
|
||||
#define TAG "location_gps"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t AWAIT_TIMEOUT_MS = 250;
|
||||
constexpr uint32_t DEVICE_POLL_INTERVAL_MS = 1000;
|
||||
constexpr uint64_t START_RETRY_INTERVAL_MS = 30 * 1000;
|
||||
constexpr float METERS_PER_SECOND_PER_KNOT = 0.514444f;
|
||||
// Typical user equivalent range error of consumer receivers, which turns dilution of precision into meters
|
||||
constexpr float RANGE_ERROR_METERS = 5.0f;
|
||||
|
||||
/** The sentences received for one GPS epoch */
|
||||
struct Epoch {
|
||||
minmea_time time;
|
||||
bool hasRmc;
|
||||
bool hasGga;
|
||||
bool reported;
|
||||
minmea_sentence_rmc rmc;
|
||||
minmea_sentence_gga gga;
|
||||
};
|
||||
|
||||
/** The state of a provider run. Allocated in external memory when available. */
|
||||
struct GpsProvider {
|
||||
LocationHost* host = nullptr;
|
||||
Thread* thread = nullptr;
|
||||
std::atomic<bool> running = false;
|
||||
// Guarded by mutex
|
||||
Device* device = nullptr;
|
||||
// The device that this provider started, which it stops when it's done. Guarded by mutex.
|
||||
Device* startedDevice = nullptr;
|
||||
uint64_t nextStartAttemptMs = 0;
|
||||
// Guarded by mutex
|
||||
GpsSubscription subscription {};
|
||||
Epoch epoch {};
|
||||
};
|
||||
|
||||
struct GlobalRecursiveMutex {
|
||||
RecursiveMutex handle {};
|
||||
GlobalRecursiveMutex() { recursive_mutex_construct(&handle); }
|
||||
~GlobalRecursiveMutex() { recursive_mutex_destruct(&handle); }
|
||||
};
|
||||
|
||||
// Guards instance and the provider state that the device listener uses.
|
||||
// Recursive, because stopping startedDevice notifies the device listener on the same thread.
|
||||
GlobalRecursiveMutex mutex;
|
||||
// The running provider, for the device listener. A listener call can still be in progress after it's removed.
|
||||
GpsProvider* instance = nullptr;
|
||||
|
||||
double toDegrees(const minmea_float& value) {
|
||||
const double raw = static_cast<double>(value.value) / value.scale;
|
||||
const int degrees = static_cast<int>(raw / 100);
|
||||
return degrees + (raw - degrees * 100) / 60.0;
|
||||
}
|
||||
|
||||
// Days from 1970-01-01 to a date in the proleptic Gregorian calendar
|
||||
int64_t daysSinceEpoch(int year, int month, int day) {
|
||||
year -= month <= 2;
|
||||
const int64_t era = (year >= 0 ? year : year - 399) / 400;
|
||||
const int yearOfEra = static_cast<int>(year - era * 400);
|
||||
const int dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
|
||||
const int dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear;
|
||||
return era * 146097 + dayOfEra - 719468;
|
||||
}
|
||||
|
||||
uint64_t getUptimeMs() {
|
||||
return get_micros_since_boot() / 1000;
|
||||
}
|
||||
|
||||
bool isSameTime(const minmea_time& a, const minmea_time& b) {
|
||||
return a.hours == b.hours && a.minutes == b.minutes && a.seconds == b.seconds && a.microseconds == b.microseconds;
|
||||
}
|
||||
|
||||
void report(GpsProvider& provider, Epoch& epoch) {
|
||||
Location location;
|
||||
if (gps_location_from_nmea(&epoch.rmc, epoch.hasGga ? &epoch.gga : nullptr, &location)) {
|
||||
if (location_get_precision() == LOCATION_PRECISION_APPROXIMATE) {
|
||||
location_make_approximate(&location);
|
||||
}
|
||||
location_host_report(provider.host, &location);
|
||||
}
|
||||
epoch.reported = true;
|
||||
}
|
||||
|
||||
void onSentence(GpsProvider& provider, const GpsEvent& event) {
|
||||
auto& epoch = provider.epoch;
|
||||
const minmea_time& time = event.type == GPS_EVENT_MESSAGE_RMC ? event.data.rmc.time : event.data.gga.time;
|
||||
if (!isSameTime(epoch.time, time)) {
|
||||
// Receivers that don't send GGA only get reported once the next epoch starts
|
||||
if (epoch.hasRmc && !epoch.reported) {
|
||||
report(provider, epoch);
|
||||
}
|
||||
epoch = {};
|
||||
epoch.time = time;
|
||||
}
|
||||
|
||||
if (event.type == GPS_EVENT_MESSAGE_RMC) {
|
||||
epoch.rmc = event.data.rmc;
|
||||
epoch.hasRmc = true;
|
||||
} else {
|
||||
epoch.gga = event.data.gga;
|
||||
epoch.hasGga = true;
|
||||
}
|
||||
|
||||
if (epoch.hasRmc && epoch.hasGga && !epoch.reported) {
|
||||
report(provider, epoch);
|
||||
}
|
||||
}
|
||||
|
||||
// Unsubscribes before the driver stops, as the subscription can't be removed afterwards
|
||||
void onDeviceEvent(Device* device, DeviceEvent event, void*) {
|
||||
if (event != DEVICE_EVENT_STOPPING) {
|
||||
return;
|
||||
}
|
||||
recursive_mutex_lock(&mutex.handle);
|
||||
if (instance != nullptr) {
|
||||
if (device == instance->device) {
|
||||
gps_event_unsubscribe(device, &instance->subscription);
|
||||
instance->device = nullptr;
|
||||
}
|
||||
if (device == instance->startedDevice) {
|
||||
instance->startedDevice = nullptr;
|
||||
}
|
||||
}
|
||||
recursive_mutex_unlock(&mutex.handle);
|
||||
}
|
||||
|
||||
/** Must be called with the mutex held */
|
||||
void startFirstDevice(GpsProvider& provider) {
|
||||
const uint64_t now = getUptimeMs();
|
||||
if (now < provider.nextStartAttemptMs) {
|
||||
return;
|
||||
}
|
||||
// The reference keeps the device from being freed while it's started. A removed device fails to start.
|
||||
Device* device;
|
||||
if (device_get_first_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
if (!device_is_ready(device)) {
|
||||
if (device_start(device) == ERROR_NONE) {
|
||||
provider.startedDevice = device;
|
||||
} else {
|
||||
LOG_W(TAG, "Failed to start %s", device->name);
|
||||
provider.nextStartAttemptMs = now + START_RETRY_INTERVAL_MS;
|
||||
}
|
||||
}
|
||||
device_put(device);
|
||||
}
|
||||
|
||||
/** Must be called with the mutex held */
|
||||
bool subscribe(GpsProvider& provider) {
|
||||
Device* device;
|
||||
if (device_get_first_active_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||
startFirstDevice(provider);
|
||||
if (device_get_first_active_by_type(&GPS_TYPE, &device) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const error_t error = gps_event_subscribe(device, &provider.subscription);
|
||||
device_put(device);
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to subscribe to %s", device->name);
|
||||
return false;
|
||||
}
|
||||
LOG_I(TAG, "Using %s", device->name);
|
||||
provider.device = device;
|
||||
provider.epoch = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Must be called with the mutex held and a device subscribed */
|
||||
void receive(GpsProvider& provider) {
|
||||
auto& sub = provider.subscription;
|
||||
if (sub.sequence != sub.consumed_sequence) {
|
||||
sub.consumed_sequence = sub.sequence;
|
||||
} else if (gps_event_await(provider.device, &sub, millis_to_ticks(AWAIT_TIMEOUT_MS)) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (sub.event.type) {
|
||||
case GPS_EVENT_MESSAGE_RMC:
|
||||
case GPS_EVENT_MESSAGE_GGA:
|
||||
onSentence(provider, sub.event);
|
||||
break;
|
||||
case GPS_EVENT_UNSUBSCRIBED:
|
||||
provider.device = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t threadMain(void* context) {
|
||||
auto& provider = *static_cast<GpsProvider*>(context);
|
||||
while (provider.running) {
|
||||
recursive_mutex_lock(&mutex.handle);
|
||||
const bool subscribed = provider.device != nullptr || subscribe(provider);
|
||||
if (subscribed) {
|
||||
receive(provider);
|
||||
}
|
||||
recursive_mutex_unlock(&mutex.handle);
|
||||
|
||||
if (!subscribed) {
|
||||
delay_millis(DEVICE_POLL_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
recursive_mutex_lock(&mutex.handle);
|
||||
if (provider.device != nullptr) {
|
||||
gps_event_unsubscribe(provider.device, &provider.subscription);
|
||||
provider.device = nullptr;
|
||||
}
|
||||
if (provider.startedDevice != nullptr) {
|
||||
Device* device = provider.startedDevice;
|
||||
provider.startedDevice = nullptr;
|
||||
if (device_stop(device) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Failed to stop %s", device->name);
|
||||
}
|
||||
}
|
||||
recursive_mutex_unlock(&mutex.handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* create(LocationHost* host) {
|
||||
auto* provider = tt::newObject<GpsProvider>();
|
||||
if (provider == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
provider->host = host;
|
||||
provider->running = true;
|
||||
provider->thread = thread_alloc_full("location_gps", 4096, threadMain, provider, -1);
|
||||
if (provider->thread == nullptr) {
|
||||
tt::deleteObject(provider);
|
||||
return nullptr;
|
||||
}
|
||||
// Settings are loaded before providers start, so the thread doesn't access flash
|
||||
thread_set_stack_external(provider->thread, true);
|
||||
|
||||
recursive_mutex_lock(&mutex.handle);
|
||||
instance = provider;
|
||||
recursive_mutex_unlock(&mutex.handle);
|
||||
device_listener_add(onDeviceEvent, nullptr);
|
||||
|
||||
if (thread_start(provider->thread) != ERROR_NONE) {
|
||||
device_listener_remove(onDeviceEvent, nullptr);
|
||||
recursive_mutex_lock(&mutex.handle);
|
||||
instance = nullptr;
|
||||
recursive_mutex_unlock(&mutex.handle);
|
||||
thread_free(provider->thread);
|
||||
tt::deleteObject(provider);
|
||||
return nullptr;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
void requestStop(void* context) {
|
||||
static_cast<GpsProvider*>(context)->running = false;
|
||||
}
|
||||
|
||||
bool waitStopped(void* context, TickType_t timeout) {
|
||||
auto* provider = static_cast<GpsProvider*>(context);
|
||||
if (thread_join(provider->thread, timeout, millis_to_ticks(10)) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
device_listener_remove(onDeviceEvent, nullptr);
|
||||
recursive_mutex_lock(&mutex.handle);
|
||||
instance = nullptr;
|
||||
recursive_mutex_unlock(&mutex.handle);
|
||||
thread_free(provider->thread);
|
||||
tt::deleteObject(provider);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool gps_location_from_nmea(const minmea_sentence_rmc* rmc, const minmea_sentence_gga* gga, Location* location) {
|
||||
if (!rmc->valid || rmc->latitude.scale == 0 || rmc->longitude.scale == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*location = Location {
|
||||
.latitude = toDegrees(rmc->latitude),
|
||||
.longitude = toDegrees(rmc->longitude),
|
||||
.horizontal_accuracy = NAN,
|
||||
.altitude = NAN,
|
||||
.ellipsoidal_altitude = NAN,
|
||||
.vertical_accuracy = NAN,
|
||||
.speed = minmea_tofloat(&rmc->speed) * METERS_PER_SECOND_PER_KNOT,
|
||||
.speed_accuracy = NAN,
|
||||
.course = minmea_tofloat(&rmc->course),
|
||||
.course_accuracy = NAN,
|
||||
.satellites = 0,
|
||||
.time_ms = 0,
|
||||
.uptime_ms = 0,
|
||||
.provider = {},
|
||||
};
|
||||
|
||||
tm dateTime;
|
||||
if (minmea_getdatetime(&dateTime, &rmc->date, &rmc->time) == 0) {
|
||||
const int64_t seconds = daysSinceEpoch(dateTime.tm_year + 1900, dateTime.tm_mon + 1, dateTime.tm_mday) * 86400 +
|
||||
dateTime.tm_hour * 3600 + dateTime.tm_min * 60 + dateTime.tm_sec;
|
||||
location->time_ms = seconds * 1000 + rmc->time.microseconds / 1000;
|
||||
}
|
||||
|
||||
if (gga != nullptr && gga->fix_quality > 0) {
|
||||
location->horizontal_accuracy = minmea_tofloat(&gga->hdop) * RANGE_ERROR_METERS;
|
||||
location->altitude = minmea_tofloat(&gga->altitude);
|
||||
location->ellipsoidal_altitude = location->altitude + minmea_tofloat(&gga->height);
|
||||
location->satellites = static_cast<uint8_t>(gga->satellites_tracked);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi gps_location_provider = {
|
||||
.id = "gps",
|
||||
.fallback = false,
|
||||
.create = create,
|
||||
.request_stop = requestStop,
|
||||
.wait_stopped = waitStopped,
|
||||
};
|
||||
447
Modules/location-module/source/ip_provider.cpp
Normal file
447
Modules/location-module/source/ip_provider.cpp
Normal file
@ -0,0 +1,447 @@
|
||||
// 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()) {
|
||||
nextAttemptMs = std::max(nextAttemptMs, getUptimeMs() + CHECK_INTERVAL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t network = trackedNetwork;
|
||||
if (cache != nullptr && cache->network == network) {
|
||||
if (!reportedCache) {
|
||||
report(provider, cache->latitude, cache->longitude);
|
||||
reportedCache = true;
|
||||
}
|
||||
nextAttemptMs = std::max(nextAttemptMs, getUptimeMs() + CHECK_INTERVAL_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t attemptMs = getUptimeMs();
|
||||
if (attemptMs < nextAttemptMs) {
|
||||
continue;
|
||||
}
|
||||
attempted = true;
|
||||
lastAttemptMs = attemptMs;
|
||||
nextAttemptMs = attemptMs + lookup(provider, network);
|
||||
reportedCache = cache != nullptr && cache->network == network;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* create(LocationHost* host) {
|
||||
auto* provider = tt::newObject<IpProvider>();
|
||||
if (provider == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
provider->host = host;
|
||||
provider->running = true;
|
||||
task_event_group_construct(&provider->group);
|
||||
if (task_event_group_claim_bit(&provider->group, &provider->stopBit) != ERROR_NONE) {
|
||||
task_event_group_destruct(&provider->group);
|
||||
tt::deleteObject(provider);
|
||||
return nullptr;
|
||||
}
|
||||
provider->networkSubscription.event.type = KERNEL_EVENT_NETWORK_CONNECTED;
|
||||
if (system_event_subscribe(&provider->networkSubscription, &provider->group) != ERROR_NONE) {
|
||||
task_event_group_destruct(&provider->group);
|
||||
tt::deleteObject(provider);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
provider->thread = thread_alloc_full("location_ip", 4096, threadMain, provider, -1);
|
||||
if (provider->thread != nullptr) {
|
||||
// Settings are loaded before providers start, so the thread doesn't access flash
|
||||
thread_set_stack_external(provider->thread, true);
|
||||
}
|
||||
if (provider->thread == nullptr || thread_start(provider->thread) != ERROR_NONE) {
|
||||
if (provider->thread != nullptr) {
|
||||
thread_free(provider->thread);
|
||||
}
|
||||
system_event_unsubscribe(&provider->networkSubscription);
|
||||
task_event_group_destruct(&provider->group);
|
||||
tt::deleteObject(provider);
|
||||
return nullptr;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
void requestStop(void* context) {
|
||||
auto* provider = static_cast<IpProvider*>(context);
|
||||
provider->running = false;
|
||||
task_event_group_signal(&provider->group, provider->stopBit);
|
||||
}
|
||||
|
||||
bool waitStopped(void* context, TickType_t timeout) {
|
||||
auto* provider = static_cast<IpProvider*>(context);
|
||||
if (thread_join(provider->thread, timeout, millis_to_ticks(10)) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
thread_free(provider->thread);
|
||||
system_event_unsubscribe(&provider->networkSubscription);
|
||||
task_event_group_destruct(&provider->group);
|
||||
tt::deleteObject(provider);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string_view nextLine(std::string_view& text) {
|
||||
const size_t end = text.find('\n');
|
||||
std::string_view line = text.substr(0, end);
|
||||
text = end == std::string_view::npos ? std::string_view {} : text.substr(end + 1);
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.remove_suffix(1);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
int parseHeaderNumber(std::string_view line, const char* name) {
|
||||
const size_t nameLength = strlen(name);
|
||||
if (line.size() <= nameLength || line[nameLength] != ':' || strncasecmp(line.data(), name, nameLength) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return atoi(tt::std::string(line.substr(nameLength + 1)).c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
error_t ip_provider_track_network(void) {
|
||||
return system_event_callback_add(KERNEL_EVENT_NETWORK_CONNECTED, onNetworkConnected, nullptr);
|
||||
}
|
||||
|
||||
void ip_provider_untrack_network(void) {
|
||||
system_event_callback_remove(KERNEL_EVENT_NETWORK_CONNECTED, onNetworkConnected);
|
||||
if (cache != nullptr) {
|
||||
tt::deleteObject(cache);
|
||||
cache = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool ip_api_parse_response(const char* response, IpApiResponse* out) {
|
||||
*out = IpApiResponse {
|
||||
.status_code = 0,
|
||||
.rate_remaining = -1,
|
||||
.rate_reset_seconds = -1,
|
||||
.success = false,
|
||||
.message = {},
|
||||
.latitude = NAN,
|
||||
.longitude = NAN,
|
||||
};
|
||||
|
||||
std::string_view text = response;
|
||||
const std::string_view statusLine = nextLine(text);
|
||||
if (!statusLine.starts_with("HTTP/1.")) {
|
||||
return false;
|
||||
}
|
||||
const size_t space = statusLine.find(' ');
|
||||
if (space == std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
out->status_code = atoi(tt::std::string(statusLine.substr(space + 1)).c_str());
|
||||
|
||||
while (true) {
|
||||
if (text.empty()) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view header = nextLine(text);
|
||||
if (header.empty()) {
|
||||
break;
|
||||
}
|
||||
const int remaining = parseHeaderNumber(header, "X-Rl");
|
||||
if (remaining >= 0) {
|
||||
out->rate_remaining = remaining;
|
||||
}
|
||||
const int resetSeconds = parseHeaderNumber(header, "X-Ttl");
|
||||
if (resetSeconds >= 0) {
|
||||
out->rate_reset_seconds = resetSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
if (out->status_code != 200) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fields are returned one per line, in the order of the API documentation
|
||||
const std::string_view status = nextLine(text);
|
||||
if (status == "success") {
|
||||
const tt::std::string latitude(nextLine(text));
|
||||
const tt::std::string longitude(nextLine(text));
|
||||
char* latitudeEnd;
|
||||
char* longitudeEnd;
|
||||
out->latitude = strtod(latitude.c_str(), &latitudeEnd);
|
||||
out->longitude = strtod(longitude.c_str(), &longitudeEnd);
|
||||
if (latitude.empty() || *latitudeEnd != '\0' || longitude.empty() || *longitudeEnd != '\0' ||
|
||||
!std::isfinite(out->latitude) || !std::isfinite(out->longitude) ||
|
||||
std::fabs(out->latitude) > 90.0 || std::fabs(out->longitude) > 180.0) {
|
||||
return false;
|
||||
}
|
||||
out->success = true;
|
||||
} else if (status == "fail") {
|
||||
const std::string_view message = nextLine(text).substr(0, IP_API_MESSAGE_MAX_LENGTH);
|
||||
memcpy(out->message, message.data(), message.size());
|
||||
out->message[message.size()] = '\0';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi ip_location_provider = {
|
||||
.id = "ip",
|
||||
.fallback = true,
|
||||
.create = create,
|
||||
.request_stop = requestStop,
|
||||
.wait_stopped = waitStopped,
|
||||
};
|
||||
533
Modules/location-module/source/location_service.cpp
Normal file
533
Modules/location-module/source/location_service.cpp
Normal file
@ -0,0 +1,533 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/location.h>
|
||||
#include <location/location_service.h>
|
||||
#include <location/location_settings.h>
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
#include <service/manifest.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/concurrent/thread.h>
|
||||
#include <tactility/delay.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <TactilityCpp/Memory.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
|
||||
#define TAG "location"
|
||||
|
||||
enum class HostState {
|
||||
Stopped,
|
||||
Running,
|
||||
Stopping,
|
||||
};
|
||||
|
||||
struct LocationHost {
|
||||
const LocationProviderApi* api;
|
||||
void* provider;
|
||||
HostState state;
|
||||
// The worker is calling the provider without holding the mutex
|
||||
bool busy;
|
||||
// Unregistering, which waits for the provider to stop
|
||||
bool removing;
|
||||
// Not created again until the provider is no longer desired
|
||||
bool createFailed;
|
||||
|
||||
bool fallback;
|
||||
bool hasLocation;
|
||||
Location location;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
/** A fix of a provider that isn't a fallback is preferred over fallback fixes for this long */
|
||||
constexpr uint64_t FRESH_MS = 120000;
|
||||
// About 2 km north to south
|
||||
constexpr double APPROXIMATE_GRID_DEGREES = 0.02;
|
||||
// The worker waits this long for a stopping provider before it handles the other providers
|
||||
constexpr uint32_t STOP_WAIT_MS = 100;
|
||||
constexpr uint32_t UNREGISTER_POLL_MS = 10;
|
||||
constexpr size_t WORKER_STACK_SIZE = 4096;
|
||||
|
||||
struct GlobalMutex {
|
||||
Mutex handle {};
|
||||
GlobalMutex() { mutex_construct(&handle); }
|
||||
~GlobalMutex() { mutex_destruct(&handle); }
|
||||
};
|
||||
|
||||
// Not held while calling provider functions
|
||||
GlobalMutex mutex;
|
||||
|
||||
struct HostStorage {
|
||||
LocationHost hosts[LOCATION_PROVIDER_MAX_COUNT] {};
|
||||
};
|
||||
|
||||
// Allocated in external memory when available, while providers are registered
|
||||
HostStorage* hostStorage = nullptr;
|
||||
bool running = false;
|
||||
LocationSubscription* subscriptions = nullptr;
|
||||
// Starts and stops providers. It only exists while a provider isn't in its desired state.
|
||||
Thread* worker = nullptr;
|
||||
bool workerActive = false;
|
||||
|
||||
GlobalMutex settingsMutex;
|
||||
LocationSettings settings {};
|
||||
bool settingsLoaded = false;
|
||||
|
||||
/** Must be called with settingsMutex held */
|
||||
void ensureSettingsLoaded() {
|
||||
if (!settingsLoaded) {
|
||||
location_settings_load(&settings);
|
||||
settingsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Must be called with mutex held */
|
||||
std::span<LocationHost> getHosts() {
|
||||
return hostStorage != nullptr ? std::span<LocationHost>(hostStorage->hosts) : std::span<LocationHost>();
|
||||
}
|
||||
|
||||
/** Frees the hosts when no provider is registered and the worker doesn't use them. Must be called with mutex held. */
|
||||
void freeUnusedHosts() {
|
||||
if (hostStorage == nullptr || workerActive) {
|
||||
return;
|
||||
}
|
||||
for (const auto& host : hostStorage->hosts) {
|
||||
if (host.api != nullptr) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
tt::deleteObject(hostStorage);
|
||||
hostStorage = nullptr;
|
||||
}
|
||||
|
||||
uint64_t getUptimeMs() {
|
||||
return get_micros_since_boot() / 1000;
|
||||
}
|
||||
|
||||
/** Must be called with mutex held */
|
||||
int getRank(const LocationHost& host, uint64_t now) {
|
||||
if (host.fallback) {
|
||||
return 1;
|
||||
}
|
||||
return now - host.location.uptime_ms < FRESH_MS ? 2 : 0;
|
||||
}
|
||||
|
||||
/** Must be called with mutex held */
|
||||
const LocationHost* findBest() {
|
||||
const uint64_t now = getUptimeMs();
|
||||
const LocationHost* best = nullptr;
|
||||
int bestRank = -1;
|
||||
for (const auto& host : getHosts()) {
|
||||
if (!host.hasLocation) {
|
||||
continue;
|
||||
}
|
||||
const int rank = getRank(host, now);
|
||||
if (rank > bestRank || (rank == bestRank && host.location.uptime_ms > best->location.uptime_ms)) {
|
||||
best = &host;
|
||||
bestRank = rank;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Must be called with mutex held */
|
||||
bool isDesired(const LocationHost& host) {
|
||||
return host.api != nullptr && !host.removing && running && subscriptions != nullptr;
|
||||
}
|
||||
|
||||
/** Must be called with mutex held */
|
||||
bool isSettled(const LocationHost& host) {
|
||||
switch (host.state) {
|
||||
case HostState::Running:
|
||||
return isDesired(host);
|
||||
case HostState::Stopped:
|
||||
return !isDesired(host) || host.createFailed;
|
||||
case HostState::Stopping:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Takes one step towards the desired state. Must be called with mutex held, which is released during the step. */
|
||||
void step(LocationHost& host) {
|
||||
const LocationProviderApi* api = host.api;
|
||||
void* provider = host.provider;
|
||||
const HostState state = host.state;
|
||||
host.busy = true;
|
||||
mutex_unlock(&mutex.handle);
|
||||
|
||||
switch (state) {
|
||||
case HostState::Stopped: {
|
||||
void* created = api->create(&host);
|
||||
mutex_lock(&mutex.handle);
|
||||
if (created != nullptr) {
|
||||
host.provider = created;
|
||||
host.state = HostState::Running;
|
||||
} else {
|
||||
LOG_E(TAG, "Failed to start provider %s", api->id);
|
||||
host.createFailed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HostState::Running:
|
||||
api->request_stop(provider);
|
||||
mutex_lock(&mutex.handle);
|
||||
host.state = HostState::Stopping;
|
||||
break;
|
||||
case HostState::Stopping: {
|
||||
const bool stopped = api->wait_stopped(provider, millis_to_ticks(STOP_WAIT_MS));
|
||||
mutex_lock(&mutex.handle);
|
||||
if (stopped) {
|
||||
host.provider = nullptr;
|
||||
host.state = HostState::Stopped;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
host.busy = false;
|
||||
}
|
||||
|
||||
int32_t workerMain(void*) {
|
||||
mutex_lock(&mutex.handle);
|
||||
bool settled = false;
|
||||
while (!settled) {
|
||||
settled = true;
|
||||
for (auto& host : getHosts()) {
|
||||
if (!host.busy && !isSettled(host)) {
|
||||
settled = false;
|
||||
step(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
workerActive = false;
|
||||
freeUnusedHosts();
|
||||
mutex_unlock(&mutex.handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Frees the worker after it exited. Must be called with mutex held. */
|
||||
void freeFinishedWorker() {
|
||||
if (worker != nullptr && !workerActive) {
|
||||
// The worker released the mutex for the last time, so it exits right away
|
||||
thread_join(worker, portMAX_DELAY, 1);
|
||||
thread_free(worker);
|
||||
worker = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the worker when a provider isn't in its desired state. Must be called with mutex held. */
|
||||
void reconcile() {
|
||||
bool settled = true;
|
||||
for (auto& host : getHosts()) {
|
||||
if (!isDesired(host)) {
|
||||
host.createFailed = false;
|
||||
}
|
||||
settled = settled && isSettled(host);
|
||||
}
|
||||
if (settled || workerActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The worker doesn't access flash, so its stack can be in external memory
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
|
||||
freeFinishedWorker();
|
||||
worker = thread_alloc_full("location", WORKER_STACK_SIZE, workerMain, nullptr, -1);
|
||||
if (worker == nullptr) {
|
||||
LOG_E(TAG, "Failed to allocate worker");
|
||||
return;
|
||||
}
|
||||
thread_set_stack_external(worker, true);
|
||||
if (thread_start(worker) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to start worker");
|
||||
thread_free(worker);
|
||||
worker = nullptr;
|
||||
return;
|
||||
}
|
||||
workerActive = true;
|
||||
}
|
||||
|
||||
void* createService(const ServiceManifest*) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void destroyService(const ServiceManifest*, void*) {}
|
||||
|
||||
error_t onStart(ServiceInstance*, void*) {
|
||||
mutex_lock(&mutex.handle);
|
||||
running = true;
|
||||
reconcile();
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void onStop(ServiceInstance*, void*) {
|
||||
mutex_lock(&mutex.handle);
|
||||
running = false;
|
||||
for (auto& host : getHosts()) {
|
||||
host.hasLocation = false;
|
||||
}
|
||||
reconcile();
|
||||
freeFinishedWorker();
|
||||
mutex_unlock(&mutex.handle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t location_set_enabled(bool enabled) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
settings.enabled = enabled;
|
||||
const LocationSettings copy = settings;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
|
||||
const error_t saved = location_settings_save(©);
|
||||
error_t result;
|
||||
if (enabled) {
|
||||
result = service_manager_start(LOCATION_SERVICE_ID);
|
||||
if (result == ERROR_INVALID_STATE) {
|
||||
result = ERROR_NONE;
|
||||
}
|
||||
} else {
|
||||
result = service_manager_stop(LOCATION_SERVICE_ID);
|
||||
if (result == ERROR_NOT_FOUND) {
|
||||
result = ERROR_NONE;
|
||||
}
|
||||
}
|
||||
return result != ERROR_NONE ? result : saved;
|
||||
}
|
||||
|
||||
bool location_is_enabled(void) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
const bool enabled = settings.enabled;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
error_t location_set_precision(LocationPrecision precision) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
settings.precision = precision;
|
||||
const LocationSettings copy = settings;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
|
||||
if (precision == LOCATION_PRECISION_APPROXIMATE) {
|
||||
// Precise fixes are dropped until their providers report again. Coarse fixes are rounded.
|
||||
mutex_lock(&mutex.handle);
|
||||
for (auto& host : getHosts()) {
|
||||
if (host.hasLocation) {
|
||||
if (host.location.horizontal_accuracy >= LOCATION_APPROXIMATE_ACCURACY_METERS) {
|
||||
location_make_approximate(&host.location);
|
||||
} else {
|
||||
host.hasLocation = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
mutex_unlock(&mutex.handle);
|
||||
}
|
||||
return location_settings_save(©);
|
||||
}
|
||||
|
||||
LocationPrecision location_get_precision(void) {
|
||||
mutex_lock(&settingsMutex.handle);
|
||||
ensureSettingsLoaded();
|
||||
const LocationPrecision precision = settings.precision;
|
||||
mutex_unlock(&settingsMutex.handle);
|
||||
return precision;
|
||||
}
|
||||
|
||||
void location_make_approximate(Location* location) {
|
||||
location->latitude = std::round(location->latitude / APPROXIMATE_GRID_DEGREES) * APPROXIMATE_GRID_DEGREES;
|
||||
location->longitude = std::round(location->longitude / APPROXIMATE_GRID_DEGREES) * APPROXIMATE_GRID_DEGREES;
|
||||
if (!(location->horizontal_accuracy >= LOCATION_APPROXIMATE_ACCURACY_METERS)) {
|
||||
location->horizontal_accuracy = LOCATION_APPROXIMATE_ACCURACY_METERS;
|
||||
}
|
||||
location->altitude = NAN;
|
||||
location->ellipsoidal_altitude = NAN;
|
||||
location->vertical_accuracy = NAN;
|
||||
location->speed = NAN;
|
||||
location->speed_accuracy = NAN;
|
||||
location->course = NAN;
|
||||
location->course_accuracy = NAN;
|
||||
location->satellites = 0;
|
||||
}
|
||||
|
||||
error_t location_provider_register(const LocationProviderApi* api) {
|
||||
if (strlen(api->id) > LOCATION_PROVIDER_ID_MAX_LENGTH) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
mutex_lock(&mutex.handle);
|
||||
if (hostStorage == nullptr) {
|
||||
hostStorage = tt::newObject<HostStorage>();
|
||||
if (hostStorage == nullptr) {
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
}
|
||||
LocationHost* freeHost = nullptr;
|
||||
for (auto& host : getHosts()) {
|
||||
if (host.api == nullptr) {
|
||||
if (freeHost == nullptr) {
|
||||
freeHost = &host;
|
||||
}
|
||||
} else if (strcmp(host.api->id, api->id) == 0) {
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
}
|
||||
if (freeHost == nullptr) {
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
freeHost->api = api;
|
||||
freeHost->fallback = api->fallback;
|
||||
reconcile();
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t location_provider_unregister(const LocationProviderApi* api) {
|
||||
mutex_lock(&mutex.handle);
|
||||
LocationHost* host = nullptr;
|
||||
for (auto& candidate : getHosts()) {
|
||||
if (candidate.api == api && !candidate.removing) {
|
||||
host = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (host == nullptr) {
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
host->removing = true;
|
||||
while (host->state != HostState::Stopped || host->busy) {
|
||||
reconcile();
|
||||
mutex_unlock(&mutex.handle);
|
||||
delay_millis(UNREGISTER_POLL_MS);
|
||||
mutex_lock(&mutex.handle);
|
||||
}
|
||||
host->hasLocation = false;
|
||||
host->api = nullptr;
|
||||
host->removing = false;
|
||||
host->createFailed = false;
|
||||
freeUnusedHosts();
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void location_host_report(LocationHost* host, const Location* location) {
|
||||
mutex_lock(&mutex.handle);
|
||||
// Providers that are being stopped don't report anymore
|
||||
if (host->state != HostState::Running || !isDesired(*host)) {
|
||||
mutex_unlock(&mutex.handle);
|
||||
return;
|
||||
}
|
||||
host->location = *location;
|
||||
strcpy(host->location.provider, host->api->id);
|
||||
host->location.uptime_ms = getUptimeMs();
|
||||
host->hasLocation = true;
|
||||
if (findBest() == host) {
|
||||
for (auto* sub = subscriptions; sub != nullptr; sub = sub->internal.next) {
|
||||
task_event_group_signal(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
}
|
||||
mutex_unlock(&mutex.handle);
|
||||
}
|
||||
|
||||
bool location_host_needs_fallback(void) {
|
||||
mutex_lock(&mutex.handle);
|
||||
const uint64_t now = getUptimeMs();
|
||||
bool needed = true;
|
||||
for (const auto& host : getHosts()) {
|
||||
if (host.hasLocation && getRank(host, now) == 2) {
|
||||
needed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&mutex.handle);
|
||||
return needed;
|
||||
}
|
||||
|
||||
error_t location_get(Location* location) {
|
||||
mutex_lock(&mutex.handle);
|
||||
const auto* best = findBest();
|
||||
if (best != nullptr) {
|
||||
*location = best->location;
|
||||
}
|
||||
mutex_unlock(&mutex.handle);
|
||||
return best != nullptr ? ERROR_NONE : ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
error_t location_subscribe(LocationSubscription* sub, TaskEventGroup* event_group) {
|
||||
uint32_t bit;
|
||||
const error_t claimed = task_event_group_claim_bit(event_group, &bit);
|
||||
if (claimed != ERROR_NONE) {
|
||||
return claimed;
|
||||
}
|
||||
|
||||
mutex_lock(&mutex.handle);
|
||||
for (auto* existing = subscriptions; existing != nullptr; existing = existing->internal.next) {
|
||||
if (existing == sub) {
|
||||
mutex_unlock(&mutex.handle);
|
||||
task_event_group_release_bit(event_group, bit);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
}
|
||||
sub->bit = bit;
|
||||
sub->internal.event_group = event_group;
|
||||
sub->internal.next = subscriptions;
|
||||
subscriptions = sub;
|
||||
reconcile();
|
||||
mutex_unlock(&mutex.handle);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t location_unsubscribe(LocationSubscription* sub) {
|
||||
error_t result = ERROR_NOT_FOUND;
|
||||
mutex_lock(&mutex.handle);
|
||||
for (auto** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) {
|
||||
if (*link == sub) {
|
||||
*link = sub->internal.next;
|
||||
result = ERROR_NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
reconcile();
|
||||
freeFinishedWorker();
|
||||
mutex_unlock(&mutex.handle);
|
||||
if (result == ERROR_NONE) {
|
||||
task_event_group_release_bit(sub->internal.event_group, sub->bit);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
bool location_is_in_use(void) {
|
||||
mutex_lock(&mutex.handle);
|
||||
const bool inUse = running && subscriptions != nullptr;
|
||||
mutex_unlock(&mutex.handle);
|
||||
return inUse;
|
||||
}
|
||||
}
|
||||
|
||||
extern const ServiceManifest location_service_manifest = {
|
||||
.id = LOCATION_SERVICE_ID,
|
||||
.create_service = createService,
|
||||
.destroy_service = destroyService,
|
||||
.on_start = onStart,
|
||||
.on_stop = onStop,
|
||||
};
|
||||
66
Modules/location-module/source/location_settings.cpp
Normal file
66
Modules/location-module/source/location_settings.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/location_settings.h>
|
||||
|
||||
#include <service/paths.h>
|
||||
|
||||
#include <tactility/filesystem/fs.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/paths.h>
|
||||
#include <tactility/properties_file.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
constexpr auto* TAG = "location_settings";
|
||||
constexpr auto* SETTINGS_FILE_NAME = "settings.properties";
|
||||
constexpr auto* KEY_ENABLED = "enabled";
|
||||
constexpr auto* KEY_PRECISION = "precision";
|
||||
constexpr auto* PRECISION_APPROXIMATE = "approximate";
|
||||
constexpr auto* PRECISION_PRECISE = "precise";
|
||||
|
||||
void location_settings_load(LocationSettings* settings) {
|
||||
*settings = LocationSettings {
|
||||
.enabled = false,
|
||||
.precision = LOCATION_PRECISION_PRECISE,
|
||||
};
|
||||
|
||||
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||
if (service_paths_get_user_data_path(LOCATION_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
if (file == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
char value[32];
|
||||
if (properties_file_get(file, KEY_ENABLED, value, sizeof(value)) == ERROR_NONE) {
|
||||
settings->enabled = strcmp(value, "true") == 0;
|
||||
}
|
||||
if (properties_file_get(file, KEY_PRECISION, value, sizeof(value)) == ERROR_NONE) {
|
||||
settings->precision = strcmp(value, PRECISION_APPROXIMATE) == 0 ? LOCATION_PRECISION_APPROXIMATE : LOCATION_PRECISION_PRECISE;
|
||||
}
|
||||
properties_file_close(file);
|
||||
}
|
||||
|
||||
error_t location_settings_save(const LocationSettings* settings) {
|
||||
char directory[FILE_MAX_PATH_STRING_LENGTH];
|
||||
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||
if (service_paths_get_user_data_directory(LOCATION_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE ||
|
||||
service_paths_get_user_data_path(LOCATION_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) {
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
const error_t made = directory_make(directory, true);
|
||||
if (made != ERROR_NONE && made != ERROR_ALREADY_EXISTS) {
|
||||
LOG_E(TAG, "Failed to create %s", directory);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
PropertiesFile* file = properties_file_open(path);
|
||||
if (file == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
properties_file_set(file, KEY_ENABLED, settings->enabled ? "true" : "false");
|
||||
properties_file_set(file, KEY_PRECISION, settings->precision == LOCATION_PRECISION_APPROXIMATE ? PRECISION_APPROXIMATE : PRECISION_PRECISE);
|
||||
return properties_file_close(file);
|
||||
}
|
||||
66
Modules/location-module/source/module.cpp
Normal file
66
Modules/location-module/source/module.cpp
Normal file
@ -0,0 +1,66 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <location/gps_provider.h>
|
||||
#include <location/ip_provider.h>
|
||||
#include <location/location.h>
|
||||
#include <location/location_service.h>
|
||||
#include <location/module.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
static error_t start() {
|
||||
error_t error = ip_provider_track_network();
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
error = location_provider_register(&gps_location_provider);
|
||||
if (error != ERROR_NONE) {
|
||||
ip_provider_untrack_network();
|
||||
return error;
|
||||
}
|
||||
error = location_provider_register(&ip_location_provider);
|
||||
if (error != ERROR_NONE) {
|
||||
location_provider_unregister(&gps_location_provider);
|
||||
ip_provider_untrack_network();
|
||||
return error;
|
||||
}
|
||||
return service_manager_add(&location_service_manifest, location_is_enabled());
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
const error_t error = service_manager_stop(location_service_manifest.id);
|
||||
if (error != ERROR_NONE && error != ERROR_NOT_FOUND) {
|
||||
return error;
|
||||
}
|
||||
location_provider_unregister(&ip_location_provider);
|
||||
location_provider_unregister(&gps_location_provider);
|
||||
ip_provider_untrack_network();
|
||||
return service_manager_remove(location_service_manifest.id);
|
||||
}
|
||||
|
||||
static const ModuleSymbol location_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(location_set_enabled),
|
||||
DEFINE_MODULE_SYMBOL(location_is_enabled),
|
||||
DEFINE_MODULE_SYMBOL(location_set_precision),
|
||||
DEFINE_MODULE_SYMBOL(location_get_precision),
|
||||
DEFINE_MODULE_SYMBOL(location_get),
|
||||
DEFINE_MODULE_SYMBOL(location_subscribe),
|
||||
DEFINE_MODULE_SYMBOL(location_unsubscribe),
|
||||
DEFINE_MODULE_SYMBOL(location_is_in_use),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
Module location_module = {
|
||||
.name = "location",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.drivers = nullptr,
|
||||
.symbols = location_module_symbols,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
20
Modules/location-module/tests/CMakeLists.txt
Normal file
20
Modules/location-module/tests/CMakeLists.txt
Normal file
@ -0,0 +1,20 @@
|
||||
project(LocationModuleTests)
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(LocationModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
|
||||
target_include_directories(LocationModuleTests PRIVATE
|
||||
${DOCTESTINC}
|
||||
${PROJECT_SOURCE_DIR}/../private
|
||||
)
|
||||
|
||||
add_test(NAME LocationModuleTests COMMAND LocationModuleTests)
|
||||
|
||||
target_link_libraries(LocationModuleTests PUBLIC
|
||||
TactilityKernel
|
||||
platform-posix
|
||||
service-module
|
||||
location-module
|
||||
)
|
||||
64
Modules/location-module/tests/source/gps_provider_test.cpp
Normal file
64
Modules/location-module/tests/source/gps_provider_test.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <location/gps_provider.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto* RMC = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
||||
constexpr auto* GGA = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea converts RMC and GGA") {
|
||||
minmea_sentence_rmc rmc;
|
||||
minmea_sentence_gga gga;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, RMC));
|
||||
REQUIRE(minmea_parse_gga(&gga, GGA));
|
||||
|
||||
Location location;
|
||||
REQUIRE(gps_location_from_nmea(&rmc, &gga, &location));
|
||||
CHECK(location.latitude == doctest::Approx(48.1173));
|
||||
CHECK(location.longitude == doctest::Approx(11.516667));
|
||||
CHECK(location.speed == doctest::Approx(11.5235).epsilon(0.001));
|
||||
CHECK(location.course == doctest::Approx(84.4));
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(4.5));
|
||||
CHECK(location.altitude == doctest::Approx(545.4));
|
||||
CHECK(location.ellipsoidal_altitude == doctest::Approx(592.3));
|
||||
CHECK(std::isnan(location.vertical_accuracy));
|
||||
CHECK(location.satellites == 8);
|
||||
CHECK(location.time_ms == 764426119000LL);
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea without GGA has no altitude or accuracy") {
|
||||
minmea_sentence_rmc rmc;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, RMC));
|
||||
|
||||
Location location;
|
||||
REQUIRE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||
CHECK(location.latitude == doctest::Approx(48.1173));
|
||||
CHECK(std::isnan(location.altitude));
|
||||
CHECK(std::isnan(location.horizontal_accuracy));
|
||||
CHECK(location.satellites == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea handles the southern and western hemispheres") {
|
||||
minmea_sentence_rmc rmc;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, "$GPRMC,123519,A,3352.128,S,15112.558,W,000.0,,230394,,*00"));
|
||||
|
||||
Location location;
|
||||
REQUIRE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||
CHECK(location.latitude == doctest::Approx(-33.8688));
|
||||
CHECK(location.longitude == doctest::Approx(-151.209300));
|
||||
CHECK(location.speed == doctest::Approx(0.0));
|
||||
CHECK(std::isnan(location.course));
|
||||
}
|
||||
|
||||
TEST_CASE("gps_location_from_nmea rejects a fix that is not valid") {
|
||||
minmea_sentence_rmc rmc;
|
||||
REQUIRE(minmea_parse_rmc(&rmc, "$GPRMC,123519,V,,,,,,,230394,,*00"));
|
||||
|
||||
Location location;
|
||||
CHECK_FALSE(gps_location_from_nmea(&rmc, nullptr, &location));
|
||||
}
|
||||
57
Modules/location-module/tests/source/ip_provider_test.cpp
Normal file
57
Modules/location-module/tests/source/ip_provider_test.cpp
Normal file
@ -0,0 +1,57 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <location/ip_provider.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
TEST_CASE("ip_api_parse_response parses a successful lookup") {
|
||||
const char* response =
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
"X-Ttl: 60\r\n"
|
||||
"X-Rl: 44\r\n"
|
||||
"\r\n"
|
||||
"success\n"
|
||||
"39.03\n"
|
||||
"-77.5\n";
|
||||
IpApiResponse parsed;
|
||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||
CHECK(parsed.status_code == 200);
|
||||
CHECK(parsed.success);
|
||||
CHECK(parsed.latitude == doctest::Approx(39.03));
|
||||
CHECK(parsed.longitude == doctest::Approx(-77.5));
|
||||
CHECK(parsed.rate_remaining == 44);
|
||||
CHECK(parsed.rate_reset_seconds == 60);
|
||||
}
|
||||
|
||||
TEST_CASE("ip_api_parse_response parses a failed lookup") {
|
||||
const char* response = "HTTP/1.1 200 OK\r\nx-rl: 0\r\nx-ttl: 12\r\n\r\nfail\nprivate range\n";
|
||||
IpApiResponse parsed;
|
||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||
CHECK_FALSE(parsed.success);
|
||||
CHECK(strcmp(parsed.message, "private range") == 0);
|
||||
CHECK(parsed.rate_remaining == 0);
|
||||
CHECK(parsed.rate_reset_seconds == 12);
|
||||
}
|
||||
|
||||
TEST_CASE("ip_api_parse_response handles a throttled request") {
|
||||
const char* response = "HTTP/1.1 429 Too Many Requests\r\nX-Rl: 0\r\nX-Ttl: 30\r\n\r\n";
|
||||
IpApiResponse parsed;
|
||||
REQUIRE(ip_api_parse_response(response, &parsed));
|
||||
CHECK(parsed.status_code == 429);
|
||||
CHECK_FALSE(parsed.success);
|
||||
CHECK(parsed.rate_reset_seconds == 30);
|
||||
}
|
||||
|
||||
TEST_CASE("ip_api_parse_response rejects malformed responses") {
|
||||
IpApiResponse parsed;
|
||||
CHECK_FALSE(ip_api_parse_response("", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("garbage", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\nX-Rl: 1\r\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\nabc\n1.0\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nunknown\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\nnan\n1.0\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n1.0\ninf\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n90.5\n1.0\n", &parsed));
|
||||
CHECK_FALSE(ip_api_parse_response("HTTP/1.1 200 OK\r\n\r\nsuccess\n1.0\n-180.5\n", &parsed));
|
||||
}
|
||||
48
Modules/location-module/tests/source/main.cpp
Normal file
48
Modules/location-module/tests/source/main.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT
|
||||
#include "doctest.h"
|
||||
#include <tactility/check.h>
|
||||
|
||||
#include <tactility/dts.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
#include <tactility/kernel_init.h>
|
||||
|
||||
|
||||
typedef struct {
|
||||
int argc;
|
||||
char** argv;
|
||||
int result;
|
||||
} TestTaskData;
|
||||
|
||||
extern "C" struct Module platform_posix_module;
|
||||
|
||||
void test_task(void* parameter) {
|
||||
auto* data = (TestTaskData*)parameter;
|
||||
|
||||
doctest::Context context;
|
||||
context.applyCommandLine(data->argc, data->argv);
|
||||
context.setOption("no-breaks", true);
|
||||
|
||||
Module* dts_modules[] = { &platform_posix_module, nullptr };
|
||||
DtsDevice dts_devices[] = { DTS_DEVICE_TERMINATOR };
|
||||
check(kernel_init(dts_modules, dts_devices) == ERROR_NONE);
|
||||
|
||||
data->result = context.run();
|
||||
|
||||
vTaskEndScheduler();
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
.result = 0
|
||||
};
|
||||
|
||||
if (xTaskCreate(test_task, "test_task", 16384, &data, 1, nullptr) != pdPASS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vTaskStartScheduler();
|
||||
return data.result;
|
||||
}
|
||||
427
Modules/location-module/tests/source/service_test.cpp
Normal file
427
Modules/location-module/tests/source/service_test.cpp
Normal file
@ -0,0 +1,427 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <location/location.h>
|
||||
#include <location/gps_provider.h>
|
||||
#include <location/location_service.h>
|
||||
#include <location/provider.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// Providers start and stop on the service's worker
|
||||
std::atomic<LocationHost*> fakeHost = nullptr;
|
||||
std::atomic<int> createCount = 0;
|
||||
std::atomic<int> destroyCount = 0;
|
||||
|
||||
void* fakeCreate(LocationHost* host) {
|
||||
fakeHost = host;
|
||||
createCount++;
|
||||
return host;
|
||||
}
|
||||
|
||||
void fakeRequestStop(void*) {}
|
||||
|
||||
bool fakeWaitStopped(void*, TickType_t) {
|
||||
fakeHost = nullptr;
|
||||
destroyCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi fakeProvider = {
|
||||
.id = "fake",
|
||||
.fallback = false,
|
||||
.create = fakeCreate,
|
||||
.request_stop = fakeRequestStop,
|
||||
.wait_stopped = fakeWaitStopped,
|
||||
};
|
||||
|
||||
template <typename Predicate>
|
||||
bool waitUntil(Predicate predicate) {
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (predicate()) {
|
||||
return true;
|
||||
}
|
||||
delay_millis(10);
|
||||
}
|
||||
return predicate();
|
||||
}
|
||||
|
||||
Location makeLocation(double latitude, double longitude) {
|
||||
return Location {
|
||||
.latitude = latitude,
|
||||
.longitude = longitude,
|
||||
.horizontal_accuracy = 3.0f,
|
||||
.altitude = NAN,
|
||||
.ellipsoidal_altitude = NAN,
|
||||
.vertical_accuracy = NAN,
|
||||
.speed = NAN,
|
||||
.speed_accuracy = NAN,
|
||||
.course = NAN,
|
||||
.course_accuracy = NAN,
|
||||
.satellites = 0,
|
||||
.time_ms = 1000,
|
||||
.uptime_ms = 0,
|
||||
.provider = {},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("location service reports fixes of its providers") {
|
||||
createCount = 0;
|
||||
destroyCount = 0;
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(location_provider_register(&fakeProvider) == ERROR_ALREADY_EXISTS);
|
||||
|
||||
// Providers don't run without subscriptions
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
CHECK(createCount == 0);
|
||||
CHECK_FALSE(location_is_in_use());
|
||||
|
||||
Location location;
|
||||
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
CHECK(location_subscribe(&sub, &group) == ERROR_INVALID_STATE);
|
||||
CHECK(location_is_in_use());
|
||||
REQUIRE(waitUntil([] { return fakeHost != nullptr; }));
|
||||
CHECK(createCount == 1);
|
||||
|
||||
const Location reported = makeLocation(51.05, 3.72);
|
||||
location_host_report(fakeHost, &reported);
|
||||
|
||||
uint32_t flags = 0;
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, &flags, 0) == ERROR_NONE);
|
||||
CHECK(flags == sub.bit);
|
||||
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(location.latitude == doctest::Approx(51.05));
|
||||
CHECK(location.longitude == doctest::Approx(3.72));
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(3.0));
|
||||
CHECK(location.time_ms == 1000);
|
||||
CHECK(strcmp(location.provider, "fake") == 0);
|
||||
|
||||
// The last known fix remains available after the last subscription is removed
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NOT_FOUND);
|
||||
CHECK_FALSE(location_is_in_use());
|
||||
CHECK(waitUntil([] { return destroyCount == 1; }));
|
||||
CHECK(location_get(&location) == ERROR_NONE);
|
||||
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
CHECK(waitUntil([] { return createCount == 2; }));
|
||||
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK_FALSE(location_is_in_use());
|
||||
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||
CHECK(waitUntil([] { return destroyCount == 2; }));
|
||||
|
||||
REQUIRE(service_manager_start(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(waitUntil([] { return createCount == 3; }));
|
||||
// Unregistering waits for the provider to stop
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(destroyCount == 3);
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NOT_FOUND);
|
||||
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(destroyCount == 3);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("location provider registration is limited") {
|
||||
LocationProviderApi providers[LOCATION_PROVIDER_MAX_COUNT + 1];
|
||||
char ids[LOCATION_PROVIDER_MAX_COUNT + 1][8];
|
||||
for (size_t i = 0; i <= LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||
snprintf(ids[i], sizeof(ids[i]), "p%zu", i);
|
||||
providers[i] = { .id = ids[i], .fallback = false, .create = fakeCreate, .request_stop = fakeRequestStop, .wait_stopped = fakeWaitStopped };
|
||||
}
|
||||
for (size_t i = 0; i < LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||
REQUIRE(location_provider_register(&providers[i]) == ERROR_NONE);
|
||||
}
|
||||
CHECK(location_provider_register(&providers[LOCATION_PROVIDER_MAX_COUNT]) == ERROR_RESOURCE);
|
||||
for (size_t i = 0; i < LOCATION_PROVIDER_MAX_COUNT; i++) {
|
||||
CHECK(location_provider_unregister(&providers[i]) == ERROR_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<LocationHost*> fallbackHost = nullptr;
|
||||
|
||||
void* fallbackCreate(LocationHost* host) {
|
||||
fallbackHost = host;
|
||||
return host;
|
||||
}
|
||||
|
||||
void fallbackRequestStop(void*) {}
|
||||
|
||||
bool fallbackWaitStopped(void*, TickType_t) {
|
||||
fallbackHost = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi fakeFallbackProvider = {
|
||||
.id = "fakefallback",
|
||||
.fallback = true,
|
||||
.create = fallbackCreate,
|
||||
.request_stop = fallbackRequestStop,
|
||||
.wait_stopped = fallbackWaitStopped,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("location service prefers fixes of providers that aren't a fallback") {
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
REQUIRE(location_provider_register(&fakeFallbackProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
REQUIRE(waitUntil([] { return fakeHost != nullptr && fallbackHost != nullptr; }));
|
||||
|
||||
CHECK(location_host_needs_fallback());
|
||||
const Location fallback = makeLocation(10.0, 20.0);
|
||||
location_host_report(fallbackHost, &fallback);
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_NONE);
|
||||
|
||||
Location location;
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||
|
||||
const Location primary = makeLocation(51.05, 3.72);
|
||||
location_host_report(fakeHost, &primary);
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_NONE);
|
||||
CHECK_FALSE(location_host_needs_fallback());
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fake") == 0);
|
||||
|
||||
// A fallback fix doesn't replace a fresh fix, so subscribers aren't signalled
|
||||
location_host_report(fallbackHost, &fallback);
|
||||
CHECK(task_event_group_wait(&group, sub.bit, false, nullptr, 0) == ERROR_TIMEOUT);
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fake") == 0);
|
||||
|
||||
// Without the primary provider, the fallback fix is used again
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(location_host_needs_fallback());
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&fakeFallbackProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("location_make_approximate keeps only a coarse position") {
|
||||
Location location = makeLocation(51.054321, 3.721234);
|
||||
location.altitude = 12.0f;
|
||||
location.speed = 1.5f;
|
||||
location.satellites = 7;
|
||||
location_make_approximate(&location);
|
||||
CHECK(location.latitude == doctest::Approx(51.06));
|
||||
CHECK(location.longitude == doctest::Approx(3.72));
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(LOCATION_APPROXIMATE_ACCURACY_METERS));
|
||||
CHECK(std::isnan(location.altitude));
|
||||
CHECK(std::isnan(location.speed));
|
||||
CHECK(location.satellites == 0);
|
||||
CHECK(location.time_ms == 1000);
|
||||
|
||||
location.horizontal_accuracy = 25000.0f;
|
||||
location_make_approximate(&location);
|
||||
CHECK(location.horizontal_accuracy == doctest::Approx(25000.0));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<LocationHost*> stuckHost = nullptr;
|
||||
std::atomic<int> stuckCreateCount = 0;
|
||||
std::atomic<bool> stuck = false;
|
||||
|
||||
void* stuckCreate(LocationHost* host) {
|
||||
stuckHost = host;
|
||||
stuckCreateCount++;
|
||||
return host;
|
||||
}
|
||||
|
||||
void stuckRequestStop(void*) {}
|
||||
|
||||
bool stuckWaitStopped(void*, TickType_t timeout) {
|
||||
if (stuck) {
|
||||
vTaskDelay(timeout);
|
||||
return false;
|
||||
}
|
||||
stuckHost = nullptr;
|
||||
return true;
|
||||
}
|
||||
|
||||
const LocationProviderApi stuckProvider = {
|
||||
.id = "stuck",
|
||||
.fallback = false,
|
||||
.create = stuckCreate,
|
||||
.request_stop = stuckRequestStop,
|
||||
.wait_stopped = stuckWaitStopped,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("location service doesn't wait for a provider that is slow to stop") {
|
||||
createCount = 0;
|
||||
destroyCount = 0;
|
||||
stuckCreateCount = 0;
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
REQUIRE(location_provider_register(&stuckProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
REQUIRE(waitUntil([] { return fakeHost != nullptr && stuckHost != nullptr; }));
|
||||
LocationHost* stoppingHost = stuckHost;
|
||||
|
||||
stuck = true;
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
// The other provider stops while the stuck one is still stopping
|
||||
CHECK(waitUntil([] { return destroyCount == 1; }));
|
||||
|
||||
// A provider that is stopping doesn't report anymore
|
||||
const Location reported = makeLocation(51.05, 3.72);
|
||||
location_host_report(stoppingHost, &reported);
|
||||
Location location;
|
||||
CHECK(location_get(&location) == ERROR_NOT_FOUND);
|
||||
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
CHECK(waitUntil([] { return createCount == 2; }));
|
||||
CHECK(stuckCreateCount == 1);
|
||||
|
||||
// Once stopped, the provider starts again because it's desired again
|
||||
stuck = false;
|
||||
CHECK(waitUntil([] { return stuckCreateCount == 2; }));
|
||||
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&stuckProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** @return the names of the location tasks that weren't deleted, separated by spaces */
|
||||
std::string getLocationTasks() {
|
||||
TaskStatus_t statuses[32];
|
||||
const UBaseType_t count = uxTaskGetSystemState(statuses, 32, nullptr);
|
||||
std::string names;
|
||||
for (UBaseType_t i = 0; i < count; i++) {
|
||||
if (statuses[i].eCurrentState != eDeleted && strncmp(statuses[i].pcTaskName, "location", 8) == 0) {
|
||||
names += statuses[i].pcTaskName;
|
||||
names += ' ';
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("location service has no background task without subscriptions") {
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
REQUIRE(waitUntil([] { return fakeHost != nullptr; }));
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
REQUIRE(waitUntil([] { return fakeHost == nullptr; }));
|
||||
|
||||
// The worker exits once the provider stopped
|
||||
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||
INFO("location tasks: ", getLocationTasks());
|
||||
CHECK(getLocationTasks().empty());
|
||||
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("the gps provider has no task without subscriptions") {
|
||||
REQUIRE(location_provider_register(&gps_location_provider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
CHECK(waitUntil([] { return getLocationTasks().find("location_gps") != std::string::npos; }));
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
|
||||
// Without a GPS device, the provider polls for one every second
|
||||
CHECK(waitUntil([] { return getLocationTasks().empty(); }));
|
||||
INFO("location tasks: ", getLocationTasks());
|
||||
CHECK(getLocationTasks().empty());
|
||||
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&gps_location_provider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
|
||||
TEST_CASE("selecting approximate precision rounds coarse fixes and drops precise ones") {
|
||||
REQUIRE(location_provider_register(&fakeProvider) == ERROR_NONE);
|
||||
REQUIRE(location_provider_register(&fakeFallbackProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_add(&location_service_manifest, true) == ERROR_NONE);
|
||||
const LocationPrecision originalPrecision = location_get_precision();
|
||||
// Saving the setting fails without a data partition, but the precision changes regardless
|
||||
location_set_precision(LOCATION_PRECISION_PRECISE);
|
||||
|
||||
TaskEventGroup group;
|
||||
task_event_group_construct(&group);
|
||||
LocationSubscription sub {};
|
||||
REQUIRE(location_subscribe(&sub, &group) == ERROR_NONE);
|
||||
REQUIRE(waitUntil([] { return fakeHost != nullptr && fallbackHost != nullptr; }));
|
||||
|
||||
Location coarse = makeLocation(51.054321, 3.721234);
|
||||
coarse.horizontal_accuracy = 25000.0f;
|
||||
location_host_report(fallbackHost, &coarse);
|
||||
const Location precise = makeLocation(10.0, 20.0);
|
||||
location_host_report(fakeHost, &precise);
|
||||
|
||||
location_set_precision(LOCATION_PRECISION_APPROXIMATE);
|
||||
REQUIRE(location_get_precision() == LOCATION_PRECISION_APPROXIMATE);
|
||||
Location location;
|
||||
REQUIRE(location_get(&location) == ERROR_NONE);
|
||||
CHECK(strcmp(location.provider, "fakefallback") == 0);
|
||||
CHECK(location.latitude == doctest::Approx(51.06));
|
||||
CHECK(location.longitude == doctest::Approx(3.72));
|
||||
|
||||
location_set_precision(originalPrecision);
|
||||
CHECK(location_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&group);
|
||||
REQUIRE(service_manager_stop(location_service_manifest.id) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&fakeProvider) == ERROR_NONE);
|
||||
CHECK(location_provider_unregister(&fakeFallbackProvider) == ERROR_NONE);
|
||||
REQUIRE(service_manager_remove(location_service_manifest.id) == ERROR_NONE);
|
||||
}
|
||||
@ -68,14 +68,15 @@ error_t wifi_settings_remove(const char* ssid);
|
||||
void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, void* context));
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -42,6 +42,7 @@ list(APPEND REQUIRES_LIST
|
||||
gps-module
|
||||
graphics-module
|
||||
http-module
|
||||
location-module
|
||||
mbedtls-module
|
||||
mesh-module
|
||||
mesh-meshcore-module
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace tt::app::gpssettings {
|
||||
|
||||
void start();
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,388 +0,0 @@
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/time.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <lvgl.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gps/gps.h>
|
||||
#include <gps/gps_settings.h>
|
||||
#include <gps_generic/gps_generic.h>
|
||||
|
||||
namespace tt::app::addgps {
|
||||
extern const ::AppManifest manifest;
|
||||
}
|
||||
|
||||
namespace tt::app::gpssettings {
|
||||
|
||||
extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
struct DeviceRow {
|
||||
Device* device;
|
||||
lv_obj_t* button;
|
||||
lv_obj_t* buttonLabel;
|
||||
bool hasConfiguration = false;
|
||||
size_t configurationIndex = 0;
|
||||
};
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
lv_obj_t* deviceListWrapper = nullptr;
|
||||
std::vector<DeviceRow> deviceRows;
|
||||
std::unique_ptr<Timer> timer;
|
||||
|
||||
// Set when a delete confirmation is pending; read/cleared on this app's own thread when
|
||||
// the dialog's result arrives.
|
||||
bool hasPendingDelete = false;
|
||||
Device* pendingDeleteDevice = nullptr;
|
||||
size_t pendingDeleteIndex = 0;
|
||||
};
|
||||
|
||||
|
||||
void rebuildDeviceList(Context* ctx);
|
||||
void updateDeviceStates(Context* ctx);
|
||||
void createWidgets(lv_obj_t* parent, void* userData);
|
||||
void destroyWidgets(void* userData);
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
void onAddGpsPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
// Fire-and-forget top-level launch, matching the original (its result never fed back into
|
||||
// this app; rebuildDeviceList() runs fresh whenever this app is resumed regardless).
|
||||
(void)ctx;
|
||||
uint32_t instanceId = 0;
|
||||
AppStartContext context = app_start_context_for_manifest(&addgps::manifest);
|
||||
app_start_with_context(&context, &instanceId);
|
||||
}
|
||||
|
||||
void onDeviceButtonPressed(lv_event_t* event) {
|
||||
auto* button = lv_event_get_target_obj(event);
|
||||
auto* device = static_cast<Device*>(lv_obj_get_user_data(button));
|
||||
|
||||
bool running = device_is_ready(device);
|
||||
// device_start()/device_stop() are potentially blocking calls, so use a dispatcher to not block the UI
|
||||
getMainDispatcher().dispatch([device, running] {
|
||||
if (running) {
|
||||
device_stop(device);
|
||||
} else {
|
||||
device_start(device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Finds the persisted configuration backing `device` (matched by its parent UART's name)
|
||||
// and returns its index into gps_settings_for_each_configuration()'s ordering - the handle
|
||||
// gps_settings_remove_configuration_at() needs to delete exactly this entry, even if another
|
||||
// entry happens to have identical field values.
|
||||
// Devicetree-declared GPS_TYPE devices have no such configuration and never match.
|
||||
bool findConfigurationIndexForDevice(Device* device, size_t& outIndex) {
|
||||
auto* parent = device_get_parent(device);
|
||||
if (parent == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct FindContext {
|
||||
const char* uartName;
|
||||
size_t* outIndex;
|
||||
bool found;
|
||||
} findContext = { parent->name, &outIndex, false };
|
||||
|
||||
gps_settings_for_each_configuration(&findContext, [](const GpsConfiguration* configuration, size_t index, void* untyped_context) {
|
||||
auto* ctx = static_cast<FindContext*>(untyped_context);
|
||||
if (!ctx->found && strcmp(configuration->uart_name, ctx->uartName) == 0) {
|
||||
*ctx->outIndex = index;
|
||||
ctx->found = true;
|
||||
}
|
||||
});
|
||||
|
||||
return findContext.found;
|
||||
}
|
||||
|
||||
void onDeleteButtonPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* button = lv_event_get_target_obj(event);
|
||||
auto* device = static_cast<Device*>(lv_obj_get_user_data(button));
|
||||
|
||||
for (auto& row : ctx->deviceRows) {
|
||||
if (row.device == device && row.hasConfiguration) {
|
||||
ctx->pendingDeleteDevice = device;
|
||||
ctx->pendingDeleteIndex = row.configurationIndex;
|
||||
ctx->hasPendingDelete = true;
|
||||
alertdialog::start(ctx->appInstanceId, "Confirmation", std::string("Do you want to delete ") + device->name + "?", std::vector<std::string> { "Yes", "No" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return the bus name and speed, e.g. "ttyACM0 9600 bps" */
|
||||
std::string getConnectionDescription(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
std::string result = parent != nullptr ? parent->name : "";
|
||||
if (device_is_compatible(device, "tactility,gps-generic")) {
|
||||
const auto* config = static_cast<const GpsConfig*>(device->config);
|
||||
result += " " + std::to_string(config->baud_rate) + " bps";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void createDeviceRow(Context* ctx, Device* device) {
|
||||
auto* card = lvgl_card_create(ctx->deviceListWrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
|
||||
// Transparent, so the card provides the background
|
||||
auto* wrapper = lv_obj_create(card);
|
||||
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_border_width(wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(wrapper, 0, 0);
|
||||
lv_obj_set_style_bg_opa(wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(wrapper, false);
|
||||
|
||||
// The texts take the width that the buttons leave. Texts that don't fit scroll.
|
||||
auto* texts = lv_obj_create(wrapper);
|
||||
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_grow(texts, 1);
|
||||
lv_obj_set_flex_flow(texts, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_all(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(texts, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(texts, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(texts, false);
|
||||
|
||||
auto* name_label = lv_label_create(texts);
|
||||
lv_obj_set_width(name_label, LV_PCT(100));
|
||||
lv_label_set_long_mode(name_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
char model_name[64];
|
||||
if (gps_get_model_name(device, model_name, sizeof(model_name)) == ERROR_NONE) {
|
||||
lv_label_set_text(name_label, model_name);
|
||||
} else {
|
||||
lv_label_set_text(name_label, device->name);
|
||||
}
|
||||
|
||||
auto* connection_label = lv_label_create(texts);
|
||||
lv_obj_set_width(connection_label, LV_PCT(100));
|
||||
lv_label_set_long_mode(connection_label, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_font(connection_label, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(connection_label, getConnectionDescription(device).c_str());
|
||||
|
||||
auto* actions_wrapper = lv_obj_create(wrapper);
|
||||
lv_obj_set_size(actions_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(actions_wrapper, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_border_width(actions_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(actions_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_column(actions_wrapper, 4, 0);
|
||||
lv_obj_set_style_bg_opa(actions_wrapper, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
|
||||
auto* button = lv_button_create(actions_wrapper);
|
||||
lv_obj_add_event_cb(button, onDeviceButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
lv_obj_set_user_data(button, device);
|
||||
auto* button_label = lv_label_create(button);
|
||||
lv_label_set_text(button_label, "Start");
|
||||
|
||||
DeviceRow row { .device = device, .button = button, .buttonLabel = button_label };
|
||||
|
||||
// Only devices backed by a persisted configuration (not devicetree-declared ones) can be deleted.
|
||||
size_t configurationIndex;
|
||||
if ((device->flags & DEVICE_FLAG_DYNAMIC) && findConfigurationIndexForDevice(device, configurationIndex)) {
|
||||
auto* delete_button = lv_button_create(actions_wrapper);
|
||||
lv_obj_add_event_cb(delete_button, onDeleteButtonPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
lv_obj_set_user_data(delete_button, device);
|
||||
auto* delete_label = lv_label_create(delete_button);
|
||||
lv_obj_set_style_text_font(delete_label, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(delete_label, LVGL_ICON_SHARED_DELETE);
|
||||
|
||||
row.hasConfiguration = true;
|
||||
row.configurationIndex = configurationIndex;
|
||||
}
|
||||
|
||||
ctx->deviceRows.push_back(row);
|
||||
}
|
||||
|
||||
// Rebuilds the device list. Only needs to run when the set of devices could've changed (on
|
||||
// creation, and after returning from AddGps) - button state itself is refreshed by the timer.
|
||||
void rebuildDeviceList(Context* ctx) {
|
||||
ctx->deviceRows.clear();
|
||||
// Rebuilt by createWidgets() when the window resurfaces
|
||||
if (ctx->deviceListWrapper == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_clean(ctx->deviceListWrapper);
|
||||
|
||||
device_for_each_of_type(&GPS_TYPE, ctx, [](Device* device, void* context) {
|
||||
createDeviceRow(static_cast<Context*>(context), device);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (ctx->deviceRows.empty()) {
|
||||
auto* card = lvgl_card_create(ctx->deviceListWrapper);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
auto* label = lv_label_create(card);
|
||||
lv_label_set_text(label, "No devices configured");
|
||||
}
|
||||
}
|
||||
|
||||
void updateDeviceStates(Context* ctx) {
|
||||
lvgl_lock();
|
||||
for (const auto& row : ctx->deviceRows) {
|
||||
const char* text = "Start";
|
||||
bool enabled = true;
|
||||
|
||||
if (device_is_ready(row.device)) {
|
||||
switch (gps_get_state(row.device)) {
|
||||
case GPS_STATE_PENDING_ON:
|
||||
text = "Starting...";
|
||||
enabled = false;
|
||||
break;
|
||||
case GPS_STATE_PENDING_OFF:
|
||||
text = "Stopping...";
|
||||
enabled = false;
|
||||
break;
|
||||
default:
|
||||
text = "Stop";
|
||||
enabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lv_label_set_text(row.buttonLabel, text);
|
||||
if (enabled) {
|
||||
lv_obj_remove_state(row.button, LV_STATE_DISABLED);
|
||||
} else {
|
||||
lv_obj_add_state(row.button, LV_STATE_DISABLED);
|
||||
}
|
||||
}
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "GPS");
|
||||
// The global toolbar nav callback only knows how to stop old-model apps.
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_PLUS, onAddGpsPressed, ctx);
|
||||
|
||||
ctx->deviceListWrapper = lv_obj_create(parent);
|
||||
lv_obj_set_size(ctx->deviceListWrapper, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(ctx->deviceListWrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_grow(ctx->deviceListWrapper, 1);
|
||||
lv_obj_set_style_border_width(ctx->deviceListWrapper, 0, 0);
|
||||
|
||||
rebuildDeviceList(ctx);
|
||||
updateDeviceStates(ctx);
|
||||
}
|
||||
|
||||
// The window's widgets are deleted while another window is on top, so the timer must not reach them
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->deviceListWrapper = nullptr;
|
||||
ctx->deviceRows.clear();
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
uint32_t appInstanceId = app_scheduler_current_app_id();
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = appInstanceId;
|
||||
|
||||
// Runs for this app instance's whole lifetime - there's no push notification for GPS
|
||||
// device state changes, so this is the only way this screen finds out about them.
|
||||
ctx.timer = std::make_unique<Timer>(Timer::Type::Periodic, seconds_to_ticks(1), [&ctx] {
|
||||
updateDeviceStates(&ctx);
|
||||
});
|
||||
|
||||
TaskEventGroup event_group {};
|
||||
task_event_group_construct(&event_group);
|
||||
|
||||
AppEventSubscription sub {};
|
||||
check(app_event_subscribe(&sub, &event_group) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
ctx.timer->start();
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY);
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&sub, &event) == ERROR_NONE) {
|
||||
switch (event.type) {
|
||||
case APP_EVENT_CLOSE:
|
||||
shouldClose = true;
|
||||
break;
|
||||
case APP_EVENT_RESULT:
|
||||
if (ctx.hasPendingDelete) {
|
||||
ctx.hasPendingDelete = false;
|
||||
if (event.result.result == 0) { // 0 = Yes
|
||||
lvgl_lock();
|
||||
std::erase_if(ctx.deviceRows, [&ctx](const DeviceRow& row) {
|
||||
return row.device == ctx.pendingDeleteDevice;
|
||||
});
|
||||
lvgl_unlock();
|
||||
|
||||
gps_settings_remove_configuration_at(ctx.pendingDeleteIndex);
|
||||
ctx.pendingDeleteDevice = nullptr;
|
||||
|
||||
lvgl_lock();
|
||||
rebuildDeviceList(&ctx);
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
app_manager_stop(event.result.launch_id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (shouldClose) break;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.timer->stop();
|
||||
window_manager_remove(window);
|
||||
check(app_event_unsubscribe(&sub) == ERROR_NONE);
|
||||
task_event_group_destruct(&event_group);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
.id = "tactility.gpssettings",
|
||||
.name = "GPS",
|
||||
.category = APP_CATEGORY_SETTINGS,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.flags = 0,
|
||||
.stack = {}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@ -23,11 +23,11 @@ constexpr IconEntry ICONS[] = {
|
||||
{"tactility.btmanage", LVGL_ICON_SHARED_BLUETOOTH},
|
||||
{"tactility.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},
|
||||
|
||||
441
Tactility/Source/app/locationsettings/LocationSettings.cpp
Normal file
441
Tactility/Source/app/locationsettings/LocationSettings.cpp
Normal file
@ -0,0 +1,441 @@
|
||||
#include <Tactility/app/alertdialog/AlertDialog.h>
|
||||
#include <Tactility/lvgl/Style.h>
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/scheduler.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include <gps/gps.h>
|
||||
#include <gps/gps_settings.h>
|
||||
#include <gps_generic/gps_generic.h>
|
||||
|
||||
#include <location/location.h>
|
||||
|
||||
#include <lvgl_window_manager/window_manager.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
#include <tactility/device.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <lvgl/fonts.h>
|
||||
#include <lvgl/grid_navigation.h>
|
||||
#include <lvgl/icons/shared.h>
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/card.h>
|
||||
#include <lvgl/widgets/chip.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
|
||||
#include <TactilityCpp/std/string.h>
|
||||
#include <TactilityCpp/std/vector.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <iterator>
|
||||
|
||||
namespace tt::app::addgps {
|
||||
extern const ::AppManifest manifest;
|
||||
}
|
||||
|
||||
namespace tt::app::locationsettings {
|
||||
|
||||
extern const ::AppManifest manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
struct PrecisionOption {
|
||||
LocationPrecision precision;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
constexpr PrecisionOption PRECISION_OPTIONS[] = {
|
||||
{ LOCATION_PRECISION_APPROXIMATE, "Approximate" },
|
||||
{ LOCATION_PRECISION_PRECISE, "Precise" },
|
||||
};
|
||||
|
||||
/** A GPS device that is backed by a persisted configuration */
|
||||
struct GpsConfigurationRow {
|
||||
Device* device;
|
||||
size_t configurationIndex;
|
||||
};
|
||||
|
||||
struct Context {
|
||||
uint32_t appInstanceId;
|
||||
// GPS devices are only configurable on hardware that can connect them
|
||||
bool showGpsDevices = false;
|
||||
TaskEventGroup* eventGroup = nullptr;
|
||||
uint32_t enableBit = 0;
|
||||
uint32_t precisionBit = 0;
|
||||
::std::atomic<bool> requestedEnabled = false;
|
||||
::std::atomic<LocationPrecision> requestedPrecision = LOCATION_PRECISION_PRECISE;
|
||||
// Valid while the window is shown
|
||||
lv_obj_t* enableSwitch = nullptr;
|
||||
lv_obj_t* precisionChips = nullptr;
|
||||
lv_obj_t* statusLabel = nullptr;
|
||||
lv_obj_t* gpsDeviceList = nullptr;
|
||||
std::vector<GpsConfigurationRow> gpsConfigurationRows;
|
||||
// Set while a delete confirmation is pending
|
||||
bool hasPendingDelete = false;
|
||||
size_t pendingDeleteIndex = 0;
|
||||
};
|
||||
|
||||
LocationPrecision getChipPrecision(lv_obj_t* chip) {
|
||||
return static_cast<LocationPrecision>(reinterpret_cast<intptr_t>(lv_obj_get_user_data(chip)));
|
||||
}
|
||||
|
||||
std::string getStatusText() {
|
||||
if (!location_is_enabled()) {
|
||||
return "Location services are off";
|
||||
}
|
||||
Location location;
|
||||
if (location_get(&location) != ERROR_NONE) {
|
||||
return "Waiting for location...";
|
||||
}
|
||||
std::string text;
|
||||
auto output = ::std::back_inserter(text);
|
||||
::std::format_to(output, "{:.5f}, {:.5f}", location.latitude, location.longitude);
|
||||
if (!::std::isnan(location.horizontal_accuracy)) {
|
||||
::std::format_to(output, " (±{:.0f} m)", location.horizontal_accuracy);
|
||||
}
|
||||
::std::format_to(output, "\nSource: {}", location.provider);
|
||||
return text;
|
||||
}
|
||||
|
||||
void updateWidgets(Context* ctx) {
|
||||
if (ctx->statusLabel == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_set_state(ctx->enableSwitch, LV_STATE_CHECKED, location_is_enabled());
|
||||
|
||||
const LocationPrecision precision = location_get_precision();
|
||||
const uint32_t count = lv_obj_get_child_count(ctx->precisionChips);
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
auto* chip = lv_obj_get_child(ctx->precisionChips, static_cast<int32_t>(i));
|
||||
lv_obj_set_state(chip, LV_STATE_CHECKED, getChipPrecision(chip) == precision);
|
||||
}
|
||||
|
||||
lv_label_set_text(ctx->statusLabel, getStatusText().c_str());
|
||||
}
|
||||
|
||||
void onBackPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
app_event_emit_close(ctx->appInstanceId);
|
||||
}
|
||||
|
||||
// Starting and stopping the service blocks, so it happens on the app's own task
|
||||
void onEnableSwitchChanged(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->requestedEnabled = lv_obj_has_state(lv_event_get_target_obj(event), LV_STATE_CHECKED);
|
||||
task_event_group_signal(ctx->eventGroup, ctx->enableBit);
|
||||
}
|
||||
|
||||
void onPrecisionChipPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
ctx->requestedPrecision = getChipPrecision(lv_event_get_target_obj(event));
|
||||
task_event_group_signal(ctx->eventGroup, ctx->precisionBit);
|
||||
}
|
||||
|
||||
lv_obj_t* createContainer(lv_obj_t* parent, lv_flex_flow_t flow) {
|
||||
auto* container = lv_obj_create(parent);
|
||||
lv_obj_set_flex_flow(container, flow);
|
||||
lv_obj_set_style_border_width(container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_all(container, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_scrollable(container, false);
|
||||
return container;
|
||||
}
|
||||
|
||||
void onAddGpsPressed(lv_event_t*) {
|
||||
AppStartContext context = app_start_context_for_manifest(&addgps::manifest);
|
||||
uint32_t instanceId = 0;
|
||||
app_start_with_context(&context, &instanceId);
|
||||
}
|
||||
|
||||
void onDeleteGpsPressed(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* device = static_cast<Device*>(lv_obj_get_user_data(lv_event_get_target_obj(event)));
|
||||
for (const auto& row : ctx->gpsConfigurationRows) {
|
||||
if (row.device == device) {
|
||||
ctx->pendingDeleteIndex = row.configurationIndex;
|
||||
ctx->hasPendingDelete = true;
|
||||
alertdialog::start(ctx->appInstanceId, "Confirmation", ::std::string("Do you want to delete ") + device->name + "?", ::std::vector<::std::string> { "Yes", "No" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the persisted configuration of a device by the name of its UART.
|
||||
* The index identifies the entry for gps_settings_remove_configuration_at(), even when entries are identical.
|
||||
*/
|
||||
bool findConfigurationIndex(Device* device, size_t& outIndex) {
|
||||
auto* parent = device_get_parent(device);
|
||||
if (parent == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct FindContext {
|
||||
const char* uartName;
|
||||
size_t* outIndex;
|
||||
bool found;
|
||||
} findContext = { parent->name, &outIndex, false };
|
||||
|
||||
gps_settings_for_each_configuration(&findContext, [](const GpsConfiguration* configuration, size_t index, void* untypedContext) {
|
||||
auto* context = static_cast<FindContext*>(untypedContext);
|
||||
if (!context->found && strcmp(configuration->uart_name, context->uartName) == 0) {
|
||||
*context->outIndex = index;
|
||||
context->found = true;
|
||||
}
|
||||
});
|
||||
return findContext.found;
|
||||
}
|
||||
|
||||
/** @return the bus name and speed, e.g. "ttyACM0 9600 bps" */
|
||||
std::string getConnectionDescription(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
std::string result = parent != nullptr ? parent->name : "";
|
||||
if (device_is_compatible(device, "tactility,gps-generic")) {
|
||||
const auto* config = static_cast<const GpsConfig*>(device->config);
|
||||
::std::format_to(::std::back_inserter(result), " {} bps", config->baud_rate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void createGpsDeviceRow(Context* ctx, Device* device) {
|
||||
auto* row = createContainer(ctx->gpsDeviceList, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
// The texts take the width that the button leaves. Texts that don't fit scroll.
|
||||
auto* texts = createContainer(row, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_height(texts, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_grow(texts, 1);
|
||||
lv_obj_set_style_pad_row(texts, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* nameLabel = lv_label_create(texts);
|
||||
lv_obj_set_width(nameLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(nameLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
char modelName[64];
|
||||
lv_label_set_text(nameLabel, gps_get_model_name(device, modelName, sizeof(modelName)) == ERROR_NONE ? modelName : device->name);
|
||||
|
||||
auto* connectionLabel = lv_label_create(texts);
|
||||
lv_obj_set_width(connectionLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(connectionLabel, LV_LABEL_LONG_MODE_SCROLL_CIRCULAR);
|
||||
lv_obj_set_style_text_font(connectionLabel, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(connectionLabel, getConnectionDescription(device).c_str());
|
||||
|
||||
// Devicetree devices have no persisted configuration and can't be deleted
|
||||
size_t configurationIndex;
|
||||
if ((device->flags & DEVICE_FLAG_DYNAMIC) && findConfigurationIndex(device, configurationIndex)) {
|
||||
auto* deleteButton = lv_button_create(row);
|
||||
lv_obj_set_user_data(deleteButton, device);
|
||||
lv_obj_add_event_cb(deleteButton, onDeleteGpsPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
auto* deleteLabel = lv_label_create(deleteButton);
|
||||
lv_obj_set_style_text_font(deleteLabel, lvgl_get_shared_icon_default_font(), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(deleteLabel, LVGL_ICON_SHARED_DELETE);
|
||||
ctx->gpsConfigurationRows.push_back({ .device = device, .configurationIndex = configurationIndex });
|
||||
}
|
||||
}
|
||||
|
||||
void rebuildGpsDeviceList(Context* ctx) {
|
||||
ctx->gpsConfigurationRows.clear();
|
||||
if (ctx->gpsDeviceList == nullptr) {
|
||||
return;
|
||||
}
|
||||
lv_obj_clean(ctx->gpsDeviceList);
|
||||
|
||||
bool hasDevices = false;
|
||||
struct ForEachContext {
|
||||
Context* ctx;
|
||||
bool* hasDevices;
|
||||
} forEachContext = { ctx, &hasDevices };
|
||||
device_for_each_of_type(&GPS_TYPE, &forEachContext, [](Device* device, void* context) {
|
||||
auto* forEach = static_cast<ForEachContext*>(context);
|
||||
createGpsDeviceRow(forEach->ctx, device);
|
||||
*forEach->hasDevices = true;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!hasDevices) {
|
||||
lv_label_set_text(lv_label_create(ctx->gpsDeviceList), "No devices");
|
||||
}
|
||||
}
|
||||
|
||||
void createGpsDevices(Context* ctx, lv_obj_t* parent) {
|
||||
auto* header = createContainer(parent, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_size(header, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_align(header, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
auto* title = lv_label_create(header);
|
||||
lv_obj_set_flex_grow(title, 1);
|
||||
lv_label_set_text(title, "GPS devices");
|
||||
|
||||
auto* addButton = lv_button_create(header);
|
||||
lv_obj_add_event_cb(addButton, onAddGpsPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
lv_label_set_text(lv_label_create(addButton), LV_SYMBOL_PLUS);
|
||||
|
||||
auto* card = lvgl_card_create(parent);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_left(title, lv_obj_get_style_pad_left(card, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
|
||||
ctx->gpsDeviceList = createContainer(card, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_size(ctx->gpsDeviceList, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
rebuildGpsDeviceList(ctx);
|
||||
}
|
||||
|
||||
void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
|
||||
auto* toolbar = lvgl_toolbar_create(parent, "Location");
|
||||
lvgl_toolbar_set_nav_action(toolbar, LV_SYMBOL_CLOSE, onBackPressed, ctx);
|
||||
ctx->enableSwitch = lvgl_toolbar_add_switch_action(toolbar);
|
||||
lv_obj_add_event_cb(ctx->enableSwitch, onEnableSwitchChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
lv_obj_set_flex_grow(wrapper, 1);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lvgl::obj_set_style_bg_invisible(wrapper);
|
||||
|
||||
auto* precisionLabel = lv_label_create(wrapper);
|
||||
lv_label_set_text(precisionLabel, "Precision");
|
||||
|
||||
// The chips are centered in the card while they fit, and scroll when they don't
|
||||
auto* precisionCard = lvgl_card_create(wrapper);
|
||||
lv_obj_set_size(precisionCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(precisionCard, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(precisionCard, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_left(precisionLabel, lv_obj_get_style_pad_left(precisionCard, LV_PART_MAIN), LV_STATE_DEFAULT);
|
||||
|
||||
ctx->precisionChips = lv_obj_create(precisionCard);
|
||||
lv_obj_set_size(ctx->precisionChips, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_max_width(ctx->precisionChips, LV_PCT(100), LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(ctx->precisionChips, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_scroll_dir(ctx->precisionChips, LV_DIR_HOR);
|
||||
lv_obj_set_style_bg_opa(ctx->precisionChips, LV_OPA_TRANSP, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||
// The chips' margins leave room for their focus rings and space them apart
|
||||
lv_obj_set_style_pad_all(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_pad_column(ctx->precisionChips, 0, LV_STATE_DEFAULT);
|
||||
|
||||
for (const auto& option : PRECISION_OPTIONS) {
|
||||
auto* chip = lvgl_chip_create(ctx->precisionChips);
|
||||
lv_label_set_text(lv_label_create(chip), option.name);
|
||||
lv_obj_set_user_data(chip, reinterpret_cast<void*>(static_cast<intptr_t>(option.precision)));
|
||||
lv_obj_add_event_cb(chip, onPrecisionChipPressed, LV_EVENT_SHORT_CLICKED, ctx);
|
||||
}
|
||||
lvgl_grid_navigation_add(ctx->precisionChips);
|
||||
|
||||
auto* precisionHint = lv_label_create(precisionCard);
|
||||
lv_obj_set_width(precisionHint, LV_PCT(100));
|
||||
lv_label_set_long_mode(precisionHint, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_font(precisionHint, lvgl_get_text_font(FONT_SIZE_SMALL), LV_STATE_DEFAULT);
|
||||
lv_label_set_text(precisionHint, "Approximate location is accurate to a few kilometers. Location based on the IP address is never precise.");
|
||||
|
||||
auto* statusCard = lvgl_card_create(wrapper);
|
||||
lv_obj_set_size(statusCard, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
ctx->statusLabel = lv_label_create(statusCard);
|
||||
lv_obj_set_width(ctx->statusLabel, LV_PCT(100));
|
||||
lv_label_set_long_mode(ctx->statusLabel, LV_LABEL_LONG_WRAP);
|
||||
|
||||
if (ctx->showGpsDevices) {
|
||||
createGpsDevices(ctx, wrapper);
|
||||
}
|
||||
|
||||
updateWidgets(ctx);
|
||||
}
|
||||
|
||||
void destroyWidgets(void* userData) {
|
||||
auto* ctx = static_cast<Context*>(userData);
|
||||
ctx->enableSwitch = nullptr;
|
||||
ctx->precisionChips = nullptr;
|
||||
ctx->statusLabel = nullptr;
|
||||
ctx->gpsDeviceList = nullptr;
|
||||
ctx->gpsConfigurationRows.clear();
|
||||
}
|
||||
|
||||
int32_t appMain(int argc, char* argv[]) {
|
||||
Context ctx {};
|
||||
ctx.appInstanceId = app_scheduler_current_app_id();
|
||||
::AppManifest addGpsManifest;
|
||||
ctx.showGpsDevices = app_manager_find_manifest(addgps::manifest.id, &addGpsManifest) == ERROR_NONE;
|
||||
|
||||
TaskEventGroup eventGroup {};
|
||||
task_event_group_construct(&eventGroup);
|
||||
ctx.eventGroup = &eventGroup;
|
||||
check(task_event_group_claim_bit(&eventGroup, &ctx.enableBit) == ERROR_NONE);
|
||||
check(task_event_group_claim_bit(&eventGroup, &ctx.precisionBit) == ERROR_NONE);
|
||||
|
||||
AppEventSubscription appEvents {};
|
||||
check(app_event_subscribe(&appEvents, &eventGroup) == ERROR_NONE);
|
||||
LocationSubscription locationEvents {};
|
||||
check(location_subscribe(&locationEvents, &eventGroup) == ERROR_NONE);
|
||||
|
||||
WindowId window = window_manager_create_ext(ctx.appInstanceId, createWidgets, destroyWidgets, &ctx);
|
||||
|
||||
bool shouldClose = false;
|
||||
while (!shouldClose) {
|
||||
uint32_t flags = 0;
|
||||
task_event_group_wait_any(&eventGroup, &flags, portMAX_DELAY);
|
||||
|
||||
if (flags & ctx.enableBit) {
|
||||
location_set_enabled(ctx.requestedEnabled);
|
||||
}
|
||||
if (flags & ctx.precisionBit) {
|
||||
location_set_precision(ctx.requestedPrecision);
|
||||
}
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&appEvents, &event) == ERROR_NONE) {
|
||||
if (event.type == APP_EVENT_CLOSE) {
|
||||
shouldClose = true;
|
||||
break;
|
||||
}
|
||||
if (event.type == APP_EVENT_RESULT) {
|
||||
if (ctx.hasPendingDelete) {
|
||||
ctx.hasPendingDelete = false;
|
||||
// The first button is "Yes"
|
||||
if (event.result.result == 0) {
|
||||
gps_settings_remove_configuration_at(ctx.pendingDeleteIndex);
|
||||
lvgl_lock();
|
||||
rebuildGpsDeviceList(&ctx);
|
||||
lvgl_unlock();
|
||||
}
|
||||
}
|
||||
app_manager_stop(event.result.launch_id);
|
||||
}
|
||||
}
|
||||
|
||||
lvgl_lock();
|
||||
updateWidgets(&ctx);
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
window_manager_remove(window);
|
||||
check(location_unsubscribe(&locationEvents) == ERROR_NONE);
|
||||
check(app_event_unsubscribe(&appEvents) == ERROR_NONE);
|
||||
task_event_group_destruct(&eventGroup);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern const ::AppManifest manifest = {
|
||||
.id = "tactility.locationsettings",
|
||||
.name = "Location",
|
||||
.category = APP_CATEGORY_SETTINGS,
|
||||
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },
|
||||
.flags = 0,
|
||||
.stack = {}
|
||||
};
|
||||
|
||||
}
|
||||
@ -16,6 +16,10 @@
|
||||
#include <app/scheduler.h>
|
||||
#include <app/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);
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -320,7 +320,7 @@ void systemStart() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings::shouldEnableOnBoot()) {
|
||||
if (settings::isEnabled()) {
|
||||
start(dev);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -52,6 +52,7 @@ target_link_libraries(TactilityTests PRIVATE
|
||||
gps-module
|
||||
graphics-module
|
||||
http-module
|
||||
location-module
|
||||
mbedtls-module
|
||||
posix-symbols-module
|
||||
pthread-module
|
||||
|
||||
@ -87,6 +87,16 @@ void thread_set_name(Thread* thread, const char* name);
|
||||
*/
|
||||
void thread_set_stack_size(Thread* thread, size_t stack_size);
|
||||
|
||||
/**
|
||||
* @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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user