Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/esp32-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ jobs:
matrix:
esp-idf-target: ["esp32", "esp32c3"]
idf-version:
- 'v5.1.6'
- 'v5.2.6'
- 'v5.3.4'
- 'v5.4.3'
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ encoding/decoding options, also Elixir `(url_)encode64`/`(url_)decode64` have be
- Added `nanosecond` and `native` time unit support to `erlang:system_time/1`, `erlang:monotonic_time/1`, and `calendar:system_time_to_universal_time/2`
- Added `erlang:system_time/0`, `erlang:monotonic_time/0`, and `os:system_time/0,1` NIFs
- Added `filename:join/1` and `filename:split/1`
- Added `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in sta or sta+ap mode.

### Changed

Expand Down
144 changes: 133 additions & 11 deletions doc/src/network-programming-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ Callback functions can be specified by the following configuration parameters:
* `{connected, fun(() -> term())}` A callback function which will be called when the device connects to the target network.
* `{disconnected, fun(() -> term())}` A callback function which will be called when the device disconnects from the target network. If no callback function is provided the default behavior is to attempt to reconnect immediately. By providing a callback function the application can decide whether to reconnect, or connect to a new access point.
* `{got_ip, fun((ip_info()) -> term())}` A callback function which will be called when the device obtains an IP address. In this case, the IPv4 IP address, net mask, and gateway are provided as a parameter to the callback function.
* `{scan_done, fun((scan_results() | {error, Reason :: term()}) -> term())}` A callback function which will be called once a network scan is
completed, this allows for event-driven connection management, and prevents blocking the caller
when requesting a scan of available wifi networks.

```{warning}
IPv6 addresses are not yet supported in AtomVM.
Expand All @@ -90,30 +93,64 @@ The following example illustrates initialization of the WiFi network in STA mode
```erlang
Config = [
{sta, [
{ssid, <<"myssid">>},
{psk, <<"mypsk">>},
{connected, fun connected/0},
{got_ip, fun got_ip/1},
{disconnected, fun disconnected/0}
{disconnected, fun disconnected/0},
{scan_done, fun got_scan_results/1},
{dhcp_hostname, <<"myesp32">>}
]}
],
{ok, Pid} = network:start(Config),
ok = network:wifi_scan(),
...
```

The following callback functions will be called when the corresponding events occur during the lifetime of the network connection.
The following callback functions will be called when the corresponding events occur during the
lifetime of the network connection. This example demonstrates using callbacks to scan for networks,
and if a found network is stored in nvs with an `ssid` key value that matches, it will use the
stored `psk` key value to authenticate. After an IP address is acquired the example applications
supervised network service will be started by the `start_my_server_sup` function (this function is
left for the reader to imagine, see: [`supervisor`](./apidocs/erlang/estdlib/supervisor.md)).

```erlang
connected() ->
io:format("Connected to AP.~n").

gotIp(IpInfo) ->
io:format("Got IP: ~p~n", [IpInfo]).
got_ip(IpInfo) ->
io:format("Got IP: ~p~n", [IpInfo]),
erlang:spawn(fun() -> start_my_server_sup() end).

disconnected() ->
io:format("Disconnected from AP, attempting to reconnect~n"),
network:sta_connect().
io:format("Disconnected from AP, starting scan~n"),
erlang:spawn(fun() -> network:wifi_scan() end).

scan_done({NumResults, Results}) ->
io:format("WiFi scan found ~p networks.~n", [NumResults]),
erlang:spawn(fun() -> connect_if_known(Results) end).

connect_if_known([]) ->
io:format("No known networks found, re-scanning in 60 seconds."),
erlang:spawn(fun() ->
timer:sleep(60_000),
network:wifi_scan()
end);
connect_if_known([#{ssid := SSID, authmode := Auth} = Network | Results])
case SSID =:= esp:nvs_fetch_binary(network, ssid) of
true ->
case esp:nvs_fetch_binary(network, psk) of
undefined when Auth =:= open ->
io:format("Connecting to unsecured network ~s...~n", [SSID]),
network:sta_connect[{ssid, SSID}];
undefined ->
io:format("No psk stored in nvs for network ~s with ~s security!~n", [SSID, Auth]),
connect_if_known(Results);
PSK ->
io:format("Connecting to ~s (~s)...~n", [SSID, Auth]),
network:sta_connect[{ssid, SSID}, {psk, PSK}]
end;
false ->
connect_if_known(Results)
end.
```

In a typical application, the network should be configured and an IP address should be acquired first, before starting clients or services that have a dependency on the network.
Expand All @@ -138,10 +175,95 @@ case network:wait_for_sta(Config, 15000) of
end
```

To obtain the signal strength (in decibels) of the connection to the associated access point use [`network:sta_rssi/0`](./apidocs/erlang/eavmlib/network.md#sta_rssi0).

### STA (or AP+STA) mode functions

Some functions are only available if the device is configured in STA or AP+STA mode.

#### `sta_rssi`

Once connected to an access point, the signal strength in decibel-milliwatts (dBm) of the
connection to the associated access point may be obtained using
[`network:sta_rssi/0`](./apidocs/erlang/eavmlib/network.md#sta_rssi0). The value returned as
`{ok, Value}` will typically be a negative number, but in the
presence of a powerful signal this can be a positive number. A level of 0 dBm corresponds to the
power of 1 milliwatt. A 10 dBm decrease in level is equivalent to a ten-fold decrease in signal
power.

#### `wifi_scan`

```{notice}
This function is currently only supported on the ESP32 platform.
```

After the network has been configured for STA or AP+STA mode and started, you may scan for
available access points using
[`network:wifi_scan/0`](./apidocs/erlang/eavmlib/network.md#wifi_scan0) or
[`network:wifi_scan/1`](./apidocs/erlang/eavmlib/network.md#wifi_scan1). Scanning for access
points will temporarily inhibit other traffic on the access point network if it is in use, but
should not cause any active connections to be dropped. With no options, a default 'active' scan,
with a per-channel dwell time of 120ms will be used and will return network details for up to 6
access points. The return value for the scan takes the form of a tuple consisting of
`{ok, Results}`, where `Results = {FoundAPs, NetworkList}`. `FoundAPs` may be a number larger than
the length of the NetworkList if more access points were discovered than the number of results
requested. The entries in the `NetworkList` take the form of a map with the keys `ssid` mapped to
the network name, `rssi` for the dBm signal strength of the access point, `authmode` value is the
authentication method used by the network, `bssid` (a.k.a MAC address) of the access point, and the
`channel` key for the primary channel for the network.

Blocking example with no `scan_done` callback:
```erlang
{ok, {Num, Networks}} = network:wifi_scan(),
io:format("network scan found ~p networks.~n", [Num]),
lists:foreach(
fun(
_Network = #{ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number}
) ->
io:format(
"Network: ~p, BSSID: ~p, signal ~p dBm, Security: ~p, channel ~p~n",
[SSID, BSSID, DBm, Mode, Number]
)
end,
Networks
).
```

To avoid blocking the caller for extended lengths of time, especially on 5 Ghz capable devices,
a callback function may be configured in the network config. See
[Station mode callbacks](#station-mode-callbacks).

To minimize the risk of out-of-memory errors, this driver limits the maximum number of returned
networks depending on the target and memory configuration:
ESP32-C2 supports up to 10, ESP32-S2/ESP32-C61/ESP32-C5 up to 14, most other targets up to 20,
and ESP32-P4 or PSRAM-enabled builds up to 64.

The default scan is quite fast, and likely may not find all the available networks. Scans are
quite configurable with `active` (the default) and `passive` modes. Options should take the form of
a proplist. The per-channel scan time can be changed with the `dwell` key, the channel dwell time
can be set for up to 1500 ms. Passive scans are slower, as they always linger on each channel for
the full dwell time. Passive mode can be used by simply adding `passive` to the configuration
proplist. Keep in mind when choosing a dwell time that between each progressively scanned channel
the device must return to the home channel for a short time (typically 30ms), but for scans with a
dwell time of over 1000ms the home channel dwell time will increase to 60ms to help mitigate
beacon-timeout events. In some network configuration beacon timeout events may still occur, but
should not lead to a dropped connection, and after the scan completes the device should receive the
next beacon from the access point. The default of 6 access points in the returned `NetworkList` may
be changed with the `results` key. By default hidden networks are ignored, but can be included in
the results by adding `show_hidden` to the configuration.

For example, to do a passive scan using an ESP32-C6, including hidden networks, using the longest
allowed scan time and showing the maximum number of networks available use the following:

```erlang
{ok, Results} = network:wifi_scan([passive, {results, 20}, {dwell, 1500}, show_hidden]),
```

For convenience the default options used for `network:wifi_scan/0` may be configured along with the
`sta_config()` used to start the network driver. For the corresponding configuration keys consult
the [`network:scan_options()`](./apidocs/erlang/eavmlib/network.md#scan-options) type definition.
Comment on lines +260 to +262
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Point this section at sta_scan_config() instead of scan_options().

wifi_scan/0 defaults are configured in sta_config() with keys like default_scan_results, scan_dwell_ms, scan_show_hidden, and scan_passive. Referring readers to scan_options() here sends them to the runtime wifi_scan/1 keys instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/src/network-programming-guide.md` around lines 228 - 230, Update the
documentation paragraph that currently points readers to network:scan_options()
for wifi_scan/0 defaults so it instead refers to sta_scan_config();
specifically, change the reference from `network:scan_options()` to
`sta_scan_config()` and ensure the text explains that `wifi_scan/0` defaults are
configured via `sta_config()`/`sta_scan_config()` (keys like
`default_scan_results`, `scan_dwell_ms`, `scan_show_hidden`, `scan_passive`)
rather than the runtime `wifi_scan/1` options.

For most applications that will use wifi scan results, it is recommended to start the driver with a
configuration that uses a custom callback function for `disconnected` events, so that the driver
will remain idle and allow the use of scan results to decide if a connection should be made.

#### `sta_status`

The function [`network:sta_status/0`](./apidocs/erlang/eavmlib/network.md#sta_status0) may be used
Expand Down Expand Up @@ -191,7 +313,7 @@ The `<ap-properties>` property list may contain the following entries:

If the SSID is omitted in configuration, the SSID name `atomvm-<hexmac>` will be created, where `<hexmac>` is the hexadecimal representation of the factory-assigned MAC address of the device. This name should be sufficiently unique to disambiguate it from other reachable ESP32 devices, but it may also be difficult to read or remember.

If the password is omitted, then an _open network_ will be created, and a warning will be printed to the console. Otherwise, the AP network will be started using WPA+WPA2 authentication.
If the password is omitted, then an __open network__ will be created, and a warning will be printed to the console. Otherwise, the AP network will be started using WPA+WPA2 authentication.

If the channel is omitted the default chanel for esp32 is `1`. This setting is only used while a device is operation is AP mode only. If `ap_channel` is configured, it will be temporarily changed to match the associated access point if AP + STA mode is used and the station is associated with an access point. This is a hardware limitation due to the modem radio only being able to operate on a single channel (frequency) at a time.

Expand Down
2 changes: 2 additions & 0 deletions examples/erlang/esp32/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ pack_runnable(reformat_nvs reformat_nvs eavmlib avm_esp32)
pack_runnable(uartecho uartecho eavmlib estdlib avm_esp32)
pack_runnable(ledc_example ledc_example eavmlib estdlib avm_esp32)
pack_runnable(epmd_disterl epmd_disterl eavmlib estdlib avm_network avm_esp32)
pack_runnable(wifi_scan wifi_scan estdlib eavmlib avm_network avm_esp32)
pack_runnable(wifi_scan_callback wifi_scan_callback estdlib eavmlib avm_network avm_esp32)
70 changes: 70 additions & 0 deletions examples/erlang/esp32/wifi_scan.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
%% This file is part of AtomVM.
%%
%% Copyright (c) 2023 <winford@object.stream>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%%
%% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
%%

-module(wifi_scan).

-export([start/0]).

start() ->
{ok, _Pid} = network:start([{sta, [managed]}]),
scan_passive([show_hidden, {dwell, 1000}]),
scan_active([{dwell, 500}]).

scan_active(Config) ->
io:format(
"~nStarting active scan with configuration ~p, this may take some time depending on dwell ms used.~n~n",
[Config]
),
BeginTime = erlang:monotonic_time(millisecond),
{ok, {Num, Networks}} = network:wifi_scan(Config),
io:format("Active scan found ~p networks in ~pms.~n", [
Num, erlang:monotonic_time(millisecond) - BeginTime
]),
format_networks(Networks).

scan_passive(Config) ->
io:format(
"~nStarting passive scan with configuration: ~p, this may take some time depending on dwell ms used.~n~n",
[Config]
),
Opts = [passive | Config],
BeginTime = erlang:monotonic_time(millisecond),
ScanResults = network:wifi_scan(Opts),
{ok, {Num, Networks}} = ScanResults,
io:format("Passive scan found ~p networks in ~pms.~n", [
Num, erlang:monotonic_time(millisecond) - BeginTime
]),
format_networks(Networks).

format_networks(Networks) ->
lists:foreach(
fun(
_Network = #{
ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number
}
) ->
io:format(
"Network: ~p, BSSID: ~p, signal ~p dBm, Security: ~p, channel ~p~n",
[SSID, binary:encode_hex(BSSID), DBm, Mode, Number]
)
end,
Networks
).
63 changes: 63 additions & 0 deletions examples/erlang/esp32/wifi_scan_callback.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
%% This file is part of AtomVM.
%%
%% Copyright (c) 2023 <winford@object.stream>
%% All rights reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%%
%% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
%%

-module(wifi_scan_callback).

-export([start/0]).

start() ->
Config = [{sta, [managed, {scan_done, fun display_scan_results/1}]}],
{ok, _Pid} = network:start(Config),
io:format(
"~nStarting active scan with configuration ~p, this may take some time depending on dwell ms used.~n~n",
[Config]
),
case network:wifi_scan() of
{error, Reason} ->
io:format("wifi_scan failed for reason ~p", [Reason]);
ok ->
timer:sleep(infinity)
end.

display_scan_results(Results) ->
case Results of
{error, Reason} ->
io:format("wifi_scan failed for reason ~p.~n", [Reason]);
{Num, Networks} ->
io:format("wifi_scan callback got ~p results:~n", [Num]),
lists:foreach(
fun(
_Network = #{
ssid := SSID,
rssi := DBm,
authmode := Mode,
bssid := BSSID,
channel := Number
}
) ->
io:format(
"Network: ~s, BSSID: ~s, signal ~p dBm, Security: ~p, channel ~p~n",
[SSID, binary:encode_hex(BSSID), DBm, Mode, Number]
)
end,
Networks
)
end.
Loading
Loading