diff --git a/README.md b/README.md index 998a8f4..56826cc 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Library|Description [platypus](src/common/platypus)| Decoder for a custom image compression format suitable for dithered images (good for RGB555) and suitable for decoding on RP2040 at scanline speeds ... i.e you can easily decode a 320x240 image 60x per second to avoid storing the uncompressed image for scanout video. It gets about 50% compression (but is designed only for 4x4 fixed dithered RGB555 images, so is somewhat specific!). TODO add the encoder here :-) [usb_device](src/rp2_common/usb_device), [usb_common](src/rp2_common/usb_common)| The custom and somewhat minimal USB device stack used in the bootrom. We now use TinyUSB in the Pico SDK but kept here for posterity [usb_device_msc](src/rp2_common/usb_device_msc)| USB Mass Storage Class implementation using _usb_device_ +[wifi_settings_connect](src/rp2_common/wifi_settings_connect)| Library to manage WiFi connections. It provides Flash storage for WiFi passwords and hotspot names, and a background async\_context service to automatically connect to them. You can add Pico Extras to your project similarly to the SDK (copying [external/pico_extras_import.cmake](external/pico_extras_import.cmake) into your project) having set the `PICO_EXTRAS_PATH` variable in your environment or via cmake variable. diff --git a/src/rp2_common/CMakeLists.txt b/src/rp2_common/CMakeLists.txt index c780ab7..9a33cc2 100644 --- a/src/rp2_common/CMakeLists.txt +++ b/src/rp2_common/CMakeLists.txt @@ -10,4 +10,5 @@ pico_add_subdirectory(pico_sd_card) pico_add_subdirectory(pico_scanvideo_dpi) pico_add_subdirectory(usb_common) pico_add_subdirectory(usb_device) -pico_add_subdirectory(usb_device_msc) \ No newline at end of file +pico_add_subdirectory(usb_device_msc) +pico_add_subdirectory(wifi_settings_connect) diff --git a/src/rp2_common/wifi_settings_connect/CMakeLists.txt b/src/rp2_common/wifi_settings_connect/CMakeLists.txt new file mode 100644 index 0000000..91f7817 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/CMakeLists.txt @@ -0,0 +1,47 @@ +# +# Copyright (c) 2025 Jack Whitham +# +# SPDX-License-Identifier: BSD-3-Clause +# +# wifi_settings_connect +# +# Library to manage WiFi connections. It provides Flash storage +# for WiFi passwords and hotspot names, and a background async_context +# service to automatically connect to them. +# + +if (NOT PICO_CYW43_SUPPORTED) + message("wifi_settings_connect: WiFi hardware is required: run cmake -DPICO_BOARD=pico_w or -DPICO_BOARD=pico2_w") +elseif (NOT TARGET pico_cyw43_arch) + message("wifi_settings_connect: WiFi driver pico_cyw43_arch is not present") +elseif (NOT TARGET pico_lwip_core) + message("wifi_settings_connect: IP layer pico_lwip_core is not present") +else() + message("wifi_settings_connect: library is available.") + add_library(wifi_settings_connect INTERFACE) + set(WIFI_SETTINGS_VERSION_STRING "0.2.0c") + set(WIFI_SETTINGS_PROJECT_URL "https://github.com/jwhitham/pico-wifi-settings") + + target_compile_definitions(wifi_settings_connect INTERFACE + WIFI_SETTINGS_VERSION_STRING="${WIFI_SETTINGS_VERSION_STRING}" + WIFI_SETTINGS_PROJECT_URL="${WIFI_SETTINGS_PROJECT_URL}" + ) + + target_include_directories(wifi_settings_connect INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/include + ) + + target_sources(wifi_settings_connect INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/wifi_settings_connect.c + ${CMAKE_CURRENT_LIST_DIR}/wifi_settings_flash_storage.c + ${CMAKE_CURRENT_LIST_DIR}/wifi_settings_flash_range.c + ${CMAKE_CURRENT_LIST_DIR}/wifi_settings_hostname.c + ) + + target_link_libraries(wifi_settings_connect INTERFACE + pico_async_context_base + pico_stdlib + pico_cyw43_arch + pico_lwip_core + ) +endif() diff --git a/src/rp2_common/wifi_settings_connect/README.md b/src/rp2_common/wifi_settings_connect/README.md new file mode 100644 index 0000000..f6e956b --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/README.md @@ -0,0 +1,43 @@ +# wifi\_settings\_connect + +This is a library to manage WiFi connections. It provides Flash storage +for WiFi passwords and hotspot names, and a background async\_context +service to automatically connect to them. You can store details for +up to 100 hotspots and update them using +[picotool](https://github.com/raspberrypi/pico-sdk-tools/releases). +or a [setup application](https://github.com/jwhitham/pico-wifi-settings/releases/). +This avoids any need to specify build-time flags such as `WIFI_SSID` and `WIFI_PASSWORD`. + +WiFi hotspot details are stored in a Flash sector that isn't normally used by programs, +normally located near the end of Flash memory. This +[wifi-settings file](doc/SETTINGS_FILE.md) is a simple text file which +can be updated by USB or by installing a setup app from the +[pico-wifi-settings home page](https://github.com/jwhitham/pico-wifi-settings) +on Github. + +## Requirements + + - Raspberry Pi Pico W or Pico 2 W hardware + - a "bare metal" C/C++ application for Pico W (not FreeRTOS) + using the `cyw43` driver and `lwip` network stack + which are provided with the [Pico SDK](https://github.com/raspberrypi/pico-sdk/). + - between 2kb and 13kb of code space depending on options used + - WiFi network(s) with a DHCP server and WPA authentication + +## How to use it + +First, you need to configure the WiFi settings file +in Flash. See the [wifi-settings file documentation](doc/SETTINGS_FILE.md). + +Next, you need to modify your application to use wifi\_settings\_connect. +This involves adding a few lines of C code. +There is an [integration guide which explains what you need to do +to add wifi\_settings\_connect to your application](doc/INTEGRATION.md). + +## Enabling remote updates + +wifi\_settings\_connect is a subset of a larger library (wifi\_settings) which +also has support for remote updates of WiFi settings and over-the-air (OTA) +firmware updates. Visit the +[pico-wifi-settings home page](https://github.com/jwhitham/pico-wifi-settings) +for more information. diff --git a/src/rp2_common/wifi_settings_connect/doc/INTEGRATION.md b/src/rp2_common/wifi_settings_connect/doc/INTEGRATION.md new file mode 100644 index 0000000..7d42264 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/doc/INTEGRATION.md @@ -0,0 +1,127 @@ +# Integrating wifi\_settings\_connect into your own Pico application + +You can integrate wifi\_settings\_connect into your own Pico application +with just a few lines of code: +``` + #include "wifi_settings/wifi_settings_connect.h" // << add this + int main() { + stdio_init_all(); + if (wifi_settings_init() != 0) { // << and add this + panic(...); + } + wifi_settings_connect(); // << and add this + // and that's it... + } +``` +The following steps go through the process in more detail for +a [CMake](https://cmake.org) project stored in Git, +similar to all of the official Pico projects. + +You may find it useful to look at [the example app for wifi\_settings\_connect in +the pico-playground repository](https://github.com/raspberrypi/pico-playground/tree/master/wifi_settings_connect/example). + +## Modify CMakeLists.txt to use the library + +The `target_link_libraries` rule for your project should be extended to +add `wifi_settings_connect`. +``` + target_link_libraries(your_app + wifi_settings_connect + pico_stdlib + ) +``` +If your project does not include the `pico-extras` repository, then this +must also be added. +``` + include(pico_extras_import.cmake) +``` +You can copy the `pico_extras_import.cmake` file from [the root of +the pico-playground repository](https://github.com/raspberrypi/pico-playground). + +### Additional configuration for LwIP and mbedtls + +If your project has not previously used WiFi you will also need +to add one of the WiFi driver targets to `target_link_libraries`, e.g. +`pico_cyw43_arch_lwip_background` or `pico_cyw43_arch_lwip_poll`. + +You will also need `lwipopts.h` in the project directory (this configures +LwIP). You can copy an example from +[here](https://github.com/raspberrypi/pico-playground/tree/master/wifi_settings_connect/example). + +## Include the header file + +Your main C/C++ source file (containing `main()`) should be modified to include +`wifi_settings/wifi_settings_connect.h`: +``` + #include "wifi_settings/wifi_settings_connect.h" +``` + +## Modify your main function + +Your `main()` function should be modified to call `wifi_settings_init()` once on startup. + + - This must *replace* any call to `cyw43` initialisation functions, because + these are called from `wifi_settings_init()` (with the correct country code). + - The call should be after `stdio_init_all()`. + - If the call returns a non-zero value, an error has occurred. You do not have + to handle this error; it is still safe to call other `wifi_settings` functions, + but they will not work and will return error codes where appropriate. + +Your application should also call `wifi_settings_connect()` when it wishes to connect +to WiFi. This can be called immediately after `wifi_settings_init()` or at any later +time. `wifi_settings_connect()` does not block, as the connection takes +place in the background. + +All other modifications are optional. You can now rebuild your application +and it will include the wifi\_settings\_connect features. + +## CMake command line + +When running `cmake`, you need to provide the location of the `pico-extras` +repository as well as the `pico-sdk` repository. This is typically done +with `-DPICO_EXTRAS_PATH`, e.g.: +``` + cmake -DPICO_BOARD=pico_w \ + -DPICO_SDK_PATH=/home/user/pico-sdk \ + -DPICO_EXTRAS_PATH=/home/user/pico-extras \ + .. +``` + +# Optional modifications + +Your application can call `wifi_settings_is_connected()` at any time +to determine if the WiFi connection is available or not. + +Your application can call various status functions at any time +to get a text report on the connection status. This can be useful for debugging. +Each function should be passed a `char[]` buffer for the output, along with the +size of the buffer. + + - `wifi_settings_get_connect_status_text()` produces a line of + text showing the connection status, e.g. `WiFi is connected to ssid1=MyHomeWiFi`. + - `wifi_settings_get_hw_status_text()` produces a line of + text describing the status of the `cyw43` hardware driver; this will be empty + if the hardware is not initialised. + - `wifi_settings_get_ip_status_text()` produces a line of + text describing the status of the `lwip` network stack e.g. IP address; this will be empty + if unconnected. + - `wifi_settings_get_ip` produces the IP address by itself; this will be empty + if unconnected. + - `wifi_settings_get_ssid` produces the current SSID by itself; this will be empty + if unconnected. If connected using a BSSID, this will be reported as + a `:`-separated lower-case MAC address, e.g. `01:23:45:67:89:ab`. If the wifi-settings + file has been updated since the connection was made, then the result may be `?`, + as the SSID is found by searching the wifi-settings file. + +There is also a function to report the current connection state. +`wifi_settings_get_ssid_status()` returns +a pointer to a static string, indicating the status of a connection attempt to +an SSID, e.g. `SUCCESS`, `NOT FOUND`. + +Your application can call `wifi_settings_disconnect()` to force disconnect, +or `wifi_settings_deinit()` to deinitialise the driver, but this is never necessary +and these steps can be left out. They exist to allow the application to shut down WiFi, +e.g. to save power, or in order to control the WiFi hardware directly for some other +purpose. For example, the +[setup app](https://github.com/jwhitham/pico-wifi-settings/tree/master/doc/SETUP_APP.md) +uses this feature to perform its own WiFi scan. diff --git a/src/rp2_common/wifi_settings_connect/doc/SETTINGS_FILE.md b/src/rp2_common/wifi_settings_connect/doc/SETTINGS_FILE.md new file mode 100644 index 0000000..de178f0 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/doc/SETTINGS_FILE.md @@ -0,0 +1,162 @@ +# Creating and updating a WiFi settings file + +wifi\_settings\_connect stores WiFi hotspot names and passwords +in a Flash sector that isn't normally used by programs. This +is called the "WiFi settings file". It is similar to a file on a disk, +except that it is always at the same location, and the size is +limited to 4096 bytes. + +The file can be updated over USB by using [picotool](https://github.com/raspberrypi/pico-sdk-tools/releases). +It is a text file which can be edited with any text editor. +Here is an example of typical contents: +``` + ssid1=MyHomeWiFi + pass1=mypassword1234 + ssid2=MyPhoneHotspot + pass2=secretpassword + country=GB +``` +wifi\_settings\_connect will automatically scan for hotspots and connect to +hotspots matching the SSID names and passwords in the file. + +- On the [pico-wifi-settings home page](https://github.com/jwhitham/pico-wifi-settings) + you can also find a setup app which runs on your Pico and automates much of the + setup process. The pico-wifi-settings library is a superset of + wifi\_settings\_connect. It includes a remote update feature that allows + a new wifi-settings file to be installed via WiFi. + +# Creating the file on a computer + +Use any text editor to create a text file similar to the example above. + +Each line in the file should contain a key and a value, separated by `=`, +with no spaces around `=`, or at the beginning or end of each line. + +The file must have at least `ssid1` or `bssid1`, otherwise there will +be no connection attempts, and wifi\_settings\_connect will stay in the +STORAGE\_EMPTY\_ERROR state. + +You can also use the following: + + - `ssid` - SSID name for hotspot N (a number from 1 to 100) + - `pass` - Password for hotspot N + - `country` - Your two-letter country code from [ISO-3166-1](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) + - `bssid` - The BSSID ID for hotspot N + - `name` - The hostname of the Pico (sent to DHCP servers) + +# Copying the WiFi settings file by USB + +You can use +[picotool](https://github.com/raspberrypi/pico-sdk-tools/releases). +to copy the file from your computer to your Pico via USB. + +To use picotool, +boot the Pico in bootloader mode by holding down the BOOTSEL button while plugging it +into USB. In bootloader mode, you can upload files with picotool. +The default address is 16kb before the final address in Flash: + + - On Pico W, use `0x101fc000` as the address. + - On Pico 2 W, use `0x103fc000` as the address. + +You must also rename your WiFi settings file so that it ends with `.bin` as +picotool is not able to upload files unless they are `.bin`, `.elf` or `.uf2`. + +Here is a sample upload command for Pico W (RP2040): +``` + picotool load -o 0x101fc000 mywifisettings.bin +``` +and the equivalent for Pico 2 W (RP2350): +``` + picotool load -o 0x103fc000 mywifisettings.bin +``` + +## Location of the wifi-settings file + +The default location of the file (16kb before the final address in Flash) +has been chosen because the final three 4kb Flash sectors are already assigned +a function by the Pico SDK. The Bluetooth library uses two 4kb sectors for storage of +devices that have been paired by Bluetooth. The final 4kb sector is used for a workaround +for the RP2350-E10 bug - this sector may be erased when copying a UF2 file to a Pico 2 +via drag-and-drop. Therefore, these three sectors are avoided. + +If you wish to store the wifi-settings file at a specific address you can +do so by setting `-DWIFI_SETTINGS_FILE_ADDRESS=0x....` when running `cmake`. +The value `0x...` should be an address relative to the start of Flash, so Flash address +`0x1fc000` corresponds to absolute address `0x101fc000`. + +## Backing up a WiFi settings file + +picotool can be used to download WiFi settings files from a Pico W: +``` + picotool save -r 0x101fc000 0x101fd000 backup.bin +``` +and Pico 2 W (RP2350): +``` + picotool save -r 0x103fc000 0x103fd000 backup.bin +``` +Bytes after the end of the file will also be copied (usually either 0x00 or 0xff). +These can be safely deleted. Some text editors will allow you to delete them, +but if you have any difficulty, you can also remove them with a shell command such as: +``` + LC_ALL=C sed -i 's/[\x00\xFF]//g' backup.bin +``` +The backup is restored using `picotool load` as described in "Copying the WiFi settings file by USB". + +These examples use the default location for the wifi-settings file. If you +are using a custom location, e.g. building with +`-DWIFI_SETTINGS_FILE_ADDRESS=0x...`, then +you would need to substitute the actual address. + +# File format details + +The file format is very simple so that it can be read by a simple algorithm +that doesn't require much code space. The parser ignores any line that it +doesn't understand, and skips any keys that are not known. Here are the rules: + + - The key and the value should be separated only by an `=` character, e.g. `ssid1=HomeWiFi`. + - Lines that don't match the form `key=value` are completely ignored; + you can add text, comments etc. in order to help you manage your configuration. + - On a line that does match `key=value`, whitespace is NOT ignored. + Be careful to avoid adding extra spaces around `=`. + A space before `=` will be part of the key, and a space after `=` will be part of the value. + - Unix and Windows line endings are supported. + - The maximum size of the file is 4096 bytes. + - Values can contain any printable UTF-8 character. + - Keys can also contain any printable UTF-8 character except for '='. + - There is no maximum size for a key or a value (except for the file size). + - Values can be zero length. + - Keys must be at least 1 byte. + - If a key appears more than once in the file, the first value is used. + - The end of the file is the first byte with value 0x00, 0xff or 0x1a, or the 4097th byte, + whichever comes first. + +# WiFi settings + + - `ssid` is only checked if `ssid` is present. + - The number reflects the priority. Lower numbers take priority over higher + numbers when more than one SSID is found. + - If `pass` is not specified then wifi\_settings\_connect will assume + an open WiFi hotspot. + - If both `bssid` and `ssid` are specified, then the BSSID is used + and the SSID is ignored. + - If you don't specify a country, the default worldwide settings are used, which might work + slightly less well (e.g. fewer WiFi channels are supported). + - `bssid` should be specified as + a `:`-separated lower-case MAC address, e.g. `01:23:45:67:89:ab`. BSSIDs are + not normally required and should only be used if you have a special requirement + e.g. a "hidden" hotspot without an SSID name. + +# Custom keys and values + +The WiFi settings file can have keys which are not used by the wifi\_settings\_connect library. +Your application can obtain their values using the `wifi_settings_get_value_for_key()` function. +This can be a useful way to store additional configuration data for your Pico application. +For example you might use it to store encryption keys, server addresses, user names +or any other setting that you may wish to update without rebuilding your application. + +`wifi_settings_get_value_for_key` uses a linear search, starting at the beginning +of the file. This search does not backtrack and is fast because of the simplistic +nature of the file format. However, in algorithmic terms, this is not the best way +to implement or search a key/value store, and if you need frequent access to keys/values, +you may wish to implement something better (e.g. use a hash table to implement a dictionary) +or just load the values when your application starts up and then store them elsewhere. diff --git a/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_configuration.h b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_configuration.h new file mode 100644 index 0000000..0e4fcb9 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_configuration.h @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This header file contains default values for timeouts, + * addresses and limits within the pico-wifi-settings library. + */ + +#ifndef WIFI_SETTINGS_CONFIGURATION_H +#define WIFI_SETTINGS_CONFIGURATION_H + +#include "hardware/flash.h" + +#if defined(WIFI_SETTINGS_FILE_ADDRESS) && (WIFI_SETTINGS_FILE_ADDRESS == 0) +// If WIFI_SETTINGS_FILE_ADDRESS == 0, then use the default start location +#undef WIFI_SETTINGS_FILE_ADDRESS +#endif + +#if !defined(WIFI_SETTINGS_FILE_ADDRESS) +// The default start location of the wifi-settings file is 16kb before the end of Flash: +#define WIFI_SETTINGS_FILE_ADDRESS (PICO_FLASH_SIZE_BYTES - 0x4000) +// +// The CPU's address for this Flash location is: +// 0x101fc000 on Pico 1 W +// 0x103fc000 on Pico 2 W +// and may have other values on other RP2040/RP2350 boards. +// +// This location is chosen because the final three 4kb sectors of Flash are +// already assigned a function by the Pico SDK. The Bluetooth library uses +// two 4kb sectors for storage of devices that have been paired by Bluetooth. +// The final 4kb sector is used for a workaround for the RP2350-E10 bug - this +// sector may be erased when copying a UF2 file to a Pico 2 via drag-and-drop. +// Therefore, these three sectors are avoided. +// +// If you wish to store the wifi-settings file at a specific address you can +// do so by setting -DWIFI_SETTINGS_FILE_ADDRESS=0x.... when running +// cmake. This should be an address relative to the start of Flash, and +// should be a multiple of WIFI_SETTINGS_FILE_SIZE. +// +// Versions of pico-wifi-settings before 0.2.0 used 0x1ff000 for Pico 1 and +// 0x3fe000 for Pico 2. +#endif + +// Size of wifi-settings file in bytes +// This must be a whole number of Flash sectors. +// The setup app, documentation and examples all assume 0x1000 bytes, and that +// is the recommended value, but any positive multiple of the Flash sector size +// can be used. +#ifndef WIFI_SETTINGS_FILE_SIZE +#define WIFI_SETTINGS_FILE_SIZE (1 * FLASH_SECTOR_SIZE) // (0x1000 bytes) +#endif + +// Minimum time between initialisation and the first scan (milliseconds). +#ifndef INITIAL_SETUP_TIME_MS +#define INITIAL_SETUP_TIME_MS 1000 +#endif + +// Maximum time allowed between calling cyw43_wifi_join and getting an +// IP address (milliseconds). If this timeout expires, wifi_settings will +// try a different hotspot or rescan. The attempt to join a hotspot can fail +// sooner than this, e.g. if the password is incorrect or the hotspot vanishes. +#ifndef CONNECT_TIMEOUT_TIME_MS +#define CONNECT_TIMEOUT_TIME_MS 30000 +#endif + +// Minimum time between scans (milliseconds). If a scan fails to find any +// known hotspot, wifi_settings will always wait at least this long before +// retry. +#ifndef REPEAT_SCAN_TIME_MS +#define REPEAT_SCAN_TIME_MS 3000 +#endif + +// Minimum time between calls to the periodic function, +// wifi_settings_periodic_callback, +// which will initiate scans and connections if necessary (milliseconds). +#ifndef PERIODIC_TIME_MS +#define PERIODIC_TIME_MS 1000 +#endif + +// Maximum number of SSIDs that can be supported. This determines the size +// of the g_wifi_state.ssid_scan_info array. You can set this maximum +// to larger values if you wish, at the cost of some additional memory +// usage (1 byte per SSID), but the setup app assumes this maximum. +#ifndef MAX_NUM_SSIDS +#define MAX_NUM_SSIDS 100 +#endif + +// Validation for wifi-settings file address and size +#ifdef static_assert +static_assert((WIFI_SETTINGS_FILE_ADDRESS + WIFI_SETTINGS_FILE_SIZE) <= PICO_FLASH_SIZE_BYTES); +static_assert(WIFI_SETTINGS_FILE_ADDRESS > 0); +static_assert(WIFI_SETTINGS_FILE_SIZE > 0); +static_assert((WIFI_SETTINGS_FILE_SIZE % FLASH_SECTOR_SIZE) == 0); +static_assert((WIFI_SETTINGS_FILE_ADDRESS % WIFI_SETTINGS_FILE_SIZE) == 0); +#endif + +#endif diff --git a/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_connect.h b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_connect.h new file mode 100644 index 0000000..c228967 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_connect.h @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This header file declares functions used to initialise and connect to WiFi + * with pico-wifi-settings. + * + */ + +#ifndef _WIFI_SETTINGS_CONNECT_H_ +#define _WIFI_SETTINGS_CONNECT_H_ + +#include + +// These settings are fixed by WPA-PSK standards +#define WIFI_SSID_SIZE 33 // including '\0' character +#define WIFI_BSSID_SIZE 6 // size of a MAC address +#define WIFI_PASSWORD_SIZE 65 // including '\0' character + +/// @brief Initialise wifi_settings module +/// @return 0 on success, or an error code from cyw43_arch_init +int wifi_settings_init(); + +/// @brief Deinitialise wifi_settings module +void wifi_settings_deinit(); + +/// @brief Connect to WiFi if possible, using the settings in Flash. +/// The actual connection may take some time to be established, and +/// may not be possible. Call wifi_settings_is_connected() to see if +/// the connection is ready. +void wifi_settings_connect(); + +/// @brief Disconnect from WiFi immediately. +void wifi_settings_disconnect(); + +/// @brief Determine if connection is ready. +/// @return true if ready +bool wifi_settings_is_connected(); + +/// @brief Determine if the WiFi settings are empty - if the +/// file is empty, wifi_settings will be unable to connect. See README.md +/// for instructions on how to provide settings. +/// @return true if empty (no known SSIDs or BSSIDs) +bool wifi_settings_has_no_wifi_details(); + +/// @brief Get a report on the current connection status +/// @param[inout] text Text buffer for the report +/// @param[in] text_size Available space in the buffer (bytes) +/// @return Return code from snprintf when formatting +int wifi_settings_get_connect_status_text(char* text, int text_size); + +/// @brief Get a report on the network hardware (cyw43) status (e.g. signal strength) +/// @param[inout] text Text buffer for the report +/// @param[in] text_size Available space in the buffer (bytes) +/// @return Return code from snprintf when formatting +int wifi_settings_get_hw_status_text(char* text, int text_size); + +/// @brief Get a report on the IP stack status (e.g. IP address) +/// @param[inout] text Text buffer for the report +/// @param[in] text_size Available space in the buffer (bytes) +/// @return Return code from snprintf when formatting +int wifi_settings_get_ip_status_text(char* text, int text_size); + +/// @brief Get the IP address by itself +/// @param[inout] text Text buffer for address +/// @param[in] text_size Available space in the buffer (bytes) +/// @return Return code from snprintf when formatting +int wifi_settings_get_ip(char* text, int text_size); + +/// @brief Get the current SSID by itself +/// @param[inout] text Text buffer for SSID +/// @param[in] text_size Available space in the buffer (bytes) +/// @return Return code from snprintf when formatting +int wifi_settings_get_ssid(char* text, int text_size); + +/// @brief Get the status of a connection attempt to +/// an SSID as a static string, e.g. SUCCESS, NOT_FOUND. "" is returned +/// if the SSID index is not known. +/// @param[in] ssid_index Index matching the ssid number. +/// @return Static string containing the SSID status. +const char* wifi_settings_get_ssid_status(int ssid_index); + + +#endif diff --git a/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_connect_internal.h b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_connect_internal.h new file mode 100644 index 0000000..52be942 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_connect_internal.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This header file is intended to be internal and should not be included directly by applications. + * + */ + +#ifndef WIFI_SETTINGS_CONNECT_INTERNAL_H +#define WIFI_SETTINGS_CONNECT_INTERNAL_H + +#ifndef WIFI_SETTINGS_CONNECT_C +#error "This is an internal header intended only for use by wifi_settings_connect.c and its unit tests" +#else + +#include "wifi_settings_configuration.h" + +#include "pico/async_context.h" +#include "pico/stdlib.h" +#include "pico/time.h" +#include "pico/cyw43_arch.h" +#include "pico/platform.h" + +enum __packed wifi_connect_state_t { + UNINITIALISED = 0, // cyw43 hardware was not started + INITIALISATION_ERROR, // initialisation failed (see hw_error_code) + STORAGE_EMPTY_ERROR, // no WiFi details are known + DISCONNECTED, // call wifi_settings_connect() to connect + TRY_TO_CONNECT, // connection process begun + SCANNING, // scan running + CONNECTING, // connection running + CONNECTED_IP, // connection is ready for use +}; + +enum __packed ssid_scan_info_t { + NOT_FOUND = 0, // this SSID was not found + FOUND, // this SSID was found by the most recent scan + ATTEMPT, // we attempted to connect to this SSID + FAILED, // ... but it failed with an error + TIMEOUT, // ... but it failed with a timeout + BADAUTH, // ... but the password is wrong + SUCCESS, // ... and it worked + LOST, // we connected to this SSID but the connection dropped +}; + +#define IPV4_ADDRESS_SIZE 16 // "xxx.xxx.xxx.xxx\0" +#define KEY_SIZE 10 // e.g. "bssid0" + +struct wifi_state_t { + enum wifi_connect_state_t cstate; + enum ssid_scan_info_t ssid_scan_info[MAX_NUM_SSIDS + 1]; + struct netif* netif; + cyw43_t* cyw43; + uint selected_ssid_index; + int hw_error_code; + absolute_time_t connect_timeout_time; + absolute_time_t scan_holdoff_time; + async_context_t* context; + async_at_time_worker_t periodic_worker; +}; + +#endif +#endif diff --git a/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_flash_range.h b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_flash_range.h new file mode 100644 index 0000000..8df1412 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_flash_range.h @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This header file declares functions used to check and translate address ranges. + * + */ + +#ifndef _WIFI_SETTINGS_FLASH_RANGE_H_ +#define _WIFI_SETTINGS_FLASH_RANGE_H_ + +#include +#include + +/// @brief Represents a range of Flash memory addresses +typedef struct wifi_settings_flash_range_t { + uint32_t start_address; + uint32_t size; +} wifi_settings_flash_range_t; + +/// @brief Represents a range of logical memory addresses +typedef struct wifi_settings_logical_range_t { + void* start_address; + uint32_t size; +} wifi_settings_logical_range_t; + +/// @brief Detect if a Flash memory range entirely fits within another +/// @param[in] inner Inner Flash memory range +/// @param[in] outer Outer Flash memory range +/// @return true if inner is entirely within outer +bool wifi_settings_range_is_contained( + const wifi_settings_flash_range_t* inner, + const wifi_settings_flash_range_t* outer); + +/// @brief Detect if a Flash memory range intersects with another +/// @param[in] fr1 First range +/// @param[in] fr2 Second range +/// @return true if fr1 and fr2 overlap by one or more bytes +bool wifi_settings_range_has_overlap( + const wifi_settings_flash_range_t* fr1, + const wifi_settings_flash_range_t* fr2); + +/// @brief Determine the range of addresses that are in Flash +/// @param[out] r Flash memory range +void wifi_settings_range_get_all( + wifi_settings_flash_range_t* r); + +/// @brief Determine the range of addresses that are reusable +/// (Reusable -> not occupied by the current program, not +/// occupied by the wifi-settings file, and within the current partition, if any.) +/// @param[out] r Flash memory range +void wifi_settings_range_get_reusable( + wifi_settings_flash_range_t* r); + +/// @brief Determine the range of addresses used by the wifi-settings file +/// @param[out] r Flash memory range +/// @details This function has a weak symbol, allowing it to be reimplemented +/// by applications in order to place the file at any Flash location, +/// including a location that is determined dynamically. A different +/// static location can also be set at build time with +/// -DWIFI_SETTINGS_FILE_ADDRESS=0x... +void wifi_settings_range_get_wifi_settings_file( + wifi_settings_flash_range_t* r); + +/// @brief Determine the range of addresses used by the current program +/// @param[out] r Flash memory range +void wifi_settings_range_get_program( + wifi_settings_flash_range_t* r); + +/// @brief Determine the range of addresses used by the current partition +/// @param[out] r Flash memory range +void wifi_settings_range_get_partition( + wifi_settings_flash_range_t* r); + +/// @brief Translate Flash range to logical range +/// @param[in] fr Flash memory range +/// @param[out] lr Logical memory range +void wifi_settings_range_translate_to_logical( + const wifi_settings_flash_range_t* fr, + wifi_settings_logical_range_t* lr); + +/// @brief Align Flash range to sector boundary and size: +/// no effect if they are already aligned +/// @param[inout] fr Flash memory range (possibly unaligned) +void wifi_settings_range_align_to_sector( + wifi_settings_flash_range_t* fr); + +/// @brief Translate logical range to Flash range if possible. +/// Not possible if the logical range is outside of an accessible area of Flash: +/// (1) ROM/RAM/other non-Flash addresses, (2) outside of the current partition, +/// (3) outside of both XIP_BASE and XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE regions. +/// @param[in] lr Logical memory range +/// @param[out] fr Flash memory range +/// @return true if translation was possible +bool wifi_settings_range_translate_to_flash( + const wifi_settings_logical_range_t* lr, + wifi_settings_flash_range_t* fr); + +#endif diff --git a/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_flash_storage.h b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_flash_storage.h new file mode 100644 index 0000000..c378c05 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_flash_storage.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This header file declares a function used to access the WiFi settings + * and other key/value data in Flash. + * + */ + +#ifndef _WIFI_SETTINGS_FLASH_STORAGE_H_ +#define _WIFI_SETTINGS_FLASH_STORAGE_H_ + +#include "pico/stdlib.h" +#include +#include + +/// @brief Scan the settings file in Flash for a particular key. +/// If found, copy up to *value_size characters to value. +/// Note: value will not be '\0' terminated. +/// @param[in] key Key to be found ('\0' terminated) +/// @param[out] value Value for key (if found) - not '\0' terminated +/// @param[inout] value_size Size of the value +/// @return true if key found +/// @details This function has a weak symbol, allowing it to be reimplemented +/// by applications in order to load settings from some other storage +bool wifi_settings_get_value_for_key( + const char* key, + char* value, uint* value_size); + +#endif diff --git a/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_hostname.h b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_hostname.h new file mode 100644 index 0000000..1531d92 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/include/wifi_settings/wifi_settings_hostname.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * Hostname for wifi-settings. The hostname can be specified in + * the WiFi settings file as "name=". + */ + +#ifndef _WIFI_SETTINGS_HOSTNAME_H_ +#define _WIFI_SETTINGS_HOSTNAME_H_ + +#define MAX_HOSTNAME_SIZE 64 +#define BOARD_ID_SIZE 8 + +/// @brief Return a pointer to the hostname (which is a global variable) +/// @return pointer to the hostname +const char* wifi_settings_get_hostname(); + +/// @brief Return a pointer to the board ID in hex format (this is a global variable) +/// @return pointer to the board ID in hex format +const char* wifi_settings_get_board_id_hex(); + +/// @brief Load the hostname from the wifi-settings file +void wifi_settings_set_hostname(); + +#endif diff --git a/src/rp2_common/wifi_settings_connect/wifi_settings_connect.c b/src/rp2_common/wifi_settings_connect/wifi_settings_connect.c new file mode 100644 index 0000000..78bac79 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/wifi_settings_connect.c @@ -0,0 +1,599 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This pico-wifi-settings module manages the WiFi connection + * by calling cyw43 and LwIP functions. + * + */ + + +#define WIFI_SETTINGS_CONNECT_C +#include "wifi_settings/wifi_settings_configuration.h" +#include "wifi_settings/wifi_settings_connect.h" +#include "wifi_settings/wifi_settings_connect_internal.h" +#include "wifi_settings/wifi_settings_flash_storage.h" +#include "wifi_settings/wifi_settings_hostname.h" + +#ifdef ENABLE_REMOTE_UPDATE +#include "wifi_settings/wifi_settings_remote.h" +#endif + +#include "pico/binary_info.h" +#include "pico/error.h" + +#include +#include +#include + + +struct wifi_state_t g_wifi_state; + +enum ssid_type_t { + NONE = 0, + BSSID, + SSID, +}; + +static enum ssid_type_t fetch_ssid(uint ssid_index, char* ssid, uint8_t* bssid); + + +bool wifi_settings_has_no_wifi_details() { + char ssid[WIFI_SSID_SIZE]; + uint8_t bssid[WIFI_BSSID_SIZE]; + return fetch_ssid(1, ssid, bssid) == NONE; +} + +int wifi_settings_get_connect_status_text(char* text, int text_size) { + char ssid[WIFI_SSID_SIZE]; + uint8_t bssid[WIFI_BSSID_SIZE]; + enum ssid_type_t ssid_type; + + switch(g_wifi_state.cstate) { + case TRY_TO_CONNECT: + return snprintf(text, text_size, "WiFi did not find any known hotspot yet"); + case SCANNING: + return snprintf(text, text_size, "WiFi is scanning for hotspots"); + case CONNECTING: + ssid_type = fetch_ssid(g_wifi_state.selected_ssid_index, ssid, bssid); + return snprintf(text, text_size, + "WiFi is connecting to %sssid%u=%s", + (ssid_type == BSSID) ? "b" : "", + g_wifi_state.selected_ssid_index, + ssid); + case CONNECTED_IP: + ssid_type = fetch_ssid(g_wifi_state.selected_ssid_index, ssid, bssid); + return snprintf(text, text_size, + "WiFi is connected to %sssid%u=%s", + (ssid_type == BSSID) ? "b" : "", + g_wifi_state.selected_ssid_index, + ssid); + case DISCONNECTED: + return snprintf(text, text_size, "WiFi is disconnected"); + case UNINITIALISED: + return snprintf(text, text_size, "WiFi uninitialised"); + case INITIALISATION_ERROR: + return snprintf(text, text_size, "WiFi init error: %d", + g_wifi_state.hw_error_code); + case STORAGE_EMPTY_ERROR: + return snprintf(text, text_size, + "No WiFi details have been stored - unable to connect"); + default: + break; + } + return snprintf(text, text_size, "WiFi status is unknown (%d)", (int) g_wifi_state.cstate); +} + +int wifi_settings_get_hw_status_text(char* text, int text_size) { + if (!g_wifi_state.cyw43) { + text[0] = '\0'; + return 0; + } + + const char* hw_status_text = "?"; + switch (cyw43_wifi_link_status(g_wifi_state.cyw43, CYW43_ITF_STA)) { + case CYW43_LINK_DOWN: hw_status_text = "DOWN"; break; + case CYW43_LINK_JOIN: hw_status_text = "JOIN"; break; + case CYW43_LINK_NOIP: hw_status_text = "NOIP"; break; + case CYW43_LINK_UP: hw_status_text = "UP"; break; + case CYW43_LINK_FAIL: hw_status_text = "FAIL"; break; + case CYW43_LINK_NONET: hw_status_text = "NONET"; break; + case CYW43_LINK_BADAUTH: hw_status_text = "BADAUTH"; break; + default: break; + } + int32_t rssi = -1; + cyw43_wifi_get_rssi(g_wifi_state.cyw43, &rssi); + return snprintf(text, text_size, + "cyw43_wifi_link_status = CYW43_LINK_%s scan_active = %s rssi = %d", + hw_status_text, + cyw43_wifi_scan_active(g_wifi_state.cyw43) ? "True" : "False", + (int) rssi); +} + +int wifi_settings_get_ip_status_text(char* text, int text_size) { + if ((!g_wifi_state.netif) + || (!netif_is_link_up(g_wifi_state.netif))) { + text[0] = '\0'; + return 0; + } + char addr_buf1[IPV4_ADDRESS_SIZE]; + char addr_buf2[IPV4_ADDRESS_SIZE]; + char addr_buf3[IPV4_ADDRESS_SIZE]; + return snprintf(text, text_size, + "IPv4 address = %s netmask = %s gateway = %s", + ip4addr_ntoa_r(netif_ip4_addr(g_wifi_state.netif), addr_buf1, IPV4_ADDRESS_SIZE), + ip4addr_ntoa_r(netif_ip4_netmask(g_wifi_state.netif), addr_buf2, IPV4_ADDRESS_SIZE), + ip4addr_ntoa_r(netif_ip4_gw(g_wifi_state.netif), addr_buf3, IPV4_ADDRESS_SIZE)); +} + +int wifi_settings_get_ip(char* text, int text_size) { + if ((!g_wifi_state.netif) + || (!netif_is_link_up(g_wifi_state.netif))) { + // Not connected - return empty string + text[0] = '\0'; + return 0; + } + char addr_buf[IPV4_ADDRESS_SIZE]; + return snprintf(text, text_size, + "%s", + ip4addr_ntoa_r(netif_ip4_addr(g_wifi_state.netif), addr_buf, IPV4_ADDRESS_SIZE)); +} + +int wifi_settings_get_ssid(char* text, int text_size) { + char ssid[WIFI_SSID_SIZE]; + uint8_t bssid[WIFI_BSSID_SIZE]; + + switch(g_wifi_state.cstate) { + case CONNECTING: + case CONNECTED_IP: + (void) fetch_ssid(g_wifi_state.selected_ssid_index, ssid, bssid); + // The text buffer will contain '?' if the SSID is unknown (e.g. if + // the wifi-settings file was updated to remove the SSID while connected). + return snprintf(text, text_size, "%s", ssid); + default: + // Not connected - return empty string + text[0] = '\0'; + return 0; + } +} + +const char* wifi_settings_get_ssid_status(int ssid_index) { + if ((ssid_index >= 1) && (ssid_index <= MAX_NUM_SSIDS)) { + switch (g_wifi_state.ssid_scan_info[ssid_index]) { + case NOT_FOUND: return "NOT FOUND"; break; + case FOUND: return "FOUND"; break; + case ATTEMPT: return "ATTEMPT"; break; + case SUCCESS: return "SUCCESS"; break; + case FAILED: return "FAILED"; break; + case TIMEOUT: return "TIMEOUT"; break; + case BADAUTH: return "BADAUTH"; break; + case LOST: return "LOST"; break; + default: break; + } + } + return ""; +} + +static bool wifi_is_connected() { + if (g_wifi_state.netif) { + return netif_is_link_up(g_wifi_state.netif); + } else { + return false; + } +} + +static bool convert_string_to_bssid(const char* text, uint text_size, uint8_t* bssid) { + // A BSSID is specified in the file as bssid1=01:23:45:67:89:ab + // note 1 - ':' separators + // note 2 - exactly 17 bytes + memset(bssid, 0, WIFI_BSSID_SIZE); + if (text_size != ((WIFI_BSSID_SIZE * 3) - 1)) { + // Malformed BSSID - not exactly 17 bytes + return false; + } + for (uint i = 0; i < (WIFI_BSSID_SIZE - 1); i++) { + if (text[(i * 3) + 2] != ':') { + // Malformed BSSID - not ':' separator + return false; + } + } + for (uint i = 0; i < WIFI_BSSID_SIZE; i++) { + char copy[3]; + char* check = NULL; + copy[0] = text[(i * 3) + 0]; + copy[1] = text[(i * 3) + 1]; + copy[2] = '\0'; + bssid[i] = (uint8_t) strtol(copy, &check, 16); + if (check != ©[2]) { + // Malformed BSSID - not a hex number + return false; + } + } + // valid BSSID + return true; +} + +static enum ssid_type_t fetch_ssid(uint ssid_index, char* ssid, uint8_t* bssid) { + // Generate search key + char key[KEY_SIZE]; + snprintf(key, sizeof(key), "bssid%u", ssid_index); + + uint ssid_size = WIFI_SSID_SIZE; + memset(bssid, 0, WIFI_BSSID_SIZE); + + // A BSSID is specified in the file as bssid1=01:23:45:67:89:ab + if (wifi_settings_get_value_for_key(key, ssid, &ssid_size)) { + ssid[ssid_size] = '\0'; + if (convert_string_to_bssid(ssid, ssid_size, bssid)) { + return BSSID; + } + } + + // An SSID is specified in the file as ssid1=MyHotspotName + // and must match exactly; SSIDs cannot contain characters recognised + // as end of line or end of file (\r \n \xff \x00 \x1a) + if (wifi_settings_get_value_for_key(&key[1], ssid, &ssid_size)) { + ssid[ssid_size] = '\0'; + return SSID; + } + // Undefined SSID and BSSID + ssid[0] = '?'; + ssid[1] = '\0'; + return NONE; +} + +static int wifi_scan_callback(void* unused, const cyw43_ev_scan_result_t* scan_result) { + // Is this SSID known? Iterate through the file to see if there is a record of it. + for (uint ssid_index = 1; ssid_index <= MAX_NUM_SSIDS; ssid_index++) { + // Skip SSIDs that we already saw + if (g_wifi_state.ssid_scan_info[ssid_index] != NOT_FOUND) { + continue; + } + + // Check file entries for bssid and ssid + char ssid[WIFI_SSID_SIZE]; + uint8_t bssid[WIFI_BSSID_SIZE]; + enum ssid_type_t ssid_type = fetch_ssid(ssid_index, ssid, bssid); + switch (ssid_type) { + case BSSID: + if (memcmp(bssid, scan_result->bssid, WIFI_BSSID_SIZE) == 0) { + // BSSID match + g_wifi_state.ssid_scan_info[ssid_index] = FOUND; + } + break; + case SSID: + if ((strlen(ssid) == (uint) scan_result->ssid_len) + && (memcmp(scan_result->ssid, ssid, (uint) scan_result->ssid_len) == 0)) { + // SSID match + g_wifi_state.ssid_scan_info[ssid_index] = FOUND; + } + break; + case NONE: + // ssid doesn't exist, so ssid, ssid etc. won't be checked + return 0; + } + } + // No more entries to try + return 0; +} + +static void ensure_disconnected() { + cyw43_wifi_leave(g_wifi_state.cyw43, CYW43_ITF_STA); + g_wifi_state.netif = NULL; +} + +static void begin_connecting() { + // This function is called after a scan, to begin connecting to a new hotspot. + // It looks at the results of the scan and previous connections, via ssid_scan_info. + ensure_disconnected(); + + // Which hotspot to connect to? + g_wifi_state.selected_ssid_index = 0; + for (uint ssid_index = 1; ssid_index <= MAX_NUM_SSIDS; ssid_index++) { + if (g_wifi_state.ssid_scan_info[ssid_index] == FOUND) { + g_wifi_state.selected_ssid_index = ssid_index; + break; + } + } + + if (g_wifi_state.selected_ssid_index == 0) { + // There are no available hotspots to connect to, either because the scan + // didn't find anything, or everything is FAILED, TIMEOUT, BADAUTH or LOST. + // In this case we should scan again. + g_wifi_state.cstate = TRY_TO_CONNECT; + return; + } + + // Begin connecting + g_wifi_state.ssid_scan_info[g_wifi_state.selected_ssid_index] = ATTEMPT; + g_wifi_state.connect_timeout_time = make_timeout_time_ms(CONNECT_TIMEOUT_TIME_MS); + g_wifi_state.cstate = CONNECTING; + + // Get the password + char key[KEY_SIZE]; + snprintf(key, sizeof(key), "pass%u", g_wifi_state.selected_ssid_index); + char password[WIFI_PASSWORD_SIZE]; + uint password_size = sizeof(password) - 1; + uint32_t auth_type = CYW43_AUTH_WPA2_AES_PSK; + if (!wifi_settings_get_value_for_key(key, password, &password_size)) { + // No password specified (open WiFi) + password_size = 0; + auth_type = CYW43_AUTH_OPEN; + } + password[password_size] = '\0'; + + // Get the BSSID or SSID + char ssid[WIFI_SSID_SIZE]; + uint8_t bssid[WIFI_BSSID_SIZE]; + enum ssid_type_t ssid_type = fetch_ssid(g_wifi_state.selected_ssid_index, ssid, bssid); + if (ssid_type == NONE) { + // No valid SSID or BSSID - this could happen if the storage was updated + // between scanning and connecting. Force a rescan + g_wifi_state.selected_ssid_index = 0; + g_wifi_state.cstate = TRY_TO_CONNECT; + return; + } + + // Begin connection + if (ssid_type == BSSID) { + g_wifi_state.hw_error_code = cyw43_wifi_join(g_wifi_state.cyw43, + 0, // size_t ssid_len + NULL, // const uint8_t *ssid + password_size, // size_t key_len + (const uint8_t *) password, // const uint8_t *key + auth_type, // uint32_t auth_type + bssid, // const uint8_t *bssid + CYW43_CHANNEL_NONE); // uint32_t channel + } else { + g_wifi_state.hw_error_code = cyw43_wifi_join(g_wifi_state.cyw43, + strlen(ssid), // size_t ssid_len + (const uint8_t *) ssid, // const uint8_t *ssid + password_size, // size_t key_len + (const uint8_t *) password, // const uint8_t *key + auth_type, // uint32_t auth_type + NULL, // const uint8_t *bssid + CYW43_CHANNEL_NONE); // uint32_t channel + } +} + +static void give_up_connecting(enum ssid_scan_info_t info) { + // Mark the selected SSID as bad in some way (e.g. BADAUTH, TIMEOUT) + // so that it won't be tried again. Go back to the SCANNING state. + g_wifi_state.ssid_scan_info[g_wifi_state.selected_ssid_index] = info; + g_wifi_state.cstate = SCANNING; +} + +static bool has_valid_address() { + char address_buf[IPV4_ADDRESS_SIZE]; + if (g_wifi_state.netif) { + const char* address = ip4addr_ntoa_r(netif_ip4_addr(g_wifi_state.netif), + address_buf, IPV4_ADDRESS_SIZE); + if ((address[0] != '\0') && (strcmp("0.0.0.0", address) != 0)) { + return true; + } + } + return false; +} + +static void begin_new_scan() { + // Begin a scan. We will reset everything we know about hotspots first. + for (uint ssid_index = 1; ssid_index <= MAX_NUM_SSIDS; ssid_index++) { + g_wifi_state.ssid_scan_info[ssid_index] = NOT_FOUND; + } + // Start the scan + cyw43_wifi_scan_options_t opts; + memset(&opts, 0, sizeof(opts)); + g_wifi_state.hw_error_code = cyw43_wifi_scan(g_wifi_state.cyw43, &opts, NULL, wifi_scan_callback); + g_wifi_state.cstate = SCANNING; + g_wifi_state.scan_holdoff_time = make_timeout_time_ms(REPEAT_SCAN_TIME_MS); +} + +static void wifi_settings_periodic_callback(async_context_t* unused1, async_at_time_worker_t* unused2) { + switch (g_wifi_state.cstate) { + case TRY_TO_CONNECT: + // In this state, we are not connected, and we are waiting for a holdoff time + // before beginning a scan for available hotspots. If a scan is already running + // (e.g. due to disconnecting during a scan) we wait for it to finish. + ensure_disconnected(); + if (wifi_settings_has_no_wifi_details()) { + // This is reached if the storage file contains no SSIDs. + g_wifi_state.cstate = STORAGE_EMPTY_ERROR; + } else if (time_reached(g_wifi_state.scan_holdoff_time) && !cyw43_wifi_scan_active(g_wifi_state.cyw43)) { + begin_new_scan(); + } + break; + case SCANNING: + // In this state, we are waiting for a hotspot scan to complete. + // If it already completed, and we have some results, we can go directly to CONNECTING. + if (!cyw43_wifi_scan_active(g_wifi_state.cyw43)) { + begin_connecting(); + } + break; + case CONNECTING: + // In this state, we are joining a WiFi hotspot, having found at least one + // possibility during the scan. + switch (cyw43_wifi_link_status(g_wifi_state.cyw43, CYW43_ITF_STA)) { + case CYW43_LINK_DOWN: + case CYW43_LINK_FAIL: + case CYW43_LINK_NONET: + // Connection failed - this hotspot must have disappeared + give_up_connecting(FAILED); + break; + case CYW43_LINK_BADAUTH: + // Connection failed because the password is incorrect + give_up_connecting(BADAUTH); + break; + case CYW43_LINK_JOIN: + case CYW43_LINK_NOIP: + case CYW43_LINK_UP: + // Connection still in progress or completed + g_wifi_state.netif = netif_default; + if (wifi_is_connected() && has_valid_address()) { + // Successful + g_wifi_state.ssid_scan_info[g_wifi_state.selected_ssid_index] = SUCCESS; + g_wifi_state.cstate = CONNECTED_IP; + } else if (time_reached(g_wifi_state.connect_timeout_time)) { + // Connection failed with a timeout + give_up_connecting(TIMEOUT); + } + break; + default: + // Fallback -> connection failure + give_up_connecting(FAILED); + break; + } + break; + case CONNECTED_IP: + // In this state we should be connected, but the connection could drop at any time + if (!wifi_is_connected() || !has_valid_address()) { + // Connection lost + give_up_connecting(LOST); + // It may be some time since the last scan, so scan again + g_wifi_state.cstate = TRY_TO_CONNECT; + } + break; + case STORAGE_EMPTY_ERROR: + // This state is reached if the storage file contains no SSIDs. + // Wait for the file to be updated. + if (!wifi_settings_has_no_wifi_details()) { + g_wifi_state.cstate = TRY_TO_CONNECT; + } + break; + case INITIALISATION_ERROR: + case UNINITIALISED: + case DISCONNECTED: + // nothing to do + break; + default: + break; + } + // trigger again after the period + g_wifi_state.periodic_worker.next_time = + delayed_by_ms(g_wifi_state.periodic_worker.next_time, + PERIODIC_TIME_MS); + async_context_add_at_time_worker( + g_wifi_state.context, + &g_wifi_state.periodic_worker); +} + +int wifi_settings_init() { + if (g_wifi_state.cstate != UNINITIALISED) { + return PICO_ERROR_INVALID_STATE; + } + // Put wifi-settings library version into the binary info + bi_decl_if_func_used(bi_program_feature("pico-wifi-settings v" WIFI_SETTINGS_VERSION_STRING)); + + // Start with globals in known state + memset(&g_wifi_state, 0, sizeof(g_wifi_state)); + g_wifi_state.cstate = UNINITIALISED; + g_wifi_state.cyw43 = &cyw43_state; // from Pico SDK, lib/cyw43-driver (MAC layer) + + // Which country should be used? + // You can put "country=" in the WiFi settings file to set a different value. + // The code is a two-byte ISO-3166-1 country code + // such as AU (Australia), SE (Sweden) or GB (United Kingdom). + char value[2]; + uint value_size = sizeof(value); + uint32_t country = PICO_CYW43_ARCH_DEFAULT_COUNTRY_CODE; // worldwide default + + if ((wifi_settings_get_value_for_key("country", value, &value_size)) + && (value_size == 2)) { + country = CYW43_COUNTRY(value[0], value[1], 0); + } + + // Set the hostname from wifi-settings "name=" or use unique board id + wifi_settings_set_hostname(); + + // Hardware init + g_wifi_state.hw_error_code = cyw43_arch_init_with_country(country); + if (g_wifi_state.hw_error_code) { + g_wifi_state.cstate = INITIALISATION_ERROR; + return g_wifi_state.hw_error_code; + } + + // After initialisation, any call to LWIP requires this lock (callback functions + // are always holding it already, but this function is not a callback) + cyw43_arch_lwip_begin(); + + // Set up to connect to an access point + cyw43_arch_enable_sta_mode(); + + // State initialised + g_wifi_state.connect_timeout_time = make_timeout_time_ms(CONNECT_TIMEOUT_TIME_MS); + g_wifi_state.scan_holdoff_time = make_timeout_time_ms(INITIAL_SETUP_TIME_MS); + g_wifi_state.cstate = DISCONNECTED; + + // Use cyw43 async context + g_wifi_state.context = cyw43_arch_async_context(); + + // Start periodic worker + g_wifi_state.periodic_worker.next_time = g_wifi_state.scan_holdoff_time; + g_wifi_state.periodic_worker.do_work = wifi_settings_periodic_callback; + async_context_add_at_time_worker( + g_wifi_state.context, + &g_wifi_state.periodic_worker); + +#ifdef ENABLE_REMOTE_UPDATE + // Start remote access service + g_wifi_state.hw_error_code = wifi_settings_remote_init(); +#endif + // set lwip hostname (overriding the default set by cyw43_cb_tcpip_init) + netif_set_hostname(netif_default, wifi_settings_get_hostname()); + + // Ready to run LWIP functions + cyw43_arch_lwip_end(); + + return g_wifi_state.hw_error_code; +} + +void wifi_settings_deinit() { + if (g_wifi_state.cstate == UNINITIALISED) { + return; + } + ensure_disconnected(); + if (g_wifi_state.context) { + // stop periodic task + async_context_remove_at_time_worker( + g_wifi_state.context, + &g_wifi_state.periodic_worker); + } + g_wifi_state.context = NULL; + cyw43_arch_deinit(); + g_wifi_state.cstate = UNINITIALISED; + g_wifi_state.selected_ssid_index = 0; +} + +void wifi_settings_connect() { + if (g_wifi_state.cstate == DISCONNECTED) { + // Try to connect when periodic worker is next called + cyw43_arch_lwip_begin(); + if (g_wifi_state.cstate == DISCONNECTED) { + g_wifi_state.cstate = TRY_TO_CONNECT; + } + cyw43_arch_lwip_end(); + } +} + +void wifi_settings_disconnect() { + // Immediate disconnect + if ((g_wifi_state.cstate != UNINITIALISED) + && (g_wifi_state.cstate != INITIALISATION_ERROR)) { + cyw43_arch_lwip_begin(); + ensure_disconnected(); + g_wifi_state.cstate = DISCONNECTED; + g_wifi_state.selected_ssid_index = 0; + cyw43_arch_lwip_end(); + } +} + +bool wifi_settings_is_connected() { + bool rc = false; + if (g_wifi_state.cstate == CONNECTED_IP) { + // wifi_is_connected calls LWIP functions, so the lock is needed + cyw43_arch_lwip_begin(); + rc = wifi_is_connected(); + cyw43_arch_lwip_end(); + } + return rc; +} diff --git a/src/rp2_common/wifi_settings_connect/wifi_settings_flash_range.c b/src/rp2_common/wifi_settings_connect/wifi_settings_flash_range.c new file mode 100644 index 0000000..24bdeb2 --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/wifi_settings_flash_range.c @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This file defines functions used to check and translate address ranges. + * + */ + +#include "wifi_settings/wifi_settings_configuration.h" +#include "wifi_settings/wifi_settings_flash_range.h" + +#include "pico/platform.h" +#include "hardware/regs/addressmap.h" +#ifdef XIP_QMI_BASE +#include "hardware/structs/qmi.h" +#endif + +// Calculate end address for a range +static uint32_t get_end_address(const wifi_settings_flash_range_t* r) { + return r->start_address + r->size; +} + +// A valid range contains at least 1 byte, and the end address is greater than +// the start address. (So, a valid range never wraps over UINT_MAX.) +static uint32_t is_valid(const wifi_settings_flash_range_t* r) { + return get_end_address(r) > r->start_address; +} + +// Determine if inner is within outer (or exactly the same as outer) +bool wifi_settings_range_is_contained( + const wifi_settings_flash_range_t* inner, + const wifi_settings_flash_range_t* outer) { + + if ((!is_valid(inner)) || (!is_valid(outer))) { + // At least one of the ranges is not valid (must contain at least 1 byte + // and not wrap over UINT_MAX.) + return false; + } + if (inner->start_address < outer->start_address) { + // inner starts before outer + return false; + } + if (get_end_address(inner) > get_end_address(outer)) { + // inner ends after outer + return false; + } + // inner is no larger than outer + return true; +} + +// Detect if a Flash memory range intersects with another +bool wifi_settings_range_has_overlap( + const wifi_settings_flash_range_t* fr1, + const wifi_settings_flash_range_t* fr2) +{ + if ((!is_valid(fr1)) || (!is_valid(fr2))) { + // Invalid ranges are considered non-overlapping + return false; + } + if (fr1->start_address >= get_end_address(fr2)) { + return false; // gap: fr1 is after fr2 + } + if (fr2->start_address >= get_end_address(fr1)) { + return false; // gap: fr2 is after fr1 + } + // no gap: fr1 and fr2 must overlap + return true; +} + +// Determine the range of addresses that are in Flash +void wifi_settings_range_get_all(wifi_settings_flash_range_t* r) { + r->start_address = 0; + r->size = PICO_FLASH_SIZE_BYTES; +} + + +extern char __flash_binary_end; + +// Determine the range of Flash addresses used by the current program +void wifi_settings_range_get_program(wifi_settings_flash_range_t* r) { + // Initial setup: assume the program is not in Flash + r->start_address = 0; + r->size = 0; + + // If the program is in Flash, determine the size + const uintptr_t end_of_program = ((uintptr_t) &__flash_binary_end); + if ((end_of_program > XIP_BASE) + && (end_of_program <= (XIP_BASE + PICO_FLASH_SIZE_BYTES))) { + // get start of partition + wifi_settings_range_get_partition(r); + // get program size + r->size = end_of_program - XIP_BASE; + } +} + +// Determine the range of Flash addresses for the current partition +void wifi_settings_range_get_partition(wifi_settings_flash_range_t* r) { +#ifdef XIP_QMI_BASE + // Partition base and size is in the ATRANS0 register + const uint32_t atrans0 = *((io_ro_32*)(XIP_QMI_BASE + QMI_ATRANS0_OFFSET)); + const uint32_t base = atrans0 & 0xfff; + const uint32_t size = (atrans0 >> 16) & 0x7ff; + r->start_address = base * FLASH_SECTOR_SIZE; + r->size = size * FLASH_SECTOR_SIZE; +#else + // Partition is the whole of Flash + r->start_address = 0; + r->size = PICO_FLASH_SIZE_BYTES; +#endif +} + +// Determine the range of addresses used by the wifi-settings file +// This function can be reimplemented in order to set the file location dynamically; +// this default version uses values from wifi_settings_configuration.h which are +// guaranteed to be valid because of static assertions in the header +__weak void wifi_settings_range_get_wifi_settings_file(wifi_settings_flash_range_t* r) { + r->start_address = WIFI_SETTINGS_FILE_ADDRESS; + r->size = WIFI_SETTINGS_FILE_SIZE; +} + +// Determine the range of addresses that are reusable +// They are between the end of the program and either the start of the +// wifi-settings file, or the end of the partition, whichever comes first +void wifi_settings_range_get_reusable(wifi_settings_flash_range_t* r) { + wifi_settings_flash_range_t program_range; + wifi_settings_flash_range_t partition_range; + wifi_settings_flash_range_t wifi_settings_file_range; + + wifi_settings_range_get_program(&program_range); + wifi_settings_range_get_partition(&partition_range); + wifi_settings_range_get_wifi_settings_file(&wifi_settings_file_range); + + // round up the program size + wifi_settings_range_align_to_sector(&program_range); + + const uint32_t end_of_partition = get_end_address(&partition_range); + const uint32_t start_of_settings_file = wifi_settings_file_range.start_address; + const uint32_t end_of_reusable_space = + (end_of_partition < start_of_settings_file) ? end_of_partition : start_of_settings_file; + + r->start_address = get_end_address(&program_range); + if (end_of_reusable_space <= r->start_address) { + // There is no reusable space + r->start_address = 0; + r->size = 0; + } else { + r->size = end_of_reusable_space - r->start_address; + } +} + +// Translate Flash range to logical range +void wifi_settings_range_translate_to_logical( + const wifi_settings_flash_range_t* fr, + wifi_settings_logical_range_t* lr) { + +#ifdef XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE + // if the Flash memory might use address translation, use a non-translated address (used for Pico 2) + lr->start_address = (void*) ((uintptr_t) (fr->start_address + XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE)); +#else + // XIP_BASE represents flash address 0 + lr->start_address = (void*) ((uintptr_t) (fr->start_address + XIP_BASE)); +#endif + lr->size = fr->size; +} + +void wifi_settings_range_align_to_sector(wifi_settings_flash_range_t* fr) { + // Start address is rounded down (no effect if already aligned) + fr->start_address &= ~(FLASH_SECTOR_SIZE - 1); + // Size is rounded up (no effect if already aligned) + if ((fr->size & (FLASH_SECTOR_SIZE - 1)) != 0) { + fr->size |= FLASH_SECTOR_SIZE - 1; + fr->size++; + } +} + +// Translate logical range to Flash range, if possible +bool wifi_settings_range_translate_to_flash( + const wifi_settings_logical_range_t* lr, + wifi_settings_flash_range_t* fr) { + + const uintptr_t start_address = (uintptr_t) lr->start_address; + const uintptr_t end_address = start_address + lr->size; + + fr->start_address = 0; + fr->size = lr->size; + + if (end_address < start_address) { + return false; // Range is too large (overflow) + } + + if ((start_address >= XIP_BASE) +#ifdef XIP_NOALLOC_BASE + && (end_address < XIP_NOALLOC_BASE) // Pico 1: end of main region of Flash +#endif +#ifdef XIP_END + && (end_address < XIP_END) // Pico 2: end of main region of Flash +#endif + && (end_address < SRAM_BASE)) { // Fallback: end of all Flash addresses + + // Address is within the part of Flash that can use address translation; + // apply address translation based on the partition size if relevant. + wifi_settings_flash_range_t pr; + wifi_settings_range_get_partition(&pr); + fr->start_address = start_address + pr.start_address - XIP_BASE; + // If the resulting range is still contained in the partition, then the + // translation was successful + return wifi_settings_range_is_contained(fr, &pr); + } + +#ifdef XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE + if ((start_address >= XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE) + && (end_address < SRAM_BASE)) { + // Pico 2: Address is within the untranslated region of Flash + wifi_settings_flash_range_t ar; + wifi_settings_range_get_all(&ar); + fr->start_address = start_address - XIP_NOCACHE_NOALLOC_NOTRANSLATE_BASE; + // If the resulting range is still contained in Flash, then the + // translation was successful + return wifi_settings_range_is_contained(fr, &ar); + } +#endif + + // Not in Flash, or not in an area of Flash that can be supported + return false; +} + diff --git a/src/rp2_common/wifi_settings_connect/wifi_settings_flash_storage.c b/src/rp2_common/wifi_settings_connect/wifi_settings_flash_storage.c new file mode 100644 index 0000000..93b2a0e --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/wifi_settings_flash_storage.c @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * This pico-wifi-settings module reads WiFi settings + * information in Flash. + * + */ + +#include "wifi_settings/wifi_settings_flash_storage.h" +#include "wifi_settings/wifi_settings_flash_range.h" + +#include "pico/platform.h" + +#include + + +// Scan the settings file in Flash for a particular key. +// This function can be reimplemented in order to load settings from some other storage +__weak bool wifi_settings_get_value_for_key( + const char* key, char* value, uint* value_size) { + + wifi_settings_flash_range_t fr; + wifi_settings_logical_range_t lr; + + wifi_settings_range_get_wifi_settings_file(&fr); + wifi_settings_range_translate_to_logical(&fr, &lr); + + const char* file = (const char*) lr.start_address; + const uint file_size = lr.size; + + enum parse_state_t { + NEW_LINE, + KEY, + SEPARATOR, + VALUE, + WAIT_FOR_NEW_LINE, + } parse_state = NEW_LINE; + uint value_index = 0; + uint key_index = 0; + + if (key[0] == '\0') { + // Invalid key - must contain at least 1 character + return false; + } + + for (uint file_index = 0; + (file_index < file_size) + && (file[file_index] != '\0') + && (file[file_index] != '\x1a') // CPM EOF character + && (file[file_index] != '\xff'); // Flash padding character + file_index++) { + + if ((file[file_index] == '\n') || (file[file_index] == '\r')) { + // End of line reached (Unix or DOS line endings) + if (parse_state == VALUE) { + // This is the end of the value + *value_size = value_index; + return true; + } else { + // Reset the parsing state + parse_state = NEW_LINE; + continue; + } + } + + switch (parse_state) { + case NEW_LINE: + // At the beginning of a new line - ignore whitespace before the key + key_index = 0; + if (key[key_index] == file[file_index]) { + // Matched the first character in the key + key_index++; + if (key[key_index] == '\0') { + // There is only one character in the key + parse_state = SEPARATOR; + } else { + // Match the other characters in the key + parse_state = KEY; + } + } else { + // Non-matching character: a different key, + // a comment - wait for the next newline + parse_state = WAIT_FOR_NEW_LINE; + } + break; + case KEY: + if (key[key_index] == file[file_index]) { + // Still matching the key + key_index++; + if (key[key_index] == '\0') { + // There are no more characters in the key + parse_state = SEPARATOR; + } + } else { + // Non-matching character in the key + parse_state = WAIT_FOR_NEW_LINE; + } + break; + case SEPARATOR: + if (file[file_index] == '=') { + // Key is recognised - copy the value + parse_state = VALUE; + } else { + // Key is not immediately followed by '=': not valid + parse_state = WAIT_FOR_NEW_LINE; + } + break; + case VALUE: + if (value_index >= *value_size) { + // Unable to copy more value characters - value is complete + return true; + } else { + value[value_index] = file[file_index]; + value_index++; + } + break; + case WAIT_FOR_NEW_LINE: + // Do nothing in this state, as the state will be reset when + // the next newline is seen + break; + } + } + if (parse_state == VALUE) { + // Reached end of file while parsing the value - value is complete + *value_size = value_index; + return true; + } + // Key was not found + return false; +} diff --git a/src/rp2_common/wifi_settings_connect/wifi_settings_hostname.c b/src/rp2_common/wifi_settings_connect/wifi_settings_hostname.c new file mode 100644 index 0000000..e0b559a --- /dev/null +++ b/src/rp2_common/wifi_settings_connect/wifi_settings_hostname.c @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025 Jack Whitham + * + * SPDX-License-Identifier: BSD-3-Clause + * + * Hostname for wifi-settings. The hostname can be specified in + * the WiFi settings file as "name=". + */ + +#include "wifi_settings/wifi_settings_hostname.h" +#include "wifi_settings/wifi_settings_flash_storage.h" + +#include "pico/unique_id.h" +#include +#include + +static char g_hostname[MAX_HOSTNAME_SIZE]; +static char g_board_id_hex[(BOARD_ID_SIZE * 2) + 1]; + + +const char* wifi_settings_get_hostname() { + return g_hostname; +} + +const char* wifi_settings_get_board_id_hex() { + return g_board_id_hex; +} + +void wifi_settings_set_hostname() { + // Convert board id to uppercase hex + pico_unique_board_id_t id; + pico_get_unique_board_id(&id); + for (int i = 0; (i < PICO_UNIQUE_BOARD_ID_SIZE_BYTES) && (i < BOARD_ID_SIZE); i++) { + snprintf(&g_board_id_hex[i * 2], 3, "%02X", id.id[i]); + } + + // Load host name from the settings file (if set) + uint name_size = sizeof(g_hostname) - 1; + if ((wifi_settings_get_value_for_key("name", g_hostname, &name_size)) && (name_size > 0)) { + // name= is valid + g_hostname[name_size] = '\0'; + } else { + // host name fallback: PicoW- + snprintf(g_hostname, sizeof(g_hostname), "PicoW-%s", g_board_id_hex); + } +}