Created wifi-module (#663)

- Moved WiFi logic from `TactilityKernel/` to `wifi-module`
- Created commandline app to control Wi-Fi
- Update `tactility.py` to v6.1.0 so it can be executed directly
This commit is contained in:
Ken Van Hoeylandt 2026-09-26 18:31:31 +02:00 committed by GitHub
parent 7c508bbaa0
commit 33898ddc4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 2059 additions and 1424 deletions

View File

@ -8,3 +8,4 @@ lvgl-module
lvgl-window-manager-module
service-module
graphics-module
wifi-module

View File

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

View File

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

View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: Apache-2.0
#include <functional>
#ifdef ESP_PLATFORM
// Out-of-line definitions for the std::function<long()> 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<long()>::operator()() const;
template std::function<long()>::function(const std::function<long()>&);
#endif

View File

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

View File

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

View File

@ -0,0 +1,5 @@
# Wi-Fi Module
This module contains:
- Settings API and a commandline app to manage them.
- Service that manages auto-connect behaviour.

View File

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

View File

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

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <tactility/error.h>
#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

View File

@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#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

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0
#include <wifi/module.h>
#include <wifi/private/wifi_service.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <service/manager.h>
#include <tactility/error.h>
#include <tactility/module.h>
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,
};
}

View File

@ -0,0 +1,311 @@
// SPDX-License-Identifier: Apache-2.0
#include <wifi/wifi_autoconnect.h>
#include <wifi/wifi_settings.h>
#include <wifi/private/wifi_service.h>
#include <service/manifest.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/concurrent/thread.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <tactility/system_event.h>
#include <tactility/time.h>
#include <tactility/wifi_auto_scan.h>
#include <atomic>
#include <new>
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<bool> pause_until_connected {false};
// Only set/cleared through wifi_auto_scan_set_paused()
static std::atomic<bool> 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<bool> 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<WifiAutoConnect*>(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<WifiAutoConnect*>(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<WifiAutoConnect*>(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<WifiAutoConnect*>(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<WifiAutoConnect*>(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,
};
}

View File

@ -0,0 +1,427 @@
// SPDX-License-Identifier: Apache-2.0
#include <wifi/wifi_autoconnect.h>
#include <wifi/wifi_settings.h>
#include <wifi/private/wifi_service.h>
#include <app/manifest.h>
#include <tactility/concurrent/task_event_group.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/time.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
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 <command>\n"
" status\n"
" radio on|off\n"
" scan\n"
" connect -s <ssid> [-p <password>] [-c <channel>] [--no-save]\n"
" disconnect\n"
" forget -s <ssid>\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<long>(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<long>(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 <ssid> [-p <password>] [-c <channel>] [--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<int32_t>(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<const WifiApSettings*>(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 <ssid>\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<size_t*>(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<void*>(wifi_command_main) },
.flags = APP_MANIFEST_FLAG_HIDDEN | APP_MANIFEST_FLAG_HEADLESS,
.stack = { .depth = 5120, .desired_memory_capability = 0 },
};
}

View File

@ -0,0 +1,133 @@
// SPDX-License-Identifier: Apache-2.0
#include <wifi/wifi_settings.h>
#include <wifi/private/wifi_service.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <tactility/properties_file.h>
#include <dirent.h>
#include <sys/stat.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
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<int32_t>(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<size_t>(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<size_t>(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);
}
}
}

View File

@ -0,0 +1,336 @@
// SPDX-License-Identifier: Apache-2.0
#include <wifi/wifi_settings.h>
#include <wifi/private/wifi_service.h>
#include <crypt/crypt.h>
#include <service/paths.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <tactility/properties_file.h>
#include <dirent.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
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<size_t>(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<uint8_t>(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<char*>(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<int32_t>(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<long>(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<size_t>(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;
}
}

View File

@ -45,6 +45,7 @@ list(APPEND REQUIRES_LIST
gps-generic-module
gps-meshtastic-module
service-module
wifi-module
# Libraries/*
lv_screenshot
minitar

View File

@ -1,94 +0,0 @@
#pragma once
#include <Tactility/PubSub.h>
#include <memory>
#include <string>
#include <vector>
#include <tactility/drivers/wifi.h>
#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<WifiApRecord> 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

View File

@ -1,58 +0,0 @@
#pragma once
#include <string>
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

View File

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

View File

@ -1,9 +0,0 @@
#pragma once
namespace tt::service::wifi::settings {
void setEnableOnBoot(bool enable);
bool shouldEnableOnBoot();
} // namespace

View File

@ -1,8 +1,12 @@
#pragma once
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/RecursiveMutex.h>
#include <tactility/drivers/wifi.h>
#include <string>
#include <vector>
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<WifiApRecord> apRecords;
std::string connectSsid;
@ -25,10 +31,17 @@ public:
bool hasScannedAfterRadioOn() const { return scannedAfterRadioOn; }
void setRadioState(service::wifi::RadioState state);
service::wifi::RadioState getRadioState() const;
void setRadioState(WifiRadioState state);
WifiRadioState getRadioState() const;
void updateApRecords();
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(Device* device);
template <std::invocable<const std::vector<WifiApRecord>&> Func>
void withApRecords(Func&& onApRecords) const {

View File

@ -5,7 +5,6 @@
#include <Tactility/PubSub.h>
#include <Tactility/Mutex.h>
#include <Tactility/service/wifi/Wifi.h>
// 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

View File

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

View File

@ -1,10 +0,0 @@
#pragma once
#include <Tactility/service/ServiceContext.h>
#include <memory>
namespace tt::service::wifi {
std::shared_ptr<ServiceContext> findServiceContext();
}

View File

@ -63,6 +63,7 @@
#include <freertos/module.h>
#include <gps/module.h>
#include <wifi/module.h>
#include <gps_generic/module.h>
#include <gps_meshtastic/module.h>
#include <graphics/module.h>
@ -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);

View File

@ -4,7 +4,6 @@
#include <Tactility/app/apphub/AppHubEntry.h>
#include <Tactility/app/apphubdetails/AppHubDetailsApp.h>
#include <Tactility/file/File.h>
#include <Tactility/service/wifi/Wifi.h>
#include <app/event.h>
#include <app/manager.h>
@ -16,6 +15,8 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl/lvgl.h>
@ -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();

View File

@ -3,7 +3,6 @@
#include <Tactility/lvgl/Style.h>
#include <Tactility/service/development/DevelopmentService.h>
#include <Tactility/service/development/DevelopmentSettings.h>
#include <Tactility/service/wifi/Wifi.h>
#include <app/event.h>
#include <app/manager.h>
@ -14,6 +13,8 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl.h>
@ -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);

View File

@ -6,7 +6,7 @@
#include <Tactility/app/timezone/TimeZone.h>
#include <Tactility/app/wifimanage/WifiManage.h>
#include <Tactility/file/File.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
@ -17,6 +17,8 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl/fonts.h>
@ -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);
}
}

View File

@ -1,7 +1,6 @@
#include <Tactility/Tactility.h>
#include <Tactility/settings/WebServerSettings.h>
#include <Tactility/service/webserver/WebServerService.h>
#include <Tactility/service/wifi/Wifi.h>
#include <app/event.h>
#include <app/manager.h>
@ -11,6 +10,8 @@
#include <lvgl_window_manager/window_manager.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl.h>
@ -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";

View File

@ -1,7 +1,6 @@
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
@ -11,11 +10,15 @@
#include <lvgl_window_manager/window_manager.h>
#include <wifi/wifi_autoconnect.h>
#include <wifi/wifi_settings.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/toolbar.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
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<Context*>(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_obj_t*>(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<Context*>(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<Context*>(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;
}

View File

@ -1,8 +1,6 @@
#include <Tactility/app/wificonnect/WifiConnect.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <Tactility/service/wifi/WifiGlobals.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
@ -12,12 +10,15 @@
#include <lvgl_window_manager/window_manager.h>
#include <wifi/wifi_settings.h>
#include <lvgl/lvgl.h>
#include <lvgl/widgets/spinner.h>
#include <lvgl/widgets/toolbar.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl.h>
@ -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) {

View File

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

View File

@ -6,8 +6,6 @@
#include <Tactility/app/wifimanage/View.h>
#include <Tactility/app/wifimanage/WifiManagePrivate.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/service/wifi/WifiSettings.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
@ -16,6 +14,8 @@
#include <tactility/log.h>
#include <lvgl/lvgl.h>
#include <wifi/wifi_settings.h>
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<void*>(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);

View File

@ -3,6 +3,7 @@
#include <Tactility/app/wifiapsettings/WifiApSettings.h>
#include <Tactility/app/wificonnect/WifiConnect.h>
#include <Tactility/Tactility.h>
#include <app/event.h>
#include <app/manager.h>
@ -12,8 +13,12 @@
#include <lvgl_window_manager/window_manager.h>
#include <wifi/wifi_autoconnect.h>
#include <wifi/wifi_settings.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <lvgl/lvgl.h>
@ -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);
}

View File

@ -5,11 +5,12 @@
#if defined(CONFIG_SOC_WIFI_SUPPORTED) && !defined(CONFIG_ESP_HOSTED_ENABLED)
#include <Tactility/service/espnow/EspNowBackend.h>
#include <Tactility/service/wifi/Wifi.h>
#include <esp_now.h>
#include <esp_wifi.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
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

View File

@ -6,7 +6,6 @@
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/wifi/Wifi.h>
#include <tactility/check.h>
#include <tactility/device.h>
@ -18,6 +17,7 @@
#include <tactility/drivers/usb_host_hid.h>
#include <tactility/drivers/usb_host_midi.h>
#include <tactility/drivers/usb_host_msc.h>
#include <tactility/drivers/wifi.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/log.h>
@ -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:
return LVGL_ICON_STATUSBAR_SIGNAL_WIFI_OFF;
case ConnectionActive:
rssi = wifi::getRssi();
return getWifiStatusIconForRssi(rssi);
default:
check(false, "not implemented");
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 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);

View File

@ -15,9 +15,10 @@
#include <Tactility/network/HttpServerReq.h>
#include <Tactility/network/HttpdReq.h>
#include <Tactility/network/Url.h>
#include <Tactility/service/wifi/Wifi.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/filesystem/file_system.h>
#include <tactility/log.h>
@ -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 << ",";

View File

@ -1,593 +0,0 @@
#include <Tactility/service/wifi/Wifi.h>
#include <Tactility/CoreDefines.h>
#include <Tactility/LogMessages.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/Tactility.h>
#include <Tactility/Thread.h>
#include <Tactility/Timer.h>
#include <Tactility/service/Service.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/wifi/WifiBootSplashInit.h>
#include <Tactility/service/wifi/WifiGlobals.h>
#include <tactility/check.h>
#include <tactility/device.h>
#include <tactility/drivers/wifi.h>
#include <tactility/log.h>
#include <tactility/system_event.h>
#include <tactility/time.h>
#include <tactility/wifi_auto_scan.h>
#include <algorithm>
#include <atomic>
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<ServiceContext> 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<bool> externalScanPause{false};
bool connectionTargetRemember = false;
settings::WifiApSettings connectionTarget;
uint16_t scanRecordLimit = TT_WIFI_SCAN_RECORD_LIMIT;
TickType_t lastScanTime = MAX_TICKS;
std::unique_ptr<Timer> 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<bool> 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<WifiApRecord> getScanResults() {
std::vector<WifiApRecord> 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>(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<WifiService>
};
} // namespace tt::service::wifi

View File

@ -1,244 +0,0 @@
#include <Tactility/service/wifi/WifiPrivate.h>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/file/File.h>
#include <crypt/crypt.h>
#include <Tactility/service/ServicePaths.h>
#include <tactility/log.h>
#include <cstring>
#include <format>
#include <iomanip>
#include <ranges>
#include <sstream>
#include <string>
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<int>(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<uint8_t>(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<ServicePaths> 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<uint8_t*>(calloc(encrypted_length, 1));
memcpy(padded_plaintext, plaintext.c_str(), length);
auto* buffer = static_cast<uint8_t*>(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<uint8_t*>(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<uint8_t*>(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<char*>(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<std::string, std::string> 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<std::string, std::string> 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;
}
}

View File

@ -1,146 +0,0 @@
#include <Tactility/service/wifi/WifiBootSplashInit.h>
#include "Tactility/service/wifi/Wifi.h"
#include "Tactility/service/wifi/WifiSettings.h"
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/MountPoints.h>
#include <Tactility/file/File.h>
#include <Tactility/service/wifi/WifiApSettings.h>
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/Tactility.h>
#include <tactility/log.h>
#include <dirent.h>
#include <format>
#include <map>
#include <string>
#include <vector>
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<std::string, std::string> 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> 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");
});
}
}

View File

@ -1,84 +0,0 @@
#include <Tactility/service/wifi/WifiSettings.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <Tactility/service/ServicePaths.h>
#include <Tactility/service/wifi/WifiPrivate.h>
#include <tactility/log.h>
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<ServiceContext> context) {
std::string settings_path = context->getPaths()->getUserDataPath("settings.properties");
return file::isFile(settings_path);
}
static bool load(std::shared_ptr<ServiceContext> context, WifiSettings& settings) {
std::map<std::string, std::string> 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<ServiceContext> context, const WifiSettings& settings) {
std::map<std::string, std::string> 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

View File

@ -56,6 +56,7 @@ target_link_libraries(TactilityTests PRIVATE
gps-generic-module
gps-meshtastic-module
service-module
wifi-module
lv_screenshot
minitar
lvgl

View File

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

4
Tests/SdkIntegration/tactility.py Normal file → Executable file
View File

@ -1,3 +1,5 @@
#!/usr/bin/env python3
import json
import os
import re
@ -12,7 +14,7 @@ import tarfile
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