1c2806bddf
+ other improvements
529 lines
17 KiB
C++
529 lines
17 KiB
C++
#include <Tactility/service/wifi/Wifi.h>
|
|
|
|
#include <Tactility/CoreDefines.h>
|
|
#include <Tactility/LogMessages.h>
|
|
#include <Tactility/RecursiveMutex.h>
|
|
#include <Tactility/SystemEvents.h>
|
|
#include <Tactility/Tactility.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/service/wifi/WifiSettings.h>
|
|
|
|
#include <tactility/check.h>
|
|
#include <tactility/device.h>
|
|
#include <tactility/drivers/wifi.h>
|
|
#include <tactility/log.h>
|
|
|
|
#include <algorithm>
|
|
|
|
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;
|
|
std::shared_ptr<PubSub<WifiEvent>> pubsub = std::make_shared<PubSub<WifiEvent>>();
|
|
RecursiveMutex mutex;
|
|
bool secureConnection = false;
|
|
bool pauseAutoConnect = false;
|
|
bool connectionTargetRemember = false;
|
|
settings::WifiApSettings connectionTarget;
|
|
uint16_t scanRecordLimit = TT_WIFI_SCAN_RECORD_LIMIT;
|
|
TickType_t lastScanTime = kernel::MAX_TICKS;
|
|
std::unique_ptr<Timer> autoConnectTimer;
|
|
kernel::SystemEventSubscription bootEventSubscription = kernel::NoSystemEventSubscription;
|
|
};
|
|
|
|
WifiServiceState state;
|
|
bool started = false;
|
|
|
|
void onWifiDeviceEvent(Device* device, void* context, ::WifiEvent event);
|
|
|
|
// ---- Helpers ----
|
|
|
|
void publish(WifiEvent event) {
|
|
state.pubsub->publish(event);
|
|
}
|
|
|
|
void publishRadioState(WifiRadioState radio_state) {
|
|
WifiEvent event = {};
|
|
event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED;
|
|
event.radio_state = radio_state;
|
|
publish(event);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ---- Dispatched work (runs on the main task) ----
|
|
|
|
void dispatchSetEnabled(bool enabled) {
|
|
LOG_I(TAG, "dispatchSetEnabled(%d)", (int)enabled);
|
|
if (!started || state.device == nullptr) return;
|
|
|
|
bool ready = device_is_ready(state.device);
|
|
if (enabled == ready) {
|
|
LOG_W(TAG, "Can't enable/disable from current state");
|
|
return;
|
|
}
|
|
|
|
if (enabled) {
|
|
publishRadioState(WIFI_RADIO_STATE_ON_PENDING);
|
|
|
|
if (device_start(state.device) != ERROR_NONE) {
|
|
LOG_E(TAG, "Failed to start WiFi device");
|
|
publishRadioState(WIFI_RADIO_STATE_OFF);
|
|
return;
|
|
}
|
|
|
|
if (wifi_add_event_callback(state.device, nullptr, onWifiDeviceEvent) != ERROR_NONE) {
|
|
LOG_E(TAG, "Failed to register WiFi event callback");
|
|
device_stop(state.device);
|
|
publishRadioState(WIFI_RADIO_STATE_OFF);
|
|
return;
|
|
}
|
|
|
|
state.pauseAutoConnect = false;
|
|
state.lastScanTime = 0;
|
|
publishRadioState(WIFI_RADIO_STATE_ON);
|
|
} else {
|
|
publishRadioState(WIFI_RADIO_STATE_OFF_PENDING);
|
|
|
|
if (device_stop(state.device) != ERROR_NONE) {
|
|
LOG_E(TAG, "Failed to stop WiFi device");
|
|
publishRadioState(WIFI_RADIO_STATE_ON);
|
|
return;
|
|
}
|
|
|
|
wifi_remove_event_callback(state.device, onWifiDeviceEvent);
|
|
|
|
state.secureConnection = false;
|
|
publishRadioState(WIFI_RADIO_STATE_OFF);
|
|
}
|
|
}
|
|
|
|
void dispatchScan() {
|
|
LOG_I(TAG, "dispatchScan()");
|
|
if (!started || state.device == nullptr || !device_is_ready(state.device)) return;
|
|
|
|
state.lastScanTime = kernel::getTicks();
|
|
|
|
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());
|
|
|
|
error_t result = wifi_station_connect(state.device, target.ssid.c_str(), target.password.c_str(), target.channel);
|
|
if (result != ERROR_NONE) {
|
|
LOG_E(TAG, "Failed to connect to %s (%s)", target.ssid.c_str(), error_to_string(result));
|
|
WifiEvent event = {};
|
|
event.type = WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT;
|
|
// The driver couldn't even initiate the connection attempt; there's no
|
|
// more specific WifiStationConnectionError for that.
|
|
event.connection_error = WIFI_STATION_CONNECTION_ERROR_TIMEOUT;
|
|
publish(event);
|
|
}
|
|
// On success, WIFI_EVENT_TYPE_STATION_STATE_CHANGED / _CONNECTION_RESULT arrive
|
|
// asynchronously via onWifiDeviceEvent().
|
|
}
|
|
|
|
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) {
|
|
// A manual disconnect() or an in-progress manual connect() has paused
|
|
// auto-connect. 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.
|
|
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;
|
|
if (!radio_scannable) return false;
|
|
|
|
TickType_t current_time = kernel::getTicks();
|
|
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*/, void* /*context*/, ::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.
|
|
kernel::publishSystemEvent(kernel::SystemEvent::NetworkDisconnected);
|
|
}
|
|
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");
|
|
}
|
|
kernel::publishSystemEvent(kernel::SystemEvent::NetworkConnected);
|
|
} 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;
|
|
}
|
|
|
|
// Forward the event as-is: subscribers inspect event.type and the
|
|
// relevant union field directly, same as this function does.
|
|
publish(event);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// region Public functions
|
|
|
|
std::shared_ptr<PubSub<WifiEvent>> getPubsub() {
|
|
return state.pubsub;
|
|
}
|
|
|
|
RadioState getRadioState() {
|
|
if (!started || state.device == nullptr || !device_is_ready(state.device)) {
|
|
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 = !device_is_ready(state.device);
|
|
}
|
|
|
|
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 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 {
|
|
|
|
class WifiService final : public Service {
|
|
|
|
public:
|
|
|
|
bool onStart(ServiceContext& /*service*/) override {
|
|
check(!started);
|
|
|
|
state.device = wifi_find_first_registered_device();
|
|
if (state.device == nullptr) {
|
|
LOG_W(TAG, "No WiFi device found");
|
|
}
|
|
|
|
state.bootEventSubscription = kernel::subscribeSystemEvent(kernel::SystemEvent::BootSplash, [](auto) {
|
|
bootSplashInit();
|
|
});
|
|
|
|
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.
|
|
|
|
kernel::unsubscribeSystemEvent(state.bootEventSubscription);
|
|
state.bootEventSubscription = kernel::NoSystemEventSubscription;
|
|
|
|
if (state.device != nullptr && device_is_ready(state.device)) {
|
|
wifi_remove_event_callback(state.device, onWifiDeviceEvent);
|
|
device_stop(state.device);
|
|
}
|
|
|
|
state.secureConnection = false;
|
|
state.pauseAutoConnect = false;
|
|
state.device = nullptr;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
extern const ServiceManifest manifest = {
|
|
.id = "wifi",
|
|
.createService = create<WifiService>
|
|
};
|
|
|
|
} // namespace tt::service::wifi
|