diff --git a/Buildscripts/release-sdk-modules.txt b/Buildscripts/release-sdk-modules.txt index d5741d149..b16665281 100644 --- a/Buildscripts/release-sdk-modules.txt +++ b/Buildscripts/release-sdk-modules.txt @@ -7,4 +7,5 @@ http-module lvgl-module lvgl-window-manager-module service-module -graphics-module \ No newline at end of file +graphics-module +wifi-module diff --git a/CMakeLists.txt b/CMakeLists.txt index e5bda6c2c..de4ec1a12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION}) add_subdirectory(Modules/posix-symbols-module) add_subdirectory(Modules/pthread-module) add_subdirectory(Modules/service-module) + add_subdirectory(Modules/wifi-module) add_subdirectory(Modules/app-module) add_subdirectory(Modules/coreutils-module) add_subdirectory(Modules/app-posix-module) diff --git a/Documentation/ideas.md b/Documentation/ideas.md index d07b0d7fa..b052f38ec 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -10,7 +10,6 @@ - coreutils apps: Only emit colour escapes when stdout is the terminal (command_support.h color() function) - CrashDiagnostics shouldn't show a QR when there's no callstack - Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp -- Get rid of WiFi service (Wifi.cpp/h) in Tactility.cpp - Add bold fonts for e-ink readability improvement - Improve Setup: Add keyboard/keypad navigation explanation - Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module. @@ -23,6 +22,7 @@ ## Medium Priority +- netutils-module: ping, wget, ifconfig, nslookup, etc. - Core2: support power off via software - Improve Setup: Show "Step done" screen - Make it more clear to end-users that an SD card is required to run Tactility diff --git a/Modules/cpp-symbols-module/source/function_instantiations.cpp b/Modules/cpp-symbols-module/source/function_instantiations.cpp new file mode 100644 index 000000000..2bb299fd8 --- /dev/null +++ b/Modules/cpp-symbols-module/source/function_instantiations.cpp @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#ifdef ESP_PLATFORM + +// Out-of-line definitions for the std::function members that module.cpp exports. +// On ESP32, int32_t is long, so this is Thread::MainFunction. Nothing else is guaranteed to emit +// these, so they are explicitly instantiated here. +// Kept out of module.cpp: it declares these same symbols by their mangled names as extern "C". +template long std::function::operator()() const; +template std::function::function(const std::function&); + +#endif diff --git a/Modules/wifi-module/CMakeLists.txt b/Modules/wifi-module/CMakeLists.txt new file mode 100644 index 000000000..3bb2708c7 --- /dev/null +++ b/Modules/wifi-module/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) + +include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake") + +file(GLOB_RECURSE SOURCE_FILES "source/*.c*") + +tactility_add_module(wifi-module + SRCS ${SOURCE_FILES} + PRIV_INCLUDE_DIRS private/ + INCLUDE_DIRS include/ + REQUIRES TactilityKernel app-module service-module crypt-module +) diff --git a/Modules/wifi-module/LICENSE-Apache-2.0.md b/Modules/wifi-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Modules/wifi-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/wifi-module/README.md b/Modules/wifi-module/README.md new file mode 100644 index 000000000..dbaca1d5c --- /dev/null +++ b/Modules/wifi-module/README.md @@ -0,0 +1,5 @@ +# Wi-Fi Module + +This module contains: +- Settings API and a commandline app to manage them. +- Service that manages auto-connect behaviour. diff --git a/Modules/wifi-module/include/wifi/module.h b/Modules/wifi-module/include/wifi/module.h new file mode 100644 index 000000000..4e0ce1113 --- /dev/null +++ b/Modules/wifi-module/include/wifi/module.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** Registers and starts the Wi-Fi auto-connect service. */ +extern struct Module wifi_module; + +#ifdef __cplusplus +} +#endif diff --git a/Modules/wifi-module/include/wifi/wifi_autoconnect.h b/Modules/wifi-module/include/wifi/wifi_autoconnect.h new file mode 100644 index 000000000..6df61f80f --- /dev/null +++ b/Modules/wifi-module/include/wifi/wifi_autoconnect.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Pause auto-connect until the next connection attempt finishes or the radio is turned on. + * Call this before a user-initiated disconnect, so auto-connect doesn't immediately reconnect. + */ +void wifi_autoconnect_pause_until_connected(void); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/wifi-module/include/wifi/wifi_settings.h b/Modules/wifi-module/include/wifi/wifi_settings.h new file mode 100644 index 000000000..08305424e --- /dev/null +++ b/Modules/wifi-module/include/wifi/wifi_settings.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** 32 characters/octets, according to IEEE 802.11-2020 spec */ +#define WIFI_SETTINGS_SSID_LIMIT 32 +/** 64 characters/octets, according to IEEE 802.11-2020 spec */ +#define WIFI_SETTINGS_PASSWORD_LIMIT 64 +/** Default auto-connect setting for new access points */ +#define WIFI_SETTINGS_AUTO_CONNECT_DEFAULT true + +/** The persisted settings of an access point. */ +struct WifiApSettings { + char ssid[WIFI_SETTINGS_SSID_LIMIT + 1]; + char password[WIFI_SETTINGS_PASSWORD_LIMIT + 1]; + bool auto_connect; + /** The Wi-Fi channel, or 0 for any */ + int32_t channel; +}; + +/** + * @param[in] ssid the access point to look for + * @return true if settings exist for the provided SSID + */ +bool wifi_settings_contains(const char* ssid); + +/** + * Load the settings for the provided SSID. + * @param[in] ssid the access point to look for + * @param[out] settings the loaded settings + * @retval ERROR_NOT_FOUND no settings exist for the SSID + * @retval ERROR_RESOURCE the settings could not be read or decrypted + * @retval ERROR_NONE on success + */ +error_t wifi_settings_load(const char* ssid, struct WifiApSettings* settings); + +/** + * Save the settings of an access point. The password is stored encrypted. + * @param[in] settings the settings to save + * @retval ERROR_INVALID_ARGUMENT the SSID is empty + * @retval ERROR_RESOURCE the settings could not be written + * @retval ERROR_NONE on success + */ +error_t wifi_settings_save(const struct WifiApSettings* settings); + +/** + * Remove settings that were saved previously. + * @param[in] ssid the access point to remove the settings for + * @retval ERROR_NOT_FOUND no settings exist for the SSID + * @retval ERROR_RESOURCE the settings could not be removed + * @retval ERROR_NONE on success + */ +error_t wifi_settings_remove(const char* ssid); + +/** + * Iterate over the SSIDs of all saved access points. + * @param[in] context passed through to on_ssid, can be NULL + * @param[in] on_ssid called once per saved access point, return false to stop iterating + */ +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 + * @retval ERROR_RESOURCE the setting could not be written + * @retval ERROR_NONE on success + */ +error_t wifi_settings_set_enable_on_boot(bool enable); + +/** @return true when the radio should be turned on when booting */ +bool wifi_settings_get_enable_on_boot(void); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/wifi-module/private/wifi/private/wifi_service.h b/Modules/wifi-module/private/wifi/private/wifi_service.h new file mode 100644 index 000000000..d87809509 --- /dev/null +++ b/Modules/wifi-module/private/wifi/private/wifi_service.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** The service id, which also determines where the settings are stored */ +#define WIFI_SERVICE_ID "tactility.wifi" + +extern const struct ServiceManifest wifi_service_manifest; + +/** The "wifi" command-line app */ +extern const struct AppManifest wifi_command_manifest; + +/** Imports the "*.ap.properties" files from the "provisioning" directory in the data path. */ +void wifi_provisioning_import(void); + +/** + * Create a directory and its missing parents. + * @retval ERROR_RESOURCE a directory could not be created + * @retval ERROR_NONE on success + */ +error_t wifi_ensure_directory_exists(const char* path); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/wifi-module/source/module.cpp b/Modules/wifi-module/source/module.cpp new file mode 100644 index 000000000..e56d7dec6 --- /dev/null +++ b/Modules/wifi-module/source/module.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include + +#include +#include + +extern "C" { + +static error_t start() { + error_t error = app_manager_add(&wifi_command_manifest); + if (error != ERROR_NONE) { + return error; + } + error = service_manager_add(&wifi_service_manifest, /*auto_start=*/true); + if (error != ERROR_NONE) { + app_manager_remove(wifi_command_manifest.id); + } + return error; +} + +static error_t stop() { + error_t error = service_manager_stop(wifi_service_manifest.id); + if (error != ERROR_NONE && error != ERROR_NOT_FOUND) { + return error; + } + error = service_manager_remove(wifi_service_manifest.id); + if (error != ERROR_NONE) { + return error; + } + return app_manager_remove(wifi_command_manifest.id); +} + +Module wifi_module = { + .name = "wifi", + .start = start, + .stop = stop, + .drivers = nullptr, + .symbols = nullptr, + .internal = nullptr, +}; + +} diff --git a/Modules/wifi-module/source/wifi_autoconnect.cpp b/Modules/wifi-module/source/wifi_autoconnect.cpp new file mode 100644 index 000000000..1545ff91c --- /dev/null +++ b/Modules/wifi-module/source/wifi_autoconnect.cpp @@ -0,0 +1,311 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +constexpr auto* TAG = "wifi_autoconnect"; +constexpr size_t SCAN_RECORD_LIMIT = 16; +constexpr size_t THREAD_STACK_SIZE = 5120; +constexpr TickType_t AUTO_SCAN_INTERVAL = pdMS_TO_TICKS(10000); +// Scan more often than AUTO_SCAN_INTERVAL in case of startup or scan lock failure +constexpr TickType_t AUTO_SCAN_POLL_INTERVAL = pdMS_TO_TICKS(2000); + +// Set by wifi_autoconnect_pause_until_connected(), cleared by a connection result or the radio turning on. +// Distinct from external_scan_pause: the two must not clobber each other, otherwise a caller's explicit +// pause (e.g. AutoScanPauseGuard during a co-processor OTA) can be silently cleared by an unrelated +// connection attempt finishing. +static std::atomic pause_until_connected {false}; +// Only set/cleared through wifi_auto_scan_set_paused() +static std::atomic external_scan_pause {false}; + +struct WifiAutoConnect { + Device* device = nullptr; + // All work (wifi events, periodic scans, boot) runs on this thread, so the ESP-IDF esp_event + // task never runs any of it. + Thread* thread = nullptr; + std::atomic running {false}; + TaskEventGroup event_group {}; + WifiEventSubscription wifi_event_sub {}; + bool wifi_event_subscribed = false; + uint32_t stop_bit = 0; + uint32_t boot_completed_bit = 0; + bool boot_event_subscribed = false; + bool has_scanned = false; + TickType_t last_scan_time = 0; +}; + +static bool is_radio_on(WifiAutoConnect* data) { + WifiRadioState radio = WIFI_RADIO_STATE_OFF; + return wifi_get_radio_state(data->device, &radio) == ERROR_NONE && radio == WIFI_RADIO_STATE_ON; +} + +static bool is_station_active(WifiAutoConnect* data) { + WifiStationState station = WIFI_STATION_STATE_DISCONNECTED; + return wifi_get_station_state(data->device, &station) == ERROR_NONE && station != WIFI_STATION_STATE_DISCONNECTED; +} + +static bool is_paused() { + return pause_until_connected.load() || external_scan_pause.load(); +} + +static bool find_auto_connect_ap(WifiAutoConnect* data, WifiApSettings* out) { + WifiApRecord records[SCAN_RECORD_LIMIT]; + size_t count = SCAN_RECORD_LIMIT; + if (wifi_get_scan_results(data->device, records, &count) != ERROR_NONE) { + return false; + } + + for (size_t i = 0; i < count; i++) { + if (!wifi_settings_contains(records[i].ssid)) { + continue; + } + if (wifi_settings_load(records[i].ssid, out) != ERROR_NONE) { + LOG_E(TAG, "Failed to load credentials for ssid %s", records[i].ssid); + } else if (out->auto_connect) { + return true; + } + } + return false; +} + +static void auto_connect(WifiAutoConnect* data) { + // This runs on every SCAN_FINISHED, not just this service's own scans (e.g. WifiManage re-scans + // on show), so it must honor the pauses. The radio-off check matters because a scan that was + // already in flight can finish after the user turns the radio off. + if (is_paused() || !is_radio_on(data)) { + return; + } + // Already connected or connecting (including a manual attempt by an app): reconnecting would + // override that attempt, or force a pointless disconnect/reconnect blip. + if (is_station_active(data)) { + return; + } + + WifiApSettings target; + if (find_auto_connect_ap(data, &target)) { + LOG_I(TAG, "Auto-connecting to %s", target.ssid); + error_t result = wifi_station_connect(data->device, target.ssid, target.password, target.channel); + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to auto-connect (%s)", error_to_string(result)); + } + } +} + +static void scan_if_needed(WifiAutoConnect* data) { + if (is_paused() || !is_radio_on(data) || is_station_active(data) || wifi_is_scanning(data->device)) { + return; + } + + TickType_t now = get_ticks(); + if (data->has_scanned && (now - data->last_scan_time) < AUTO_SCAN_INTERVAL) { + return; + } + + data->has_scanned = true; + data->last_scan_time = now; + error_t result = wifi_scan(data->device); + if (result != ERROR_NONE) { + LOG_I(TAG, "Can't start scan (%s)", error_to_string(result)); + } +} + +static void on_wifi_event(WifiAutoConnect* data, const WifiEvent& event) { + switch (event.type) { + case WIFI_EVENT_TYPE_RADIO_STATE_CHANGED: + if (event.radio_state == WIFI_RADIO_STATE_ON) { + // Resume auto-connect and scan right away + pause_until_connected = false; + data->has_scanned = false; + } + break; + + case WIFI_EVENT_TYPE_SCAN_FINISHED: + auto_connect(data); + break; + + case WIFI_EVENT_TYPE_STATION_STATE_CHANGED: + if (event.station_state == WIFI_STATION_STATE_DISCONNECTED) { + // Don't touch pause_until_connected here: a deliberate disconnect sets it and relies + // on it staying set until a new connection is established. + NetworkDisconnectedEvent disconnected_event = { .device = data->device }; + system_event_emit(KERNEL_EVENT_NETWORK_DISCONNECTED, &disconnected_event, sizeof(disconnected_event)); + } + break; + + case WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT: + // A finished attempt (manual or automatic) ends the pause, so a failed manual attempt + // lets auto-connect try other saved access points. + pause_until_connected = false; + break; + + default: + break; + } +} + +static void on_boot_completed(WifiAutoConnect* data) { + wifi_provisioning_import(); + + if (wifi_settings_get_enable_on_boot()) { + LOG_I(TAG, "Auto-enabling WiFi"); + if (wifi_set_radio_on(data->device) != ERROR_NONE) { + LOG_E(TAG, "Failed to enable WiFi radio"); + } + } +} + +static int32_t thread_main(void* context) { + auto* data = static_cast(context); + while (data->running.load()) { + uint32_t flags = 0; + task_event_group_wait_any(&data->event_group, &flags, AUTO_SCAN_POLL_INTERVAL); + if (!data->running.load()) { + break; + } + + if ((flags & data->boot_completed_bit) != 0) { + on_boot_completed(data); + } + + WifiEvent event {}; + while (wifi_event_poll(&data->wifi_event_sub, &event) == ERROR_NONE) { + on_wifi_event(data, event); + } + + scan_if_needed(data); + } + return 0; +} + +static void on_boot_completed_event(SystemEvent* /*event*/, void* context) { + auto* data = static_cast(context); + task_event_group_signal(&data->event_group, data->boot_completed_bit); +} + +static void set_external_scan_paused(bool paused) { + LOG_I(TAG, "set_external_scan_paused(%d)", (int)paused); + external_scan_pause = paused; +} + +static void* create_service(const ServiceManifest* /*manifest*/) { + return new (std::nothrow) WifiAutoConnect(); +} + +static void destroy_service(const ServiceManifest* /*manifest*/, void* data) { + delete static_cast(data); +} + +static void release_resources(WifiAutoConnect* data) { + if (data->boot_event_subscribed) { + system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed_event); + data->boot_event_subscribed = false; + } + if (data->wifi_event_subscribed) { + wifi_event_unsubscribe(data->device, &data->wifi_event_sub); + data->wifi_event_subscribed = false; + } + task_event_group_destruct(&data->event_group); + device_stop(data->device); + device_put(data->device); + data->device = nullptr; +} + +static error_t on_start(ServiceInstance* /*instance*/, void* context) { + auto* data = static_cast(context); + + wifi_auto_scan_set_paused_function(set_external_scan_paused); + + Device* device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return ERROR_NONE; + } + if (device_start(device) != ERROR_NONE) { + LOG_E(TAG, "Failed to start WiFi device"); + device_put(device); + return ERROR_NONE; + } + data->device = device; + + task_event_group_construct(&data->event_group); + if (task_event_group_claim_bit(&data->event_group, &data->stop_bit) != ERROR_NONE || + task_event_group_claim_bit(&data->event_group, &data->boot_completed_bit) != ERROR_NONE) { + release_resources(data); + return ERROR_RESOURCE; + } + + if (wifi_event_subscribe(device, &data->wifi_event_sub, &data->event_group) == ERROR_NONE) { + data->wifi_event_subscribed = true; + } else { + LOG_E(TAG, "Failed to subscribe to WiFi events"); + } + + if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed_event, data) == ERROR_NONE) { + data->boot_event_subscribed = true; + } + + data->running = true; + data->thread = thread_alloc_full("wifi-autoconnect", THREAD_STACK_SIZE, thread_main, data, -1); + if (data->thread == nullptr || thread_start(data->thread) != ERROR_NONE) { + data->running = false; + if (data->thread != nullptr) { + thread_free(data->thread); + data->thread = nullptr; + } + release_resources(data); + return ERROR_RESOURCE; + } + + return ERROR_NONE; +} + +static void on_stop(ServiceInstance* /*instance*/, void* context) { + auto* data = static_cast(context); + + if (data->device != nullptr) { + data->running = false; + task_event_group_signal(&data->event_group, data->stop_bit); + thread_join(data->thread, portMAX_DELAY, pdMS_TO_TICKS(10)); + thread_free(data->thread); + data->thread = nullptr; + + if (is_radio_on(data)) { + wifi_set_radio_off(data->device); + } + release_resources(data); + } + + pause_until_connected = false; + wifi_auto_scan_set_paused_function(nullptr); +} + +extern "C" { + +void wifi_autoconnect_pause_until_connected(void) { + LOG_I(TAG, "wifi_autoconnect_pause_until_connected()"); + pause_until_connected = true; +} + +const ServiceManifest wifi_service_manifest = { + .id = WIFI_SERVICE_ID, + .create_service = create_service, + .destroy_service = destroy_service, + .on_start = on_start, + .on_stop = on_stop, +}; + +} diff --git a/Modules/wifi-module/source/wifi_command.cpp b/Modules/wifi-module/source/wifi_command.cpp new file mode 100644 index 000000000..4be75207e --- /dev/null +++ b/Modules/wifi-module/source/wifi_command.cpp @@ -0,0 +1,427 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +constexpr size_t SCAN_RECORD_LIMIT = 32; +constexpr TickType_t SCAN_TIMEOUT = pdMS_TO_TICKS(15000); +constexpr TickType_t CONNECT_TIMEOUT = pdMS_TO_TICKS(20000); +constexpr TickType_t DISCONNECT_TIMEOUT = pdMS_TO_TICKS(5000); + +static void print_usage() { + printf( + "usage: wifi \n" + " status\n" + " radio on|off\n" + " scan\n" + " connect -s [-p ] [-c ] [--no-save]\n" + " disconnect\n" + " forget -s \n" + " saved\n" + ); +} + +static int report_error(const char* action, error_t error) { + printf("wifi: %s failed: %s\n", action, error_to_string(error)); + return 1; +} + +static const char* radio_state_to_string(WifiRadioState state) { + switch (state) { + case WIFI_RADIO_STATE_OFF: return "off"; + case WIFI_RADIO_STATE_ON_PENDING: return "turning on"; + case WIFI_RADIO_STATE_ON: return "on"; + case WIFI_RADIO_STATE_OFF_PENDING: return "turning off"; + } + return "unknown"; +} + +static const char* station_state_to_string(WifiStationState state) { + switch (state) { + case WIFI_STATION_STATE_DISCONNECTED: return "disconnected"; + case WIFI_STATION_STATE_CONNECTION_PENDING: return "connecting"; + case WIFI_STATION_STATE_CONNECTED: return "connected"; + } + return "unknown"; +} + +static const char* connection_error_to_string(WifiStationConnectionError error) { + switch (error) { + case WIFI_STATION_CONNECTION_ERROR_NONE: return "none"; + case WIFI_STATION_CONNECTION_ERROR_WRONG_CREDENTIALS: return "wrong credentials"; + case WIFI_STATION_CONNECTION_ERROR_TIMEOUT: return "timeout"; + case WIFI_STATION_CONNECTION_ERROR_TARGET_NOT_FOUND: return "access point not found"; + } + return "unknown"; +} + +/** + * Subscribes to the device's events, runs the action and waits for an event that matches. + * The action runs after subscribing, so an event it causes can't be missed. + * @retval ERROR_TIMEOUT no matching event arrived in time + */ +static error_t run_and_wait( + Device* device, + error_t (*action)(Device* device, void* context), + void* context, + bool (*matches)(const WifiEvent& event), + TickType_t timeout, + WifiEvent* out_event +) { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + WifiEventSubscription subscription {}; + error_t error = wifi_event_subscribe(device, &subscription, &event_group); + if (error != ERROR_NONE) { + task_event_group_destruct(&event_group); + return error; + } + + error = action(device, context); + if (error == ERROR_NONE) { + error = ERROR_TIMEOUT; + TickType_t start = get_ticks(); + TickType_t elapsed = 0; + while (error == ERROR_TIMEOUT && elapsed < timeout) { + task_event_group_wait_any(&event_group, nullptr, timeout - elapsed); + WifiEvent event {}; + while (wifi_event_poll(&subscription, &event) == ERROR_NONE) { + if (matches(event)) { + *out_event = event; + error = ERROR_NONE; + break; + } + } + elapsed = get_ticks() - start; + } + } + + wifi_event_unsubscribe(device, &subscription); + task_event_group_destruct(&event_group); + return error; +} + +static int command_status(Device* device) { + WifiRadioState radio_state = WIFI_RADIO_STATE_OFF; + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + wifi_get_radio_state(device, &radio_state); + wifi_get_station_state(device, &station_state); + + printf("radio: %s\n", radio_state_to_string(radio_state)); + if (radio_state != WIFI_RADIO_STATE_ON) { + return 0; + } + + printf("station: %s\n", station_state_to_string(station_state)); + if (station_state == WIFI_STATION_STATE_DISCONNECTED) { + return 0; + } + + char ssid[33] = {}; + if (wifi_station_get_target_ssid(device, ssid) == ERROR_NONE) { + printf("ssid: %s\n", ssid); + } + if (station_state == WIFI_STATION_STATE_CONNECTED) { + char ip[16] = {}; + if (wifi_station_get_ipv4_address(device, ip) == ERROR_NONE) { + printf("ip: %s\n", ip); + } + int32_t rssi = 0; + if (wifi_station_get_rssi(device, &rssi) == ERROR_NONE) { + printf("rssi: %ld dBm\n", static_cast(rssi)); + } + } + return 0; +} + +static int command_radio(Device* device, int argc, char* argv[]) { + if (argc != 3 || (strcmp(argv[2], "on") != 0 && strcmp(argv[2], "off") != 0)) { + printf("usage: wifi radio on|off\n"); + return 1; + } + + bool on = strcmp(argv[2], "on") == 0; + error_t error = on ? wifi_set_radio_on(device) : wifi_set_radio_off(device); + if (error != ERROR_NONE) { + return report_error(on ? "radio on" : "radio off", error); + } + printf("radio: %s\n", on ? "on" : "off"); + return 0; +} + +static void print_scan_record(const WifiApRecord& record) { + const char* auth = record.authentication_type == WIFI_AUTHENTICATION_TYPE_OPEN ? "open" : "secured"; + const char* saved = wifi_settings_contains(record.ssid) ? "*" : ""; + printf("%-32s %4d dBm ch %2ld %-7s %s\n", record.ssid, record.rssi, static_cast(record.channel), auth, saved); +} + +static int command_scan(Device* device) { + error_t error = wifi_set_radio_on(device); + if (error != ERROR_NONE) { + return report_error("radio on", error); + } + + WifiEvent event {}; + error = run_and_wait( + device, + [](Device* device, void*) { + // A scan that is already running also ends with SCAN_FINISHED + return wifi_is_scanning(device) ? ERROR_NONE : wifi_scan(device); + }, + nullptr, + [](const WifiEvent& event) { return event.type == WIFI_EVENT_TYPE_SCAN_FINISHED; }, + SCAN_TIMEOUT, + &event + ); + if (error != ERROR_NONE) { + return report_error("scan", error); + } + + WifiApRecord records[SCAN_RECORD_LIMIT]; + size_t count = SCAN_RECORD_LIMIT; + error = wifi_get_scan_results(device, records, &count); + if (error != ERROR_NONE) { + return report_error("scan", error); + } + + if (count == 0) { + printf("No access points found\n"); + return 0; + } + for (size_t i = 0; i < count; i++) { + print_scan_record(records[i]); + } + printf("(* = saved)\n"); + return 0; +} + +static error_t disconnect_and_wait(Device* device) { + WifiEvent event {}; + return run_and_wait( + device, + [](Device* device, void*) { return wifi_station_disconnect(device); }, + nullptr, + [](const WifiEvent& event) { + return event.type == WIFI_EVENT_TYPE_STATION_STATE_CHANGED && event.station_state == WIFI_STATION_STATE_DISCONNECTED; + }, + DISCONNECT_TIMEOUT, + &event + ); +} + +static int command_connect(Device* device, int argc, char* argv[]) { + const char* ssid = nullptr; + const char* password = nullptr; + const char* channel = nullptr; + bool save = true; + for (int i = 2; i < argc; i++) { + if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) { + ssid = argv[++i]; + } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { + password = argv[++i]; + } else if (strcmp(argv[i], "-c") == 0 && i + 1 < argc) { + channel = argv[++i]; + } else if (strcmp(argv[i], "--no-save") == 0) { + save = false; + } else { + ssid = nullptr; + break; + } + } + + if (ssid == nullptr) { + printf("usage: wifi connect -s [-p ] [-c ] [--no-save]\n"); + return 1; + } + if (strlen(ssid) == 0 || strlen(ssid) > WIFI_SETTINGS_SSID_LIMIT) { + printf("wifi: SSID must be 1 to %d characters\n", WIFI_SETTINGS_SSID_LIMIT); + return 1; + } + if (password != nullptr && strlen(password) > WIFI_SETTINGS_PASSWORD_LIMIT) { + printf("wifi: password must be at most %d characters\n", WIFI_SETTINGS_PASSWORD_LIMIT); + return 1; + } + + WifiApSettings settings {}; + bool from_saved_settings = password == nullptr && wifi_settings_load(ssid, &settings) == ERROR_NONE; + if (!from_saved_settings) { + strcpy(settings.ssid, ssid); + strcpy(settings.password, password != nullptr ? password : ""); + settings.auto_connect = WIFI_SETTINGS_AUTO_CONNECT_DEFAULT; + settings.channel = 0; + } + if (channel != nullptr) { + settings.channel = static_cast(strtol(channel, nullptr, 10)); + } + + error_t error = wifi_set_radio_on(device); + if (error != ERROR_NONE) { + return report_error("radio on", error); + } + + // Keeps auto-connect out of the way until this attempt finishes + wifi_autoconnect_pause_until_connected(); + + // Disconnecting first keeps the old connection's disconnect from being reported as the result of this attempt + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + wifi_get_station_state(device, &station_state); + if (station_state != WIFI_STATION_STATE_DISCONNECTED) { + error = disconnect_and_wait(device); + if (error != ERROR_NONE) { + return report_error("disconnect", error); + } + } + + printf("Connecting to %s...\n", settings.ssid); + WifiEvent event {}; + error = run_and_wait( + device, + [](Device* device, void* context) { + auto* settings = static_cast(context); + return wifi_station_connect(device, settings->ssid, settings->password, settings->channel); + }, + &settings, + [](const WifiEvent& event) { return event.type == WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT; }, + CONNECT_TIMEOUT, + &event + ); + if (error != ERROR_NONE) { + return report_error("connect", error); + } + if (event.connection_error != WIFI_STATION_CONNECTION_ERROR_NONE) { + printf("wifi: connect failed: %s\n", connection_error_to_string(event.connection_error)); + return 1; + } + + printf("Connected to %s\n", settings.ssid); + if (save && !from_saved_settings) { + error = wifi_settings_save(&settings); + if (error != ERROR_NONE) { + return report_error("saving credentials", error); + } + printf("Credentials saved\n"); + } + return 0; +} + +static int command_disconnect(Device* device) { + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + wifi_get_station_state(device, &station_state); + if (station_state == WIFI_STATION_STATE_DISCONNECTED) { + printf("Not connected\n"); + return 0; + } + + // Keeps auto-connect from immediately reconnecting + wifi_autoconnect_pause_until_connected(); + error_t error = disconnect_and_wait(device); + if (error != ERROR_NONE) { + return report_error("disconnect", error); + } + printf("Disconnected\n"); + return 0; +} + +static int command_forget(int argc, char* argv[]) { + if (argc != 4 || strcmp(argv[2], "-s") != 0) { + printf("usage: wifi forget -s \n"); + return 1; + } + + error_t error = wifi_settings_remove(argv[3]); + if (error == ERROR_NOT_FOUND) { + printf("wifi: %s is not saved\n", argv[3]); + return 1; + } else if (error != ERROR_NONE) { + return report_error("forget", error); + } + printf("Removed %s\n", argv[3]); + return 0; +} + +static int command_saved() { + size_t count = 0; + wifi_settings_for_each(&count, [](const char* ssid, void* context) { + printf("%s\n", ssid); + (*static_cast(context))++; + return true; + }); + if (count == 0) { + printf("No saved access points\n"); + } + return 0; +} + +static int run_device_command(const char* command, int argc, char* argv[]) { + Device* device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) { + printf("wifi: No WiFi device found\n"); + return 1; + } + + int result; + if (strcmp(command, "status") == 0) { + result = command_status(device); + } else if (strcmp(command, "radio") == 0) { + result = command_radio(device, argc, argv); + } else if (strcmp(command, "scan") == 0) { + result = command_scan(device); + } else if (strcmp(command, "connect") == 0) { + result = command_connect(device, argc, argv); + } else { + result = command_disconnect(device); + } + + device_put(device); + return result; +} + +static int32_t wifi_command_main(int argc, char* argv[]) { + if (argc < 2 || strcmp(argv[1], "help") == 0) { + print_usage(); + return 0; + } + + const char* command = argv[1]; + if (strcmp(command, "forget") == 0) { + return command_forget(argc, argv); + } else if (strcmp(command, "saved") == 0) { + return command_saved(); + } else if ( + strcmp(command, "status") == 0 || + strcmp(command, "radio") == 0 || + strcmp(command, "scan") == 0 || + strcmp(command, "connect") == 0 || + strcmp(command, "disconnect") == 0 + ) { + return run_device_command(command, argc, argv); + } + + printf("wifi: unknown command: %s\n", command); + print_usage(); + return 1; +} + +extern "C" { + +const AppManifest wifi_command_manifest = { + .id = "wifi", + .name = "wifi", + .category = APP_CATEGORY_SYSTEM, + .location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast(wifi_command_main) }, + .flags = APP_MANIFEST_FLAG_HIDDEN | APP_MANIFEST_FLAG_HEADLESS, + .stack = { .depth = 5120, .desired_memory_capability = 0 }, +}; + +} diff --git a/Modules/wifi-module/source/wifi_provisioning.cpp b/Modules/wifi-module/source/wifi_provisioning.cpp new file mode 100644 index 000000000..e01010c3e --- /dev/null +++ b/Modules/wifi-module/source/wifi_provisioning.cpp @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +constexpr auto* TAG = "wifi_provisioning"; + +constexpr auto* AP_FILE_SUFFIX = ".ap.properties"; +constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid"; +constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password"; +constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect"; +constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel"; +constexpr auto* AP_PROPERTIES_KEY_AUTO_REMOVE = "autoRemovePropertiesFile"; + +static bool ends_with(const char* text, const char* suffix) { + size_t text_length = strlen(text); + size_t suffix_length = strlen(suffix); + return text_length >= suffix_length && strcmp(text + text_length - suffix_length, suffix) == 0; +} + +static void import_ap(const char* path) { + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + LOG_E(TAG, "Failed to load AP properties at %s", path); + return; + } + + WifiApSettings settings = { + .ssid = {}, + .password = {}, + .auto_connect = true, + .channel = 0 + }; + char value[16]; + bool auto_remove = false; + bool valid = properties_file_get(file, AP_PROPERTIES_KEY_SSID, settings.ssid, sizeof(settings.ssid)) == ERROR_NONE; + if (valid) { + // A missing password means an open network + if (properties_file_get(file, AP_PROPERTIES_KEY_PASSWORD, settings.password, sizeof(settings.password)) == ERROR_BUFFER_OVERFLOW) { + valid = false; + } + if (properties_file_get(file, AP_PROPERTIES_KEY_AUTO_CONNECT, value, sizeof(value)) == ERROR_NONE) { + settings.auto_connect = strcmp(value, "true") == 0; + } + if (properties_file_get(file, AP_PROPERTIES_KEY_CHANNEL, value, sizeof(value)) == ERROR_NONE) { + settings.channel = static_cast(strtol(value, nullptr, 10)); + } + if (properties_file_get(file, AP_PROPERTIES_KEY_AUTO_REMOVE, value, sizeof(value)) == ERROR_NONE) { + auto_remove = strcmp(value, "true") == 0; + } + } + properties_file_close(file); + + if (!valid) { + LOG_E(TAG, "%s is missing a valid ssid or has an invalid password", path); + return; + } + + if (!wifi_settings_contains(settings.ssid)) { + if (wifi_settings_save(&settings) != ERROR_NONE) { + LOG_E(TAG, "Failed to save settings for %s", settings.ssid); + } else { + LOG_I(TAG, "Imported %s from %s", settings.ssid, path); + } + } + + if (auto_remove) { + if (remove(path) != 0) { + LOG_E(TAG, "Failed to auto-remove %s", path); + } else { + LOG_I(TAG, "Auto-removed %s", path); + } + } +} + +extern "C" { + +void wifi_provisioning_import(void) { + char data_path[FILE_MAX_PATH_STRING_LENGTH]; + if (paths_get_data_path(data_path, sizeof(data_path)) != ERROR_NONE) { + LOG_I(TAG, "Skip provisioning: no data path"); + return; + } + + char provisioning_path[FILE_MAX_PATH_STRING_LENGTH]; + int written = snprintf(provisioning_path, sizeof(provisioning_path), "%s/provisioning", data_path); + if (written < 0 || static_cast(written) >= sizeof(provisioning_path)) { + return; + } + + DIR* dir = opendir(provisioning_path); + if (dir == nullptr) { + LOG_I(TAG, "Skip provisioning: no files at %s", provisioning_path); + return; + } + + bool found = false; + dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + if (!ends_with(entry->d_name, AP_FILE_SUFFIX)) { + continue; + } + + char file_path[FILE_MAX_PATH_STRING_LENGTH]; + written = snprintf(file_path, sizeof(file_path), "%s/%s", provisioning_path, entry->d_name); + if (written < 0 || static_cast(written) >= sizeof(file_path)) { + continue; + } + + struct stat file_stat {}; + if (stat(file_path, &file_stat) == 0 && S_ISREG(file_stat.st_mode)) { + found = true; + import_ap(file_path); + } + } + closedir(dir); + + if (!found) { + LOG_W(TAG, "No AP files found at %s", provisioning_path); + } +} + +} diff --git a/Modules/wifi-module/source/wifi_settings.cpp b/Modules/wifi-module/source/wifi_settings.cpp new file mode 100644 index 000000000..4d228ce2f --- /dev/null +++ b/Modules/wifi-module/source/wifi_settings.cpp @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +constexpr auto* TAG = "wifi_settings"; + +constexpr auto* AP_FILE_SUFFIX = ".ap.properties"; +constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid"; +constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password"; +constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect"; +constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel"; + +constexpr auto* SETTINGS_FILE_NAME = "settings.properties"; +constexpr auto* SETTINGS_KEY_ENABLE_ON_BOOT = "enableOnBoot"; + +constexpr size_t CRYPT_BLOCK_SIZE = 16; +// The password is padded to a multiple of the block size before encryption +constexpr size_t ENCRYPTED_PASSWORD_LIMIT = ((WIFI_SETTINGS_PASSWORD_LIMIT + CRYPT_BLOCK_SIZE - 1) / CRYPT_BLOCK_SIZE) * CRYPT_BLOCK_SIZE; +constexpr size_t ENCRYPTED_PASSWORD_HEX_LIMIT = ENCRYPTED_PASSWORD_LIMIT * 2; + +static bool enable_on_boot_cached = false; +static bool enable_on_boot = false; + +static bool is_file(const char* path) { + struct stat path_stat {}; + return stat(path, &path_stat) == 0 && S_ISREG(path_stat.st_mode); +} + +error_t wifi_ensure_directory_exists(const char* path) { + char buffer[FILE_MAX_PATH_STRING_LENGTH]; + if (strlen(path) >= sizeof(buffer)) { + return ERROR_BUFFER_OVERFLOW; + } + strcpy(buffer, path); + + for (char* p = buffer + 1; *p != '\0'; p++) { + if (*p == '/') { + *p = '\0'; + if (mkdir(buffer, 0755) != 0 && errno != EEXIST) { + return ERROR_RESOURCE; + } + *p = '/'; + } + } + if (mkdir(buffer, 0755) != 0 && errno != EEXIST) { + return ERROR_RESOURCE; + } + return ERROR_NONE; +} + +// TODO: The SSID could contain invalid filename characters (e.g. "/", "\" and more) +static error_t get_ap_file_path(const char* ssid, char* out_path, size_t out_path_size) { + char directory[FILE_MAX_PATH_STRING_LENGTH]; + error_t error = service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = snprintf(out_path, out_path_size, "%s/%s%s", directory, ssid, AP_FILE_SUFFIX); + if (written < 0 || static_cast(written) >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +// The IV is derived from the SSID rather than the password/ciphertext, because the SSID is the one +// value that's known and identical at both encrypt time (save) and decrypt time (load). +static bool encrypt_password(const char* ssid, const char* password, char out_hex[ENCRYPTED_PASSWORD_HEX_LIMIT + 1]) { + const size_t length = strlen(password); + const size_t encrypted_length = ((length + CRYPT_BLOCK_SIZE - 1) / CRYPT_BLOCK_SIZE) * CRYPT_BLOCK_SIZE; + + uint8_t padded_password[ENCRYPTED_PASSWORD_LIMIT] = {}; + memcpy(padded_password, password, length); + + uint8_t encrypted[ENCRYPTED_PASSWORD_LIMIT]; + uint8_t iv[16]; + crypt_get_iv(ssid, strlen(ssid), iv); + if (crypt_encrypt(iv, padded_password, encrypted, encrypted_length) != 0) { + LOG_E(TAG, "Failed to encrypt"); + return false; + } + + for (size_t i = 0; i < encrypted_length; i++) { + snprintf(&out_hex[i * 2], 3, "%02x", encrypted[i]); + } + out_hex[encrypted_length * 2] = '\0'; + return true; +} + +static bool decrypt_password(const char* ssid, const char* hex, char out_password[WIFI_SETTINGS_PASSWORD_LIMIT + 1]) { + const size_t hex_length = strlen(hex); + if (hex_length % 2 != 0 || hex_length > ENCRYPTED_PASSWORD_HEX_LIMIT) { + LOG_E(TAG, "Invalid encrypted password length"); + return false; + } + + const size_t encrypted_length = hex_length / 2; + uint8_t encrypted[ENCRYPTED_PASSWORD_LIMIT]; + char byte_hex[3] = {}; + for (size_t i = 0; i < encrypted_length; i++) { + byte_hex[0] = hex[i * 2]; + byte_hex[1] = hex[i * 2 + 1]; + encrypted[i] = static_cast(strtoul(byte_hex, nullptr, 16)); + } + + uint8_t iv[16]; + crypt_get_iv(ssid, strlen(ssid), iv); + + // Space for the null terminator + uint8_t decrypted[ENCRYPTED_PASSWORD_LIMIT + 1] = {}; + int result = crypt_decrypt(iv, encrypted, decrypted, encrypted_length); + if (result != 0) { + LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssid, result); + return false; + } + + strncpy(out_password, reinterpret_cast(decrypted), WIFI_SETTINGS_PASSWORD_LIMIT); + out_password[WIFI_SETTINGS_PASSWORD_LIMIT] = '\0'; + return true; +} + +extern "C" { + +bool wifi_settings_contains(const char* ssid) { + char path[FILE_MAX_PATH_STRING_LENGTH]; + return get_ap_file_path(ssid, path, sizeof(path)) == ERROR_NONE && is_file(path); +} + +error_t wifi_settings_load(const char* ssid, WifiApSettings* settings) { + char path[FILE_MAX_PATH_STRING_LENGTH]; + error_t error = get_ap_file_path(ssid, path, sizeof(path)); + if (error != ERROR_NONE) { + return error; + } + if (!is_file(path)) { + return ERROR_NOT_FOUND; + } + + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + LOG_E(TAG, "Failed to load properties from %s", path); + return ERROR_RESOURCE; + } + + WifiApSettings loaded = { + .ssid = {}, + .password = {}, + .auto_connect = true, + .channel = 0 + }; + char value[ENCRYPTED_PASSWORD_HEX_LIMIT + 1]; + error = ERROR_NONE; + + if (properties_file_get(file, AP_PROPERTIES_KEY_SSID, loaded.ssid, sizeof(loaded.ssid)) != ERROR_NONE) { + LOG_E(TAG, "File does not contain a valid SSID: %s", path); + error = ERROR_RESOURCE; + } else if (properties_file_get(file, AP_PROPERTIES_KEY_PASSWORD, value, sizeof(value)) == ERROR_NONE && + value[0] != '\0' && + !decrypt_password(ssid, value, loaded.password)) { + LOG_E(TAG, "Failed to decrypt password from %s", path); + error = ERROR_RESOURCE; + } else { + if (properties_file_get(file, AP_PROPERTIES_KEY_AUTO_CONNECT, value, sizeof(value)) == ERROR_NONE) { + loaded.auto_connect = strcmp(value, "true") == 0; + } + if (properties_file_get(file, AP_PROPERTIES_KEY_CHANNEL, value, sizeof(value)) == ERROR_NONE) { + loaded.channel = static_cast(strtol(value, nullptr, 10)); + } + } + + properties_file_close(file); + + if (error == ERROR_NONE) { + *settings = loaded; + } + return error; +} + +error_t wifi_settings_save(const WifiApSettings* settings) { + if (settings->ssid[0] == '\0') { + return ERROR_INVALID_ARGUMENT; + } + + char directory[FILE_MAX_PATH_STRING_LENGTH]; + char path[FILE_MAX_PATH_STRING_LENGTH]; + if (service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE || + get_ap_file_path(settings->ssid, path, sizeof(path)) != ERROR_NONE) { + return ERROR_RESOURCE; + } + + if (wifi_ensure_directory_exists(directory) != ERROR_NONE) { + LOG_E(TAG, "Failed to create %s", directory); + return ERROR_RESOURCE; + } + + char password_encrypted[ENCRYPTED_PASSWORD_HEX_LIMIT + 1] = {}; + if (settings->password[0] != '\0' && !encrypt_password(settings->ssid, settings->password, password_encrypted)) { + return ERROR_RESOURCE; + } + + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + LOG_E(TAG, "Failed to open %s", path); + return ERROR_RESOURCE; + } + + char channel[12]; + snprintf(channel, sizeof(channel), "%ld", static_cast(settings->channel)); + + properties_file_set(file, AP_PROPERTIES_KEY_SSID, settings->ssid); + properties_file_set(file, AP_PROPERTIES_KEY_PASSWORD, password_encrypted); + properties_file_set(file, AP_PROPERTIES_KEY_AUTO_CONNECT, settings->auto_connect ? "true" : "false"); + properties_file_set(file, AP_PROPERTIES_KEY_CHANNEL, channel); + + return properties_file_close(file); +} + +error_t wifi_settings_remove(const char* ssid) { + char path[FILE_MAX_PATH_STRING_LENGTH]; + error_t error = get_ap_file_path(ssid, path, sizeof(path)); + if (error != ERROR_NONE) { + return error; + } + if (!is_file(path)) { + return ERROR_NOT_FOUND; + } + return remove(path) == 0 ? ERROR_NONE : ERROR_RESOURCE; +} + +void wifi_settings_for_each(void* context, bool (*on_ssid)(const char* ssid, void* context)) { + char directory[FILE_MAX_PATH_STRING_LENGTH]; + if (service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE) { + return; + } + + DIR* dir = opendir(directory); + if (dir == nullptr) { + return; // No settings saved yet + } + + const size_t suffix_length = strlen(AP_FILE_SUFFIX); + dirent* entry; + while ((entry = readdir(dir)) != nullptr) { + size_t name_length = strlen(entry->d_name); + if (name_length <= suffix_length || strcmp(entry->d_name + name_length - suffix_length, AP_FILE_SUFFIX) != 0) { + continue; + } + + char path[FILE_MAX_PATH_STRING_LENGTH]; + int written = snprintf(path, sizeof(path), "%s/%s", directory, entry->d_name); + if (written < 0 || static_cast(written) >= sizeof(path) || !is_file(path)) { + continue; + } + + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + continue; + } + char ssid[WIFI_SETTINGS_SSID_LIMIT + 1]; + bool has_ssid = properties_file_get(file, AP_PROPERTIES_KEY_SSID, ssid, sizeof(ssid)) == ERROR_NONE; + properties_file_close(file); + + if (has_ssid && !on_ssid(ssid, context)) { + break; + } + } + closedir(dir); +} + +error_t wifi_settings_set_enable_on_boot(bool enable) { + enable_on_boot = enable; + enable_on_boot_cached = true; + + char directory[FILE_MAX_PATH_STRING_LENGTH]; + char path[FILE_MAX_PATH_STRING_LENGTH]; + if (service_paths_get_user_data_directory(WIFI_SERVICE_ID, directory, sizeof(directory)) != ERROR_NONE || + service_paths_get_user_data_path(WIFI_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE) { + return ERROR_RESOURCE; + } + + if (wifi_ensure_directory_exists(directory) != ERROR_NONE) { + LOG_E(TAG, "Failed to create %s", directory); + return ERROR_RESOURCE; + } + + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + LOG_E(TAG, "Failed to open %s", path); + return ERROR_RESOURCE; + } + properties_file_set(file, SETTINGS_KEY_ENABLE_ON_BOOT, enable ? "true" : "false"); + return properties_file_close(file); +} + +bool wifi_settings_get_enable_on_boot(void) { + if (enable_on_boot_cached) { + return enable_on_boot; + } + + char path[FILE_MAX_PATH_STRING_LENGTH]; + if (service_paths_get_user_data_path(WIFI_SERVICE_ID, SETTINGS_FILE_NAME, path, sizeof(path)) != ERROR_NONE || !is_file(path)) { + return enable_on_boot; + } + + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + LOG_I(TAG, "Failed to load settings, using defaults"); + return enable_on_boot; + } + + char value[8]; + if (properties_file_get(file, SETTINGS_KEY_ENABLE_ON_BOOT, value, sizeof(value)) == ERROR_NONE) { + enable_on_boot = strcmp(value, "true") == 0; + enable_on_boot_cached = true; + } else { + LOG_I(TAG, "Failed to load settings, using defaults"); + } + properties_file_close(file); + return enable_on_boot; +} + +} diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index b48d28aa5..2d12f6cdb 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -45,6 +45,7 @@ list(APPEND REQUIRES_LIST gps-generic-module gps-meshtastic-module service-module + wifi-module # Libraries/* lv_screenshot minitar diff --git a/Tactility/Include/Tactility/service/wifi/Wifi.h b/Tactility/Include/Tactility/service/wifi/Wifi.h deleted file mode 100644 index 5a23a8af5..000000000 --- a/Tactility/Include/Tactility/service/wifi/Wifi.h +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -#include - -#include "WifiApSettings.h" - -namespace tt::service::wifi { - -/** The kernel wifi driver's event type: a WifiEventType tag plus a union - * payload (radio_state/station_state/access_point_state/connection_error - * depending on the tag). See tactility/drivers/wifi.h. */ -using WifiEvent = ::WifiEvent; - -enum class RadioState { - OnPending, - On, - ConnectionPending, - ConnectionActive, - OffPending, - Off, -}; - -/** @return Get the current radio state */ -RadioState getRadioState(); - -/** For logging purposes */ -const char* radioStateToString(RadioState state); - -/** - * @brief Request scanning update. Returns immediately. Results are through pubsub. - */ -void scan(); - -/** @return true if wifi is actively scanning */ -bool isScanning(); - -/** @return true the ssid name or empty string */ -std::string getConnectionTarget(); - -/** @return the access points from the last scan (if any). It only contains public APs. */ -std::vector getScanResults(); - -/** - * @brief Overrides the default scan result size of 16. - * @param[in] records the record limit for the scan result (84 bytes per record!) - */ -void setScanRecords(uint16_t records); - -/** - * @brief Enable/disable the radio. Ignores input if desired state matches current state. - * @param[in] enabled - */ -void setEnabled(bool enabled); - -/** - * @return the IPv4 address or empty string - */ -std::string getIp(); - -/** - * @brief Connect to a network. Disconnects any existing connection. - * Returns immediately but runs in the background. Results are through pubsub. - * @param[in] ap - * @param[in] remember whether to save the ap data to the settings upon successful connection - */ -void connect(const settings::WifiApSettings& ap, bool remember); - -/** @brief Disconnect from the access point. Doesn't have any effect when not connected. */ -void disconnect(); - -/** @return true if the connection isn't unencrypted. */ -bool isConnectionSecure(); - -/** - * @brief Suspend or resume the periodic background auto-connect scan. - * @param[in] paused when true, the auto-connect timer stops issuing scans until resumed. - * Intended for narrow, short-lived windows where any WiFi/co-processor traffic would be - * unsafe (e.g. a known co-processor reboot in progress) - callers must resume when done. - * Independent of the internal connect()/disconnect() auto-connect pause bookkeeping: only - * a matching setAutoScanPaused(false) clears this, it is never cleared implicitly by a - * connection succeeding/failing or the radio being enabled. - */ -void setAutoScanPaused(bool paused); - -/** @return the RSSI value (negative number) or return 1 when not connected. */ -int getRssi(); - -} // namespace diff --git a/Tactility/Include/Tactility/service/wifi/WifiApSettings.h b/Tactility/Include/Tactility/service/wifi/WifiApSettings.h deleted file mode 100644 index 07fb4934c..000000000 --- a/Tactility/Include/Tactility/service/wifi/WifiApSettings.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once - -#include - -namespace tt::service::wifi::settings { - -/** - * This struct is stored as-is into NVS flash. - * - * The SSID and secret are increased by 1 byte to facilitate string null termination. - * This makes it easier to use the char array as a string in various places. - */ -struct WifiApSettings { - std::string ssid; - std::string password; - bool autoConnect; - int32_t channel; - - WifiApSettings( - std::string ssid, - std::string password, - bool autoConnect = true, - int32_t channel = 0 - ) : ssid(ssid), password(password), autoConnect(autoConnect), channel(channel) {} - - WifiApSettings() : ssid(""), password(""), autoConnect(true), channel(0) {} -}; - -/** - * Check if settings exist for the provided SSID - * @param[in] ssid the access point to look for - * @return true if the settings exist - */ -bool contains(const std::string& ssid); - -/** - * Load the settings for the provided SSID - * @param[in] ssid the access point to look for - * @param[out] settings the output settings - * @return true if the settings were loaded successfully - */ -bool load(const std::string& ssid, WifiApSettings& settings); - -/** - * Save settings - * @param settings the settings to save - * @return true when the settings were saved successfully - */ -bool save(const WifiApSettings& settings); - -/** - * Remove settings that were saved previously. - * @param ssid the name of the SSID for the settings to remove - * @return true when settings were found and removed - */ -bool remove(const std::string& ssid); - -} // namespace diff --git a/Tactility/Include/Tactility/service/wifi/WifiGlobals.h b/Tactility/Include/Tactility/service/wifi/WifiGlobals.h deleted file mode 100644 index 3b604ce3d..000000000 --- a/Tactility/Include/Tactility/service/wifi/WifiGlobals.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#define TT_WIFI_AUTO_CONNECT true // Default setting for new Wi-Fi entries - -#define TT_WIFI_SCAN_RECORD_LIMIT 16 // default, can be overridden - -#define TT_WIFI_SSID_LIMIT 32 // 32 characters/octets, according to IEEE 802.11-2020 spec -#define TT_WIFI_CREDENTIALS_PASSWORD_LIMIT 64 // 64 characters/octets, according to IEEE 802.11-2020 spec diff --git a/Tactility/Include/Tactility/service/wifi/WifiSettings.h b/Tactility/Include/Tactility/service/wifi/WifiSettings.h deleted file mode 100644 index 4ea9027fe..000000000 --- a/Tactility/Include/Tactility/service/wifi/WifiSettings.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -namespace tt::service::wifi::settings { - -void setEnableOnBoot(bool enable); - -bool shouldEnableOnBoot(); - -} // namespace diff --git a/Tactility/Private/Tactility/app/wifimanage/State.h b/Tactility/Private/Tactility/app/wifimanage/State.h index 40ee84c02..ca0b4b542 100644 --- a/Tactility/Private/Tactility/app/wifimanage/State.h +++ b/Tactility/Private/Tactility/app/wifimanage/State.h @@ -1,8 +1,12 @@ #pragma once -#include #include +#include + +#include +#include + namespace tt::app::wifimanage { /** @@ -13,7 +17,9 @@ class State final { RecursiveMutex mutex; bool scanning = false; bool scannedAfterRadioOn = false; - service::wifi::RadioState radioState; + WifiRadioState radioState = WIFI_RADIO_STATE_OFF; + WifiStationState stationState = WIFI_STATION_STATE_DISCONNECTED; + std::string connectionTarget; std::vector apRecords; std::string connectSsid; @@ -25,10 +31,17 @@ class State final { bool hasScannedAfterRadioOn() const { return scannedAfterRadioOn; } - void setRadioState(service::wifi::RadioState state); - service::wifi::RadioState getRadioState() const; + void setRadioState(WifiRadioState state); + WifiRadioState getRadioState() const; + + void setStationState(WifiStationState state); + WifiStationState getStationState() const; + + /** @param[in] ssid the SSID the station is connected or connecting to, or an empty string */ + void setConnectionTarget(const std::string& ssid); + std::string getConnectionTarget() const; - void updateApRecords(); + void updateApRecords(Device* device); template &> Func> void withApRecords(Func&& onApRecords) const { diff --git a/Tactility/Private/Tactility/app/wifimanage/WifiManagePrivate.h b/Tactility/Private/Tactility/app/wifimanage/WifiManagePrivate.h index ce495e107..c39df5c69 100644 --- a/Tactility/Private/Tactility/app/wifimanage/WifiManagePrivate.h +++ b/Tactility/Private/Tactility/app/wifimanage/WifiManagePrivate.h @@ -5,7 +5,6 @@ #include #include -#include // Context (the app's actual runtime state) is defined inside WifiManage.cpp's own anonymous // namespace - View.cpp doesn't need it (Bindings*/State* pointers and a raw appInstanceId are diff --git a/Tactility/Private/Tactility/service/wifi/WifiBootSplashInit.h b/Tactility/Private/Tactility/service/wifi/WifiBootSplashInit.h deleted file mode 100644 index e96fdf97b..000000000 --- a/Tactility/Private/Tactility/service/wifi/WifiBootSplashInit.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -namespace tt::service::wifi { - -/** - * Called during boot, this function loads WiFi settings from SD card (when available). - */ -void bootSplashInit(); - -} \ No newline at end of file diff --git a/Tactility/Private/Tactility/service/wifi/WifiPrivate.h b/Tactility/Private/Tactility/service/wifi/WifiPrivate.h deleted file mode 100644 index 0e2e15b0a..000000000 --- a/Tactility/Private/Tactility/service/wifi/WifiPrivate.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include -#include - -namespace tt::service::wifi { - -std::shared_ptr findServiceContext(); - -} \ No newline at end of file diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index c083b56fb..ae52fadf2 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -63,6 +63,7 @@ #include #include +#include #include #include #include @@ -130,7 +131,6 @@ namespace service { // Primary namespace audio { extern const ServiceManifest manifest; } namespace autorotate { extern const ServiceManifest manifest; } - namespace wifi { extern const ServiceManifest manifest; } namespace development { extern const ServiceManifest manifest; } #if defined(CONFIG_SOC_WIFI_SUPPORTED) || defined(CONFIG_ESP_HOSTED_ENABLED) namespace espnow { extern const ServiceManifest manifest; } @@ -335,7 +335,7 @@ static void registerAndStartServices() { if (device_exists_of_type(&AUDIO_STREAM_TYPE)) { addService(service::audio::manifest); } - addService(service::wifi::manifest); + check(module_ensure_started(&wifi_module) == ERROR_NONE); addService(service::development::manifest); addService(service::webserver::manifest); diff --git a/Tactility/Source/app/apphub/AppHubApp.cpp b/Tactility/Source/app/apphub/AppHubApp.cpp index 670a6a696..f944a914f 100644 --- a/Tactility/Source/app/apphub/AppHubApp.cpp +++ b/Tactility/Source/app/apphub/AppHubApp.cpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -16,6 +15,8 @@ #include #include +#include +#include #include #include @@ -174,7 +175,14 @@ void refresh(Context* ctx) { lv_obj_add_flag(ctx->refreshButton, LV_OBJ_FLAG_HIDDEN); lvgl_unlock(); - if (service::wifi::getRadioState() != service::wifi::RadioState::ConnectionActive) { + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + wifi_get_station_state(wifi_device, &station_state); + device_put(wifi_device); + } + + if (station_state != WIFI_STATION_STATE_CONNECTED) { lvgl_lock(); showNoInternet(ctx); lvgl_unlock(); diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index cb9e0be82..04af25fb0 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -14,6 +13,8 @@ #include #include +#include +#include #include #include @@ -78,13 +79,21 @@ void onEnableOnBootSwitchChanged(lv_event_t* event) { } void updateViewState(Context* ctx) { + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + char ip[16] = {}; + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + wifi_get_station_state(wifi_device, &station_state); + wifi_station_get_ipv4_address(wifi_device, ip); + device_put(wifi_device); + } + if (!ctx->service->isEnabled()) { lv_label_set_text(ctx->statusLabel, "Service disabled"); - } else if (service::wifi::getRadioState() != service::wifi::RadioState::ConnectionActive) { + } else if (station_state != WIFI_STATION_STATE_CONNECTED) { lv_label_set_text(ctx->statusLabel, "Waiting for connection..."); } else { // enabled and connected to wifi - auto ip = service::wifi::getIp(); - if (ip.empty()) { + if (ip[0] == '\0') { lv_label_set_text(ctx->statusLabel, "Waiting for IP..."); } else { const std::string status = std::format("Available at {}", ip); diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp index 47dd81398..279425f8d 100644 --- a/Tactility/Source/app/setup/Setup.cpp +++ b/Tactility/Source/app/setup/Setup.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include @@ -17,6 +17,8 @@ #include #include +#include +#include #include #include @@ -221,7 +223,13 @@ int32_t appMain(int argc, char* argv[]) { .title = "Wi-Fi Setup", .description = "Let's connect to a Wi-Fi access point.", .run = [&ctx] { - service::wifi::setEnabled(true); + getMainDispatcher().dispatch([] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + wifi_set_radio_on(wifi_device); + device_put(wifi_device); + } + }); ctx.pendingStepDialogId = wifimanage::start(ctx.appInstanceId); } } diff --git a/Tactility/Source/app/webserversettings/WebServerSettings.cpp b/Tactility/Source/app/webserversettings/WebServerSettings.cpp index ebc24b8d4..461de78f0 100644 --- a/Tactility/Source/app/webserversettings/WebServerSettings.cpp +++ b/Tactility/Source/app/webserversettings/WebServerSettings.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include @@ -11,6 +10,8 @@ #include #include +#include +#include #include #include @@ -164,8 +165,13 @@ void updateUrlDisplay(Context* ctx) { url += "192.168.4.1"; } else { // Station mode - try to get actual IP - std::string ip = service::wifi::getIp(); - if (!ip.empty()) { + char ip[16] = {}; + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + wifi_station_get_ipv4_address(wifi_device, ip); + device_put(wifi_device); + } + if (ip[0] != '\0') { url += ip; } else { url = "Not connected"; diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 8345f2be9..11b1197c4 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -1,7 +1,6 @@ #include #include -#include -#include +#include #include #include @@ -11,11 +10,15 @@ #include +#include +#include + #include #include #include #include +#include #include namespace tt::app::wifiapsettings { @@ -46,6 +49,53 @@ struct Context { void updateViews(Context* ctx); +/** Turns the radio on when needed and connects. Runs on the main dispatcher, as turning the radio on blocks. */ +void connectToAp(const WifiApSettings& ap) { + getMainDispatcher().dispatch([ap] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return; + } + error_t result = wifi_set_radio_on(wifi_device); + if (result == ERROR_NONE) { + result = wifi_station_connect(wifi_device, ap.ssid, ap.password, ap.channel); + } + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to connect (%s)", error_to_string(result)); + } + device_put(wifi_device); + }); +} + +void disconnectFromAp() { + // Keeps auto-connect from immediately reconnecting + wifi_autoconnect_pause_until_connected(); + getMainDispatcher().dispatch([] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return; + } + error_t result = wifi_station_disconnect(wifi_device); + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to disconnect (%s)", error_to_string(result)); + } + device_put(wifi_device); + }); +} + +/** @return true when the station is connected to this app's SSID */ +bool isConnectedToSsid(Context* ctx) { + if (ctx->wifiDevice == nullptr) return false; + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + char ssid[33] = {}; + return wifi_get_station_state(ctx->wifiDevice, &station_state) == ERROR_NONE && + station_state == WIFI_STATION_STATE_CONNECTED && + wifi_station_get_target_ssid(ctx->wifiDevice, ssid) == ERROR_NONE && + ctx->ssid == ssid; +} + void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); app_event_emit_close(ctx->appInstanceId); @@ -61,10 +111,10 @@ void onToggleAutoConnect(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); - service::wifi::settings::WifiApSettings settings; - if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) { - settings.autoConnect = is_on; - if (!service::wifi::settings::save(settings)) { + WifiApSettings settings; + if (wifi_settings_load(ctx->ssid.c_str(), &settings) == ERROR_NONE) { + settings.auto_connect = is_on; + if (wifi_settings_save(&settings) != ERROR_NONE) { LOG_E(TAG, "Failed to save settings"); } } else { @@ -74,22 +124,23 @@ void onToggleAutoConnect(lv_event_t* event) { void onPressConnect(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - service::wifi::settings::WifiApSettings settings; - if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) { + WifiApSettings settings; + if (wifi_settings_load(ctx->ssid.c_str(), &settings) == ERROR_NONE) { auto* button = lv_event_get_target_obj(event); lv_obj_add_state(button, LV_STATE_DISABLED); - service::wifi::connect(settings, false); + connectToAp(settings); } } -void onPressDisconnect(lv_event_t*) { - if (service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive) { - service::wifi::disconnect(); +void onPressDisconnect(lv_event_t* event) { + auto* ctx = static_cast(lv_event_get_user_data(event)); + if (isConnectedToSsid(ctx)) { + disconnectFromAp(); } } void updateConnectButton(Context* ctx) { - if (service::wifi::getConnectionTarget() == ctx->ssid && service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive) { + if (isConnectedToSsid(ctx)) { lv_obj_remove_flag(ctx->disconnectButton, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(ctx->connectButton, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_state(ctx->disconnectButton, LV_STATE_DISABLED); @@ -101,19 +152,23 @@ void updateConnectButton(Context* ctx) { } void updateBusySpinner(Context* ctx) { - if (service::wifi::getRadioState() == service::wifi::RadioState::ConnectionPending) { + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + if (ctx->wifiDevice != nullptr) { + wifi_get_station_state(ctx->wifiDevice, &station_state); + } + if (station_state == WIFI_STATION_STATE_CONNECTION_PENDING) { lv_obj_remove_flag(ctx->busySpinner, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(ctx->busySpinner, LV_OBJ_FLAG_HIDDEN); } } -// Touches the filesystem (service::wifi::settings::load()) - callers must not run this on the +// Touches the filesystem (wifi_settings_load()) - callers must not run this on the // LVGL task (see updateViews()'s callers). void updateAutoConnectSection(Context* ctx) { - service::wifi::settings::WifiApSettings settings; - if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) { - if (settings.autoConnect) { + WifiApSettings settings; + if (wifi_settings_load(ctx->ssid.c_str(), &settings) == ERROR_NONE) { + if (settings.auto_connect) { lv_obj_add_state(ctx->autoConnectSwitch, LV_STATE_CHECKED); } else { lv_obj_remove_state(ctx->autoConnectSwitch, LV_STATE_CHECKED); @@ -241,7 +296,7 @@ int32_t appMain(int argc, char* argv[]) { WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); - // The file-I/O-touching part of the view (updateAutoConnectSection()'s settings::load()) + // The file-I/O-touching part of the view (updateAutoConnectSection()'s wifi_settings_load()) // runs here, on this app's own task, not the LVGL task createWidgets() requestViewUpdate(&ctx); @@ -269,15 +324,12 @@ int32_t appMain(int argc, char* argv[]) { break; case APP_EVENT_RESULT: if (event.result.launch_id == ctx.forgetDialogId && event.result.result == 0) { // 0 = Yes - if (!service::wifi::settings::remove(ctx.ssid.c_str())) { + if (wifi_settings_remove(ctx.ssid.c_str()) != ERROR_NONE) { LOG_E(TAG, "Failed to remove SSID"); } else { LOG_I(TAG, "Removed SSID"); - if ( - service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && - service::wifi::getConnectionTarget() == ctx.ssid - ) { - service::wifi::disconnect(); + if (isConnectedToSsid(&ctx)) { + disconnectFromAp(); } shouldClose = true; } diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index d76580836..f0961b2c3 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -1,8 +1,6 @@ #include -#include -#include -#include +#include #include #include @@ -12,12 +10,15 @@ #include +#include + #include #include #include #include #include +#include #include #include @@ -39,6 +40,9 @@ struct Context { bool connecting = false; bool connectionError = false; + // The access point of the pending connection attempt, saved when it succeeds and rememberAp is set + WifiApSettings pendingAp; + bool rememberAp = false; lv_obj_t* ssid_textarea = nullptr; lv_obj_t* ssid_error = nullptr; @@ -73,6 +77,14 @@ void onWifiEvent(Context* ctx, WifiEvent event) { if (ctx->connecting) { ctx->connecting = false; shouldClose = true; + if (ctx->rememberAp) { + // Dispatch it, so file IO doesn't block the UI + getMainDispatcher().dispatch([ap = ctx->pendingAp] { + if (wifi_settings_save(&ap) != ERROR_NONE) { + LOG_E(TAG, "Failed to store credentials"); + } + }); + } } } else { if (ctx->connecting) { @@ -133,7 +145,7 @@ void onConnectPressed(lv_event_t* event) { const char* ssid = lv_textarea_get_text(ctx->ssid_textarea); size_t ssid_len = strlen(ssid); - if (ssid_len > TT_WIFI_SSID_LIMIT) { + if (ssid_len > WIFI_SETTINGS_SSID_LIMIT) { LOG_E(TAG, "SSID too long"); lv_label_set_text(ctx->ssid_error, "SSID too long"); lv_obj_remove_flag(ctx->ssid_error, LV_OBJ_FLAG_HIDDEN); @@ -142,7 +154,7 @@ void onConnectPressed(lv_event_t* event) { const char* password = lv_textarea_get_text(ctx->password_textarea); size_t password_len = strlen(password); - if (password_len > TT_WIFI_CREDENTIALS_PASSWORD_LIMIT) { + if (password_len > WIFI_SETTINGS_PASSWORD_LIMIT) { LOG_E(TAG, "Password too long"); lv_label_set_text(ctx->password_error, "Password too long"); lv_obj_remove_flag(ctx->password_error, LV_OBJ_FLAG_HIDDEN); @@ -153,14 +165,32 @@ void onConnectPressed(lv_event_t* event) { setLoading(ctx, true); - service::wifi::settings::WifiApSettings settings; - settings.password = password; - settings.ssid = ssid; + // Lengths were validated above + WifiApSettings settings {}; + strcpy(settings.ssid, ssid); + strcpy(settings.password, password); settings.channel = 0; - settings.autoConnect = TT_WIFI_AUTO_CONNECT; // No UI yet, so use global setting + settings.auto_connect = WIFI_SETTINGS_AUTO_CONNECT_DEFAULT; // No UI yet, so use global setting ctx->connecting = true; - service::wifi::connect(settings, store); + ctx->pendingAp = settings; + ctx->rememberAp = store; + + getMainDispatcher().dispatch([settings] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return; + } + error_t result = wifi_set_radio_on(wifi_device); + if (result == ERROR_NONE) { + result = wifi_station_connect(wifi_device, settings.ssid, settings.password, settings.channel); + } + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to connect (%s)", error_to_string(result)); + } + device_put(wifi_device); + }); } void createBottomButtons(Context* ctx, lv_obj_t* parent) { diff --git a/Tactility/Source/app/wifimanage/State.cpp b/Tactility/Source/app/wifimanage/State.cpp index 084a4d84a..a6a0986c3 100644 --- a/Tactility/Source/app/wifimanage/State.cpp +++ b/Tactility/Source/app/wifimanage/State.cpp @@ -2,6 +2,8 @@ namespace tt::app::wifimanage { +constexpr size_t SCAN_RECORD_LIMIT = 16; + void State::setScanning(bool isScanning) { mutex.lock(); scanning = isScanning; @@ -9,22 +11,48 @@ void State::setScanning(bool isScanning) { mutex.unlock(); } -void State::setRadioState(service::wifi::RadioState state) { +void State::setRadioState(WifiRadioState state) { mutex.lock(); radioState = state; - if (radioState == service::wifi::RadioState::Off) { + if (radioState == WIFI_RADIO_STATE_OFF) { scannedAfterRadioOn = false; } mutex.unlock(); } -service::wifi::RadioState State::getRadioState() const { +WifiRadioState State::getRadioState() const { mutex.lock(); auto result = radioState; mutex.unlock(); return result; } +void State::setStationState(WifiStationState state) { + mutex.lock(); + stationState = state; + mutex.unlock(); +} + +WifiStationState State::getStationState() const { + mutex.lock(); + auto result = stationState; + mutex.unlock(); + return result; +} + +void State::setConnectionTarget(const std::string& ssid) { + mutex.lock(); + connectionTarget = ssid; + mutex.unlock(); +} + +std::string State::getConnectionTarget() const { + mutex.lock(); + auto result = connectionTarget; + mutex.unlock(); + return result; +} + bool State::isScanning() const { mutex.lock(); bool result = scanning; @@ -32,9 +60,16 @@ bool State::isScanning() const { return result; } -void State::updateApRecords() { +void State::updateApRecords(Device* device) { + std::vector records(SCAN_RECORD_LIMIT); + size_t count = records.size(); + if (device == nullptr || wifi_get_scan_results(device, records.data(), &count) != ERROR_NONE) { + count = 0; + } + records.resize(count); + mutex.lock(); - apRecords = service::wifi::getScanResults(); + apRecords = std::move(records); mutex.unlock(); } diff --git a/Tactility/Source/app/wifimanage/View.cpp b/Tactility/Source/app/wifimanage/View.cpp index 6fbf96954..0c9c2f31e 100644 --- a/Tactility/Source/app/wifimanage/View.cpp +++ b/Tactility/Source/app/wifimanage/View.cpp @@ -6,8 +6,6 @@ #include #include #include -#include -#include #include #include @@ -16,6 +14,8 @@ #include #include +#include + namespace tt::app::wifimanage { constexpr auto* TAG = "WifiManageView"; @@ -49,7 +49,7 @@ static void onEnableOnBootSwitchChanged(lv_event_t* 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] { - service::wifi::settings::setEnableOnBoot(is_on); + wifi_settings_set_enable_on_boot(is_on); }); } @@ -80,7 +80,7 @@ void View::connect(lv_event_t* event) { LOG_I(TAG, "Clicked %zu/%zu", index, ap_records.size() - 1); std::string ssid = ap_records[index].ssid; LOG_I(TAG, "Clicked AP: %s", ssid.c_str()); - std::string connection_target = service::wifi::getConnectionTarget(); + std::string connection_target = self->state->getConnectionTarget(); if (connection_target == ssid) { self->bindings->onDisconnect(); } else { @@ -117,7 +117,7 @@ void View::createSsidListItem(const WifiApRecord& record, bool isConnecting, siz const auto label = std::format("{} {}{}%", std::string(record.ssid), auth_info, percentage); auto* button = lv_list_add_button(networks_list, nullptr, label.c_str()); lv_obj_set_user_data(button, reinterpret_cast(index)); - if (service::wifi::settings::contains(record.ssid)) { + if (wifi_settings_contains(record.ssid)) { lv_obj_add_event_cb(button, showDetails, LV_EVENT_SHORT_CLICKED, this); } else { lv_obj_add_event_cb(button, connect, LV_EVENT_SHORT_CLICKED, this); @@ -130,19 +130,10 @@ void View::updateConnectToHidden() { return; } - using enum service::wifi::RadioState; - switch (state->getRadioState()) { - case On: - case ConnectionPending: - case ConnectionActive: - lv_obj_remove_flag(connect_to_hidden, LV_OBJ_FLAG_HIDDEN); - break; - - case OnPending: - case OffPending: - case Off: - lv_obj_add_flag(connect_to_hidden, LV_OBJ_FLAG_HIDDEN); - break; + if (state->getRadioState() == WIFI_RADIO_STATE_ON) { + lv_obj_remove_flag(connect_to_hidden, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(connect_to_hidden, LV_OBJ_FLAG_HIDDEN); } } @@ -174,19 +165,17 @@ void View::updateNetworkList() { updateEnableOnBootToggle(); switch (state->getRadioState()) { - using enum service::wifi::RadioState; - case OnPending: - case On: - case ConnectionPending: - case ConnectionActive: { + case WIFI_RADIO_STATE_ON_PENDING: + case WIFI_RADIO_STATE_ON: { - std::string connection_target = service::wifi::getConnectionTarget(); + std::string connection_target = state->getConnectionTarget(); + auto station_state = state->getStationState(); // Make safe copy auto ap_records = state->getApRecords(); bool is_connected = !connection_target.empty() && - state->getRadioState() == ConnectionActive; + station_state == WIFI_STATION_STATE_CONNECTED; bool added_connected = false; if (is_connected && !ap_records.empty()) { for (int i = 0; i < ap_records.size(); ++i) { @@ -208,7 +197,7 @@ void View::updateNetworkList() { if (!used_ssids.contains(record.ssid)) { bool connection_target_match = (record.ssid == connection_target); bool is_connecting = connection_target_match - && state->getRadioState() == ConnectionPending && + && station_state == WIFI_STATION_STATE_CONNECTION_PENDING && !connection_target.empty(); bool skip = connection_target_match && added_connected; if (!skip) { @@ -245,7 +234,7 @@ void View::updateNetworkList() { } void View::updateScanning() { - if (state->getRadioState() == service::wifi::RadioState::On && state->isScanning()) { + if (state->getRadioState() == WIFI_RADIO_STATE_ON && state->getStationState() == WIFI_STATION_STATE_DISCONNECTED && state->isScanning()) { lv_obj_remove_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(scanning_spinner, LV_OBJ_FLAG_HIDDEN); @@ -255,21 +244,18 @@ void View::updateScanning() { void View::updateWifiToggle() { lv_obj_clear_state(enable_switch, LV_STATE_ANY); switch (state->getRadioState()) { - using enum service::wifi::RadioState; - case On: - case ConnectionPending: - case ConnectionActive: + case WIFI_RADIO_STATE_ON: lv_obj_add_state(enable_switch, LV_STATE_CHECKED); break; - case OnPending: + case WIFI_RADIO_STATE_ON_PENDING: lv_obj_add_state(enable_switch, LV_STATE_CHECKED); lv_obj_add_state(enable_switch, LV_STATE_DISABLED); break; - case Off: + case WIFI_RADIO_STATE_OFF: lv_obj_remove_state(enable_switch, LV_STATE_CHECKED); lv_obj_remove_state(enable_switch, LV_STATE_DISABLED); break; - case OffPending: + case WIFI_RADIO_STATE_OFF_PENDING: lv_obj_remove_state(enable_switch, LV_STATE_CHECKED); lv_obj_add_state(enable_switch, LV_STATE_DISABLED); break; @@ -279,7 +265,7 @@ void View::updateWifiToggle() { void View::updateEnableOnBootToggle() { if (enable_on_boot_switch != nullptr) { lv_obj_clear_state(enable_on_boot_switch, LV_STATE_ANY); - if (service::wifi::settings::shouldEnableOnBoot()) { + 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); diff --git a/Tactility/Source/app/wifimanage/WifiManage.cpp b/Tactility/Source/app/wifimanage/WifiManage.cpp index bd6fd488a..bbd295b68 100644 --- a/Tactility/Source/app/wifimanage/WifiManage.cpp +++ b/Tactility/Source/app/wifimanage/WifiManage.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -12,8 +13,12 @@ #include +#include +#include + #include #include +#include #include #include @@ -47,11 +52,47 @@ struct Context { }; +/** Turns the radio on when needed and connects. Runs on the main dispatcher, as turning the radio on blocks. */ +static void connectToAp(const WifiApSettings& ap) { + getMainDispatcher().dispatch([ap] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return; + } + error_t result = wifi_set_radio_on(wifi_device); + if (result == ERROR_NONE) { + result = wifi_station_connect(wifi_device, ap.ssid, ap.password, ap.channel); + } + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to connect (%s)", error_to_string(result)); + } + device_put(wifi_device); + }); +} + +static void disconnectFromAp() { + // Keeps auto-connect from immediately reconnecting + wifi_autoconnect_pause_until_connected(); + getMainDispatcher().dispatch([] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return; + } + error_t result = wifi_station_disconnect(wifi_device); + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to disconnect (%s)", error_to_string(result)); + } + device_put(wifi_device); + }); +} + static void onConnect(const std::string& ssid) { - service::wifi::settings::WifiApSettings settings; - if (service::wifi::settings::load(ssid, settings)) { + WifiApSettings settings; + if (wifi_settings_load(ssid.c_str(), &settings) == ERROR_NONE) { LOG_I(TAG, "Connecting with known credentials"); - service::wifi::connect(settings, false); + connectToAp(settings); } else { LOG_I(TAG, "Starting connection dialog"); wificonnect::start(ssid); @@ -63,11 +104,22 @@ static void onShowApSettings(const std::string& ssid) { } static void onDisconnect() { - service::wifi::disconnect(); + disconnectFromAp(); } static void onWifiToggled(bool enabled) { - service::wifi::setEnabled(enabled); + getMainDispatcher().dispatch([enabled] { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { + LOG_W(TAG, "No WiFi device found"); + return; + } + error_t result = enabled ? wifi_set_radio_on(wifi_device) : wifi_set_radio_off(wifi_device); + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to set radio state (%s)", error_to_string(result)); + } + device_put(wifi_device); + }); } static void onConnectToHidden() { @@ -85,21 +137,43 @@ void updateView(Context* ctx) { lvgl_unlock(); } -void onWifiEvent(Context* ctx, WifiEvent event) { - auto radio_state = service::wifi::getRadioState(); - LOG_I(TAG, "Update with state %s", service::wifi::radioStateToString(radio_state)); +void updateStateFromDevice(Context* ctx) { + WifiRadioState radio_state = WIFI_RADIO_STATE_OFF; + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + char ssid[33] = {}; + wifi_get_radio_state(ctx->wifiDevice, &radio_state); + wifi_get_station_state(ctx->wifiDevice, &station_state); + if (station_state != WIFI_STATION_STATE_DISCONNECTED) { + wifi_station_get_target_ssid(ctx->wifiDevice, ssid); + } ctx->state.setRadioState(radio_state); + ctx->state.setStationState(station_state); + ctx->state.setConnectionTarget(ssid); +} + +void scanIfIdle(Context* ctx) { + if (!wifi_is_scanning(ctx->wifiDevice)) { + error_t result = wifi_scan(ctx->wifiDevice); + if (result != ERROR_NONE) { + LOG_I(TAG, "Can't start scan (%s)", error_to_string(result)); + } + } +} + +void onWifiEvent(Context* ctx, WifiEvent event) { + updateStateFromDevice(ctx); + LOG_I(TAG, "Update with radio state %d, station state %d", (int)ctx->state.getRadioState(), (int)ctx->state.getStationState()); switch (event.type) { case WIFI_EVENT_TYPE_SCAN_STARTED: ctx->state.setScanning(true); break; case WIFI_EVENT_TYPE_SCAN_FINISHED: ctx->state.setScanning(false); - ctx->state.updateApRecords(); + ctx->state.updateApRecords(ctx->wifiDevice); break; case WIFI_EVENT_TYPE_RADIO_STATE_CHANGED: - if (event.radio_state == WIFI_RADIO_STATE_ON && !service::wifi::isScanning()) { - service::wifi::scan(); + if (event.radio_state == WIFI_RADIO_STATE_ON) { + scanIfIdle(ctx); } break; default: @@ -136,11 +210,6 @@ int32_t appMain(int argc, char* argv[]) { .onConnectToHidden = onConnectToHidden }; - // State update (it has its own locking) - ctx.state.setRadioState(service::wifi::getRadioState()); - ctx.state.setScanning(service::wifi::isScanning()); - ctx.state.updateApRecords(); - TaskEventGroup event_group {}; task_event_group_construct(&event_group); ctx.eventGroup = &event_group; @@ -163,20 +232,17 @@ int32_t appMain(int argc, char* argv[]) { LOG_W(TAG, "No WiFi device found"); } + // State update (it has its own locking) + if (ctx.wifiDevice != nullptr) { + updateStateFromDevice(&ctx); + ctx.state.setScanning(wifi_is_scanning(ctx.wifiDevice)); + ctx.state.updateApRecords(ctx.wifiDevice); + } + WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); - service::wifi::RadioState radio_state = service::wifi::getRadioState(); - bool can_scan = radio_state == service::wifi::RadioState::On || - radio_state == service::wifi::RadioState::ConnectionPending || - radio_state == service::wifi::RadioState::ConnectionActive; - std::string connection_target = service::wifi::getConnectionTarget(); - LOG_I(TAG, "Radio: %s, Scanning: %d, Connected to: %s, Can scan: %d", - service::wifi::radioStateToString(radio_state), - (int)service::wifi::isScanning(), - connection_target.empty() ? "(none)" : connection_target.c_str(), - (int)can_scan); - if (can_scan && !service::wifi::isScanning()) { - service::wifi::scan(); + if (ctx.wifiDevice != nullptr && ctx.state.getRadioState() == WIFI_RADIO_STATE_ON) { + scanIfIdle(&ctx); } bool shouldClose = false; @@ -195,6 +261,10 @@ int32_t appMain(int argc, char* argv[]) { if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) { if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { ctx.wifiDevice = retry_device; + updateStateFromDevice(&ctx); + ctx.state.setScanning(wifi_is_scanning(ctx.wifiDevice)); + ctx.state.updateApRecords(ctx.wifiDevice); + ctx.needsRefresh = true; } else { device_put(retry_device); } diff --git a/Tactility/Source/service/espnow/EspNowBackendNative.cpp b/Tactility/Source/service/espnow/EspNowBackendNative.cpp index 10597cdf7..60596f5b4 100644 --- a/Tactility/Source/service/espnow/EspNowBackendNative.cpp +++ b/Tactility/Source/service/espnow/EspNowBackendNative.cpp @@ -5,11 +5,12 @@ #if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_ESP_HOSTED_ENABLED) #include -#include #include #include +#include +#include #include namespace tt::service::espnow::backend { @@ -23,8 +24,15 @@ static uint8_t savedProtocolBitmap = 0; static bool deinitWifi(); static bool initWifi(const EspNowConfig& config) { - auto wifi_state = wifi::getRadioState(); - bool wifi_already_running = (wifi_state != wifi::RadioState::Off && wifi_state != wifi::RadioState::OffPending); + WifiRadioState radio_state = WIFI_RADIO_STATE_OFF; + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + wifi_get_radio_state(wifi_device, &radio_state); + wifi_get_station_state(wifi_device, &station_state); + device_put(wifi_device); + } + bool wifi_already_running = (radio_state != WIFI_RADIO_STATE_OFF && radio_state != WIFI_RADIO_STATE_OFF_PENDING); wifi_mode_t mode; if (config.mode == Mode::Station) { @@ -70,7 +78,7 @@ static bool initWifi(const EspNowConfig& config) { return false; } } else if (config.channel != 0 && - wifi_state != wifi::RadioState::ConnectionActive && wifi_state != wifi::RadioState::ConnectionPending) { + station_state == WIFI_STATION_STATE_DISCONNECTED) { // WifiService already owns the radio but isn't associated to an AP: an unassociated STA's // operating channel is otherwise left undefined/wherever it last scanned to, which silently // breaks ESP-NOW (esp_now_send() reports success but nothing reaches a peer sitting on a diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index 12215311a..d12fe8df5 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -18,6 +17,7 @@ #include #include #include +#include #include #include @@ -49,23 +49,35 @@ const char* getWifiStatusIconForRssi(int rssi) { } } -static const char* getWifiStatusIcon(wifi::RadioState state) { - int rssi; - switch (state) { - using enum wifi::RadioState; - case On: - case OnPending: - case ConnectionPending: - return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_0_BAR; - case OffPending: - case Off: +static const char* getWifiStatusIcon() { + Device* device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) { + return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_OFF; + } + + WifiRadioState radio_state = WIFI_RADIO_STATE_OFF; + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + int32_t rssi = 1; + wifi_get_radio_state(device, &radio_state); + wifi_get_station_state(device, &station_state); + if (station_state == WIFI_STATION_STATE_CONNECTED) { + wifi_station_get_rssi(device, &rssi); + } + device_put(device); + + switch (radio_state) { + case WIFI_RADIO_STATE_OFF: + case WIFI_RADIO_STATE_OFF_PENDING: return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_OFF; - case ConnectionActive: - rssi = wifi::getRssi(); - return getWifiStatusIconForRssi(rssi); - default: - check(false, "not implemented"); + case WIFI_RADIO_STATE_ON_PENDING: + return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_0_BAR; + case WIFI_RADIO_STATE_ON: + if (station_state == WIFI_STATION_STATE_CONNECTED) { + return getWifiStatusIconForRssi(rssi); + } + return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_0_BAR; } + check(false, "not implemented"); } static const char* getBluetoothStatusIcon(tt::bluetooth::RadioState state, bool scanning, bool connected) { @@ -207,8 +219,7 @@ class StatusbarService final : public Service { } void updateWifiIcon() { - wifi::RadioState radio_state = wifi::getRadioState(); - const char* desired_icon = getWifiStatusIcon(radio_state); + const char* desired_icon = getWifiStatusIcon(); if (wifi_last_icon != desired_icon) { if (desired_icon != nullptr) { lvgl::statusbar_icon_set_image(wifi_icon_id, desired_icon); diff --git a/Tactility/Source/service/webserver/WebServerService.cpp b/Tactility/Source/service/webserver/WebServerService.cpp index 23105311f..44f1bba04 100644 --- a/Tactility/Source/service/webserver/WebServerService.cpp +++ b/Tactility/Source/service/webserver/WebServerService.cpp @@ -15,9 +15,10 @@ #include #include #include -#include #include +#include +#include #include #include @@ -1433,14 +1434,17 @@ error_t WebServerService::handleApiAppsInstall(HttpServerRequest* request, void* } // Helper to convert radio state to string -static const char* radioStateToJsonString(wifi::RadioState state) { - switch (state) { - case wifi::RadioState::On: return "on"; - case wifi::RadioState::OnPending: return "turning_on"; - case wifi::RadioState::Off: return "off"; - case wifi::RadioState::OffPending: return "turning_off"; - case wifi::RadioState::ConnectionPending: return "connecting"; - case wifi::RadioState::ConnectionActive: return "connected"; +static const char* radioStateToJsonString(WifiRadioState radioState, WifiStationState stationState) { + switch (radioState) { + case WIFI_RADIO_STATE_ON: + switch (stationState) { + case WIFI_STATION_STATE_CONNECTION_PENDING: return "connecting"; + case WIFI_STATION_STATE_CONNECTED: return "connected"; + default: return "on"; + } + case WIFI_RADIO_STATE_ON_PENDING: return "turning_on"; + case WIFI_RADIO_STATE_OFF: return "off"; + case WIFI_RADIO_STATE_OFF_PENDING: return "turning_off"; default: return "unknown"; } } @@ -1449,15 +1453,41 @@ static const char* radioStateToJsonString(wifi::RadioState state) { error_t WebServerService::handleApiWifi(HttpServerRequest* request, void*) { LOG_I(TAG, "GET /api/wifi"); - auto state = wifi::getRadioState(); - auto ip = wifi::getIp(); - auto ssid = wifi::getConnectionTarget(); - auto rssi = wifi::getRssi(); - bool secure = wifi::isConnectionSecure(); + WifiRadioState radio_state = WIFI_RADIO_STATE_OFF; + WifiStationState station_state = WIFI_STATION_STATE_DISCONNECTED; + char ip[16] = {}; + char ssid[33] = {}; + int32_t rssi = 1; + bool secure = false; + + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + wifi_get_radio_state(wifi_device, &radio_state); + wifi_get_station_state(wifi_device, &station_state); + if (station_state != WIFI_STATION_STATE_DISCONNECTED) { + wifi_station_get_target_ssid(wifi_device, ssid); + } + if (station_state == WIFI_STATION_STATE_CONNECTED) { + wifi_station_get_ipv4_address(wifi_device, ip); + wifi_station_get_rssi(wifi_device, &rssi); + // The driver doesn't report the active connection's authentication, so derive it from the last scan + WifiApRecord records[16]; + size_t record_count = std::size(records); + if (wifi_get_scan_results(wifi_device, records, &record_count) == ERROR_NONE) { + for (size_t i = 0; i < record_count; ++i) { + if (strcmp(records[i].ssid, ssid) == 0) { + secure = records[i].authentication_type != WIFI_AUTHENTICATION_TYPE_OPEN; + break; + } + } + } + } + device_put(wifi_device); + } std::ostringstream json; json << "{"; - json << "\"state\":\"" << radioStateToJsonString(state) << "\","; + json << "\"state\":\"" << radioStateToJsonString(radio_state, station_state) << "\","; json << "\"ip\":\"" << escapeJson(ip) << "\","; json << "\"ssid\":\"" << escapeJson(ssid) << "\","; json << "\"rssi\":" << rssi << ","; diff --git a/Tactility/Source/service/wifi/Wifi.cpp b/Tactility/Source/service/wifi/Wifi.cpp deleted file mode 100644 index 2f8cc1146..000000000 --- a/Tactility/Source/service/wifi/Wifi.cpp +++ /dev/null @@ -1,593 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace tt::service::wifi { - -constexpr auto* TAG = "WifiService"; -constexpr auto AUTO_SCAN_INTERVAL = 10000; // ms - -const char* radioStateToString(RadioState state) { - switch (state) { - using enum RadioState; - case OnPending: - return TT_STRINGIFY(OnPending); - case On: - return TT_STRINGIFY(On); - case ConnectionPending: - return TT_STRINGIFY(ConnectionPending); - case ConnectionActive: - return TT_STRINGIFY(ConnectionActive); - case OffPending: - return TT_STRINGIFY(OffPending); - case Off: - return TT_STRINGIFY(Off); - } - check(false, "not implemented"); -} - -extern const ServiceManifest manifest; - -std::shared_ptr findServiceContext() { - return findServiceContextById(manifest.id); -} - -namespace { - -// Everything below wraps a TactilityKernel WIFI_TYPE device: the driver owns -// the radio state, station state and scan results, this file only tracks the -// bits the kernel driver doesn't (in-flight connection target/credentials, -// auto-connect bookkeeping). - -/** State lives for the entire process; only ever (re)initialized by onStart(). */ -struct WifiServiceState { - Device* device = nullptr; - RecursiveMutex mutex; - bool secureConnection = false; - // Internal: set by connect()/disconnect() while a manual attempt is in flight, cleared on - // connection success/failure. Distinct from externalScanPause below - the two must not - // clobber each other, otherwise a caller's explicit pause (e.g. AutoScanPauseGuard during a - // co-processor OTA) can be silently cleared by an unrelated connect/disconnect finishing. - bool pauseAutoConnect = false; - // External: only setAutoScanPaused() may set/clear this. Read alongside pauseAutoConnect to - // gate scan scheduling (both must be false to scan). - std::atomic externalScanPause{false}; - bool connectionTargetRemember = false; - settings::WifiApSettings connectionTarget; - uint16_t scanRecordLimit = TT_WIFI_SCAN_RECORD_LIMIT; - TickType_t lastScanTime = MAX_TICKS; - std::unique_ptr autoConnectTimer; - bool bootEventSubscribed = false; - - // Dedicated consumer for WifiEvents, alive for the service's whole lifetime (started in - // onStart(), stopped in onStop() - see dispatchSetEnabled()'s comment on why this outlives - // radio on/off toggles): runs onWifiDeviceEvent() on its own stack instead of the ESP-IDF - // esp_event task's, by blocking in task_event_group_wait_any() rather than being called back - // directly from fire_event(). - TaskEventGroup wifiEventGroup {}; - WifiEventSubscription wifiEventSub {}; - Thread* wifiEventThread = nullptr; - std::atomic wifiEventThreadRunning {false}; -}; - -WifiServiceState state; -bool started = false; - -void onWifiDeviceEvent(Device* device, ::WifiEvent event); - -// ---- Helpers ---- - -// state.device is started (bookkeeping allocated) for the service's entire lifetime now - see -// dispatchSetEnabled()'s comment - so device_is_ready() no longer tracks radio-on state; query -// the driver directly instead. -bool isRadioOn() { - if (state.device == nullptr) return false; - WifiRadioState radio = WIFI_RADIO_STATE_OFF; - return wifi_get_radio_state(state.device, &radio) == ERROR_NONE && radio == WIFI_RADIO_STATE_ON; -} - -RadioState combineRadioState(WifiRadioState radio, WifiStationState station) { - switch (radio) { - case WIFI_RADIO_STATE_OFF: return RadioState::Off; - case WIFI_RADIO_STATE_ON_PENDING: return RadioState::OnPending; - case WIFI_RADIO_STATE_OFF_PENDING: return RadioState::OffPending; - case WIFI_RADIO_STATE_ON: - switch (station) { - case WIFI_STATION_STATE_CONNECTION_PENDING: return RadioState::ConnectionPending; - case WIFI_STATION_STATE_CONNECTED: return RadioState::ConnectionActive; - case WIFI_STATION_STATE_DISCONNECTED: default: return RadioState::On; - } - } - return RadioState::Off; -} - -// ---- WifiEvent consumer thread ---- -// Runs onWifiDeviceEvent() on its own stack (see WifiServiceState::wifiEventGroup's comment). - -constexpr configSTACK_DEPTH_TYPE WIFI_EVENT_THREAD_STACK_SIZE = 4096; - -int32_t wifiEventThreadMain() { - // The 250ms timeout only bounds how promptly a stop request (wifiEventThreadRunning going - // false) is noticed; a real event still wakes this immediately regardless, since - // task_event_group_wait_any() returns as soon as the bit is signalled, whichever comes first. - while (state.wifiEventThreadRunning.load()) { - task_event_group_wait_any(&state.wifiEventGroup, nullptr, pdMS_TO_TICKS(250)); - - WifiEvent event {}; - while (wifi_event_poll(&state.wifiEventSub, &event) == ERROR_NONE) { - onWifiDeviceEvent(state.device, event); - } - } - return 0; -} - -bool startWifiEventThread() { - task_event_group_construct(&state.wifiEventGroup); - if (wifi_event_subscribe(state.device, &state.wifiEventSub, &state.wifiEventGroup) != ERROR_NONE) { - task_event_group_destruct(&state.wifiEventGroup); - return false; - } - - state.wifiEventThreadRunning = true; - state.wifiEventThread = new Thread("wifi-events", WIFI_EVENT_THREAD_STACK_SIZE, [] { return wifiEventThreadMain(); }); - state.wifiEventThread->start(); - return true; -} - -void stopWifiEventThread() { - if (state.wifiEventThread == nullptr) return; - - state.wifiEventThreadRunning = false; - state.wifiEventThread->join(); - delete state.wifiEventThread; - state.wifiEventThread = nullptr; - - wifi_event_unsubscribe(state.device, &state.wifiEventSub); - task_event_group_destruct(&state.wifiEventGroup); -} - -// ---- Dispatched work (runs on the main task) ---- - -// state.device is started (device_start()) once, in onStart(), and never stopped until onStop() - -// this only toggles the radio itself, so the wifi-events thread (and any app subscribed directly -// to the driver) stays subscribed across on/off toggles instead of having to resubscribe. -void dispatchSetEnabled(bool enabled) { - LOG_I(TAG, "dispatchSetEnabled(%d)", (int)enabled); - if (!started || state.device == nullptr) return; - - if (enabled == isRadioOn()) { - LOG_W(TAG, "Can't enable/disable from current state"); - return; - } - - if (enabled) { - - if (wifi_set_radio_on(state.device) != ERROR_NONE) { - LOG_E(TAG, "Failed to enable WiFi radio"); - return; - } - - state.pauseAutoConnect = false; - state.lastScanTime = 0; - } else { - - if (wifi_set_radio_off(state.device) != ERROR_NONE) { - LOG_E(TAG, "Failed to disable WiFi radio"); - return; - } - - state.secureConnection = false; - } -} - -void dispatchScan() { - LOG_I(TAG, "dispatchScan()"); - if (!started || state.device == nullptr || !isRadioOn()) return; - - state.lastScanTime = get_ticks(); - - error_t result = wifi_scan(state.device); - if (result != ERROR_NONE) { - LOG_I(TAG, "Can't start scan (%s)", error_to_string(result)); - } -} - -void dispatchConnect() { - LOG_I(TAG, "dispatchConnect()"); - if (!started || state.device == nullptr) return; - - settings::WifiApSettings target; - { - auto lock = state.mutex.asScopedLock(); - if (!lock.lock(50 / portTICK_PERIOD_MS)) { - LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED_FMT, "dispatchConnect()"); - return; - } - target = state.connectionTarget; - } - - LOG_I(TAG, "Connecting to %s", target.ssid.c_str()); - - wifi_station_connect(state.device, target.ssid.c_str(), target.password.c_str(), target.channel); -} - -void dispatchDisconnect() { - LOG_I(TAG, "dispatchDisconnect()"); - if (!started || state.device == nullptr) return; - - error_t result = wifi_station_disconnect(state.device); - if (result != ERROR_NONE) { - LOG_E(TAG, "Failed to disconnect (%s)", error_to_string(result)); - } - // The Disconnected event arrives asynchronously via onWifiDeviceEvent(). -} - -bool findAutoConnectAp(settings::WifiApSettings& out) { - for (const auto& record : getScanResults()) { - if (settings::contains(record.ssid)) { - settings::WifiApSettings loaded; - if (settings::load(record.ssid, loaded)) { - if (loaded.autoConnect) { - out = loaded; - return true; - } - } else { - LOG_E(TAG, "Failed to load credentials for ssid %s", record.ssid); - } - } - } - return false; -} - -void dispatchAutoConnect() { - LOG_I(TAG, "dispatchAutoConnect()"); - if (state.pauseAutoConnect || state.externalScanPause.load() || !isRadioOn()) { - // A manual disconnect() or an in-progress manual connect() has paused - // auto-connect, or a caller (e.g. AutoScanPauseGuard) has externally paused it. - // This is called on every SCAN_FINISHED, not just the auto-connect timer's own - // scans (e.g. WifiManage re-scans on show), so it must honor the pause instead of - // reconnecting unconditionally. The radio-off check matters because a scan that was - // already in flight can finish after the user turns the radio off. Without it, - // connect() would call dispatchSetEnabled(true) and turn the radio back on. - return; - } - RadioState radio_state = getRadioState(); - if (radio_state == RadioState::ConnectionActive || radio_state == RadioState::ConnectionPending) { - // Already connected (or connecting): reconnecting to the same AP would just - // force a pointless disconnect/reconnect blip, e.g. when WifiManage's - // on-show scan finishes while we're already on the saved auto-connect AP. - return; - } - settings::WifiApSettings target; - if (findAutoConnectAp(target)) { - LOG_I(TAG, "Auto-connecting to %s", target.ssid.c_str()); - connect(target, false); - // connect() pauses auto-connect (it assumes a manual/user call); undo that - // since this call was automatic. - state.pauseAutoConnect = false; - } -} - -bool shouldScanForAutoConnect() { - bool radio_scannable = getRadioState() == RadioState::On && !isScanning() && - !state.pauseAutoConnect && !state.externalScanPause.load(); - if (!radio_scannable) return false; - - TickType_t current_time = get_ticks(); - bool scan_time_has_looped = current_time < state.lastScanTime; - bool no_recent_scan = (current_time - state.lastScanTime) > (AUTO_SCAN_INTERVAL / portTICK_PERIOD_MS); - return scan_time_has_looped || no_recent_scan; -} - -void onAutoConnectTimer() { - if (!started || state.device == nullptr) return; - if (shouldScanForAutoConnect()) { - getMainDispatcher().dispatch([] { dispatchScan(); }); - } -} - -// ---- Kernel driver event bridge ---- - -void onWifiDeviceEvent(Device* device, ::WifiEvent event) { - switch (event.type) { - case WIFI_EVENT_TYPE_SCAN_FINISHED: - getMainDispatcher().dispatch([] { dispatchAutoConnect(); }); - break; - - case WIFI_EVENT_TYPE_STATION_STATE_CHANGED: - if (event.station_state == WIFI_STATION_STATE_DISCONNECTED) { - // Don't touch pauseAutoConnect here: a deliberate disconnect() sets it - // and relies on it staying set until a new connection is established. - // Resetting it on every disconnect (including deliberate ones) would - // let auto-connect immediately reconnect the user. Attempts that fail - // while pending are unpaused via WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT below. - NetworkDisconnectedEvent disconnected_event = { .device = device }; - system_event_emit(KERNEL_EVENT_NETWORK_DISCONNECTED, &disconnected_event, sizeof(disconnected_event)); - } - break; - - case WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT: - if (event.connection_error == WIFI_STATION_CONNECTION_ERROR_NONE) { - settings::WifiApSettings target; - bool remember; - { - auto lock = state.mutex.asScopedLock(); - if (lock.lock(50 / portTICK_PERIOD_MS)) { - target = state.connectionTarget; - remember = state.connectionTargetRemember; - state.secureConnection = !target.password.empty(); - } else { - remember = false; - } - } - { - auto lock = state.mutex.asScopedLock(); - if (lock.lock(50 / portTICK_PERIOD_MS)) { - state.pauseAutoConnect = false; - } - } - LOG_I(TAG, "Connected to %s", target.ssid.c_str()); - if (remember && !settings::save(target)) { - LOG_E(TAG, "Failed to store credentials"); - } - } else { - // The pending connection attempt (which paused auto-connect via connect()) - // failed; unpause so auto-connect can try other saved APs. - auto lock = state.mutex.asScopedLock(); - if (lock.lock(50 / portTICK_PERIOD_MS)) { - state.pauseAutoConnect = false; - } - } - break; - - default: - break; - } -} - -void autoScanSetPaused(bool paused) { - LOG_I(TAG, "autoScanSetPaused(%d)", (int)paused); - state.externalScanPause = paused; -} - -} // namespace - -// region Public functions - -RadioState getRadioState() { - if (!started || state.device == nullptr) { - return RadioState::Off; - } - - WifiRadioState radio = WIFI_RADIO_STATE_OFF; - WifiStationState station = WIFI_STATION_STATE_DISCONNECTED; - wifi_get_radio_state(state.device, &radio); - wifi_get_station_state(state.device, &station); - return combineRadioState(radio, station); -} - -std::string getConnectionTarget() { - RadioState radio_state = getRadioState(); - if (radio_state != RadioState::ConnectionPending && radio_state != RadioState::ConnectionActive) { - return ""; - } - - char ssid[33] = {}; - if (wifi_station_get_target_ssid(state.device, ssid) != ERROR_NONE) { - return ""; - } - return { ssid }; -} - -void scan() { - LOG_I(TAG, "scan()"); - if (!started || state.device == nullptr) return; - getMainDispatcher().dispatch([] { dispatchScan(); }); -} - -bool isScanning() { - if (!started || state.device == nullptr) return false; - return wifi_is_scanning(state.device); -} - -void connect(const settings::WifiApSettings& ap, bool remember) { - LOG_I(TAG, "connect(%s, %d)", ap.ssid.c_str(), (int)remember); - if (!started || state.device == nullptr) return; - - bool radio_off; - { - auto lock = state.mutex.asScopedLock(); - if (!lock.lock(10 / portTICK_PERIOD_MS)) { - LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); - return; - } - // Stop auto-connecting until the connection is established. - state.pauseAutoConnect = true; - state.connectionTarget = ap; - state.connectionTargetRemember = remember; - radio_off = !isRadioOn(); - } - - getMainDispatcher().dispatch([radio_off] { - if (radio_off) { - dispatchSetEnabled(true); - } - dispatchConnect(); - }); -} - -void disconnect() { - LOG_I(TAG, "disconnect()"); - if (!started || state.device == nullptr) return; - - { - auto lock = state.mutex.asScopedLock(); - if (!lock.lock(10 / portTICK_PERIOD_MS)) { - LOG_E(TAG, LOG_MESSAGE_MUTEX_LOCK_FAILED); - return; - } - state.connectionTarget = settings::WifiApSettings("", ""); - // Manual disconnect (e.g. via app) should stop auto-connecting until a new connection is established. - state.pauseAutoConnect = true; - } - - getMainDispatcher().dispatch([] { dispatchDisconnect(); }); -} - -void setAutoScanPaused(bool paused) { - autoScanSetPaused(paused); -} - -void setScanRecords(uint16_t records) { - LOG_I(TAG, "setScanRecords(%u)", records); - if (!started) return; - auto lock = state.mutex.asScopedLock(); - if (lock.lock(10 / portTICK_PERIOD_MS)) { - state.scanRecordLimit = records; - } -} - -std::vector getScanResults() { - std::vector records; - if (!started || state.device == nullptr) return records; - - records.resize(state.scanRecordLimit); - size_t count = records.size(); - if (wifi_get_scan_results(state.device, records.data(), &count) != ERROR_NONE) { - records.clear(); - return records; - } - - records.resize(count); - return records; -} - -void setEnabled(bool enabled) { - LOG_I(TAG, "setEnabled(%d)", (int)enabled); - if (!started || state.device == nullptr) return; - getMainDispatcher().dispatch([enabled] { dispatchSetEnabled(enabled); }); -} - -bool isConnectionSecure() { - return state.secureConnection; -} - -int getRssi() { - if (!started || state.device == nullptr) return 1; - int32_t rssi = 0; - if (wifi_station_get_rssi(state.device, &rssi) == ERROR_NONE) { - return rssi; - } - return 1; -} - -std::string getIp() { - if (!started || state.device == nullptr) return ""; - char ipv4[16] = {}; - if (wifi_station_get_ipv4_address(state.device, ipv4) != ERROR_NONE) { - return ""; - } - return { ipv4 }; -} - -// endregion Public functions - -namespace { - -void onBootCompleted(struct SystemEvent* /*event*/, void* /*context*/) { - bootSplashInit(); -} - -class WifiService final : public Service { - -public: - - bool onStart(ServiceContext& /*service*/) override { - check(!started); - - wifi_auto_scan_set_paused_function(autoScanSetPaused); - - Device* wifi_device = nullptr; - if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { - LOG_W(TAG, "No WiFi device found"); - } else if (device_start(wifi_device) != ERROR_NONE) { - LOG_E(TAG, "Failed to start WiFi device"); - device_put(wifi_device); - } else { - state.device = wifi_device; - if (!startWifiEventThread()) { - LOG_E(TAG, "Failed to subscribe to WiFi events"); - } - } - - if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) { - state.bootEventSubscribed = true; - } - - auto timer_interval = std::min(2000, AUTO_SCAN_INTERVAL); - state.autoConnectTimer = std::make_unique(Timer::Type::Periodic, timer_interval, [] { onAutoConnectTimer(); }); - // We want to try and scan more often in case of startup or scan lock failure. - state.autoConnectTimer->start(); - - started = true; - return true; - } - - void onStop(ServiceContext& /*service*/) override { - check(started); - started = false; - - state.autoConnectTimer->stop(); - state.autoConnectTimer = nullptr; // Must release as it holds a reference via its callback. - - if (state.bootEventSubscribed) { - system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted); - state.bootEventSubscribed = false; - } - - if (state.device != nullptr) { - if (isRadioOn()) { - wifi_set_radio_off(state.device); - } - stopWifiEventThread(); - device_stop(state.device); - device_put(state.device); - } - - state.secureConnection = false; - state.pauseAutoConnect = false; - state.device = nullptr; - - wifi_auto_scan_set_paused_function(nullptr); - } -}; - -} // namespace - -extern const ServiceManifest manifest = { - .id = "tactility.wifi", - .createService = create -}; - -} // namespace tt::service::wifi diff --git a/Tactility/Source/service/wifi/WifiApSettings.cpp b/Tactility/Source/service/wifi/WifiApSettings.cpp deleted file mode 100644 index 0b6424d46..000000000 --- a/Tactility/Source/service/wifi/WifiApSettings.cpp +++ /dev/null @@ -1,244 +0,0 @@ -#include -#include -#include - -#include - -#include - -#include - -#include - -#include -#include -#include -#include -#include -#include - -namespace tt::service::wifi::settings { - -constexpr auto* TAG = "WifiApSettings"; - -constexpr auto* AP_SETTINGS_FORMAT = "{}/{}.ap.properties"; - -constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid"; -constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password"; -constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect"; -constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel"; - -std::string toHexString(const uint8_t *data, int length) { - std::stringstream stream; - stream << std::hex; - for( int i(0) ; i < length; ++i ) - stream << std::setw(2) << std::setfill('0') << static_cast(data[i]); - return stream.str(); -} - -bool readHex(const std::string& input, uint8_t* buffer, int length) { - if (input.size() / 2 != length) { - LOG_E(TAG, "readHex() length mismatch"); - return false; - } - - char hex[3] = { 0 }; - for (int i = 0; i < length; i++) { - hex[0] = input[i * 2]; - hex[1] = input[i * 2 + 1]; - char* endptr; - buffer[i] = static_cast(strtoul(hex, &endptr, 16)); - } - - return true; -} - -// TODO: The SSID could contain invalid filename characters (e.g. "/", "\" and more) so we have to refactor this. -static std::string getApPropertiesFilePath(std::shared_ptr paths, const std::string& ssid) { - return std::format(AP_SETTINGS_FORMAT, paths->getUserDataDirectory(), ssid); -} - -// The IV is derived from the SSID rather than the password/ciphertext, because the SSID is the one -// value that's known and identical at both encrypt time (save) and decrypt time (load). -static bool encrypt(const std::string& ssid, const std::string& plaintext, std::string& ciphertextOutput) { - uint8_t iv[16]; - const auto length = plaintext.size(); - constexpr size_t chunk_size = 16; - const auto encrypted_length = ((length / chunk_size) + (length % chunk_size ? 1 : 0)) * chunk_size; - - // crypt_encrypt reads encrypted_length bytes, but plaintext.c_str() only guarantees length + 1 bytes, - // so pad the input into a zero-filled buffer of encrypted_length first to avoid reading past it. - auto* padded_plaintext = static_cast(calloc(encrypted_length, 1)); - memcpy(padded_plaintext, plaintext.c_str(), length); - - auto* buffer = static_cast(malloc(encrypted_length)); - - crypt_get_iv(ssid.c_str(), ssid.size(), iv); - if (crypt_encrypt(iv, padded_plaintext, buffer, encrypted_length) != 0) { - LOG_E(TAG, "Failed to encrypt"); - free(padded_plaintext); - free(buffer); - return false; - } - - free(padded_plaintext); - ciphertextOutput = toHexString(buffer, encrypted_length); - free(buffer); - - return true; -} - -static bool decrypt(const std::string& ssid, const std::string& ciphertextInput, std::string& plaintextOutput) { - assert(!ciphertextInput.empty()); - assert(ciphertextInput.size() % 2 == 0); - auto* data = static_cast(malloc(ciphertextInput.size() / 2)); - if (!readHex(ciphertextInput, data, ciphertextInput.size() / 2)) { - LOG_E(TAG, "Failed to read hex"); - free(data); - return false; - } - - uint8_t iv[16]; - crypt_get_iv(ssid.c_str(), ssid.size(), iv); - - auto result_length = ciphertextInput.size() / 2; - // Allocate correct length plus space for string null terminator - auto* result = static_cast(malloc(result_length + 1)); - result[result_length] = 0; - - int decrypt_result = crypt_decrypt( - iv, - data, - result, - ciphertextInput.size() / 2 - ); - - free(data); - - if (decrypt_result != 0) { - LOG_E(TAG, "Failed to decrypt credentials for \"%s\": %d", ssid.c_str(), decrypt_result); - free(result); - return false; - } - - plaintextOutput = reinterpret_cast(result); - free(result); - return true; -} - -bool contains(const std::string& ssid) { - auto service_context = findServiceContext(); - if (service_context == nullptr) { - return false; - } - const auto file_path = getApPropertiesFilePath(service_context->getPaths(), ssid); - return file::isFile(file_path); -} - -bool load(const std::string& ssid, WifiApSettings& apSettings) { - auto service_context = findServiceContext(); - if (service_context == nullptr) { - LOG_E(TAG, "No service context"); - return false; - } - const auto file_path = getApPropertiesFilePath(service_context->getPaths(), ssid); - if (!file::isFile(file_path)) { - LOG_E(TAG, "Not a file: %s", file_path.c_str()); - return false; - } - - std::map map; - if (!file::loadPropertiesFile(file_path, map)) { - LOG_E(TAG, "Failed to load properties from %s", file_path.c_str()); - return false; - } - - // SSID is required - if (!map.contains(AP_PROPERTIES_KEY_SSID)) { - LOG_E(TAG, "File does not contain SSID: %s", file_path.c_str()); - return false; - } - - apSettings.ssid = map[AP_PROPERTIES_KEY_SSID]; - assert(ssid == apSettings.ssid); - - if (map.contains(AP_PROPERTIES_KEY_PASSWORD)) { - std::string password_decrypted; - const auto& encrypted_password = map[AP_PROPERTIES_KEY_PASSWORD]; - if (encrypted_password.empty()) { - apSettings.password = ""; - } else if (decrypt(ssid, encrypted_password, password_decrypted)) { - apSettings.password = password_decrypted; - } else { - LOG_E(TAG, "Failed to decrypt password from %s", file_path.c_str()); - return false; - } - } else { - apSettings.password = ""; - } - - if (map.contains(AP_PROPERTIES_KEY_AUTO_CONNECT)) { - apSettings.autoConnect = (map[AP_PROPERTIES_KEY_AUTO_CONNECT] == "true"); - } else { - apSettings.autoConnect = true; - } - - if (map.contains(AP_PROPERTIES_KEY_CHANNEL)) { - apSettings.channel = std::stoi(map[AP_PROPERTIES_KEY_CHANNEL]); - } else { - apSettings.channel = 0; - } - - return true; - -} - -bool save(const WifiApSettings& apSettings) { - if (apSettings.ssid.empty()) { - return false; - } - - auto service_context = findServiceContext(); - if (service_context == nullptr) { - return false; - } - - const auto file_path = getApPropertiesFilePath(service_context->getPaths(), apSettings.ssid); - if (!file::findOrCreateParentDirectory(file_path, 0755)) { - LOG_E(TAG, "Failed to create %s", file_path.c_str()); - return false; - } - - std::map map; - - std::string password_encrypted; - if (!apSettings.password.empty()) { - if (!encrypt(apSettings.ssid, apSettings.password, password_encrypted)) { - return false; - } - } else { - password_encrypted = ""; - } - - map[AP_PROPERTIES_KEY_PASSWORD] = password_encrypted; - map[AP_PROPERTIES_KEY_SSID] = apSettings.ssid; - map[AP_PROPERTIES_KEY_AUTO_CONNECT] = apSettings.autoConnect ? "true" : "false"; - map[AP_PROPERTIES_KEY_CHANNEL] = std::to_string(apSettings.channel); - - return file::savePropertiesFile(file_path, map); -} - -bool remove(const std::string& ssid) { - auto service_context = findServiceContext(); - if (service_context == nullptr) { - return false; - } - - const auto path = getApPropertiesFilePath(service_context->getPaths(), ssid); - if (!file::isFile(path)) { - return false; - } - return ::remove(path.c_str()) == 0; -} - -} diff --git a/Tactility/Source/service/wifi/WifiBootSplashInit.cpp b/Tactility/Source/service/wifi/WifiBootSplashInit.cpp deleted file mode 100644 index b279f1a7f..000000000 --- a/Tactility/Source/service/wifi/WifiBootSplashInit.cpp +++ /dev/null @@ -1,146 +0,0 @@ -#include - -#include "Tactility/service/wifi/Wifi.h" -#include "Tactility/service/wifi/WifiSettings.h" - -#include - -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include -#include -#include - -namespace tt::service::wifi { - -constexpr auto* TAG = "WifiBootSplashInit"; - -constexpr auto* AP_PROPERTIES_KEY_SSID = "ssid"; -constexpr auto* AP_PROPERTIES_KEY_PASSWORD = "password"; -constexpr auto* AP_PROPERTIES_KEY_AUTO_CONNECT = "autoConnect"; -constexpr auto* AP_PROPERTIES_KEY_CHANNEL = "channel"; -constexpr auto* AP_PROPERTIES_KEY_AUTO_REMOVE = "autoRemovePropertiesFile"; - -struct ApProperties { - std::string ssid; - std::string password; - bool autoConnect; - int32_t channel; - bool autoRemovePropertiesFile; -}; - -static void importWifiAp(const std::string& filePath) { - std::map map; - if (!file::loadPropertiesFile(filePath, map)) { - LOG_E(TAG, "Failed to load AP properties at %s", filePath.c_str()); - return; - } - - const auto ssid_iterator = map.find(AP_PROPERTIES_KEY_SSID); - if (ssid_iterator == map.end()) { - LOG_E(TAG, "%s is missing ssid", filePath.c_str()); - return; - } - const auto ssid = ssid_iterator->second; - - if (!settings::contains(ssid)) { - - const auto password_iterator = map.find(AP_PROPERTIES_KEY_PASSWORD); - const auto password = password_iterator == map.end() ? "" : password_iterator->second; - - const auto auto_connect_iterator = map.find(AP_PROPERTIES_KEY_AUTO_CONNECT); - const auto auto_connect = auto_connect_iterator == map.end() ? true : (auto_connect_iterator->second == "true"); - - const auto channel_iterator = map.find(AP_PROPERTIES_KEY_CHANNEL); - const auto channel = channel_iterator == map.end() ? 0 : std::stoi(channel_iterator->second); - - settings::WifiApSettings settings( - ssid, - password, - auto_connect, - channel - ); - - if (!settings::save(settings)) { - LOG_E(TAG, "Failed to save settings for %s", ssid.c_str()); - } else { - LOG_I(TAG, "Imported %s from %s", ssid.c_str(), filePath.c_str()); - } - } - - const auto auto_remove_iterator = map.find(AP_PROPERTIES_KEY_AUTO_REMOVE); - if (auto_remove_iterator != map.end() && auto_remove_iterator->second == "true") { - if (!remove(filePath.c_str())) { - LOG_E(TAG, "Failed to auto-remove %s", filePath.c_str()); - } else { - LOG_I(TAG, "Auto-removed %s", filePath.c_str()); - } - } -} - -static void importWifiApSettingsFromDir(const std::string& path) { - std::vector dirent_list; - if (file::scandir(path, dirent_list, [](const dirent* entry) { - switch (entry->d_type) { - case file::TT_DT_DIR: - case file::TT_DT_CHR: - case file::TT_DT_LNK: - return -1; - case file::TT_DT_REG: - default: { - std::string name = entry->d_name; - if (name.ends_with(".ap.properties")) { - return 0; - } else { - return -1; - } - } - } - }, nullptr) == 0) { - // keep original behavior: if scandir returns 0, give up silently - return; - } - - if (dirent_list.empty()) { - LOG_W(TAG, "No AP files found at %s", path.c_str()); - return; - } - - for (auto& dirent : dirent_list) { - std::string absolute_path = std::format("{}/{}", path, dirent.d_name); - importWifiAp(absolute_path); - } -} - -void bootSplashInit() { - LOG_I(TAG, "bootSplashInit dispatch"); - getMainDispatcher().dispatch([] { - LOG_I(TAG, "bootSplashInit dispatch begin"); - // Import any provisioning files placed on the system data partition. - const std::string provisioning_path = file::getChildPath(getDataPath(), "provisioning"); - if (file::isDirectory(provisioning_path)) { - importWifiApSettingsFromDir(provisioning_path); - } else { - LOG_I(TAG, "Skip provisioning: no files at %s", provisioning_path.c_str()); - } - - // Dispatch WiFi on - if (settings::shouldEnableOnBoot()) { - LOG_I(TAG, "Auto-enabling WiFi"); - getMainDispatcher().dispatch([] -> void { setEnabled(true); }); - } - - LOG_I(TAG, "bootSplashInit dispatch end"); - }); -} - -} diff --git a/Tactility/Source/service/wifi/WifiSettings.cpp b/Tactility/Source/service/wifi/WifiSettings.cpp deleted file mode 100644 index 7d061147a..000000000 --- a/Tactility/Source/service/wifi/WifiSettings.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include - -#include -#include -#include -#include - -#include - -namespace tt::service::wifi::settings { - -constexpr auto* TAG = "WifiSettings"; -constexpr auto* SETTINGS_KEY_ENABLE_ON_BOOT = "enableOnBoot"; - -struct WifiSettings { - bool enableOnBoot; -}; - -static WifiSettings cachedSettings { - .enableOnBoot = false -}; - -static bool cached = false; - -static bool hasWifiSettingsFile(std::shared_ptr context) { - std::string settings_path = context->getPaths()->getUserDataPath("settings.properties"); - return file::isFile(settings_path); -} - -static bool load(std::shared_ptr context, WifiSettings& settings) { - std::map map; - std::string settings_path = context->getPaths()->getUserDataPath("settings.properties"); - if (!file::loadPropertiesFile(settings_path, map)) { - return false; - } - - if (!map.contains(SETTINGS_KEY_ENABLE_ON_BOOT)) { - return false; - } - - auto enable_on_boot_string = map[SETTINGS_KEY_ENABLE_ON_BOOT]; - settings.enableOnBoot = (enable_on_boot_string == "true"); - return true; -} - -static bool save(std::shared_ptr context, const WifiSettings& settings) { - std::map map; - map[SETTINGS_KEY_ENABLE_ON_BOOT] = settings.enableOnBoot ? "true" : "false"; - std::string settings_path = context->getPaths()->getUserDataPath("settings.properties"); - if (!file::findOrCreateParentDirectory(settings_path, 0755)) { - LOG_E(TAG, "Failed to create %s", settings_path.c_str()); - return false; - } - return file::savePropertiesFile(settings_path, map); -} - -WifiSettings getCachedOrLoad() { - if (!cached) { - auto context = findServiceContext(); - if (context && hasWifiSettingsFile(context)) { - if (load(context, cachedSettings)) { - cached = true; - } else { - LOG_I(TAG, "Failed to load settings, using defaults"); - } - } - } - - return cachedSettings; -} - -void setEnableOnBoot(bool enable) { - cachedSettings.enableOnBoot = enable; - auto context = findServiceContext(); - if (context && !save(context, cachedSettings)) { - LOG_E(TAG, "Failed to save settings"); - } -} - -bool shouldEnableOnBoot() { - return getCachedOrLoad().enableOnBoot; -} - -} // namespace diff --git a/Tactility/Tests/CMakeLists.txt b/Tactility/Tests/CMakeLists.txt index 12bb51579..418f4b9cd 100644 --- a/Tactility/Tests/CMakeLists.txt +++ b/Tactility/Tests/CMakeLists.txt @@ -56,6 +56,7 @@ target_link_libraries(TactilityTests PRIVATE gps-generic-module gps-meshtastic-module service-module + wifi-module lv_screenshot minitar lvgl diff --git a/TactilityKernel/include/tactility/wifi_auto_scan.h b/TactilityKernel/include/tactility/wifi_auto_scan.h index b53529000..b0d864560 100644 --- a/TactilityKernel/include/tactility/wifi_auto_scan.h +++ b/TactilityKernel/include/tactility/wifi_auto_scan.h @@ -8,17 +8,17 @@ extern "C" { #endif /** - * @brief Suspend or resume WifiService's periodic background auto-connect scan. + * @brief Suspend or resume the wifi-module's periodic background auto-connect scan. * * Not a device operation - see tactility/drivers/wifi.h for the actual radio API. This exists * for narrow, short-lived windows where any WiFi/co-processor traffic would be unsafe (e.g. a * known co-processor reboot in progress during an OTA update) - callers must resume when done. - * Independent of WifiService's own internal connect()/disconnect() pause bookkeeping: it is + * Independent of wifi_autoconnect_pause_until_connected(): it is * never cleared implicitly by a connection succeeding/failing or the radio being enabled, only a * matching wifi_auto_scan_set_paused(false) clears it. * - * The real implementation lives in the Tactility WiFi service - * (Tactility/Source/service/wifi/Wifi.cpp), a layer above TactilityKernel - TactilityKernel + * The real implementation lives in the wifi-module's auto-connect service + * (Modules/wifi-module/source/wifi_autoconnect.cpp), a layer above TactilityKernel - TactilityKernel * can't call up into it directly (and mustn't link against it: TactilityKernelTests links * TactilityKernel alone, without Tactility). Tactility registers its implementation at startup * via wifi_auto_scan_set_paused_function(); until then (or on a build that never links Tactility, e.g. a @@ -29,7 +29,7 @@ extern "C" { */ void wifi_auto_scan_set_paused(bool paused); -/** @brief Register the real implementation. Called once by the Tactility WiFi service. */ +/** @brief Register the real implementation. Called by the wifi-module auto-connect service. */ void wifi_auto_scan_set_paused_function(void (*set_paused)(bool paused)); #ifdef __cplusplus diff --git a/Tests/SdkIntegration/tactility.py b/Tests/SdkIntegration/tactility.py old mode 100644 new mode 100755 index ef8c5f521..51fdb6271 --- a/Tests/SdkIntegration/tactility.py +++ b/Tests/SdkIntegration/tactility.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import json import os import re @@ -12,7 +14,7 @@ from urllib.parse import urlparse ttbuild_path = ".tactility" -ttbuild_version = "6.0.0" +ttbuild_version = "6.1.0" ttbuild_cdn = "https://cdn.tactilityproject.org" ttbuild_sdk_json_validity = 3600 # seconds ttport = 6666