diff --git a/examples/elixir/esp32/CMakeLists.txt b/examples/elixir/esp32/CMakeLists.txt index 9a15d37cd9..062c0e20eb 100644 --- a/examples/elixir/esp32/CMakeLists.txt +++ b/examples/elixir/esp32/CMakeLists.txt @@ -24,6 +24,7 @@ include(BuildElixir) pack_runnable(Blink Blink estdlib eavmlib exavmlib) pack_runnable(Ledc_x4 Ledc_x4 estdlib eavmlib exavmlib) +pack_runnable(WifiScan WifiScan estdlib eavmlib exavmlib) if(NOT (AVM_DISABLE_FP)) pack_runnable(SHT31 SHT31 estdlib eavmlib exavmlib) endif() diff --git a/examples/elixir/esp32/WifiScan.ex b/examples/elixir/esp32/WifiScan.ex new file mode 100644 index 0000000000..ef8e3ea543 --- /dev/null +++ b/examples/elixir/esp32/WifiScan.ex @@ -0,0 +1,147 @@ +# +# This file is part of AtomVM. +# +# Copyright 2025 AtomVM Contributors +# +# 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 +# + +defmodule WifiScan do + @moduledoc """ + Example demonstrating WiFi network scanning on ESP32. + + This example shows how to: + - Initialize the network driver in STA mode + - Scan for available WiFi networks + - Display information about discovered access points + """ + + def start() do + IO.puts("Starting WiFi Scan Example...") + + case scan_wifi() do + {:ok, access_points} -> + aps = + sort_by_rssi(access_points) + |> filter_hidden() + + IO.puts("\n=== Scan Results ===") + count = :erlang.length(aps) + IO.puts("Found #{count} visible network(s)\n") + + for ap <- aps do + print_access_point(ap) + end + + IO.puts("\nScan completed successfully!") + :ok + + {:error, reason} -> + IO.puts("Scan failed: #{inspect(reason)}") + {:error, reason} + end + end + + defp sort_by_rssi(access_points) do + # Sort by RSSI (signal strength) descending using lists:sort/2 + :lists.sort( + fn {_ssid1, rssi1, _ch1, _auth1, _bssid1}, {_ssid2, rssi2, _ch2, _auth2, _bssid2} -> + rssi1 >= rssi2 + end, + access_points + ) + end + + defp filter_hidden(sorted), + do: Enum.filter(sorted, &(elem(&1, 0) != "")) + + defp scan_wifi() do + # Initialize network in STA mode (required for scanning) + # We don't need to connect to a network, just initialize WiFi + config = [sta: [ssid: "", psk: ""]] + + IO.puts("Initializing WiFi driver...") + + case :network.start(config) do + {:ok, _pid} -> + IO.puts("WiFi driver initialized. Starting scan...") + # Give the WiFi driver a moment to initialize + Process.sleep(1000) + + perform_scan() + + {:error, reason} -> + IO.puts("Failed to start network driver: #{inspect(reason)}") + {:error, reason} + end + end + + defp perform_scan() do + try do + case :network.scan() do + {:ok, access_points} -> + IO.puts("Scan completed. Processing results...") + {:ok, access_points} + + {:error, reason} -> + IO.puts("Scan error: #{inspect(reason)}") + {:error, reason} + + other -> + IO.puts("Unexpected scan result: #{inspect(other)}") + {:error, :unexpected_result} + end + catch + kind, error -> + IO.puts("Exception during scan: #{inspect(kind)} - #{inspect(error)}") + {:error, {:exception, kind, error}} + end + end + + defp print_access_point(ap) do + case ap do + {ssid, rssi, _channel, _authmode, _bssid} -> + IO.puts(" ") + IO.puts(" SSID: " <> format_ssid(ssid)) + IO.puts(" RSSI: " <> format_signal_strength(rssi)) + + _ -> + IO.puts("N/A") + end + end + + defp format_ssid(ssid) when is_binary(ssid) do + if ssid == "" do + "" + else + # Just use the binary directly - io:format will handle it + ssid + end + end + + defp format_ssid(_), do: "" + + defp format_signal_strength(rssi) when is_integer(rssi) do + cond do + rssi >= -50 -> "[████] Excellent" + rssi >= -60 -> "[███ ] Good" + rssi >= -70 -> "[██ ] Fair" + rssi >= -80 -> "[█ ] Weak" + true -> "[ ] Very Weak" + end + end + + defp format_signal_strength(_), do: "Unknown" +end diff --git a/examples/erlang/esp32/CMakeLists.txt b/examples/erlang/esp32/CMakeLists.txt index f28afeb8fe..ecdfe03929 100644 --- a/examples/erlang/esp32/CMakeLists.txt +++ b/examples/erlang/esp32/CMakeLists.txt @@ -38,3 +38,4 @@ pack_runnable(reformat_nvs reformat_nvs eavmlib) pack_runnable(uartecho uartecho eavmlib estdlib) pack_runnable(ledc_example ledc_example eavmlib estdlib) pack_runnable(epmd_disterl epmd_disterl eavmlib estdlib) +pack_runnable(wifi_scan wifi_scan estdlib eavmlib) diff --git a/examples/erlang/esp32/wifi_scan.erl b/examples/erlang/esp32/wifi_scan.erl new file mode 100644 index 0000000000..1943388e08 --- /dev/null +++ b/examples/erlang/esp32/wifi_scan.erl @@ -0,0 +1,57 @@ +-module(wifi_scan). +-export([start/0]). + +start() -> + io:format("Starting WiFi Scan Example...~n"), + + %% Configure for STA mode. + %% We provide an empty SSID to initialize STA mode without connecting, + %% which allows us to perform a scan immediately. + Config = [ + {sta, [ + {ssid, ""}, + {psk, ""} + ]} + ], + + io:format("Starting network...~n"), + case network:start(Config) of + {ok, _Pid} -> + io:format("Network started. Waiting a bit...~n"), + timer:sleep(1000), %% Give it a moment to initialize + + io:format("Scanning for networks...~n"), + try network:scan() of + {ok, Results} -> + io:format("DEBUG: Got ok response from scan~n"), + try + Len = length(Results), + io:format("Scan complete. Found ~p networks.~n", [Len]), + % Just print the first one to be safe + case Results of + [First | _] -> + io:format("First network: ~p~n", [First]); + [] -> + io:format("No networks found~n") + end + catch + EC:EE:ES -> + io:format("Error processing results: ~p:~p~nStack: ~p~n", [EC, EE, ES]) + end; + {error, Reason} -> + io:format("Scan failed: ~p~n", [Reason]); + Other -> + io:format("Scan returned unexpected: ~p~n", [Other]) + catch + Class:Error:Stack -> + io:format("Scan crashed: ~p:~p~nStack: ~p~n", [Class, Error, Stack]) + end, + + io:format("Stopping network...~n"), + % network:stop(); + io:format("Done.~n"), + ok; + Error -> + io:format("Failed to start network: ~p~n", [Error]), + {error, Error} + end. diff --git a/libs/eavmlib/src/network.erl b/libs/eavmlib/src/network.erl index 8015671010..433437e612 100644 --- a/libs/eavmlib/src/network.erl +++ b/libs/eavmlib/src/network.erl @@ -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, + scan/0 ]). -export([start/1, start_link/1, stop/0]). -export([ @@ -158,7 +159,8 @@ port :: port(), ref :: reference(), sta_ip_info :: ip_info(), - mdns :: pid() | undefined + mdns :: pid() | undefined, + scan_from :: {pid(), term()} | undefined }). %%----------------------------------------------------------------------------- @@ -322,7 +324,7 @@ sta_rssi() -> init(Config) -> Port = get_port(), Ref = make_ref(), - {ok, #state{config = Config, port = Port, ref = Ref}, {continue, start_port}}. + {ok, #state{config = Config, port = Port, ref = Ref, scan_from = undefined}, {continue, start_port}}. handle_continue(start_port, #state{config = Config, port = Port, ref = Ref} = State) -> Port ! {self(), Ref, {start, Config}}, @@ -334,6 +336,19 @@ handle_continue(start_port, #state{config = Config, port = Port, ref = Ref} = St end. %% @hidden +handle_call(scan, From, #state{port = Port, ref = Ref} = State) -> + Port ! {self(), Ref, scan}, + receive + {Ref, ok} -> + %% Scan started successfully, store the caller and wait for results in handle_info + {noreply, State#state{scan_from = From}}; + {Ref, Error} -> + io:format("network:scan received initial error: ~p~n", [Error]), + {reply, Error, State} + after 5000 -> + io:format("ERROR: Timeout waiting for scan start~n"), + {reply, {error, timeout}, State} + end; handle_call(_Msg, _From, State) -> {reply, {error, unknown_message}, State}. @@ -373,6 +388,29 @@ handle_info( handle_info({Ref, {sntp_sync, TimeVal}} = _Msg, #state{ref = Ref, config = Config} = State) -> maybe_sntp_sync_callback(Config, TimeVal), {noreply, State}; +handle_info({Ref, {scan_results, Results}} = _Msg, #state{ref = Ref, scan_from = From} = State) -> + try + case From of + undefined -> + io:format("Received scan_results but no caller waiting~n"), + {noreply, State}; + _ -> + Reply = {ok, Results}, + gen_server:reply(From, Reply), + {noreply, State#state{scan_from = undefined}} + end + catch + Class:Error:Stacktrace -> + io:format("ERROR in handle_info scan_results: ~p:~p~nStacktrace: ~p~n", [Class, Error, Stacktrace]), + case From of + undefined -> ok; + _ -> + try gen_server:reply(From, {error, internal_error}) + catch _:_ -> io:format("Failed to send error reply~n") + end + end, + {noreply, State#state{scan_from = undefined}} + end; handle_info(Msg, State) -> io:format("Received spurious message ~p~n", [Msg]), {noreply, State}. @@ -398,6 +436,10 @@ wait_for_port_close(PortMonitor, Port) -> {error, timeout} end. +-spec scan() -> {ok, list()} | {error, Reason :: term()}. +scan() -> + gen_server:call(?SERVER, scan, infinity). + %% %% Internal operations %% diff --git a/libs/estdlib/src/code_server.erl b/libs/estdlib/src/code_server.erl index cad58aa626..dc2d51207b 100644 --- a/libs/estdlib/src/code_server.erl +++ b/libs/estdlib/src/code_server.erl @@ -204,7 +204,7 @@ load(Module) -> %% Currently, the only live stream backend that needs estimate is mmap %% and it should not be passed a value too large to not slow down valgrind too %% much during tests. A factor of 32 is more than enough, the largest observed -%% ration is 21 for aarch64 and Elixir code. Also apply a minimum of 128 kb +%% ratio is 21 for aarch64 and Elixir code. Also apply a minimum of 128 kb %% which shouldn't affect valgrind too much. %% jit_stream_flash and jit_stream_binary ignore the size parameter. %% @return size in bytes diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index cc209f96e4..18b46fd952 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -80,6 +80,7 @@ static const char *const sta_beacon_timeout_atom = ATOM_STR("\x12", "sta_beacon_ static const char *const sta_disconnected_atom = ATOM_STR("\x10", "sta_disconnected"); static const char *const sta_got_ip_atom = ATOM_STR("\xA", "sta_got_ip"); static const char *const network_down_atom = ATOM_STR("\x0C", "network_down"); +static const char *const scan_results_atom = ATOM_STR("\xC", "scan_results"); ESP_EVENT_DECLARE_BASE(sntp_event_base); ESP_EVENT_DEFINE_BASE(sntp_event_base); @@ -95,13 +96,15 @@ enum network_cmd // TODO add support for scan, ifconfig NetworkStartCmd, NetworkRssiCmd, - NetworkStopCmd + NetworkStopCmd, + NetworkScanCmd }; static const AtomStringIntPair cmd_table[] = { { ATOM_STR("\x5", "start"), NetworkStartCmd }, { ATOM_STR("\x4", "rssi"), NetworkRssiCmd }, { ATOM_STR("\x4", "stop"), NetworkStopCmd }, + { ATOM_STR("\x4", "scan"), NetworkScanCmd }, SELECT_INT_DEFAULT(NetworkInvalidCmd) }; @@ -257,6 +260,87 @@ static void send_sntp_sync(struct ClientData *data, struct timeval *tv) END_WITH_STACK_HEAP(heap, data->global); } +static void send_scan_results(struct ClientData *data) +{ + uint16_t ap_count = 0; + esp_wifi_scan_get_ap_num(&ap_count); + ESP_LOGI(TAG, "Scan found %d APs", ap_count); + + if (ap_count == 0) { + // Send empty list + BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE + TUPLE_SIZE(2), heap); + { + term reply_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(reply_tuple, 0, make_atom(data->global, scan_results_atom)); + term_put_tuple_element(reply_tuple, 1, term_nil()); + send_term(&heap, data, reply_tuple); + } + END_WITH_STACK_HEAP(heap, data->global); + return; + } + + wifi_ap_record_t *ap_list = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * ap_count); + if (IS_NULL_PTR(ap_list)) { + ESP_LOGE(TAG, "Failed to allocate memory for scan results"); + return; + } + + // Get the actual records. ap_count might be updated if fewer records are returned. + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_count, ap_list)); + ESP_LOGI(TAG, "Processing %d AP records", ap_count); + + size_t heap_size = PORT_REPLY_SIZE + TUPLE_SIZE(2); + for (int i = 0; i < ap_count; i++) { + heap_size += CONS_SIZE; + heap_size += TUPLE_SIZE(5); + // Ensure we don't read past 32 bytes for SSID + size_t ssid_len = strnlen((char *)ap_list[i].ssid, 32); + heap_size += TERM_BINARY_HEAP_SIZE(ssid_len); + heap_size += TERM_BINARY_HEAP_SIZE(6); + } + + ESP_LOGI(TAG, "Allocating heap size: %d", heap_size); + + // Add some padding to be safe + heap_size += 64; + + Heap heap; + if (UNLIKELY(memory_init_heap(&heap, heap_size) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Unable to allocate heap space for scan results"); + free(ap_list); + return; + } + + term list = term_nil(); + for (int i = ap_count - 1; i >= 0; i--) { + size_t ssid_len = strnlen((char *)ap_list[i].ssid, 32); + term ssid_term = term_from_literal_binary(ap_list[i].ssid, ssid_len, &heap, data->global); + term rssi_term = term_from_int(ap_list[i].rssi); + term channel_term = term_from_int(ap_list[i].primary); + term authmode_term = term_from_int(ap_list[i].authmode); + term bssid_term = term_from_literal_binary(ap_list[i].bssid, 6, &heap, data->global); + + term tuple = term_alloc_tuple(5, &heap); + term_put_tuple_element(tuple, 0, ssid_term); + term_put_tuple_element(tuple, 1, rssi_term); + term_put_tuple_element(tuple, 2, channel_term); + term_put_tuple_element(tuple, 3, authmode_term); + term_put_tuple_element(tuple, 4, bssid_term); + + list = term_list_prepend(tuple, list, &heap); + } + free(ap_list); + + term reply_tuple = term_alloc_tuple(2, &heap); + term_put_tuple_element(reply_tuple, 0, make_atom(data->global, scan_results_atom)); + term_put_tuple_element(reply_tuple, 1, list); + + send_term(&heap, data, reply_tuple); + ESP_LOGI(TAG, "Scan results sent"); + + memory_destroy_heap(&heap, data->global); +} + #define UNLIKELY_NOT_ESP_OK(E) UNLIKELY((E) != ESP_OK) // @@ -324,6 +408,12 @@ static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_ break; } + case WIFI_EVENT_SCAN_DONE: { + ESP_LOGI(TAG, "WIFI_EVENT_SCAN_DONE received."); + send_scan_results(data); + break; + } + default: ESP_LOGI(TAG, "Unhandled wifi event: %" PRIi32 ".", event_id); break; @@ -879,6 +969,37 @@ static void get_sta_rssi(Context *ctx, term pid, term ref) port_send_reply(ctx, pid, ref, reply); } +static void scan_network(Context *ctx, term pid, term ref) +{ + wifi_mode_t mode; + esp_err_t err = esp_wifi_get_mode(&mode); + if (err != ESP_OK) { + term error = port_create_error_tuple(ctx, term_from_int(err)); + port_send_reply(ctx, pid, ref, error); + return; + } + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = true + }; + err = esp_wifi_scan_start(&scan_config, false); + if (err != ESP_OK) { + term error = port_create_error_tuple(ctx, term_from_int(err)); + port_send_reply(ctx, pid, ref, error); + return; + } + + size_t heap_size = PORT_REPLY_SIZE; + if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { + ESP_LOGE(TAG, "Unable to allocate heap space for scan_network reply"); + return; + } + port_send_reply(ctx, pid, ref, OK_ATOM); +} + static NativeHandlerResult consume_mailbox(Context *ctx) { bool cmd_terminate = false; @@ -914,6 +1035,9 @@ static NativeHandlerResult consume_mailbox(Context *ctx) case NetworkRssiCmd: get_sta_rssi(ctx, pid, ref); break; + case NetworkScanCmd: + scan_network(ctx, pid, ref); + break; case NetworkStopCmd: cmd_terminate = true; stop_network(ctx);