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