diff --git a/firmware/include/config.hpp b/firmware/include/config.hpp index 404859e..3e86588 100644 --- a/firmware/include/config.hpp +++ b/firmware/include/config.hpp @@ -25,6 +25,12 @@ namespace wifi { constexpr const char *SSID_KEY = "wifi_ssid"; constexpr const char *PASSWORD_KEY = "wifi_password"; +constexpr std::uint32_t RECONNECT_DELAY_MS = 5000; + +// Max WiFi TX power in 0.25 dBm units (34 = 8.5 dBm); lowered for the SuperMini +// antenna. +constexpr std::int8_t MAX_TX_POWER = 34; + } // namespace wifi namespace mqtt { diff --git a/firmware/partitions.csv b/firmware/partitions.csv new file mode 100644 index 0000000..9ab7038 --- /dev/null +++ b/firmware/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size +# NVS kept at 0x9000 / 0x6000 to preserve the host provisioner contract. +# Larger app partition to fit the BLE + Wi-Fi binary. +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x300000, diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 6871df4..de5c234 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -16,3 +16,4 @@ platform = espressif32@6.12.0 board = esp32-c3-devkitm-1 framework = espidf monitor_speed = 115200 +board_build.partitions = partitions.csv diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults index 9f5e576..b0a2b26 100644 --- a/firmware/sdkconfig.defaults +++ b/firmware/sdkconfig.defaults @@ -26,3 +26,8 @@ CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y CONFIG_BT_NIMBLE_ROLE_CENTRAL=n CONFIG_BT_NIMBLE_ROLE_OBSERVER=n + +# --- Wi-Fi station + BLE/Wi-Fi software coexistence -------------------------- +# The connectivity/wifi module runs in station mode alongside BLE provisioning; +# ESP32-C3 shares one radio, so software coexistence must be enabled. +CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y diff --git a/firmware/src/CMakeLists.txt b/firmware/src/CMakeLists.txt index 370a387..c5b16a0 100644 --- a/firmware/src/CMakeLists.txt +++ b/firmware/src/CMakeLists.txt @@ -1,2 +1,3 @@ FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) -idf_component_register(SRCS ${app_sources} REQUIRES bt) +idf_component_register(SRCS ${app_sources} + REQUIRES bt esp_wifi esp_event esp_netif esp_timer) diff --git a/firmware/src/connectivity/config/infrastructure/flash_device_config.hpp b/firmware/src/connectivity/config/infrastructure/flash_device_config.hpp index dbab64f..2e929dc 100644 --- a/firmware/src/connectivity/config/infrastructure/flash_device_config.hpp +++ b/firmware/src/connectivity/config/infrastructure/flash_device_config.hpp @@ -34,7 +34,21 @@ class FlashDeviceConfig : public IDeviceConfig { FlashDeviceConfig(flash_memory::FlashService &flash, const DeviceConfigParams ¶ms); + /** + * @brief Reads the device id from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if not provisioned, or an ESP-IDF + * error. + */ esp_err_t getId(std::string &out) const override; + + /** + * @brief Returns the injected project name. + * + * @param out Receives the project name. + * @return ESP_OK on success. + */ esp_err_t getProjectName(std::string &out) const override; private: diff --git a/firmware/src/connectivity/config/infrastructure/flash_mqtt_config.hpp b/firmware/src/connectivity/config/infrastructure/flash_mqtt_config.hpp index 071d571..3b609a5 100644 --- a/firmware/src/connectivity/config/infrastructure/flash_mqtt_config.hpp +++ b/firmware/src/connectivity/config/infrastructure/flash_mqtt_config.hpp @@ -37,16 +37,68 @@ class FlashMqttConfig : public IMqttConfig { FlashMqttConfig(flash_memory::FlashService &flash, const MqttConfigKeys &keys); + /** + * @brief Reads the broker host from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if unset, or an ESP-IDF error. + */ esp_err_t getHost(std::string &out) const override; + + /** + * @brief Persists the broker host. + * + * @param value Value to store. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t setHost(const std::string &value) override; + /** + * @brief Reads the broker port from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if unset, or an ESP-IDF error. + */ esp_err_t getPort(std::string &out) const override; + + /** + * @brief Persists the broker port. + * + * @param value Value to store. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t setPort(const std::string &value) override; + /** + * @brief Reads the user from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if unset, or an ESP-IDF error. + */ esp_err_t getUser(std::string &out) const override; + + /** + * @brief Persists the user. + * + * @param value Value to store. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t setUser(const std::string &value) override; + /** + * @brief Reads the password from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if unset, or an ESP-IDF error. + */ esp_err_t getPassword(std::string &out) const override; + + /** + * @brief Persists the password. + * + * @param value Value to store. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t setPassword(const std::string &value) override; private: diff --git a/firmware/src/connectivity/config/infrastructure/flash_wifi_config.hpp b/firmware/src/connectivity/config/infrastructure/flash_wifi_config.hpp index e3dc75e..c3cbbee 100644 --- a/firmware/src/connectivity/config/infrastructure/flash_wifi_config.hpp +++ b/firmware/src/connectivity/config/infrastructure/flash_wifi_config.hpp @@ -35,10 +35,36 @@ class FlashWifiConfig : public IWifiConfig { FlashWifiConfig(flash_memory::FlashService &flash, const WifiConfigKeys &keys); + /** + * @brief Reads the SSID from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if unset, or an ESP-IDF error. + */ esp_err_t getSsid(std::string &out) const override; + + /** + * @brief Persists the SSID. + * + * @param value Value to store. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t setSsid(const std::string &value) override; + /** + * @brief Reads the password from storage. + * + * @param out Receives the value on success. + * @return ESP_OK, ESP_ERR_NVS_NOT_FOUND if unset, or an ESP-IDF error. + */ esp_err_t getPassword(std::string &out) const override; + + /** + * @brief Persists the password. + * + * @param value Value to store. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t setPassword(const std::string &value) override; private: diff --git a/firmware/src/connectivity/provisioning/infrastructure/nimble_gatt_server.hpp b/firmware/src/connectivity/provisioning/infrastructure/nimble_gatt_server.hpp index a7a4f2e..b6a0838 100644 --- a/firmware/src/connectivity/provisioning/infrastructure/nimble_gatt_server.hpp +++ b/firmware/src/connectivity/provisioning/infrastructure/nimble_gatt_server.hpp @@ -17,8 +17,26 @@ namespace connectivity::provisioning { */ class NimbleGattServer : public IGattServer { public: + /** + * @brief Brings up the NimBLE host (first call) and starts advertising. + * + * Registers the GATT service from @p service on the first call; later calls + * only re-start advertising under @p deviceName. + * + * @param deviceName Advertised device name. + * @param service Service and characteristics to expose. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t start(const std::string &deviceName, const ServiceSpec &service) override; + + /** + * @brief Stops advertising and drops the active connection. + * + * The NimBLE host stays initialized so a later @c start() is cheap. + * + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ esp_err_t stop() override; }; diff --git a/firmware/src/connectivity/wifi/application/wifi_service.cpp b/firmware/src/connectivity/wifi/application/wifi_service.cpp new file mode 100644 index 0000000..e5bb490 --- /dev/null +++ b/firmware/src/connectivity/wifi/application/wifi_service.cpp @@ -0,0 +1,42 @@ +#include "connectivity/wifi/application/wifi_service.hpp" + +#include +#include + +#include "connectivity/wifi/tag.hpp" + +namespace connectivity::wifi { + +WifiService::WifiService(IWifiStation &station, config::IWifiConfig &config) + : _station(station), _config(config) {} + +esp_err_t WifiService::connect() { + std::string ssid; + esp_err_t err = _config.getSsid(ssid); + if (err == ESP_ERR_NVS_NOT_FOUND || (err == ESP_OK && ssid.empty())) { + ESP_LOGW(TAG, "no WiFi credentials; provision via BLE first"); + + return ESP_ERR_INVALID_STATE; + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "failed to read ssid: %s", esp_err_to_name(err)); + + return err; + } + + std::string password; + err = _config.getPassword(password); + if (err == ESP_ERR_NVS_NOT_FOUND) { + password.clear(); + } else if (err != ESP_OK) { + ESP_LOGE(TAG, "failed to read password: %s", esp_err_to_name(err)); + + return err; + } + + return _station.connect(ssid, password); +} + +bool WifiService::isConnected() const { return _station.isConnected(); } + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/application/wifi_service.hpp b/firmware/src/connectivity/wifi/application/wifi_service.hpp new file mode 100644 index 0000000..d356730 --- /dev/null +++ b/firmware/src/connectivity/wifi/application/wifi_service.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include "connectivity/config/domain/i_wifi_config.hpp" +#include "connectivity/wifi/domain/i_wifi_station.hpp" + +namespace connectivity::wifi { + +/** + * @brief Application service that drives the WiFi station from stored config. + * + * Reads the credentials from the configuration store and asks the station to + * connect. The radio handles reconnection on its own with the stored config. + */ +class WifiService { + public: + /** + * @brief Builds the service over a station and a configuration store. + * + * @param station WiFi station port. Must outlive this instance. + * @param config WiFi configuration store. Must outlive this instance. + */ + WifiService(IWifiStation &station, config::IWifiConfig &config); + + /** + * @brief Connects using the credentials in storage. + * + * @return ESP_OK if the attempt was launched, ESP_ERR_INVALID_STATE if there + * are no credentials yet, or an ESP-IDF error code on failure. + */ + esp_err_t connect(); + + /** + * @brief Whether the station currently holds a connection. + */ + bool isConnected() const; + + private: + IWifiStation &_station; + config::IWifiConfig &_config; +}; + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/domain/i_wifi_station.hpp b/firmware/src/connectivity/wifi/domain/i_wifi_station.hpp new file mode 100644 index 0000000..e67c647 --- /dev/null +++ b/firmware/src/connectivity/wifi/domain/i_wifi_station.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include +#include + +namespace connectivity::wifi { + +/// Connection state of the WiFi station. +enum class Status { Disconnected, Connecting, Connected }; + +/// Notified on every station status transition. +using StatusHandler = std::function; + +/** + * @brief Port for a WiFi station radio. + * + * Abstracts the connection mechanics (esp_wifi here) behind start/connect/stop + * operations and an asynchronous status callback. Keeps the radio stack out of + * the application layer. + */ +class IWifiStation { + public: + virtual ~IWifiStation() = default; + + /** + * @brief One-time initialization of the network stack and event handlers. + * + * @param onStatus Callback invoked on every status transition. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ + virtual esp_err_t begin(StatusHandler onStatus) = 0; + + /** + * @brief Connects to the given network (non-blocking). + * + * On drop, the station retries on its own. Status is reported via the + * callback registered in @c begin(). + * + * @param ssid Network SSID. + * @param password Network password (empty for an open network). + * @return ESP_OK if the attempt was launched, or an ESP-IDF error. + */ + virtual esp_err_t connect(const std::string &ssid, + const std::string &password) = 0; + + /** + * @brief Stops auto-reconnect and disconnects from the network. + * + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ + virtual esp_err_t disconnect() = 0; + + /** + * @brief Whether the station currently holds an IP-level connection. + */ + virtual bool isConnected() const = 0; +}; + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/infrastructure/esp_wifi_station.cpp b/firmware/src/connectivity/wifi/infrastructure/esp_wifi_station.cpp new file mode 100644 index 0000000..643316d --- /dev/null +++ b/firmware/src/connectivity/wifi/infrastructure/esp_wifi_station.cpp @@ -0,0 +1,198 @@ +#include "connectivity/wifi/infrastructure/esp_wifi_station.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "connectivity/wifi/tag.hpp" + +namespace connectivity::wifi { + +namespace { + +// Single station per device, so the esp_wifi/event state lives at file scope. +StatusHandler g_onStatus; +bool g_connected = false; +bool g_initialized = false; +bool g_autoReconnect = false; +std::uint32_t g_reconnectDelayMs = 5000; +std::int8_t g_maxTxPower = 80; +esp_timer_handle_t g_reconnectTimer = nullptr; + +void notify(Status status) { + if (g_onStatus) { + g_onStatus(status); + } +} + +void reconnectTimerCb(void *) { + ESP_LOGI(TAG, "retrying connection"); + notify(Status::Connecting); + esp_wifi_connect(); +} + +void scheduleReconnect() { + if (g_reconnectTimer == nullptr) { + return; + } + + esp_timer_stop(g_reconnectTimer); + esp_timer_start_once(g_reconnectTimer, + static_cast(g_reconnectDelayMs) * 1000); +} + +void wifiEventHandler(void *, esp_event_base_t base, std::int32_t id, void *) { + if (base != WIFI_EVENT) { + return; + } + + if (id == WIFI_EVENT_STA_DISCONNECTED) { + g_connected = false; + notify(Status::Disconnected); + if (g_autoReconnect) { + ESP_LOGW(TAG, "disconnected; retrying in %u ms", + static_cast(g_reconnectDelayMs)); + scheduleReconnect(); + } + } +} + +void ipEventHandler(void *, esp_event_base_t base, std::int32_t id, + void *data) { + if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { + auto *event = static_cast(data); + ESP_LOGI(TAG, "connected; got IP " IPSTR, IP2STR(&event->ip_info.ip)); + g_connected = true; + notify(Status::Connected); + } +} + +} // namespace + +EspWifiStation::EspWifiStation(std::uint32_t reconnectDelayMs, + std::int8_t maxTxPower) { + g_reconnectDelayMs = reconnectDelayMs; + g_maxTxPower = maxTxPower; +} + +esp_err_t EspWifiStation::begin(StatusHandler onStatus) { + if (g_initialized) { + return ESP_OK; + } + + g_onStatus = std::move(onStatus); + + esp_err_t err = esp_netif_init(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err)); + + return err; + } + + err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", + esp_err_to_name(err)); + + return err; + } + + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + err = esp_wifi_init(&cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); + + return err; + } + + err = esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &wifiEventHandler, nullptr, nullptr); + if (err != ESP_OK) { + return err; + } + + err = esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, + &ipEventHandler, nullptr, nullptr); + if (err != ESP_OK) { + return err; + } + + esp_timer_create_args_t timerArgs = {}; + timerArgs.callback = &reconnectTimerCb; + timerArgs.name = "wifi_reconnect"; + err = esp_timer_create(&timerArgs, &g_reconnectTimer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_timer_create failed: %s", esp_err_to_name(err)); + + return err; + } + + err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + return err; + } + + err = esp_wifi_start(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err)); + + return err; + } + + // Cap TX power after start (antenna workaround for ESP32-C3 SuperMini). + err = esp_wifi_set_max_tx_power(g_maxTxPower); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_wifi_set_max_tx_power failed: %s", esp_err_to_name(err)); + } else { + ESP_LOGI(TAG, "max tx power set to %d (0.25 dBm units)", g_maxTxPower); + } + + g_initialized = true; + ESP_LOGI(TAG, "wifi station started"); + + return ESP_OK; +} + +esp_err_t EspWifiStation::connect(const std::string &ssid, + const std::string &password) { + wifi_config_t cfg = {}; + std::strncpy(reinterpret_cast(cfg.sta.ssid), ssid.c_str(), + sizeof(cfg.sta.ssid) - 1); + std::strncpy(reinterpret_cast(cfg.sta.password), password.c_str(), + sizeof(cfg.sta.password) - 1); + cfg.sta.threshold.authmode = + password.empty() ? WIFI_AUTH_OPEN : WIFI_AUTH_WPA2_PSK; + + esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err)); + + return err; + } + + g_autoReconnect = true; + ESP_LOGI(TAG, "connecting to '%s'", ssid.c_str()); + notify(Status::Connecting); + + return esp_wifi_connect(); +} + +esp_err_t EspWifiStation::disconnect() { + g_autoReconnect = false; + if (g_reconnectTimer != nullptr) { + esp_timer_stop(g_reconnectTimer); + } + g_connected = false; + + return esp_wifi_disconnect(); +} + +bool EspWifiStation::isConnected() const { return g_connected; } + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/infrastructure/esp_wifi_station.hpp b/firmware/src/connectivity/wifi/infrastructure/esp_wifi_station.hpp new file mode 100644 index 0000000..49156d0 --- /dev/null +++ b/firmware/src/connectivity/wifi/infrastructure/esp_wifi_station.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include "connectivity/wifi/domain/i_wifi_station.hpp" + +namespace connectivity::wifi { + +/** + * @brief esp_wifi station adapter. + * + * Implements @c IWifiStation in station mode. There is a single station per + * device, so the esp_wifi/event-loop state lives at file scope in the + * implementation; this keeps the WiFi stack headers out of the rest of the + * codebase. On disconnect it retries indefinitely with a fixed delay. + */ +class EspWifiStation : public IWifiStation { + public: + /** + * @brief Builds the adapter. + * + * @param reconnectDelayMs Delay between reconnection attempts. + * @param maxTxPower Maximum TX power in 0.25 dBm units, applied after + * the station starts (antenna workaround). + */ + EspWifiStation(std::uint32_t reconnectDelayMs, std::int8_t maxTxPower); + + /** + * @brief Initializes the network stack and starts the station. + * + * Sets up esp_netif, the default event loop, the WiFi driver and the WiFi/IP + * event handlers, then starts the station in STA mode. Idempotent: a no-op if + * already initialized. + * + * @param onStatus Callback invoked on every status transition. + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ + esp_err_t begin(StatusHandler onStatus) override; + + /** + * @brief Launches a non-blocking connection attempt. + * + * Stores the credentials in the driver and calls esp_wifi_connect(); the + * outcome arrives asynchronously via the status callback (Connected on + * GOT_IP, Disconnected otherwise). Enables auto-reconnect. + * + * @param ssid Network SSID. + * @param password Network password (empty selects an open network). + * @return ESP_OK if the attempt was launched, or an ESP-IDF error. + */ + esp_err_t connect(const std::string &ssid, + const std::string &password) override; + + /** + * @brief Disables auto-reconnect, cancels any pending retry and disconnects. + * + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ + esp_err_t disconnect() override; + + /** + * @brief Whether the station currently holds an IP-level connection. + * + * Set when an IP is acquired and cleared on disconnect. + */ + bool isConnected() const override; +}; + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/tag.hpp b/firmware/src/connectivity/wifi/tag.hpp new file mode 100644 index 0000000..fd79002 --- /dev/null +++ b/firmware/src/connectivity/wifi/tag.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace connectivity::wifi { + +constexpr const char *TAG = "connectivity/wifi"; + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/wifi.cpp b/firmware/src/connectivity/wifi/wifi.cpp new file mode 100644 index 0000000..506e516 --- /dev/null +++ b/firmware/src/connectivity/wifi/wifi.cpp @@ -0,0 +1,30 @@ +#include "connectivity/wifi/wifi.hpp" + +namespace connectivity::wifi { + +Wifi::Wifi(config::IWifiConfig &config, std::uint32_t reconnectDelayMs, + std::int8_t maxTxPower) + : _station(reconnectDelayMs, maxTxPower), _service(_station, config) {} + +esp_err_t Wifi::start() { + esp_err_t err = _station.begin([this](Status status) { + if (_userHandler) { + _userHandler(status); + } + }); + if (err != ESP_OK) { + return err; + } + + return _service.connect(); +} + +esp_err_t Wifi::stop() { return _station.disconnect(); } + +bool Wifi::isConnected() const { return _service.isConnected(); } + +void Wifi::onStatusChange(StatusHandler handler) { + _userHandler = std::move(handler); +} + +} // namespace connectivity::wifi diff --git a/firmware/src/connectivity/wifi/wifi.hpp b/firmware/src/connectivity/wifi/wifi.hpp new file mode 100644 index 0000000..5e4a0b1 --- /dev/null +++ b/firmware/src/connectivity/wifi/wifi.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include "connectivity/config/domain/i_wifi_config.hpp" +#include "connectivity/wifi/application/wifi_service.hpp" +#include "connectivity/wifi/domain/i_wifi_station.hpp" +#include "connectivity/wifi/infrastructure/esp_wifi_station.hpp" + +namespace connectivity::wifi { + +/** + * @brief Module facade for WiFi station connectivity. + * + * Composition root of the wifi module: owns the esp_wifi adapter and the + * application service, wiring them to the WiFi configuration store. Connects + * asynchronously using the stored credentials and keeps the link up with + * indefinite reconnection. Exposes the connection status for the application. + */ +class Wifi { + public: + /** + * @brief Builds the WiFi module. + * + * @param config WiFi configuration store. Must outlive this + * instance. + * @param reconnectDelayMs Delay between reconnection attempts. + * @param maxTxPower Maximum TX power in 0.25 dBm units (antenna + * workaround for ESP32-C3 SuperMini boards). + */ + Wifi(config::IWifiConfig &config, std::uint32_t reconnectDelayMs, + std::int8_t maxTxPower); + + /** + * @brief Brings the station up and connects using stored credentials. + * + * @return ESP_OK if the connection attempt was launched, + * ESP_ERR_INVALID_STATE if there are no credentials yet, or an ESP-IDF error + * code on failure. + */ + esp_err_t start(); + + /** + * @brief Stops auto-reconnect and disconnects. + * + * @return ESP_OK on success, or an ESP-IDF error code on failure. + */ + esp_err_t stop(); + + /** + * @brief Whether the station currently holds a connection. + */ + bool isConnected() const; + + /** + * @brief Registers a callback notified on every status transition. + */ + void onStatusChange(StatusHandler handler); + + private: + EspWifiStation _station; + WifiService _service; + StatusHandler _userHandler; +}; + +} // namespace connectivity::wifi diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 51779c8..25b177b 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -8,6 +8,7 @@ #include "connectivity/config/infrastructure/flash_mqtt_config.hpp" #include "connectivity/config/infrastructure/flash_wifi_config.hpp" #include "connectivity/provisioning/provisioning.hpp" +#include "connectivity/wifi/wifi.hpp" #include "peripherals/flash_memory/flash_memory.hpp" #include "peripherals/led/led.hpp" #include "peripherals/relay/relay.hpp" @@ -15,6 +16,10 @@ static const char *TAG = "app/main"; static flash_memory::FlashMemory flashMemory; +static relay::Relay loadRelay(config::relay::GPIO, config::relay::INVERTED); +static led::Led builtinLed(config::builtin_led::GPIO, + config::builtin_led::INVERTED); + static connectivity::config::FlashDeviceConfig deviceConfig( flashMemory, {config::nvs::NAMESPACE, config::device::ID_KEY, config::device::PROJECT_NAME}); @@ -25,9 +30,7 @@ static connectivity::config::FlashMqttConfig mqttConfig( flashMemory, {config::nvs::NAMESPACE, config::mqtt::HOST_KEY, config::mqtt::PORT_KEY, config::mqtt::USER_KEY, config::mqtt::PASSWORD_KEY}); -static relay::Relay loadRelay(config::relay::GPIO, config::relay::INVERTED); -static led::Led builtinLed(config::builtin_led::GPIO, - config::builtin_led::INVERTED); + static connectivity::provisioning::Provisioning provisioning( deviceConfig, wifiConfig, mqttConfig, {.service = config::ble::SERVICE_UUID, @@ -39,6 +42,10 @@ static connectivity::provisioning::Provisioning provisioning( .mqttUser = config::ble::MQTT_USER_UUID, .mqttPassword = config::ble::MQTT_PASSWORD_UUID}); +static connectivity::wifi::Wifi wifi(wifiConfig, + config::wifi::RECONNECT_DELAY_MS, + config::wifi::MAX_TX_POWER); + extern "C" void app_main() { if (beginPeripherals(flashMemory, loadRelay, builtinLed) != ESP_OK) { ESP_LOGE(TAG, "setup failed; halting"); @@ -51,6 +58,11 @@ extern "C" void app_main() { ESP_LOGE(TAG, "provisioning start failed: %s", esp_err_to_name(err)); } + err = wifi.start(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "wifi start failed: %s", esp_err_to_name(err)); + } + // Idle loop: keeps app_main alive until the real application logic is added. while (true) { vTaskDelay(pdMS_TO_TICKS(1000));