From 6e437d3288b07ec6bd79cfe7ddf2aee40fe9c12a Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Tue, 13 Oct 2026 23:13:30 +0200 Subject: [PATCH] Location services, kernel Device improvements, de-incubate devices, and more (#691) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **New Features** * Added location services with GPS and network-based positioning, approximate or precise accuracy, and live status updates. * Added a Location Settings app to view status, control location access and accuracy, and manage GPS devices. * Added location choices during device setup and a location toggle in the system menu. * Added support for thread stacks in external memory when available, with fallback to existing memory options. **Improvements** * Wi-Fi and Bluetooth enabled states are saved when changed and applied at startup; separate “Enable on boot” switches are no longer shown in their management screens. * GPS setup can stop promptly when interrupted. * Improved device removal handling during startup and while references are outstanding. * Updated several device listings to no longer be marked as incubating. --- CMakeLists.txt | 6 + Devices/lilygo-t5-epd47-s3/device.properties | 2 +- Devices/lilygo-tdeck-max/device.properties | 2 +- Devices/lilygo-tdeck-pro/device.properties | 2 +- Devices/lilygo-tdisplay/device.properties | 2 +- Devices/lilygo-tdongle-s3/device.properties | 2 +- Devices/m5stack-papers3/device.properties | 2 +- .../waveshare-esp32-s3-geek/device.properties | 2 +- Devices/waveshare-s3-lcd-13/device.properties | 2 +- .../device.properties | 2 +- .../device.properties | 2 +- Documentation/ideas.md | 10 +- .../private/gps_generic/private/init.h | 5 +- .../private/gps_generic/private/interrupt.h | 11 + .../private/gps_generic/private/probe.h | 6 +- .../private/gps_generic/private/ublox.h | 5 +- .../source/gps_meshtastic.cpp | 48 +- Drivers/gps-meshtastic-module/source/init.cpp | 131 +++-- .../source/interrupt.cpp | 20 + .../gps-meshtastic-module/source/probe.cpp | 36 +- .../gps-meshtastic-module/source/ublox.cpp | 361 +++++++----- LICENSE.md | 45 +- Modules/gps-module/source/gps_ledger.cpp | 47 +- Modules/location-module/CMakeLists.txt | 24 + Modules/location-module/LICENSE-Apache-2.0.md | 195 +++++++ .../include/location/location.h | 120 ++++ .../location-module/include/location/module.h | 14 + .../include/location/provider.h | 79 +++ .../private/location/gps_provider.h | 17 + .../private/location/ip_provider.h | 41 ++ .../private/location/location_service.h | 6 + .../private/location/location_settings.h | 16 + .../location-module/source/gps_provider.cpp | 337 +++++++++++ .../location-module/source/ip_provider.cpp | 445 +++++++++++++++ .../source/location_service.cpp | 533 ++++++++++++++++++ .../source/location_settings.cpp | 66 +++ Modules/location-module/source/module.cpp | 66 +++ Modules/location-module/tests/CMakeLists.txt | 20 + .../tests/source/gps_provider_test.cpp | 64 +++ .../tests/source/ip_provider_test.cpp | 57 ++ Modules/location-module/tests/source/main.cpp | 48 ++ .../tests/source/service_test.cpp | 427 ++++++++++++++ .../wifi-module/include/wifi/wifi_settings.h | 9 +- .../wifi-module/source/wifi_autoconnect.cpp | 2 +- Modules/wifi-module/source/wifi_command.cpp | 4 + Modules/wifi-module/source/wifi_settings.cpp | 4 +- .../freertos/freertos_task_hooks.c | 12 + .../freertos/freertos_task_hooks.h | 8 + Tactility/CMakeLists.txt | 1 + .../Tactility/app/gpssettings/GpsSettings.h | 7 - .../Tactility/bluetooth/BluetoothSettings.h | 4 +- .../Private/Tactility/app/btmanage/View.h | 2 - .../Private/Tactility/app/wifimanage/View.h | 2 - Tactility/Source/InitApps.cpp | 4 +- Tactility/Source/InitServices.cpp | 2 + Tactility/Source/app/btmanage/BtManage.cpp | 6 + Tactility/Source/app/btmanage/View.cpp | 54 -- .../Source/app/gpssettings/GpsSettings.cpp | 388 ------------- .../Source/app/launcher/SettingsMode.cpp | 2 +- .../app/locationsettings/LocationSettings.cpp | 441 +++++++++++++++ Tactility/Source/app/setup/Setup.cpp | 25 +- .../app/wifiapsettings/WifiApSettings.cpp | 3 + .../Source/app/wificonnect/WifiConnect.cpp | 3 + Tactility/Source/app/wifimanage/View.cpp | 51 -- .../Source/app/wifimanage/WifiManage.cpp | 9 +- Tactility/Source/bluetooth/Bluetooth.cpp | 2 +- .../Source/bluetooth/BluetoothSettings.cpp | 4 +- Tactility/Source/lvgl/SystemMenu.cpp | 55 +- .../Source/service/statusbar/Statusbar.cpp | 6 +- Tactility/Tests/CMakeLists.txt | 1 + .../include/tactility/concurrent/thread.h | 10 + TactilityKernel/include/tactility/device.h | 27 +- TactilityKernel/source/concurrent/thread.cpp | 44 +- TactilityKernel/source/device.cpp | 29 +- .../tests/source/device_get_put_test.cpp | 110 ++++ Tests/CMakeLists.txt | 2 + 76 files changed, 3809 insertions(+), 850 deletions(-) create mode 100644 Drivers/gps-meshtastic-module/private/gps_generic/private/interrupt.h create mode 100644 Drivers/gps-meshtastic-module/source/interrupt.cpp create mode 100644 Modules/location-module/CMakeLists.txt create mode 100644 Modules/location-module/LICENSE-Apache-2.0.md create mode 100644 Modules/location-module/include/location/location.h create mode 100644 Modules/location-module/include/location/module.h create mode 100644 Modules/location-module/include/location/provider.h create mode 100644 Modules/location-module/private/location/gps_provider.h create mode 100644 Modules/location-module/private/location/ip_provider.h create mode 100644 Modules/location-module/private/location/location_service.h create mode 100644 Modules/location-module/private/location/location_settings.h create mode 100644 Modules/location-module/source/gps_provider.cpp create mode 100644 Modules/location-module/source/ip_provider.cpp create mode 100644 Modules/location-module/source/location_service.cpp create mode 100644 Modules/location-module/source/location_settings.cpp create mode 100644 Modules/location-module/source/module.cpp create mode 100644 Modules/location-module/tests/CMakeLists.txt create mode 100644 Modules/location-module/tests/source/gps_provider_test.cpp create mode 100644 Modules/location-module/tests/source/ip_provider_test.cpp create mode 100644 Modules/location-module/tests/source/main.cpp create mode 100644 Modules/location-module/tests/source/service_test.cpp delete mode 100644 Tactility/Include/Tactility/app/gpssettings/GpsSettings.h delete mode 100644 Tactility/Source/app/gpssettings/GpsSettings.cpp create mode 100644 Tactility/Source/app/locationsettings/LocationSettings.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ccfbf9e6d..dffa1ef7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/Devices/lilygo-t5-epd47-s3/device.properties b/Devices/lilygo-t5-epd47-s3/device.properties index ba0436a20..2d88fe9c7 100644 --- a/Devices/lilygo-t5-epd47-s3/device.properties +++ b/Devices/lilygo-t5-epd47-s3/device.properties @@ -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 diff --git a/Devices/lilygo-tdeck-max/device.properties b/Devices/lilygo-tdeck-max/device.properties index 83b658bbd..483c24698 100644 --- a/Devices/lilygo-tdeck-max/device.properties +++ b/Devices/lilygo-tdeck-max/device.properties @@ -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 diff --git a/Devices/lilygo-tdeck-pro/device.properties b/Devices/lilygo-tdeck-pro/device.properties index 780b8d705..7f5cd169f 100644 --- a/Devices/lilygo-tdeck-pro/device.properties +++ b/Devices/lilygo-tdeck-pro/device.properties @@ -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 diff --git a/Devices/lilygo-tdisplay/device.properties b/Devices/lilygo-tdisplay/device.properties index 079899106..e326f10c6 100644 --- a/Devices/lilygo-tdisplay/device.properties +++ b/Devices/lilygo-tdisplay/device.properties @@ -1,6 +1,6 @@ general.vendor=LilyGO general.name=T-Display -general.incubating=true +general.incubating=false apps.autoStartAppId=tactility.apwebserver hardware.target=ESP32 diff --git a/Devices/lilygo-tdongle-s3/device.properties b/Devices/lilygo-tdongle-s3/device.properties index 3c693beee..2b7605813 100644 --- a/Devices/lilygo-tdongle-s3/device.properties +++ b/Devices/lilygo-tdongle-s3/device.properties @@ -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 diff --git a/Devices/m5stack-papers3/device.properties b/Devices/m5stack-papers3/device.properties index 08a821fa3..c58cf727c 100644 --- a/Devices/m5stack-papers3/device.properties +++ b/Devices/m5stack-papers3/device.properties @@ -1,6 +1,6 @@ general.vendor=M5Stack general.name=PaperS3 -general.incubating=true +general.incubating=false hardware.target=esp32s3 hardware.flashSize=16MB diff --git a/Devices/waveshare-esp32-s3-geek/device.properties b/Devices/waveshare-esp32-s3-geek/device.properties index f65c8ad21..010bf73c0 100644 --- a/Devices/waveshare-esp32-s3-geek/device.properties +++ b/Devices/waveshare-esp32-s3-geek/device.properties @@ -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 diff --git a/Devices/waveshare-s3-lcd-13/device.properties b/Devices/waveshare-s3-lcd-13/device.properties index 45db52e30..67abca070 100644 --- a/Devices/waveshare-s3-lcd-13/device.properties +++ b/Devices/waveshare-s3-lcd-13/device.properties @@ -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 diff --git a/Devices/waveshare-s3-touch-lcd-128/device.properties b/Devices/waveshare-s3-touch-lcd-128/device.properties index 3551cd60c..81959e7cc 100644 --- a/Devices/waveshare-s3-touch-lcd-128/device.properties +++ b/Devices/waveshare-s3-touch-lcd-128/device.properties @@ -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 diff --git a/Devices/waveshare-s3-touch-lcd-147/device.properties b/Devices/waveshare-s3-touch-lcd-147/device.properties index 19783924c..a57fdff95 100644 --- a/Devices/waveshare-s3-touch-lcd-147/device.properties +++ b/Devices/waveshare-s3-touch-lcd-147/device.properties @@ -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 diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 689681620..7127f06cf 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -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. diff --git a/Drivers/gps-meshtastic-module/private/gps_generic/private/init.h b/Drivers/gps-meshtastic-module/private/gps_generic/private/init.h index 1b23058a8..95b4a7df9 100644 --- a/Drivers/gps-meshtastic-module/private/gps_generic/private/init.h +++ b/Drivers/gps-meshtastic-module/private/gps_generic/private/init.h @@ -3,9 +3,12 @@ #include +#include + 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& interrupted); diff --git a/Drivers/gps-meshtastic-module/private/gps_generic/private/interrupt.h b/Drivers/gps-meshtastic-module/private/gps_generic/private/interrupt.h new file mode 100644 index 000000000..cbbb4c6a9 --- /dev/null +++ b/Drivers/gps-meshtastic-module/private/gps_generic/private/interrupt.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0 +#pragma once + +#include +#include + +/** + * Delays unless the GPS thread is interrupted. + * @return false when interrupted + */ +bool gps_delay_millis(const std::atomic& interrupted, uint32_t millis); diff --git a/Drivers/gps-meshtastic-module/private/gps_generic/private/probe.h b/Drivers/gps-meshtastic-module/private/gps_generic/private/probe.h index 39444be3a..aa3aee48f 100644 --- a/Drivers/gps-meshtastic-module/private/gps_generic/private/probe.h +++ b/Drivers/gps-meshtastic-module/private/gps_generic/private/probe.h @@ -3,10 +3,12 @@ #include +#include + 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& interrupted); diff --git a/Drivers/gps-meshtastic-module/private/gps_generic/private/ublox.h b/Drivers/gps-meshtastic-module/private/gps_generic/private/ublox.h index 4bba28ad8..b57bb3a31 100644 --- a/Drivers/gps-meshtastic-module/private/gps_generic/private/ublox.h +++ b/Drivers/gps-meshtastic-module/private/gps_generic/private/ublox.h @@ -3,6 +3,7 @@ #include +#include #include #include @@ -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& interrupted); -bool init(Device* uart, GpsModel model); +bool init(Device* uart, GpsModel model, const std::atomic& interrupted); } diff --git a/Drivers/gps-meshtastic-module/source/gps_meshtastic.cpp b/Drivers/gps-meshtastic-module/source/gps_meshtastic.cpp index 5fe0875e8..8512b5360 100644 --- a/Drivers/gps-meshtastic-module/source/gps_meshtastic.cpp +++ b/Drivers/gps-meshtastic-module/source/gps_meshtastic.cpp @@ -19,6 +19,7 @@ #include +#include #include #include // 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 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(context); auto* internal = static_cast(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)) { diff --git a/Drivers/gps-meshtastic-module/source/init.cpp b/Drivers/gps-meshtastic-module/source/init.cpp index a629f49a8..b406b4675 100644 --- a/Drivers/gps-meshtastic-module/source/init.cpp +++ b/Drivers/gps-meshtastic-module/source/init.cpp @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 #include #include +#include #include #include #include -#include #include #include #include @@ -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& interrupted); +bool init_mtk_l76b(Device* uart, const std::atomic& interrupted); +bool init_mtk_pa1616s(Device* uart, const std::atomic& interrupted); +bool init_atgm336h(Device* uart, const std::atomic& interrupted); +bool init_uc6580(Device* uart, const std::atomic& interrupted); +bool init_ag33xx(Device* uart, const std::atomic& 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& 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& 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(type)); return false; } -bool init_ag33xx(Device* uart) { +bool init_ag33xx(Device* uart, const std::atomic& 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& 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& 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& 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& 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& 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; } diff --git a/Drivers/gps-meshtastic-module/source/interrupt.cpp b/Drivers/gps-meshtastic-module/source/interrupt.cpp new file mode 100644 index 000000000..f1ae1879a --- /dev/null +++ b/Drivers/gps-meshtastic-module/source/interrupt.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0 +#include + +#include + +#include + +constexpr uint32_t DELAY_SLICE_MILLIS = 50; + +bool gps_delay_millis(const std::atomic& 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; +} diff --git a/Drivers/gps-meshtastic-module/source/probe.cpp b/Drivers/gps-meshtastic-module/source/probe.cpp index f56adfd8b..036f81746 100644 --- a/Drivers/gps-meshtastic-module/source/probe.cpp +++ b/Drivers/gps-meshtastic-module/source/probe.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 #include +#include #include #include @@ -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& 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& interrupted) { // Close all NMEA sentences // Valid for L76K, ATGM336H and likely other AT6558 devices uart_controller_write_bytes(uart, reinterpret_cast("$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("$PAIR062,3,0*3D\r\n"), 17, 500); // Save configuration uart_controller_write_bytes(uart, reinterpret_cast("$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("$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 { diff --git a/Drivers/gps-meshtastic-module/source/ublox.cpp b/Drivers/gps-meshtastic-module/source/ublox.cpp index dfd5c15a3..858fd887a 100644 --- a/Drivers/gps-meshtastic-module/source/ublox.cpp +++ b/Drivers/gps-meshtastic-module/source/ublox.cpp @@ -1,14 +1,15 @@ // SPDX-License-Identifier: GPL-3.0 #include #include +#include #include #include -#include #include #include #include +#include #include #include @@ -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& interrupted); +bool init_ublox_789(Device* uart, GpsModel model, const std::atomic& interrupted); +bool init_ublox_10(Device* uart, const std::atomic& 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& 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& 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& 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(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& 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& 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& 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& 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!"); diff --git a/LICENSE.md b/LICENSE.md index e8759e701..f908eb7c3 100644 --- a/LICENSE.md +++ b/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. diff --git a/Modules/gps-module/source/gps_ledger.cpp b/Modules/gps-module/source/gps_ledger.cpp index 4b13a2225..02d87702e 100644 --- a/Modules/gps-module/source/gps_ledger.cpp +++ b/Modules/gps-module/source/gps_ledger.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -14,6 +16,8 @@ #include 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 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(device); + return; + } + delay_millis(DESTROY_RETRY_INTERVAL_MS); } - device_remove(device); - device_destruct(device); - delete reinterpret_cast(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(device); + return true; + }); } static bool is_ledger_owned(const Device* device) { @@ -136,6 +170,7 @@ static std::vector collect_owned_devices() { void gps_ledger_sync() { mutex_lock(&ledger_mutex.mutex); + destroy_pending_devices(); std::vector 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); diff --git a/Modules/location-module/CMakeLists.txt b/Modules/location-module/CMakeLists.txt new file mode 100644 index 000000000..16b39e520 --- /dev/null +++ b/Modules/location-module/CMakeLists.txt @@ -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} +) diff --git a/Modules/location-module/LICENSE-Apache-2.0.md b/Modules/location-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Modules/location-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### 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. + diff --git a/Modules/location-module/include/location/location.h b/Modules/location-module/include/location/location.h new file mode 100644 index 000000000..dd2fdfea0 --- /dev/null +++ b/Modules/location-module/include/location/location.h @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include +#include + +#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 diff --git a/Modules/location-module/include/location/module.h b/Modules/location-module/include/location/module.h new file mode 100644 index 000000000..458154739 --- /dev/null +++ b/Modules/location-module/include/location/module.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern struct Module location_module; + +#ifdef __cplusplus +} +#endif diff --git a/Modules/location-module/include/location/provider.h b/Modules/location-module/include/location/provider.h new file mode 100644 index 000000000..a0e244a62 --- /dev/null +++ b/Modules/location-module/include/location/provider.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +#include + +#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 diff --git a/Modules/location-module/private/location/gps_provider.h b/Modules/location-module/private/location/gps_provider.h new file mode 100644 index 000000000..3c450f01c --- /dev/null +++ b/Modules/location-module/private/location/gps_provider.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include + +/** 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); diff --git a/Modules/location-module/private/location/ip_provider.h b/Modules/location-module/private/location/ip_provider.h new file mode 100644 index 000000000..c2d67f2f6 --- /dev/null +++ b/Modules/location-module/private/location/ip_provider.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +/** + * 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); diff --git a/Modules/location-module/private/location/location_service.h b/Modules/location-module/private/location/location_service.h new file mode 100644 index 000000000..ed5f0cb4b --- /dev/null +++ b/Modules/location-module/private/location/location_service.h @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +extern const ServiceManifest location_service_manifest; diff --git a/Modules/location-module/private/location/location_settings.h b/Modules/location-module/private/location/location_settings.h new file mode 100644 index 000000000..8a37a6d37 --- /dev/null +++ b/Modules/location-module/private/location/location_settings.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#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); diff --git a/Modules/location-module/source/gps_provider.cpp b/Modules/location-module/source/gps_provider.cpp new file mode 100644 index 000000000..afa5cecd2 --- /dev/null +++ b/Modules/location-module/source/gps_provider.cpp @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#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 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(value.value) / value.scale; + const int degrees = static_cast(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(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(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(); + 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(context)->running = false; +} + +bool waitStopped(void* context, TickType_t timeout) { + auto* provider = static_cast(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(gga->satellites_tracked); + } + + return true; +} + +const LocationProviderApi gps_location_provider = { + .id = "gps", + .fallback = false, + .create = create, + .request_stop = requestStop, + .wait_stopped = waitStopped, +}; diff --git a/Modules/location-module/source/ip_provider.cpp b/Modules/location-module/source/ip_provider.cpp new file mode 100644 index 000000000..dcd5642b1 --- /dev/null +++ b/Modules/location-module/source/ip_provider.cpp @@ -0,0 +1,445 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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 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(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(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(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(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(); + } + 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(parsed.rate_reset_seconds) * 1000); + } + return delay; +} + +int32_t threadMain(void* context) { + auto& provider = *static_cast(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(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(waitMs))); + if (!provider.running) { + break; + } + + // The public IP address can change with the network + if (system_event_poll(&provider.networkSubscription) == ERROR_NONE && attempted) { + nextAttemptMs = lastAttemptMs + MIN_INTERVAL_MS; + } + + if (!location_host_needs_fallback()) { + continue; + } + + const uint64_t network = trackedNetwork; + if (cache != nullptr && cache->network == network) { + if (!reportedCache) { + report(provider, cache->latitude, cache->longitude); + reportedCache = true; + } + continue; + } + + const uint64_t attemptMs = getUptimeMs(); + if (attemptMs < nextAttemptMs) { + continue; + } + attempted = true; + lastAttemptMs = attemptMs; + nextAttemptMs = attemptMs + lookup(provider, network); + reportedCache = cache != nullptr && cache->network == network; + } + return 0; +} + +void* create(LocationHost* host) { + auto* provider = tt::newObject(); + 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(context); + provider->running = false; + task_event_group_signal(&provider->group, provider->stopBit); +} + +bool waitStopped(void* context, TickType_t timeout) { + auto* provider = static_cast(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, +}; diff --git a/Modules/location-module/source/location_service.cpp b/Modules/location-module/source/location_service.cpp new file mode 100644 index 000000000..4a60444f2 --- /dev/null +++ b/Modules/location-module/source/location_service.cpp @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#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 getHosts() { + return hostStorage != nullptr ? std::span(hostStorage->hosts) : std::span(); +} + +/** 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(); + 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, +}; diff --git a/Modules/location-module/source/location_settings.cpp b/Modules/location-module/source/location_settings.cpp new file mode 100644 index 000000000..9d0eb7814 --- /dev/null +++ b/Modules/location-module/source/location_settings.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include +#include +#include +#include + +#include + +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); +} diff --git a/Modules/location-module/source/module.cpp b/Modules/location-module/source/module.cpp new file mode 100644 index 000000000..f66755808 --- /dev/null +++ b/Modules/location-module/source/module.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include +#include +#include + +#include + +#include +#include + +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, +}; + +} diff --git a/Modules/location-module/tests/CMakeLists.txt b/Modules/location-module/tests/CMakeLists.txt new file mode 100644 index 000000000..3973f5a84 --- /dev/null +++ b/Modules/location-module/tests/CMakeLists.txt @@ -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 +) diff --git a/Modules/location-module/tests/source/gps_provider_test.cpp b/Modules/location-module/tests/source/gps_provider_test.cpp new file mode 100644 index 000000000..ac3649756 --- /dev/null +++ b/Modules/location-module/tests/source/gps_provider_test.cpp @@ -0,0 +1,64 @@ +#include "doctest.h" + +#include + +#include + +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)); +} diff --git a/Modules/location-module/tests/source/ip_provider_test.cpp b/Modules/location-module/tests/source/ip_provider_test.cpp new file mode 100644 index 000000000..34c1c5862 --- /dev/null +++ b/Modules/location-module/tests/source/ip_provider_test.cpp @@ -0,0 +1,57 @@ +#include "doctest.h" + +#include + +#include + +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)); +} diff --git a/Modules/location-module/tests/source/main.cpp b/Modules/location-module/tests/source/main.cpp new file mode 100644 index 000000000..7adeb09aa --- /dev/null +++ b/Modules/location-module/tests/source/main.cpp @@ -0,0 +1,48 @@ +#define DOCTEST_CONFIG_IMPLEMENT +#include "doctest.h" +#include + +#include +#include +#include + + +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; +} diff --git a/Modules/location-module/tests/source/service_test.cpp b/Modules/location-module/tests/source/service_test.cpp new file mode 100644 index 000000000..fcc27f84a --- /dev/null +++ b/Modules/location-module/tests/source/service_test.cpp @@ -0,0 +1,427 @@ +#include "doctest.h" + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include + +namespace { + +// Providers start and stop on the service's worker +std::atomic fakeHost = nullptr; +std::atomic createCount = 0; +std::atomic 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 +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 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 stuckHost = nullptr; +std::atomic stuckCreateCount = 0; +std::atomic 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); +} diff --git a/Modules/wifi-module/include/wifi/wifi_settings.h b/Modules/wifi-module/include/wifi/wifi_settings.h index 08305424e..61553be8f 100644 --- a/Modules/wifi-module/include/wifi/wifi_settings.h +++ b/Modules/wifi-module/include/wifi/wifi_settings.h @@ -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 } diff --git a/Modules/wifi-module/source/wifi_autoconnect.cpp b/Modules/wifi-module/source/wifi_autoconnect.cpp index 1545ff91c..6a8ece498 100644 --- a/Modules/wifi-module/source/wifi_autoconnect.cpp +++ b/Modules/wifi-module/source/wifi_autoconnect.cpp @@ -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"); diff --git a/Modules/wifi-module/source/wifi_command.cpp b/Modules/wifi-module/source/wifi_command.cpp index 4be75207e..c9680af8f 100644 --- a/Modules/wifi-module/source/wifi_command.cpp +++ b/Modules/wifi-module/source/wifi_command.cpp @@ -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; } diff --git a/Modules/wifi-module/source/wifi_settings.cpp b/Modules/wifi-module/source/wifi_settings.cpp index 4d228ce2f..76c70b005 100644 --- a/Modules/wifi-module/source/wifi_settings.cpp +++ b/Modules/wifi-module/source/wifi_settings.cpp @@ -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; } diff --git a/Platforms/platform-posix/freertos/freertos_task_hooks.c b/Platforms/platform-posix/freertos/freertos_task_hooks.c index ddc7ae04e..c3572599a 100644 --- a/Platforms/platform-posix/freertos/freertos_task_hooks.c +++ b/Platforms/platform-posix/freertos/freertos_task_hooks.c @@ -1,6 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 #include "freertos_task_hooks.h" +#include +#include + 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); +} diff --git a/Platforms/platform-posix/freertos/freertos_task_hooks.h b/Platforms/platform-posix/freertos/freertos_task_hooks.h index 4cb3a2439..28d5f5aff 100644 --- a/Platforms/platform-posix/freertos/freertos_task_hooks.h +++ b/Platforms/platform-posix/freertos/freertos_task_hooks.h @@ -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 diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 67e570d4d..c21afd6c3 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -42,6 +42,7 @@ list(APPEND REQUIRES_LIST gps-module graphics-module http-module + location-module mbedtls-module mesh-module mesh-meshcore-module diff --git a/Tactility/Include/Tactility/app/gpssettings/GpsSettings.h b/Tactility/Include/Tactility/app/gpssettings/GpsSettings.h deleted file mode 100644 index f21380ffd..000000000 --- a/Tactility/Include/Tactility/app/gpssettings/GpsSettings.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -namespace tt::app::gpssettings { - -void start(); - -} \ No newline at end of file diff --git a/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h b/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h index e0656f4be..303a01888 100644 --- a/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h +++ b/Tactility/Include/Tactility/bluetooth/BluetoothSettings.h @@ -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(); diff --git a/Tactility/Private/Tactility/app/btmanage/View.h b/Tactility/Private/Tactility/app/btmanage/View.h index a9887900d..7a603bae7 100644 --- a/Tactility/Private/Tactility/app/btmanage/View.h +++ b/Tactility/Private/Tactility/app/btmanage/View.h @@ -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(); diff --git a/Tactility/Private/Tactility/app/wifimanage/View.h b/Tactility/Private/Tactility/app/wifimanage/View.h index 2e1c0162e..3ae1e5a50 100644 --- a/Tactility/Private/Tactility/app/wifimanage/View.h +++ b/Tactility/Private/Tactility/app/wifimanage/View.h @@ -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(); diff --git a/Tactility/Source/InitApps.cpp b/Tactility/Source/InitApps.cpp index ac220ddea..dde780f4d 100644 --- a/Tactility/Source/InitApps.cpp +++ b/Tactility/Source/InitApps.cpp @@ -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)) { diff --git a/Tactility/Source/InitServices.cpp b/Tactility/Source/InitServices.cpp index c118d3d8b..dd3c83b4b 100644 --- a/Tactility/Source/InitServices.cpp +++ b/Tactility/Source/InitServices.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #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(); diff --git a/Tactility/Source/app/btmanage/BtManage.cpp b/Tactility/Source/app/btmanage/BtManage.cpp index 45e80d427..d094ff99e 100644 --- a/Tactility/Source/app/btmanage/BtManage.cpp +++ b/Tactility/Source/app/btmanage/BtManage.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -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); diff --git a/Tactility/Source/app/btmanage/View.cpp b/Tactility/Source/app/btmanage/View.cpp index b627730ff..431841901 100644 --- a/Tactility/Source/app/btmanage/View.cpp +++ b/Tactility/Source/app/btmanage/View.cpp @@ -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_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_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(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 diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp deleted file mode 100644 index cb3e5bdd7..000000000 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ /dev/null @@ -1,388 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -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 deviceRows; - std::unique_ptr 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(lv_event_get_user_data(event)); - app_event_emit_close(ctx->appInstanceId); -} - -void onAddGpsPressed(lv_event_t* event) { - auto* ctx = static_cast(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(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(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(lv_event_get_user_data(event)); - auto* button = lv_event_get_target_obj(event); - auto* device = static_cast(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 { "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(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), 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(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(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::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(appMain) }, - .flags = 0, - .stack = {} -}; - -} // namespace diff --git a/Tactility/Source/app/launcher/SettingsMode.cpp b/Tactility/Source/app/launcher/SettingsMode.cpp index b3f1e4b91..6174845a4 100644 --- a/Tactility/Source/app/launcher/SettingsMode.cpp +++ b/Tactility/Source/app/launcher/SettingsMode.cpp @@ -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}, diff --git a/Tactility/Source/app/locationsettings/LocationSettings.cpp b/Tactility/Source/app/locationsettings/LocationSettings.cpp new file mode 100644 index 000000000..d204bf0b5 --- /dev/null +++ b/Tactility/Source/app/locationsettings/LocationSettings.cpp @@ -0,0 +1,441 @@ +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +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 requestedEnabled = false; + ::std::atomic 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 gpsConfigurationRows; + // Set while a delete confirmation is pending + bool hasPendingDelete = false; + size_t pendingDeleteIndex = 0; +}; + +LocationPrecision getChipPrecision(lv_obj_t* chip) { + return static_cast(reinterpret_cast(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(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(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(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(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(lv_event_get_user_data(event)); + auto* device = static_cast(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(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(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(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(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(static_cast(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(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(appMain) }, + .flags = 0, + .stack = {} +}; + +} diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp index 293411911..158c18fbd 100644 --- a/Tactility/Source/app/setup/Setup.cpp +++ b/Tactility/Source/app/setup/Setup.cpp @@ -16,6 +16,10 @@ #include #include +#include + +#include + #include #include @@ -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 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); diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 5f53af836..deca9f3b1 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -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) { diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index 8e51f13ed..9acad3e46 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -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) { diff --git a/Tactility/Source/app/wifimanage/View.cpp b/Tactility/Source/app/wifimanage/View.cpp index 98672b3dc..1386c2688 100644 --- a/Tactility/Source/app/wifimanage/View.cpp +++ b/Tactility/Source/app/wifimanage/View.cpp @@ -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_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_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; diff --git a/Tactility/Source/app/wifimanage/WifiManage.cpp b/Tactility/Source/app/wifimanage/WifiManage.cpp index ed84168bc..8d54d616b 100644 --- a/Tactility/Source/app/wifimanage/WifiManage.cpp +++ b/Tactility/Source/app/wifimanage/WifiManage.cpp @@ -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); diff --git a/Tactility/Source/bluetooth/Bluetooth.cpp b/Tactility/Source/bluetooth/Bluetooth.cpp index efc1dc096..c791ff41e 100644 --- a/Tactility/Source/bluetooth/Bluetooth.cpp +++ b/Tactility/Source/bluetooth/Bluetooth.cpp @@ -320,7 +320,7 @@ void systemStart() { return; } - if (settings::shouldEnableOnBoot()) { + if (settings::isEnabled()) { start(dev); } } diff --git a/Tactility/Source/bluetooth/BluetoothSettings.cpp b/Tactility/Source/bluetooth/BluetoothSettings.cpp index cd599b992..17891065a 100644 --- a/Tactility/Source/bluetooth/BluetoothSettings.cpp +++ b/Tactility/Source/bluetooth/BluetoothSettings.cpp @@ -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; } diff --git a/Tactility/Source/lvgl/SystemMenu.cpp b/Tactility/Source/lvgl/SystemMenu.cpp index c4a5e5139..010207c79 100644 --- a/Tactility/Source/lvgl/SystemMenu.cpp +++ b/Tactility/Source/lvgl/SystemMenu.cpp @@ -2,12 +2,15 @@ #include #include +#include #include #include #include -#include +#include + +#include #include #include @@ -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(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); } diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index 1cf25c08e..43f59a72e 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -30,7 +30,7 @@ #include #include -#include +#include #if TT_FEATURE_MESH #include @@ -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); diff --git a/Tactility/Tests/CMakeLists.txt b/Tactility/Tests/CMakeLists.txt index c2ad3631e..4fe147f74 100644 --- a/Tactility/Tests/CMakeLists.txt +++ b/Tactility/Tests/CMakeLists.txt @@ -52,6 +52,7 @@ target_link_libraries(TactilityTests PRIVATE gps-module graphics-module http-module + location-module mbedtls-module posix-symbols-module pthread-module diff --git a/TactilityKernel/include/tactility/concurrent/thread.h b/TactilityKernel/include/tactility/concurrent/thread.h index 230bf86dd..58402dcb8 100644 --- a/TactilityKernel/include/tactility/concurrent/thread.h +++ b/TactilityKernel/include/tactility/concurrent/thread.h @@ -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. diff --git a/TactilityKernel/include/tactility/device.h b/TactilityKernel/include/tactility/device.h index ee1713046..78d79c86c 100644 --- a/TactilityKernel/include/tactility/device.h +++ b/TactilityKernel/include/tactility/device.h @@ -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); diff --git a/TactilityKernel/source/concurrent/thread.cpp b/TactilityKernel/source/concurrent/thread.cpp index 5a3295db3..d863c7d20 100644 --- a/TactilityKernel/source/concurrent/thread.cpp +++ b/TactilityKernel/source/concurrent/thread.cpp @@ -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(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, diff --git a/TactilityKernel/source/device.cpp b/TactilityKernel/source/device.cpp index 45fd59fce..4ae9c3f7e 100644 --- a/TactilityKernel/source/device.cpp +++ b/TactilityKernel/source/device.cpp @@ -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 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; } diff --git a/TactilityKernel/tests/source/device_get_put_test.cpp b/TactilityKernel/tests/source/device_get_put_test.cpp index 458b8514f..4852828ec 100644 --- a/TactilityKernel/tests/source/device_get_put_test.cpp +++ b/TactilityKernel/tests/source/device_get_put_test.cpp @@ -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 blocking_start_entered { false }; +std::atomic 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); +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 598b78af3..57f07f7b9 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -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)