Skip to content

Commit

Permalink
Add network:wifi_scan/0,1 to esp32 network driver
Browse files Browse the repository at this point in the history
Adds the ability for devices configured for sta or ap+sta to scan for available
access points when not currently associated to an AP. With no arguments the
default maximum results returned is 6. All default options can be configured
in the `sta_config()` section of the configuration used to start the network
driver, or set in the configuration parameter of `network:wifi_scan/1`.

Signed-off-by: Winford <[email protected]>
  • Loading branch information
UncleGrumpy committed Nov 21, 2024
1 parent 1e894ab commit d78dd4c
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added a limited implementation of the OTP `ets` interface
- Added `code:all_loaded/0` and `code:all_available/0`
- Add `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in station (or sta+ap) mode.

## [0.6.6] - Unreleased

Expand Down
133 changes: 132 additions & 1 deletion 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 @@ -53,10 +58,16 @@
-type sta_beacon_timeout_config() :: {beacon_timeout, 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_beacon_timeout_config()
| sta_disconnected_config()
Expand Down Expand Up @@ -150,6 +161,37 @@
%% 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(),
port :: port(),
Expand Down Expand Up @@ -326,6 +368,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 @@ -340,6 +451,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 @@ -354,6 +471,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

0 comments on commit d78dd4c

Please sign in to comment.