diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a0178458..a36249443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ 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:connect/0,1` and `network:disconnect` to ESP32 network driver. + +### Changed + +- Using a custom callback for STA disconnected events in esp32 network driver will stop automatic re-connect, +allowing applications to use scan results or other means to decide when and where to connect. ## [0.6.6] - Unreleased diff --git a/doc/src/network-programming-guide.md b/doc/src/network-programming-guide.md index c46346b46..81b2d7d1c 100644 --- a/doc/src/network-programming-guide.md +++ b/doc/src/network-programming-guide.md @@ -27,6 +27,8 @@ The `` property list should contain the following entries: * `{ssid, string() | binary()}` The SSID to which the device should connect. * `{psk, string() | binary()}` The password required to authenticate to the network, if required. +Optionally on the ESP32 platform, using the `managed` atom in the configuration, the `ssid` and `psk` may be omitted and a connection will not be initiated immediately. This will allow for the use of `network:wifi_scan` to find available access points, and connecting with `network:connect/1`. When starting the driver in this mode all callback functions must be configured when the driver is started, and providing a callback for `disconnected` events is recommended, so the application can also control when, and to which access point, it will make a new connection. + The [`network:start/1`](./apidocs/erlang/eavmlib/network.md#start1) will immediately return `{ok, Pid}`, where `Pid` is the process ID of the network server instance, if the network was properly initialized, or `{error, Reason}`, if there was an error in configuration. However, the application may want to wait for the device to connect to the target network and obtain an IP address, for example, before starting clients or services that require network access. Applications can specify callback functions, which get triggered as events emerge from the network layer, including connection to and disconnection from the target network, as well as IP address acquisition. @@ -34,7 +36,7 @@ Applications can specify callback functions, which get triggered as events emerg 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. +* `{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. ```{warning} @@ -75,7 +77,8 @@ gotIp(IpInfo) -> io:format("Got IP: ~p~n", [IpInfo]). disconnected() -> - io:format("Disconnected from AP.~n"). + io:format("Disconnected from AP, attempting to reconnect~n"), + network:connect(). ``` 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. @@ -102,6 +105,20 @@ 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 + +#### `disconnect` + +The function `network:disconnect/0` will disconnect a station from the associated access point. Note that using this function without providing a custom `disconnected` event callback function will result in the driver immediately attempting to reconnect to the last associated access point. + +This function is currently only supported on the ESP32 platform. + +#### `connect` + +Using the function `network:connect/0` will start a connection to the last configured access point. To connect to a new access point use either a proplist consisting of `[{ssid, "Network Name"} | {psk, "Password"} | {dhcp_hostname, "hostname"}]`, or a complete `network_config()` consisting of `[sta_config() | sntp_config()]`. If any callback functions or default scan configuration options are defined in the `network_config()` they will be ignored; default scan options and callback functions must be configured when the driver is started. + +This function is currently only supported on the ESP32 platform. + ## AP mode In AP mode, the ESP32 starts a WiFi network to which other devices (laptops, mobile devices, other ESP32 devices, etc) can connect. The ESP32 will create an IPv4 network, and will assign itself the address `192.168.4.1`. Devices that attach to the ESP32 in AP mode will be assigned sequential addresses in the `192.168.4.0/24` range, e.g., `192.168.4.2`, `192.168.4.3`, etc. diff --git a/libs/eavmlib/src/network.erl b/libs/eavmlib/src/network.erl index 3f83ca82c..7b6f4e74e 100644 --- a/libs/eavmlib/src/network.erl +++ b/libs/eavmlib/src/network.erl @@ -25,7 +25,10 @@ -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, + sta_disconnect/0, + sta_connect/0, + sta_connect/1 ]). -export([start/1, start_link/1, stop/0]). -export([ @@ -47,14 +50,29 @@ -type ssid_config() :: {ssid, string() | binary()}. -type psk_config() :: {psk, string() | binary()}. +-type app_managed_config() :: managed | {managed, boolean()}. +%% Setting `{managed, true}' or including the atom `managed' in the `sta_config()' will signal to +%% the driver that the connections are managed in the user application, allowing to start the +%% driver in STA (or AP+STA) mode, but delay starting a connection by omitting `ssid' and `psk' +%% configuration values. When using this mode of operation applications may want to provide an +%% `sta_disconnected_config()' to replace the default callback, which attempts to reconnect to the +%% last network, and instead scan for available networks, or use some other means of determining +%% when, and which network to connect to. -type dhcp_hostname_config() :: {dhcp_hostname, string() | binary()}. -type sta_connected_config() :: {connected, fun(() -> term())}. -type sta_beacon_timeout_config() :: {beacon_timeout, fun(() -> term())}. -type sta_disconnected_config() :: {disconnected, fun(() -> term())}. +%% If no callback is configured the default behavior when the connection to an access point is +%% lost is to attempt to reconnect. If a callback is provided these automatic reconnections will +%% no longer occur, and the application must use `network:sta_connect/0' to reconnect to the last +%% access point, or use `network:sta_connect/1' to connect to a new access point in a manner +%% determined by the application. + -type sta_got_ip_config() :: {got_ip, fun((ip_info()) -> term())}. -type sta_config_property() :: - ssid_config() + app_managed_config() + | ssid_config() | psk_config() | dhcp_hostname_config() | sta_connected_config() @@ -148,6 +166,7 @@ -type db() :: integer(). -record(state, { + cb_ref :: reference(), config :: network_config(), port :: port(), ref :: reference(), @@ -263,9 +282,11 @@ wait_for_ap(ApConfig, Timeout) -> %% %% This function will start a network interface, which will attempt to %% connect to an AP endpoint in the background. Specify callback -%% functions to receive definitive -%% information that the connection succeeded. See the AtomVM Network -%% FSM Programming Manual for more information. +%% functions to receive definitive information that the connection +%% succeeded; specify a `sta_disconnected_config()' in the `sta_config()' +%% to manage reconnections in the application, rather than the default +%% automatic attempt to reconnect until a connection is reestablished. +%% See the AtomVM Network Programming Manual for more information. %% @end %%----------------------------------------------------------------------------- -spec start(Config :: network_config()) -> {ok, pid()} | {error, Reason :: term()}. @@ -296,6 +317,56 @@ start_link(Config) -> Error end. +%%----------------------------------------------------------------------------- +%% @returns `ok', if the network disconnects from the access point, or +%% `{error, Reason}' if a failure occurred. +%% @doc Disconnect from access point. +%% +%% This will terminate a connection to an access point. +%% +%% Note: Using this function without providing an `sta_disconnected_config()' +%% in the `sta_config()' will result in the driver immediately attempting to +%% reconnect to the same access point again. +%% @end +%%----------------------------------------------------------------------------- +-spec sta_disconnect() -> ok | {error, Reason :: term()}. +sta_disconnect() -> + gen_server:call(?SERVER, halt_sta). + +%%----------------------------------------------------------------------------- +%% @param Config The new station mode mode network configuration, if no +%% configuration is given the driver will attempt to reconnect to +%% the last access point it configured to use. +%% @returns ok, if the network interface was started, or {error, Reason} if +%% a failure occurred (e.g., due to malformed network configuration). +%% @doc Connect to a new access point after the network driver has been started. +%% +%% This function will attempt to connect to an AP endpoint in the +%% background. +%% +%% The `dhcp_hostname' and sntp server configuration can be changed, +%% but any callback settings included in the configuration will be +%% ignored, callbacks must be configured when the driver is started +%% with `network:start/1'. +%% @end +%%----------------------------------------------------------------------------- +-spec sta_connect(Config :: network_config()) -> ok | {error, Reason :: term()}. +sta_connect(Config) -> + gen_server:call(?SERVER, {connect, Config}). + +%%----------------------------------------------------------------------------- +%% @returns ok, if the network interface was started, or {error, Reason} if +%% a failure occurred (e.g., due to malformed network configuration). +%% @doc Reconnect to an access point after a network disconnection. +%% +%% This function will attempt to reconnect, in the background, to the +%% last AP endpoint that was configured. +%% @end +%%----------------------------------------------------------------------------- +-spec sta_connect() -> ok | {error, Reason :: term()}. +sta_connect() -> + gen_server:call(?SERVER, connect). + %%----------------------------------------------------------------------------- %% @returns ok, if the network interface was stopped, or {error, Reason} if %% a failure occurred. @@ -337,6 +408,19 @@ 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(halt_sta, From, State) -> + Ref = make_ref(), + network_port ! {From, Ref, halt_sta}, + wait_halt_sta_reply(Ref, From, State); +handle_call(connect, From, #state{config = Config} = State) -> + Ref = make_ref(), + network_port ! {self(), Ref, connect}, + wait_connect_reply(Ref, From, Config, State); +handle_call({connect, Config}, From, #state{config = OldConfig} = State) -> + Ref = make_ref(), + NewConfig = update_config(OldConfig, Config), + network_port ! {self(), Ref, {connect, Config}}, + wait_connect_reply(Ref, From, NewConfig, State); handle_call(_Msg, _From, State) -> {reply, {error, unknown_message}, State}. @@ -345,44 +429,75 @@ wait_start_reply(Ref, From, Port, State) -> receive {Ref, ok} -> gen_server:reply(From, ok), - {noreply, State#state{port = Port, ref = Ref}}; + {noreply, State#state{port = Port, ref = Ref, cb_ref = Ref}}; {Ref, {error, Reason} = ER} -> gen_server:reply(From, {error, Reason}), {stop, {start_failed, Reason}, ER, State} end. +%% @private +wait_connect_reply(Ref, From, NewConfig, State) -> + receive + {Ref, ok} -> + gen_server:reply(From, ok), + {noreply, State#state{ref = Ref, config = NewConfig}}; + {Ref, {error, _Reason} = ER} -> + gen_server:reply(From, ER), + {noreply, State#state{ref = Ref}} + end. + +wait_halt_sta_reply( + Ref, From, #state{config = Config, port = Port, ref = Ref, sta_ip_info = IpInfo} = _State +) -> + receive + {Ref, ok} -> + gen_server:reply(From, ok), + NewState = #state{config = Config, port = Port, ref = Ref, sta_ip_info = IpInfo}, + {noreply, NewState}; + {Ref, {error, _Reason} = Error} -> + gen_server:reply(From, Error), + NewState = #state{config = Config, port = Port, ref = Ref, sta_ip_info = IpInfo}, + {noreply, NewState}; + Other -> + gen_server:reply(From, {unhandled_message, Other}), + NewState = #state{config = Config, port = Port, ref = Ref, sta_ip_info = IpInfo}, + {noreply, NewState} + end. + %% @hidden handle_cast(_Msg, State) -> {noreply, State}. %% @hidden -handle_info({Ref, sta_connected} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info({Ref, sta_connected} = _Msg, #state{cb_ref = Ref, config = Config} = State) -> maybe_sta_connected_callback(Config), {noreply, State}; handle_info({Ref, sta_beacon_timeout} = _Msg, #state{ref = Ref, config = Config} = State) -> maybe_sta_beacon_timeout_callback(Config), {noreply, State}; -handle_info({Ref, sta_disconnected} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info({Ref, sta_disconnected} = _Msg, #state{cb_ref = Ref, config = Config} = State) -> maybe_sta_disconnected_callback(Config), {noreply, State}; -handle_info({Ref, {sta_got_ip, IpInfo}} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info({Ref, {sta_got_ip, IpInfo}} = _Msg, #state{cb_ref = Ref, config = Config} = State) -> maybe_sta_got_ip_callback(Config, IpInfo), {noreply, State#state{sta_ip_info = IpInfo}}; -handle_info({Ref, ap_started} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info({Ref, ap_started} = _Msg, #state{cb_ref = Ref, config = Config} = State) -> maybe_ap_started_callback(Config), {noreply, State}; -handle_info({Ref, {ap_sta_connected, Mac}} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info({Ref, {ap_sta_connected, Mac}} = _Msg, #state{cb_ref = Ref, config = Config} = State) -> maybe_ap_sta_connected_callback(Config, Mac), {noreply, State}; -handle_info({Ref, {ap_sta_disconnected, Mac}} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info( + {Ref, {ap_sta_disconnected, Mac}} = _Msg, #state{cb_ref = Ref, config = Config} = State +) -> maybe_ap_sta_disconnected_callback(Config, Mac), {noreply, State}; handle_info( - {Ref, {ap_sta_ip_assigned, Address}} = _Msg, #state{ref = Ref, config = Config} = State + {Ref, {ap_sta_ip_assigned, Address}} = _Msg, #state{cb_ref = Ref, config = Config} = State ) -> maybe_ap_sta_ip_assigned_callback(Config, Address), {noreply, State}; -handle_info({Ref, {sntp_sync, TimeVal}} = _Msg, #state{ref = Ref, config = Config} = State) -> +handle_info({Ref, {sntp_sync, TimeVal}} = _Msg, #state{cb_ref = Ref, config = Config} = State) -> maybe_sntp_sync_callback(Config, TimeVal), {noreply, State}; handle_info(Msg, State) -> @@ -409,7 +524,9 @@ maybe_sta_beacon_timeout_callback(Config) -> %% @private maybe_sta_disconnected_callback(Config) -> - maybe_callback0(disconnected, proplists:get_value(sta, Config)). + maybe_callback0( + disconnected, proplists:get_value(sta, Config, fun sta_disconnected_default_callback/0) + ). %% @private maybe_sta_got_ip_callback(Config, IpInfo) -> @@ -461,6 +578,43 @@ maybe_callback1({Key, Arg} = Msg, Config) -> spawn(fun() -> Fun(Arg) end) end. +%% @private +update_config(OldConfig, NewConfig) -> + OldSta = proplists:get_value(sta, OldConfig), + case proplists:get_value(sta, NewConfig, undefined) of + [{ssid, SSID}, {psk, PSK}] -> + ok; + undefined -> + SSID = proplists:get_value(ssid, NewConfig), + PSK = proplists:get_value(psk, NewConfig) + end, + SntpConfig = proplists:get_value(sntp, NewConfig, proplists:get_value(sntp, OldConfig)), + ApConfig = proplists:get_value(ap, OldConfig), + Hostname = proplists:get_value( + dhcp_hostname, NewConfig, proplists:get_value(dhcp_hostname, OldConfig) + ), + case Hostname of + undefined -> + TempList0 = OldSta; + Name -> + TempList0 = lists:keyreplace(dhcp_hostname, 1, OldSta, {dhcp_hostname, Name}) + end, + TempList1 = lists:keyreplace(ssid, 1, TempList0, {ssid, SSID}), + StaConfig = lists:keyreplace(psk, 1, TempList1, {psk, PSK}), + case ApConfig of + undefined -> + case SntpConfig of + undefined -> UpdatedConfig = [{sta, StaConfig}]; + _ -> UpdatedConfig = [{sta, StaConfig}, {sntp, SntpConfig}] + end; + _ -> + case SntpConfig of + undefined -> UpdatedConfig = [{ap, ApConfig}, {sta, StaConfig}]; + _ -> UpdatedConfig = [{ap, ApConfig}, {sta, StaConfig}, {sntp, SntpConfig}] + end + end, + UpdatedConfig. + %% @private get_port() -> case whereis(network_port) of @@ -498,3 +652,7 @@ wait_for_ap_started(Timeout) -> after Timeout -> {error, timeout} end. + +%% @private +sta_disconnected_default_callback() -> + sta_connect(). diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index 46ee0dc48..7766156ee 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -68,6 +68,7 @@ static const char *const ap_sta_ip_assigned_atom = ATOM_STR("\x12", "ap_sta_ip_a static const char *const ap_started_atom = ATOM_STR("\xA", "ap_started"); static const char *const dhcp_hostname_atom = ATOM_STR("\xD", "dhcp_hostname"); static const char *const host_atom = ATOM_STR("\x4", "host"); +static const char *const managed_atom = ATOM_STR("\x7", "managed"); static const char *const max_connections_atom = ATOM_STR("\xF", "max_connections"); static const char *const psk_atom = ATOM_STR("\x3", "psk"); static const char *const sntp_atom = ATOM_STR("\x4", "sntp"); @@ -94,13 +95,17 @@ enum network_cmd // TODO add support for scan, ifconfig NetworkStartCmd, NetworkRssiCmd, - NetworkStopCmd + NetworkStopCmd, + StaHaltCmd, + StaConnectCmd }; static const AtomStringIntPair cmd_table[] = { - { ATOM_STR("\x5", "start"), NetworkStartCmd }, - { ATOM_STR("\x4", "rssi"), NetworkRssiCmd }, - { ATOM_STR("\x4", "stop"), NetworkStopCmd }, + { ATOM_STR("\x5", "start"), NetworkStartCmd }, + { ATOM_STR("\x4", "rssi"), NetworkRssiCmd }, + { ATOM_STR("\x4", "stop"), NetworkStopCmd }, + { ATOM_STR("\x8", "halt_sta"), StaHaltCmd }, + { ATOM_STR("\x7", "connect"), StaConnectCmd }, SELECT_INT_DEFAULT(NetworkInvalidCmd) }; @@ -284,7 +289,6 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_ case WIFI_EVENT_STA_DISCONNECTED: { ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED received."); - esp_wifi_connect(); send_sta_disconnected(data); break; } @@ -382,12 +386,20 @@ static wifi_config_t *get_sta_wifi_config(term sta_config, GlobalContext *global } term ssid_term = interop_kv_get_value(sta_config, ssid_atom, global); term pass_term = interop_kv_get_value(sta_config, psk_atom, global); + term managed_term = interop_kv_get_value(sta_config, managed_atom, global); + + bool roaming = false; + if ((!term_is_invalid_term(managed_term)) && (managed_term != FALSE_ATOM)) { + roaming = true; + } // // Check parameters // if (term_is_invalid_term(ssid_term)) { - ESP_LOGE(TAG, "get_sta_wifi_config: Missing SSID"); + if (roaming != true) { + ESP_LOGE(TAG, "get_sta_wifi_config: Missing SSID"); + } return NULL; } int ok = 0; @@ -636,9 +648,18 @@ static void start_network(Context *ctx, term pid, term ref, term config) return; } + bool roaming = false; wifi_config_t *sta_wifi_config = get_sta_wifi_config(sta_config, ctx->global); + if (IS_NULL_PTR(sta_wifi_config)) { + term managed = interop_kv_get_value(sta_config, managed_atom, ctx->global); + if (!term_invalid_term(managed)) { + if (managed != FALSE_ATOM) { + roaming = true; + } + } + } wifi_config_t *ap_wifi_config = get_ap_wifi_config(ap_config, ctx->global); - if (IS_NULL_PTR(sta_wifi_config) && IS_NULL_PTR(ap_wifi_config)) { + if ((roaming == false) && IS_NULL_PTR(sta_wifi_config) && IS_NULL_PTR(ap_wifi_config)) { ESP_LOGE(TAG, "Unable to get STA or AP configuration"); term error = port_create_error_tuple(ctx, BADARG_ATOM); port_send_reply(ctx, pid, ref, error); @@ -660,7 +681,7 @@ static void start_network(Context *ctx, term pid, term ref, term config) esp_err_t err; esp_netif_t *sta_wifi_interface = NULL; - if (sta_wifi_config != NULL) { + if ((sta_wifi_config != NULL) || (roaming == true)) { sta_wifi_interface = esp_netif_create_default_wifi_sta(); if (IS_NULL_PTR(sta_wifi_interface)) { ESP_LOGE(TAG, "Failed to create network STA interface"); @@ -723,13 +744,14 @@ static void start_network(Context *ctx, term pid, term ref, term config) // Set the wifi mode // wifi_mode_t wifi_mode = WIFI_MODE_NULL; - if (!IS_NULL_PTR(sta_wifi_config) && !IS_NULL_PTR(ap_wifi_config)) { + if ((!IS_NULL_PTR(sta_wifi_config) || (roaming == true)) && !IS_NULL_PTR(ap_wifi_config)) { wifi_mode = WIFI_MODE_APSTA; - } else if (!IS_NULL_PTR(sta_wifi_config)) { - wifi_mode = WIFI_MODE_STA; - } else { + } else if (!IS_NULL_PTR(ap_wifi_config)) { wifi_mode = WIFI_MODE_AP; + } else { + wifi_mode = WIFI_MODE_STA; } + if ((err = esp_wifi_set_mode(wifi_mode)) != ESP_OK) { ESP_LOGE(TAG, "Error setting wifi mode %d", err); term error = port_create_error_tuple(ctx, term_from_int(err)); @@ -867,6 +889,106 @@ static void get_sta_rssi(Context *ctx, term pid, term ref) port_send_reply(ctx, pid, ref, reply); } +static void sta_disconnect(Context *ctx, term pid, term ref) +{ + esp_err_t err = esp_wifi_disconnect(); + if (UNLIKELY(err != ESP_OK)) { + ESP_LOGE(TAG, "Error while disconnecting from AP (%i)", err); + term error = port_create_error_tuple(ctx, term_from_int(err)); + port_send_reply(ctx, pid, ref, error); + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void sta_connect(Context *ctx, term pid, term ref, term config) +{ + // + // Check wifi mode + // + wifi_mode_t mode; + esp_err_t err = esp_wifi_get_mode(&mode); + if ((err != ESP_OK) || ((mode != WIFI_MODE_STA) && (mode != WIFI_MODE_APSTA))) { + ESP_LOGE(TAG, "sta_connect: WiFi mode must be started in either STA mode or APSTA mode to use this function"); + term error = port_create_error_tuple(ctx, ERROR_ATOM); + port_send_reply(ctx, pid, ref, error); + return; + } + + // + // Get the STA config + // + term sta_config = interop_kv_get_value_default(config, sta_atom, term_invalid_term(), ctx->global); + if (UNLIKELY(term_is_invalid_term(sta_config))) { + // Also accept a proplist containing `ssid` and `psk` key/value tuples. + if ((interop_kv_get_value_default(config, ssid_atom, term_invalid_term(), ctx->global)) != (term_invalid_term())) { + sta_config = config; + } else { + ESP_LOGE(TAG, "Expected STA configuration but got none"); + term error = port_create_error_tuple(ctx, BADARG_ATOM); + port_send_reply(ctx, pid, ref, error); + return; + } + } + + wifi_config_t *sta_wifi_config = get_sta_wifi_config(sta_config, ctx->global); + if (IS_NULL_PTR(sta_wifi_config)) { + ESP_LOGE(TAG, "Unable to get STA configuration"); + term error = port_create_error_tuple(ctx, BADARG_ATOM); + port_send_reply(ctx, pid, ref, error); + return; + } + + // + // Set up STA mode + // + if ((err = esp_wifi_set_config(ESP_IF_WIFI_STA, sta_wifi_config)) != ESP_OK) { + ESP_LOGE(TAG, "Error setting STA mode config %d", err); + free(sta_wifi_config); + term error = port_create_error_tuple(ctx, term_from_int(err)); + port_send_reply(ctx, pid, ref, error); + return; + } else { + ESP_LOGD(TAG, "STA mode configured"); + free(sta_wifi_config); + } + + // + // Set the DHCP hostname + // + esp_netif_t *sta_interface = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + set_dhcp_hostname(sta_interface, "STA", interop_kv_get_value(sta_config, dhcp_hostname_atom, ctx->global)); + + if ((err = esp_wifi_connect()) != ESP_OK) { + ESP_LOGE(TAG, "Error while connecting: %d", err); + term error = port_create_error_tuple(ctx, term_from_int(err)); + port_send_reply(ctx, pid, ref, error); + return; + } else { + ESP_LOGI(TAG, "WiFi connection started."); + } + + // + // Set up simple NTP, if configured + // + maybe_set_sntp(interop_kv_get_value(config, sntp_atom, ctx->global), ctx->global); + + port_send_reply(ctx, pid, ref, OK_ATOM); +} + +static void sta_reconnect(Context *ctx, term pid, term ref) +{ + esp_err_t err = esp_wifi_connect(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Error while connecting: %d", err); + term error = port_create_error_tuple(ctx, term_from_int(err)); + port_send_reply(ctx, pid, ref, error); + return; + } else { + ESP_LOGI(TAG, "WiFi connection started."); + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + static NativeHandlerResult consume_mailbox(Context *ctx) { bool cmd_terminate = false; @@ -905,6 +1027,15 @@ static NativeHandlerResult consume_mailbox(Context *ctx) case NetworkStopCmd: cmd_terminate = true; stop_network(ctx); + case StaHaltCmd: + sta_disconnect(ctx, pid, ref); + break; + case StaConnectCmd: + if (term_is_invalid_term(config)) { + sta_reconnect(ctx, pid, ref); + } else { + sta_connect(ctx, pid, ref, config); + } break; default: {