Improv.Wifi (improv v0.1.2)

Copy Markdown View Source

Wi-Fi operations for Improv provisioning: scan for networks, apply submitted credentials, and derive the post-provisioning redirect URL. The seam the Improv manager calls into.

All VintageNet access is guarded (Code.ensure_loaded?) so the module loads on host, and is injectable (:scan_trigger / :vintage_get / :configure_fn) so the pure shaping — the configure map, the AP→network mapping, the secured? rule — is host-tested without a radio. The Wi-Fi interface defaults to "wlan0"; pass ifname: in each call's opts to target another interface.

scan_networks/1 reads the live access_points property (kept fresh by wpa_supplicant) rather than VintageNetWiFi.quick_scan/1, whose fresh-scan + 2 s sleep read an empty list mid-scan on hardware. It also kicks an async VintageNet.scan/1 to refresh the property for the next call.

Improv only carries SSID+password, not a security type, so configure/3 infers key_mgmt: :sae (WPA3) vs :wpa_psk from the target SSID's scan flags — PSK is the fallback whenever the SSID isn't in scan results (hidden network, aged out) or the lookup itself fails.

Summary

Functions

Apply submitted credentials to the Wi-Fi interface. Looks up the target SSID's live scan flags (union across every BSSID advertising that SSID) to pick key_mgmt: :sae vs :wpa_psk — see configure_map/3. The lookup is best-effort: a raise, a nil property, or the SSID simply not appearing in scan results all fall back to [] (the PSK path) rather than failing the call. The flags come from the same live property scan_networks/1 reads, so the usual scan-then-submit provisioning flow keeps them fresh; a submit with no recent scan can miss a just-appeared SAE-only network and fall back to PSK. :vintage_get (the lookup) and :configure_fn (2-arity (ifname, config) -> term) are injectable for tests.

VintageNet config map for a submitted SSID/password. An empty password yields an open (key_mgmt: :none) network; otherwise key_mgmt: :sae (WPA3, with mandatory PMF — ieee80211w: 2) if flags say the SSID is SAE-only, else the broad-compatibility :wpa_psk fallback (also used for hidden networks, SSIDs that aged out of scan results, and WPA2/WPA3 transition-mode networks, which still accept PSK). flags is threaded in by the caller (configure/3, from a live scan) — this function itself stays pure and does no lookups.

Map a VintageNetWiFi.AccessPoint-shaped value to a network/0. Pure.

The web-UI URL to hand back to the provisioner once joined, or nil if no IPv4 address is bound yet. :vintage_get injectable for tests.

Nearby networks → {:ok, [network]} (empty SSIDs dropped, deduped by SSID keeping the strongest signal), or {:error, reason}.

Whether an AP's :flags indicate a secured network. Pure.

Types

network()

@type network() :: %{ssid: binary(), rssi: integer(), secured: boolean()}

Functions

configure(ssid, password, opts \\ [])

@spec configure(binary(), binary(), keyword()) :: term()

Apply submitted credentials to the Wi-Fi interface. Looks up the target SSID's live scan flags (union across every BSSID advertising that SSID) to pick key_mgmt: :sae vs :wpa_psk — see configure_map/3. The lookup is best-effort: a raise, a nil property, or the SSID simply not appearing in scan results all fall back to [] (the PSK path) rather than failing the call. The flags come from the same live property scan_networks/1 reads, so the usual scan-then-submit provisioning flow keeps them fresh; a submit with no recent scan can miss a just-appeared SAE-only network and fall back to PSK. :vintage_get (the lookup) and :configure_fn (2-arity (ifname, config) -> term) are injectable for tests.

configure_map(ssid, password, flags \\ [])

@spec configure_map(binary(), binary(), [atom()]) :: map()

VintageNet config map for a submitted SSID/password. An empty password yields an open (key_mgmt: :none) network; otherwise key_mgmt: :sae (WPA3, with mandatory PMF — ieee80211w: 2) if flags say the SSID is SAE-only, else the broad-compatibility :wpa_psk fallback (also used for hidden networks, SSIDs that aged out of scan results, and WPA2/WPA3 transition-mode networks, which still accept PSK). flags is threaded in by the caller (configure/3, from a live scan) — this function itself stays pure and does no lookups.

network_from_ap(map)

@spec network_from_ap(map()) :: network()

Map a VintageNetWiFi.AccessPoint-shaped value to a network/0. Pure.

redirect_url(opts \\ [])

@spec redirect_url(keyword()) :: String.t() | nil

The web-UI URL to hand back to the provisioner once joined, or nil if no IPv4 address is bound yet. :vintage_get injectable for tests.

scan_networks(opts \\ [])

@spec scan_networks(keyword()) :: {:ok, [network()]} | {:error, term()}

Nearby networks → {:ok, [network]} (empty SSIDs dropped, deduped by SSID keeping the strongest signal), or {:error, reason}.

Reads the live ["interface", ifname, "wifi", "access_points"] property that wpa_supplicant keeps refreshed via periodic background scans — NOT VintageNetWiFi.quick_scan/1, which triggers a fresh ioctl scan and only waits 2s, so it lands in the empty window mid-scan (HW-found: returned 0 while the property held 31 APs). We also kick an async VintageNet.scan/1 to keep the property fresh for the next request. :vintage_get/:scan_trigger injectable.

secured?(flags)

@spec secured?([atom()] | term()) :: boolean()

Whether an AP's :flags indicate a secured network. Pure.