Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for esp32 network driver scanning for access points #1165

Open
wants to merge 3 commits into
base: release-0.6
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.6.5] - Unreleased

### Added

- Add `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in station (or sta+ap) mode.

## [0.6.4] - 2024-08-18

### Added
Expand Down
36 changes: 35 additions & 1 deletion doc/src/network-programming-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,41 @@ 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 mode and started, you may scan for available access points at any time with [`network:wifi_scan/0`](./apidocs/erlang/eavmlib/network.md#wifi_scan1) or [`network:wifi_scan/1`](./apidocs/erlang/eavmlib/network.md#wifi_scan1). Scanning for access points will temporarily inhibit other network traffic, but will not cause an active connection 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 discoverd than the number of results requested. The entries in the `NetworkList` take the form of `{SSID, [AP_Properties]}`. `SSID` is the name of the network, and the `AP_Properties` is a proplist with the keys `rssi` for the dBm signal strength of the access point, `authmode` value is the authentication method used by the network, and the `channel` key for obtaining the primary channel for the network.

Example return results:
```erlang
{ok,{13,[{"atomvm_test_ap",[{rssi,-25},{authmode,wpa_wpa2_psk},{channel,6}]},
{"HotSpot",[{rssi,-47},{authmode,wpa2_wpa3_psk},{channel,6}]},
{"phishin hole",[{rssi,-50},{authmode,open},{channel,1}]},
{"The Neighbors Car",[{rssi,-58},{authmode,wpa3_enterprise_192},{channel,4}]},
{"nothing to see here",[{rssi,-64},{authmode,wpa3_ext_psk_mixed},{channel,11}]},
{"Public Access",[{rssi,-75},{authmode,open},{channel,1}]}]}}
```

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, 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}, 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:sta_scan_config()`](./apidocs/erlang/eavmlib/network.md) type definition.

## AP mode

Expand Down
142 changes: 138 additions & 4 deletions libs/eavmlib/src/network.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
-export([
wait_for_sta/0, wait_for_sta/1, wait_for_sta/2,
wait_for_ap/0, wait_for_ap/1, wait_for_ap/2,
sta_rssi/0
sta_rssi/0,
wifi_scan/0, wifi_scan/1
]).
-export([start/1, start_link/1, stop/0]).
-export([
Expand All @@ -37,6 +38,10 @@
]).

-define(SERVER, ?MODULE).
% These values ate used to calculate the gen_server:call/3 timeout.
-define(DEVICE_TOTAL_CHANNELS, 14).
-define(GEN_RESPONSE_MS, 5000).
-define(MAX_SHORT_DWELL, 320).

-type octet() :: 0..255.
-type ipv4_address() :: {octet(), octet(), octet(), octet()}.
Expand All @@ -52,10 +57,16 @@
-type sta_connected_config() :: {connected, fun(() -> term())}.
-type sta_disconnected_config() :: {disconnected, fun(() -> term())}.
-type sta_got_ip_config() :: {got_ip, fun((ip_info()) -> term())}.
-type sta_scan_config() ::
{default_scan_results, 1..20}
| {scan_dwell_ms, 1..1500}
| scan_show_hidden
| scan_passive.
-type sta_config_property() ::
ssid_config()
| psk_config()
| dhcp_hostname_config()
| sta_scan_config()
| sta_connected_config()
| sta_disconnected_config()
| sta_got_ip_config().
Expand Down Expand Up @@ -143,7 +154,41 @@

-type network_config() :: [sta_config() | ap_config() | sntp_config()].

-type db() :: integer().
-type dbm() :: integer().
%% `dbm()' decibel-milliwatts (or dBm) 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.

-type scan_options() ::
{results, 1..20}
| {dwell, 1..1500}
| show_hidden
| passive.
%% The `results' key is used to set the maximum number of networks returned in the
%% networks list, and the `dwell' is used to set the dwell time (in milliseconds)
%% spent on each channel. The option `show_hidden' will also include hidden networks
%% in the scan results. Default options are: `[{results, 6}, {dwell, 120}]', if
%% `passive' is used the default dwell time per channel is 360 ms.

-type auth_type() ::
open
| wep
| wpa_psk
| wpa2_psk
| wpa_wpa2_psk
| eap
| wpa3_psk
| wpa2_wpa3_psk
| wapi
| owe
| wpa3_enterprise_192
| wpa3_ext_psk
| wpa3_ext_psk_mixed.

-type network_properties() :: [
{rssi, dbm()} | [{authmode, auth_type()} | [{channel, wifi_channel()}]]
].
%% A proplist of network properties with the keys: `rssi', `authmode' and `channel'

-record(state, {
config :: network_config(),
Expand Down Expand Up @@ -305,12 +350,12 @@ stop() ->
gen_server:stop(?SERVER).

%%-----------------------------------------------------------------------------
%% @returns {ok, Rssi} in decibels, or {error, Reason}.
%% @returns {ok, Rssi} in dBm, or {error, Reason}.
%%
%% @doc Get the rssi information of AP to which the device is associated with.
%% @end
%%-----------------------------------------------------------------------------
-spec sta_rssi() -> {ok, Rssi :: db()} | {error, Reason :: term()}.
-spec sta_rssi() -> {ok, Rssi :: dbm()} | {error, Reason :: term()}.
sta_rssi() ->
Port = get_port(),
Ref = make_ref(),
Expand All @@ -321,6 +366,75 @@ sta_rssi() ->
Other -> {error, Other}
end.

%% @param Options is a list of `scan_options()'
%% @returns Scan result tuple, or {error, Reason} if a failure occurred.
%%
%% @doc Scan for available WiFi networks.
%%
%% The network must first be started in sta or sta+ap mode before scanning for access points. While
%% a scan is in progress network traffic will be inhibited, but should not cause an active connection
%% to be lost. Espressif's documentation recommends not exceeding 1500 ms per-chanel scan times or
%% network connections may be lost, this is enforced as a hard limit. The return is a tuple
%% `{ok, Results}', where Results is a tuple with the number of discovered networks and a list of
%% networks, which may be shorter than the size of the discovered networks if a smaller `MaxAPs' was
%% used. The network tuples in the list consist of network name and a proplist of network information:
%%
%% `{ok, {NumberResults, [{SSID, [{rssi, DBm}, {authmode, Mode}, {channel, Number}]}, ...]}}'
%%
%% Example:
%% <pre>
%% ...
%% {ok, {Number, Results}} = wifi_scan(10),
%% io:format("Number of networks discovered: ~p~n", [Number])
%% lists:foreach(fun(Network = {SSID, [{rssi, DBm}, {authmode, Mode}, {channel, Number}]}) ->
%% io:format("Network: ~p, signal ~p dBm, Security: ~p, channel ~p~n",
%% [SSID, DBm, Mode, Number]) end,
%% Results).
%% </pre>
%%
%% For convenience `network_wifi_scan/0' may be used to scan with default options.
%%
%% Note: If a long dwell time is used, the return time for this function can be considerably longer
%% than the default gen_server timeout, especially when performing a passive scan. Passive scans
%% always use the full dwell time for each channel, active scans with a dwell time of more than 240
%% milliseconds will have a minimum dwell of 1/2 the maximum dwell time set by the `dwell' option.
%% The timeout for these longer scans is determined by the following:
%% Timeout = (dwell * 14) + 5000.
%% That is the global maximum wifi channels multiplied by the dwell time with an additional
%% gen_server default timeout period added. The actual number of channels scanned will be
%% determined by the country code set by the devices connection to an access point. This is 13
%% channels most of the world, 11 for North America, and 14 in some parts of Asia.
%%
%% The default options may be configured by adding `sta_scan_config()' options to the
%% `sta_config()'.
%%
%% Warning: This feature is not yet available on the rp2040 platform.
%%
%% @end
%%-----------------------------------------------------------------------------
-spec wifi_scan([Options :: scan_options(), ...]) ->
{ok, {NetworksDiscovered :: 0..20, [{SSID :: string(), [ApInfo :: network_properties()]}, ...]}}
| {error, Reason :: term()}.
wifi_scan(Options) ->
Dwell = proplists:get_value(dwell, Options),
case Dwell of
undefined ->
gen_server:call(?SERVER, {scan, Options});
Millis when Millis =< ?MAX_SHORT_DWELL ->
gen_server:call(?SERVER, {scan, Options});
ChanDwellMs ->
Timeout = (ChanDwellMs * ?DEVICE_TOTAL_CHANNELS) + ?GEN_RESPONSE_MS,
gen_server:call(?SERVER, {scan, Options}, Timeout)
end.

wifi_scan() ->
Config = gen_server:call(?SERVER, get_config),
Results = proplists:get_value(default_scan_results, proplists:get_value(sta, Config), 6),
Dwell = proplists:get_value(scan_dwell_ms, proplists:get_value(sta, Config), 120),
Hidden = proplists:get_value(scan_show_hidden, proplists:get_value(sta, Config), false),
Passive = proplists:get_value(scan_passive, proplists:get_value(sta, Config), false),
wifi_scan([{results, Results}, {dwell, Dwell}, {show_hidden, Hidden}, {passive, Passive}]).

%%
%% gen_server callbacks
%%
Expand All @@ -335,6 +449,12 @@ handle_call(start, From, #state{config = Config} = State) ->
Ref = make_ref(),
Port ! {self(), Ref, {start, Config}},
wait_start_reply(Ref, From, Port, State);
handle_call({scan, ScanOpts}, From, State) ->
Ref = make_ref(),
network_port ! {self(), Ref, {scan, ScanOpts}},
wait_scan_results(Ref, From, State#state{ref = Ref});
handle_call(get_config, _From, #state{config = Config} = State) ->
{reply, Config, State};
handle_call(_Msg, _From, State) ->
{reply, {error, unknown_message}, State}.

Expand All @@ -349,6 +469,20 @@ wait_start_reply(Ref, From, Port, State) ->
{stop, {start_failed, Reason}, ER, State}
end.

%% @private
wait_scan_results(Ref, From, State) ->
receive
{Ref, {error, _Reason} = ER} ->
gen_server:reply(From, ER),
{noreply, State#state{ref = Ref}};
{Ref, Results} ->
gen_server:reply(From, Results),
{noreply, State#state{ref = Ref}};
Any ->
gen_server:reply(From, Any),
{noreply, State#state{ref = Ref}}
end.

%% @hidden
handle_cast(_Msg, State) ->
{noreply, State}.
Expand Down
Loading
Loading