Conversation
📝 WalkthroughWalkthroughAdds ESP32 Wi‑Fi scan support (sync, callback, and pid delivery) across driver, Erlang API, docs, examples, and tests; fixes AP↔STA event payload casting; and renames RSSI type from Changes
Sequence DiagramsequenceDiagram
participant Client as Erlang Client
participant GenServer as network gen_server
participant Driver as ESP32 Driver (C)
participant WiFi as ESP32 WiFi HAL
Client->>GenServer: wifi_scan(Options) / wifi_scan()
GenServer->>GenServer: validate mode/options, enforce single-scan or callback/pid
GenServer->>Driver: mailbox {scan, ParsedOptions, Ref}
Driver->>WiFi: esp_wifi_scan_start()
Driver->>Driver: register per-scan handler (WIFI_EVENT_SCAN_DONE)
WiFi->>Driver: WIFI_EVENT_SCAN_DONE
Driver->>Driver: collect AP records, build Erlang maps
Driver->>GenServer: {scan_results, Ref, {Count, ApList}} or {scan_canceled,...} / {error, Reason}
alt blocking caller
GenServer->>Client: reply {ok, {Count, ApList}} / {error, Reason}
else callback or pid delivery
GenServer->>GenServer: invoke configured callback or send to pid
else cancel/error
GenServer->>Client: reply/callback {scan_canceled, ...} or error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
libs/eavmlib/src/network.erl (2)
233-235: Incorrect type forscan_receiverfield.The field stores the
Fromargument fromgen_server:call, which is a{pid(), Tag}tuple, not a plainpid(). This should be reflected in the type for accuracy.Proposed fix
sta_state :: sta_status(), - scan_receiver :: pid() + scan_receiver :: {pid(), term()} | undefined }).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 233 - 235, The type for the record field scan_receiver is wrong: it stores the gen_server:call From value which is a {Pid, Tag} tuple, not a plain pid(); update the typespec to reflect this (e.g. change scan_receiver :: pid() to scan_receiver :: {pid(), reference()} or scan_receiver :: {pid(), tag()} and add a -type tag() :: reference() if you prefer a named alias) so the record and any callers reflect the actual {pid(), Tag} shape returned by gen_server:call.
516-522: Missing-specforwifi_scan/0.The function
wifi_scan/1has a spec (lines 501-503), butwifi_scan/0lacks one. Add a spec for consistency and documentation.Proposed fix
+%% `@doc` Equivalent to wifi_scan/1 with default options from sta_config(). +%% `@end` +-spec wifi_scan() -> + {ok, {NetworksDiscovered :: 0..20, [{SSID :: string(), [ApInfo :: network_properties()]}, ...]}} + | {error, Reason :: term()}. wifi_scan() -> Config = gen_server:call(?SERVER, get_config),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 516 - 522, Add a -spec for wifi_scan/0 to match the documented return type used by wifi_scan/1: place a -spec wifi_scan() -> <same_type_as_wifi_scan/1_spec> above the wifi_scan() function so the zero-arity clause is documented consistently with wifi_scan/1 (i.e., mirror the exact type expression used in the existing -spec for wifi_scan/1).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@doc/src/network-programming-guide.md`:
- Around line 157-186: The docs contain typos and an incorrect API name and
tuple syntax: fix the malformed return tuple example to use {ok, {Num,
Networks}} semantics consistently, correct "discoverd" → "discovered" and other
typos, and replace any mention of network:disconnect/0 with the correct API
network:sta_disconnect/0; also ensure references to network:wifi_scan/0 and
network:wifi_scan/1, network:scan_options(), and the sta_config() option text
match the actual type names and phrasing used elsewhere (e.g., "FoundAPs may be
larger than length of Networks" → clearer wording) so the prose and examples are
consistent with the real function signatures and return shapes.
In `@examples/erlang/esp32/wifi_scan.erl`:
- Around line 41-47: The current anonymous function in wifi_scan.erl matches a
tuple/proplist result ({SSID, [{rssi, DBm}, ...]}) but the scan payload now uses
maps; update the pattern in the anonymous function (and the similar block at
lines 63-69) to match a map shape such as #{ssid := SSID, rssi := DBm, authmode
:= Mode, bssid := BSSID, channel := Number} and keep the same formatting call
(using binary:encode_hex(BSSID) if BSSID is a binary) so the output logic
(io:format(..., [SSID, binary:encode_hex(BSSID), DBm, Mode, Number])) continues
to work with the new map-based scan result.
- Line 55: The current assignment uses lists:flatten/1 unnecessarily: change the
construction of Opts so it prepends the atom passive directly to the existing
flat list Config (remove lists:flatten/1). Locate the expression building Opts
(the variable Opts in wifi_scan.erl) and replace the lists:flatten([passive |
Config]) usage with a direct list cons of passive onto Config (i.e., construct
Opts by prepending passive to Config).
In `@libs/eavmlib/src/network.erl`:
- Around line 45-48: The comment above the macros (e.g., ?DEVICE_TOTAL_CHANNELS,
?GEN_RESPONSE_MS, ?MAX_SHORT_DWELL) contains a typo: change "ate used to
calculate the gen_server:call/3 timeout." to "are used to calculate the
gen_server:call/3 timeout." so the comment reads correctly; update only the
comment text near those macro definitions in network.erl.
- Around line 222-225: The type network_properties() is incorrectly defined as
nested lists while the docstring describes a flat proplist; update the type spec
for network_properties() to be a list of property tuples (i.e., each element is
one of {rssi, dbm()}, {authmode, auth_type()}, {bssid, bssid_t()} or {channel,
wifi_channel()}) so it matches the documented proplist structure and functions
that consume it.
- Around line 576-585: The handler creates a fresh Ref with make_ref() and sends
it to the port, but handle_info later matches against the state's original ref
(State#state.ref), causing replies to be ignored; fix by using the state's
existing ref (e.g., Ref = State#state.ref) when sending the scan request to
network_port instead of make_ref(), and when cancelling an in-flight scan rename
the variable (e.g., From0 or PrevFrom) to reflect that scan_receiver holds a
gen_server From tuple and call gen_server:reply(PrevFrom, {error, canceled})
only when PrevFrom is a From-tuple; finally set scan_receiver = From in the
returned state. Ensure you update the symbols in handle_call({scan, ScanOpts},
From, `#state`{scan_receiver = Scanning} = State) and the send to network_port so
they use State#state.ref and the proper From tuple handling so handle_info's
pattern match on `#state`{ref = Ref} will succeed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1255-1274: The handler reads and validates the results option
(interop_kv_get_value_default -> cfg_results -> num_results) but never enforces
it when assembling the scan reply, causing all APs to be returned; update the
scan result assembly/response code (where the list of found APs is collected and
port_send_reply is prepared) to truncate or only include up to num_results
(bounded by MAX_SCAN_RESULTS) before sending the reply so the response honors
the requested limit and memory usage is constrained.
- Around line 62-71: The heap-size calculation underestimates SSID capacity
because SSID_MAX_SIZE is defined as 16 while ESP32 SSIDs can be up to 32 bytes
(plus NUL), causing results_size and related allocations to be too small; update
SSID_MAX_SIZE to 33 (32 chars + NUL) and adjust any heap/result-size
calculations that use SSID_MAX_SIZE (e.g., the calculations that produce
results_size) so they account for the larger SSID buffer, and apply the same
change for the duplicated definitions referenced around lines 446-449 to keep
all allocations consistent.
- Around line 1310-1313: The code incorrectly calls
term_invalid_term(term_hidden) even though term_invalid_term takes no arguments;
change the check to compare term_hidden against the sentinel by using
term_invalid_term() (e.g., if ((term_hidden != term_invalid_term()) &&
(term_hidden == TRUE_ATOM)) {...}) so the logic around
interop_kv_get_value_default, the term_hidden variable, and the TRUE_ATOM
comparison remains correct and show_hidden is set appropriately; locate this in
the block using interop_kv_get_value_default, term_hidden, show_hidden, and
ctx->global.
- Around line 428-441: The error path after esp_wifi_scan_get_ap_records uses
sizeof(error) (pointer size) and leaks ap_records; change the call to
term_from_string to use strlen(error) (or strlen(error)+1 if null-termination is
required by term_from_string) instead of sizeof(error), and ensure ap_records
(allocated by calloc) is freed before returning from this error branch; update
the error-handling block around esp_err_to_name, esp_wifi_clear_ap_list,
term_from_string, send_term to free(ap_records) and then return.
- Around line 1325-1357: Check the return value of
esp_wifi_scan_start(scan_config, true): if it fails, free the allocated
wifi_scan_config_t (scan_config), log the error, build an error term with
port_create_error_tuple(ctx, <appropriate error atom>), and send it back with
port_send_reply(ctx, pid, ref, error); also ensure scan_config is freed after a
successful start (or otherwise not leaked) so the allocation is always released.
Use the existing symbols scan_config, esp_wifi_scan_start,
port_create_error_tuple, port_send_reply, pid, ref and ctx to locate and
implement the change.
---
Nitpick comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 233-235: The type for the record field scan_receiver is wrong: it
stores the gen_server:call From value which is a {Pid, Tag} tuple, not a plain
pid(); update the typespec to reflect this (e.g. change scan_receiver :: pid()
to scan_receiver :: {pid(), reference()} or scan_receiver :: {pid(), tag()} and
add a -type tag() :: reference() if you prefer a named alias) so the record and
any callers reflect the actual {pid(), Tag} shape returned by gen_server:call.
- Around line 516-522: Add a -spec for wifi_scan/0 to match the documented
return type used by wifi_scan/1: place a -spec wifi_scan() ->
<same_type_as_wifi_scan/1_spec> above the wifi_scan() function so the zero-arity
clause is documented consistently with wifi_scan/1 (i.e., mirror the exact type
expression used in the existing -spec for wifi_scan/1).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
.github/workflows/esp32-build.yamlCHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
💤 Files with no reviewable changes (1)
- .github/workflows/esp32-build.yaml
e288558 to
c97b4f4
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
src/platforms/esp32/components/avm_builtins/network_driver.c (1)
439-457:⚠️ Potential issue | 🔴 Critical
ap_recordsleaks on scan result error path.
ap_recordsis allocated at Line 426, but in theesp_wifi_scan_get_ap_recordsfailure branch (Line 440-457) it is never freed before returning.♻️ Suggested fix
if (UNLIKELY_NOT_ESP_OK(err)) { // the ap_list must be cleared on failures to prevent a memory leak esp_wifi_clear_ap_list(); ESP_LOGE(TAG, "Failed to obtain scan results"); const char *error = esp_err_to_name(err); @@ port_send_message_from_task(data->global, term_from_local_process_id(data->owner_process_id), msg); END_WITH_STACK_HEAP(heap, data->global); + free(ap_records); free(data); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 439 - 457, The failure branch after calling esp_wifi_scan_get_ap_records leaks the dynamically allocated ap_records; before sending the error reply and returning in that branch (the block handling UNLIKELY_NOT_ESP_OK(err)), free ap_records (the buffer allocated earlier) immediately after calling esp_wifi_clear_ap_list() and before building/sending the error message and calling free(data). Ensure you reference and free the same pointer variable ap_records so no double-free occurs elsewhere and preserve existing calls to esp_wifi_clear_ap_list(), port_send_message_from_task, and free(data).doc/src/network-programming-guide.md (1)
157-157:⚠️ Potential issue | 🟡 Minor
wifi_scandocs still have correctness typos and a broken anchor.
Line 157 still linkswifi_scan/0to#wifi_scan1, and the tuple example text in that paragraph remains malformed/typoed (discoverd). Line 180 also still says “using and ESP32-C6”. These should be corrected to avoid misleading users.📝 Suggested doc fix
-After the network has been configured for STA mode and started, as long as no connection has been initiated or associated, you may scan for available access points using [`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 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 discoverd than the number of results requested. +After the network has been configured for STA mode and started, as long as no connection has been initiated or associated, 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 120 ms will be used and will return network details for up to 6 access points. The return value for the scan takes the form `{ok, Results}`, where `Results = {FoundAPs, NetworkList}`. `FoundAPs` may be larger than the length of `NetworkList` if more access points were discovered than the number of results requested. @@ -For example to do a passive scan using and ESP32-C6, including hidden networks, using the longest allowed scan time and showing the maximum number of networks available use the following: +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:Also applies to: 180-180
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 157, Update the wifi_scan docs to point each function link to its correct anchor (use the correct anchors for wifi_scan/0 and wifi_scan/1 instead of both linking to `#wifi_scan1`), fix the typo "discoverd" → "discovered" in the tuple description, and correct the phrasing "using and ESP32-C6" → "using an ESP32-C6"; locate and change the links and text around the wifi_scan references (wifi_scan/0, wifi_scan/1) and the tuple paragraph to apply these corrections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 224-226: The spec/types for wifi_scan and network_properties() are
inconsistent with the actual payload (code and tests expect maps like #{ssid :=
..., rssi := ...}), so update the typespecs to describe the canonical map shape
instead of tuple lists: modify network_properties() and the wifi_scan type/spec
to reflect a map-based entry with keys such as ssid(), rssi/dbm(),
authmode/auth_type(), bssid/bssid_t(), channel/wifi_channel() and ensure the
wifi_scan() return spec references a list of these maps (so functions like
wifi_scan/0 and consumers like test_wifi_scan.erl match the spec); also apply
the same change for the other duplicated spec locations mentioned (lines
~509-511).
- Around line 642-644: The handle_info clause for handle_info({Ref,
{scan_results, Results}} = _Msg, `#state`{ref = Ref, scan_receiver = From} =
State) currently always replies gen_server:reply(From, {ok, Results}) which
wraps errors as success; change the logic in handle_info (or add
pattern-matching clauses) to detect when Results is an error tuple like {error,
Reason} and reply gen_server:reply(From, {error, Reason}) (otherwise reply {ok,
Results}), preserving the use of State#state{scan_receiver = undefined} and
matching on Ref and scan_receiver so callers receive the correct {ok, ...} or
{error, ...} per the declared API.
- Around line 531-536: The current wifi_scan/0 implementation chains
proplists:get_value(sta, Config) directly and will crash if the sta entry is
missing; first bind Sta = proplists:get_value(sta, Config, []) (or {}), then
derive Results = proplists:get_value(default_scan_results, Sta, 6), Dwell =
proplists:get_value(scan_dwell_ms, Sta, 120), Hidden =
proplists:get_value(scan_show_hidden, Sta, false), Passive =
proplists:get_value(scan_passive, Sta, false) and pass those into wifi_scan/1;
update the code around Config, Results, Dwell, Hidden, Passive in wifi_scan/0 to
use the safe Sta variable with defaults so missing sta does not cause a crash.
- Around line 590-598: In handle_call({scan, ScanOpts}, From, State) the code
mistakenly compares the stored scan_receiver against the incoming From by
binding Scanning to From, so previous callers aren't replied to; change the
logic to detect if State#state.scan_receiver is a previous caller (e.g.,
Scanning = {PrevPid, _Tag}) and, if Scanning =/= From, call
gen_server:reply(Scanning, {error, canceled}) to cancel the earlier requester
before sending the new scan to network_port and updating scan_receiver; update
references in handle_call, scan_receiver, Scanning, From and ensure you still
send network_port ! {self(), Ref, {scan, ScanOpts}} and set
State#state{scan_receiver = From}.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1462-1475: There is a duplicate call to esp_wifi_scan_start which
triggers the scan twice and overwrites the error handling; remove the first
esp_wifi_scan_start(scan_config, true) so that the function only calls
esp_wifi_scan_start once, store its return in err, then free(scan_config) and
run the existing error branch (using esp_err_to_name, port_ensure_available,
term_from_string, port_create_error_tuple, port_create_tuple2, port_send_reply)
to ensure callback/scan state is only cleaned up on the single failure path and
no scan callback state is leaked.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Line 157: Update the wifi_scan docs to point each function link to its correct
anchor (use the correct anchors for wifi_scan/0 and wifi_scan/1 instead of both
linking to `#wifi_scan1`), fix the typo "discoverd" → "discovered" in the tuple
description, and correct the phrasing "using and ESP32-C6" → "using an
ESP32-C6"; locate and change the links and text around the wifi_scan references
(wifi_scan/0, wifi_scan/1) and the tuple paragraph to apply these corrections.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 439-457: The failure branch after calling
esp_wifi_scan_get_ap_records leaks the dynamically allocated ap_records; before
sending the error reply and returning in that branch (the block handling
UNLIKELY_NOT_ESP_OK(err)), free ap_records (the buffer allocated earlier)
immediately after calling esp_wifi_clear_ap_list() and before building/sending
the error message and calling free(data). Ensure you reference and free the same
pointer variable ap_records so no double-free occurs elsewhere and preserve
existing calls to esp_wifi_clear_ap_list(), port_send_message_from_task, and
free(data).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 96d38fea-e6a3-436d-a3b6-95fef7c6de7d
📒 Files selected for processing (10)
.github/workflows/esp32-build.yamlCHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
💤 Files with no reviewable changes (1)
- .github/workflows/esp32-build.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
- examples/erlang/esp32/wifi_scan.erl
- examples/erlang/esp32/CMakeLists.txt
- src/platforms/esp32/test/main/test_main.c
There was a problem hiding this comment.
♻️ Duplicate comments (7)
libs/eavmlib/src/network.erl (4)
642-644:⚠️ Potential issue | 🟠 MajorScan errors are returned as
{ok, {error, Reason}}.This clause always wraps result as success; if the driver sends
{scan_results, {error, Reason}}, API contract is violated.Proposed fix
+handle_info({Ref, {scan_results, {error, Reason}}}, `#state`{ref = Ref, scan_receiver = From} = State) -> + gen_server:reply(From, {error, Reason}), + {noreply, State#state{scan_receiver = undefined}}; handle_info({Ref, {scan_results, Results}} = _Msg, `#state`{ref = Ref, scan_receiver = From} = State) -> gen_server:reply(From, {ok, Results}), {noreply, State#state{scan_receiver = undefined}};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 642 - 644, The handle_info clause for handle_info({Ref, {scan_results, Results}} = _Msg, `#state`{ref = Ref, scan_receiver = From} = State) always replies gen_server:reply(From, {ok, Results}) which masks driver errors sent as {scan_results, {error, Reason}}; change the reply logic to inspect Results and reply either {ok, Results} for normal results or propagate the error tuple (e.g., {error, Reason}) unchanged — update the handle_info clause that references `#state.ref` and `#state.scan_receiver` and the gen_server:reply call so errors are not wrapped in {ok, ...} and scan_receiver is still cleared (scan_receiver = undefined).
224-226:⚠️ Potential issue | 🟠 Major
wifi_scantypespecs do not match the actual map-based payload.
network_properties()and bothwifi_scanspecs still describe tuple/proplist entries, but scan consumers and the driver use map entries (#{ssid := ..., rssi := ...}), causing incorrect contracts for callers and dialyzer.Proposed fix
--type network_properties() :: [ - {rssi, dbm()} | {authmode, auth_type()} | {bssid, bssid_t()} | {channel, wifi_channel()} -]. +-type scanned_network() :: #{ + ssid := string(), + rssi := dbm(), + authmode := auth_type(), + bssid := bssid_t(), + channel := wifi_channel() +}. --spec wifi_scan([Options :: scan_options(), ...]) -> - {ok, {NetworksDiscovered :: 0..20, [{SSID :: string(), [ApInfo :: network_properties()]}, ...]}} +-spec wifi_scan([Options :: scan_options(), ...]) -> + {ok, {NetworksDiscovered :: 0..20, [scanned_network()]}} | {error, Reason :: term()}. --spec wifi_scan() -> - {ok, {NetworksDiscovered :: 0..20, [{SSID :: string(), [ApInfo :: network_properties()]}, ...]}} +-spec wifi_scan() -> + {ok, {NetworksDiscovered :: 0..20, [scanned_network()]}} | {error, Reason :: term()}.Also applies to: 509-511, 527-529
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 224 - 226, The current typespecs network_properties() and the wifi_scan specs (used in functions handling wifi_scan payloads) describe list/tuple/proplist entries but the runtime payloads and driver use maps like #{ssid := ..., rssi := ...}; update the typespecs to reflect map-based structures (e.g., a map type with keys such as ssid, rssi, authmode, bssid, channel) and adjust any related wifi_scan type aliases to match map form so Dialyzer and callers see the correct contract (update all occurrences including the other instances noted around the file).
590-594:⚠️ Potential issue | 🟠 MajorPrevious scan requester is not being canceled.
The branch binds
Scanningto the incomingFrom(= From), so prior waiting callers are usually never replied{error, canceled}.Proposed fix
handle_call({scan, ScanOpts}, From, `#state`{scan_receiver = Scanning, ref = Ref} = State) -> case Scanning of - {Pid, _Tag} = From when is_pid(Pid) -> - gen_server:reply(From, {error, canceled}); + {Pid, _Tag} = PrevFrom when is_pid(Pid), PrevFrom =/= From -> + gen_server:reply(PrevFrom, {error, canceled}); _ -> ok end, network_port ! {self(), Ref, {scan, ScanOpts}}, {noreply, State#state{scan_receiver = From}};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 590 - 594, The branch in handle_call/3 erroneously binds Scanning to the incoming From, so the previous requester stored in the state never gets canceled; change the case to detect when scan_receiver (Scanning) contains a prior From (e.g., a tuple like {PrevFromPid,Tag} or simply PrevFrom) that is different from the incoming From and call gen_server:reply(PrevFrom, {error,canceled}) to notify the previous caller, leaving the other branch for no-op; update the pattern matching in handle_call to compare Scanning against From (use a variable like PrevFrom for the stored value) rather than using "= From".
531-536:⚠️ Potential issue | 🟠 Major
wifi_scan/0can crash when STA config is missing.
proplists:get_value(sta, Config)can beundefined; passing that into subsequentproplists:get_value/3calls can fail instead of returning a clean error/default flow.Proposed fix
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), + StaCfg = proplists:get_value(sta, Config, []), + Results = proplists:get_value(default_scan_results, StaCfg, 6), + Dwell = proplists:get_value(scan_dwell_ms, StaCfg, 120), + Hidden = proplists:get_value(scan_show_hidden, StaCfg, false), + Passive = proplists:get_value(scan_passive, StaCfg, false), wifi_scan([{results, Results}, {dwell, Dwell}, {show_hidden, Hidden}, {passive, Passive}]).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 531 - 536, The current wifi_scan/0 uses proplists:get_value(sta, Config) without a default so StaConfig can be undefined and make subsequent proplists:get_value/3 calls crash; fix by first extracting a safe StaConfig (e.g., StaConfig = proplists:get_value(sta, Config, []) or an empty proplist) and then call proplists:get_value(default_scan_results, StaConfig, 6), proplists:get_value(scan_dwell_ms, StaConfig, 120), proplists:get_value(scan_show_hidden, StaConfig, false), and proplists:get_value(scan_passive, StaConfig, false) before invoking wifi_scan([{results, Results}, {dwell, Dwell}, {show_hidden, Hidden}, {passive, Passive}]).src/platforms/esp32/components/avm_builtins/network_driver.c (2)
1462-1475:⚠️ Potential issue | 🔴 CriticalDuplicate
esp_wifi_scan_startcall breaks scan lifecycle and error handling.The scan is started twice back-to-back; this can produce false error handling and inconsistent callback ownership/cleanup.
Proposed fix
- esp_wifi_scan_start(scan_config, true); - err = esp_wifi_scan_start(scan_config, true); + err = esp_wifi_scan_start(scan_config, true); free(scan_config); if (UNLIKELY(err != ESP_OK)) { const char *err_str = esp_err_to_name(err); size_t error_len = strlen(err_str); port_ensure_available(ctx, error_size + error_len); term reason = term_from_string((const uint8_t *) err_str, (uint16_t) error_len, &ctx->heap); term error = port_create_error_tuple(ctx, reason); term scan_results_atom = make_atom(ctx->global, ATOM_STR("\xc", "scan_results")); term ret = port_create_tuple2(ctx, scan_results_atom, error); port_send_reply(ctx, pid, ref, ret); + esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); + free(data); } return;#!/bin/bash # Verify duplicate start call and nearby failure cleanup in wifi_scan. rg -n -C3 'esp_wifi_scan_start\s*\(scan_config,\s*true\)' src/platforms/esp32/components/avm_builtins/network_driver.c rg -n -C8 'if \(UNLIKELY\(err != ESP_OK\)\)' src/platforms/esp32/components/avm_builtins/network_driver.c🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1462 - 1475, The code calls esp_wifi_scan_start(scan_config, true) twice which breaks the scan lifecycle; remove the first redundant call so esp_wifi_scan_start is invoked only once with its return stored in err, keep the subsequent free(scan_config) and the existing error branch that uses err/err_str/port_ensure_available/term_from_string/port_create_error_tuple/port_send_reply unchanged, and verify no additional cleanup is needed on success (ensure scan_config is freed and ownership/callbacks remain correct).
440-456:⚠️ Potential issue | 🟠 Major
ap_recordsleaks onesp_wifi_scan_get_ap_recordsfailure path.The branch frees
databut notap_records, so repeated scan failures leak heap memory.Proposed fix
if (UNLIKELY_NOT_ESP_OK(err)) { // the ap_list must be cleared on failures to prevent a memory leak esp_wifi_clear_ap_list(); ESP_LOGE(TAG, "Failed to obtain scan results"); const char *error = esp_err_to_name(err); size_t error_len = strlen(error); @@ port_send_message_from_task(data->global, term_from_local_process_id(data->owner_process_id), msg); END_WITH_STACK_HEAP(heap, data->global); + free(ap_records); free(data); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 440 - 456, The failure branch after esp_wifi_scan_get_ap_records leaks the ap_records buffer; before returning in the UNLIKELY_NOT_ESP_OK(err) block free the ap_records pointer (if non-NULL) in addition to freeing data and sending the error reply. Locate the error-handling block that calls esp_wifi_clear_ap_list(), builds the error_reply (using term_from_string, port_heap_create_tuple2, port_send_message_from_task) and add a safe free(ap_records) immediately prior to free(data) and return so the allocated AP list is released on failure.doc/src/network-programming-guide.md (1)
157-186:⚠️ Potential issue | 🟡 MinorWiFi scan docs still contain contract mismatches and broken references.
This section mixes tuple/proplist wording with map-based examples, has malformed return syntax, and links
wifi_scan/0to thewifi_scan/1anchor.Proposed fix
-... using [`network:wifi_scan/0`](./apidocs/erlang/eavmlib/network.md#wifi_scan1) or ... +... using [`network:wifi_scan/0`](./apidocs/erlang/eavmlib/network.md#wifi_scan0) or ... -... where `Results = {FoundAPs [NetworkList]}`. +... where `Results = {FoundAPs, NetworkList}`. -... if more access points were discoverd ... +... if more access points were discovered ... -... entries in the `NetworkList` take the form of `{SSID, [AP_Properties]}` ... +... entries in `NetworkList` are maps like `#{ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number}` ... -For example to do a passive scan using and ESP32-C6 ... +For example, to do a passive scan using an ESP32-C6 ...🤖 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 157 - 186, The docs mix proplist wording with map-based examples, have malformed Erlang syntax, and a wrong anchor for wifi_scan; update the text to consistently describe the scan return as {ok, {Num, NetworkList}} where Num is integer and NetworkList is a list of maps (each map like #{ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number}), fix the example Erlang to valid pattern-matching and iteration (use {ok, {Num, Networks}} = network:wifi_scan(Config) and lists:foreach(fun(Network) -> ... end, Networks) or pattern-match inside the fun), replace any "proplist" wording with "options proplist" only when referring to scan options, correct the link anchors so network:wifi_scan/0 links to the /wifi_scan0 anchor and network:wifi_scan/1 links to the /wifi_scan1 anchor, and ensure references to network:scan_options(), sta_config(), and network:sta_disconnect/0 remain accurate and consistent with the map-based result description.
🧹 Nitpick comments (1)
libs/eavmlib/src/network.erl (1)
917-919: Remove debug print fromget_device_total_channels/0default path.The
io:format/2call introduces noisy stdout side effects during normal scans.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 917 - 919, Remove the noisy debug printing in the default branch of get_device_total_channels/0: locate the clause that binds Debug and currently calls io:format("DEBUG: chip info -> ~p", [Debug]) and delete that io:format call (or guard it behind an explicit debug flag if logging is required), leaving the return value {?DEVICE_2_4GHZ_CHANNELS, 5000} unchanged; ensure no other side-effecting prints remain in get_device_total_channels/0.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 157-186: The docs mix proplist wording with map-based examples,
have malformed Erlang syntax, and a wrong anchor for wifi_scan; update the text
to consistently describe the scan return as {ok, {Num, NetworkList}} where Num
is integer and NetworkList is a list of maps (each map like #{ssid := SSID, rssi
:= DBm, authmode := Mode, bssid := BSSID, channel := Number}), fix the example
Erlang to valid pattern-matching and iteration (use {ok, {Num, Networks}} =
network:wifi_scan(Config) and lists:foreach(fun(Network) -> ... end, Networks)
or pattern-match inside the fun), replace any "proplist" wording with "options
proplist" only when referring to scan options, correct the link anchors so
network:wifi_scan/0 links to the /wifi_scan0 anchor and network:wifi_scan/1
links to the /wifi_scan1 anchor, and ensure references to
network:scan_options(), sta_config(), and network:sta_disconnect/0 remain
accurate and consistent with the map-based result description.
In `@libs/eavmlib/src/network.erl`:
- Around line 642-644: The handle_info clause for handle_info({Ref,
{scan_results, Results}} = _Msg, `#state`{ref = Ref, scan_receiver = From} =
State) always replies gen_server:reply(From, {ok, Results}) which masks driver
errors sent as {scan_results, {error, Reason}}; change the reply logic to
inspect Results and reply either {ok, Results} for normal results or propagate
the error tuple (e.g., {error, Reason}) unchanged — update the handle_info
clause that references `#state.ref` and `#state.scan_receiver` and the
gen_server:reply call so errors are not wrapped in {ok, ...} and scan_receiver
is still cleared (scan_receiver = undefined).
- Around line 224-226: The current typespecs network_properties() and the
wifi_scan specs (used in functions handling wifi_scan payloads) describe
list/tuple/proplist entries but the runtime payloads and driver use maps like
#{ssid := ..., rssi := ...}; update the typespecs to reflect map-based
structures (e.g., a map type with keys such as ssid, rssi, authmode, bssid,
channel) and adjust any related wifi_scan type aliases to match map form so
Dialyzer and callers see the correct contract (update all occurrences including
the other instances noted around the file).
- Around line 590-594: The branch in handle_call/3 erroneously binds Scanning to
the incoming From, so the previous requester stored in the state never gets
canceled; change the case to detect when scan_receiver (Scanning) contains a
prior From (e.g., a tuple like {PrevFromPid,Tag} or simply PrevFrom) that is
different from the incoming From and call gen_server:reply(PrevFrom,
{error,canceled}) to notify the previous caller, leaving the other branch for
no-op; update the pattern matching in handle_call to compare Scanning against
From (use a variable like PrevFrom for the stored value) rather than using "=
From".
- Around line 531-536: The current wifi_scan/0 uses proplists:get_value(sta,
Config) without a default so StaConfig can be undefined and make subsequent
proplists:get_value/3 calls crash; fix by first extracting a safe StaConfig
(e.g., StaConfig = proplists:get_value(sta, Config, []) or an empty proplist)
and then call proplists:get_value(default_scan_results, StaConfig, 6),
proplists:get_value(scan_dwell_ms, StaConfig, 120),
proplists:get_value(scan_show_hidden, StaConfig, false), and
proplists:get_value(scan_passive, StaConfig, false) before invoking
wifi_scan([{results, Results}, {dwell, Dwell}, {show_hidden, Hidden}, {passive,
Passive}]).
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1462-1475: The code calls esp_wifi_scan_start(scan_config, true)
twice which breaks the scan lifecycle; remove the first redundant call so
esp_wifi_scan_start is invoked only once with its return stored in err, keep the
subsequent free(scan_config) and the existing error branch that uses
err/err_str/port_ensure_available/term_from_string/port_create_error_tuple/port_send_reply
unchanged, and verify no additional cleanup is needed on success (ensure
scan_config is freed and ownership/callbacks remain correct).
- Around line 440-456: The failure branch after esp_wifi_scan_get_ap_records
leaks the ap_records buffer; before returning in the UNLIKELY_NOT_ESP_OK(err)
block free the ap_records pointer (if non-NULL) in addition to freeing data and
sending the error reply. Locate the error-handling block that calls
esp_wifi_clear_ap_list(), builds the error_reply (using term_from_string,
port_heap_create_tuple2, port_send_message_from_task) and add a safe
free(ap_records) immediately prior to free(data) and return so the allocated AP
list is released on failure.
---
Nitpick comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 917-919: Remove the noisy debug printing in the default branch of
get_device_total_channels/0: locate the clause that binds Debug and currently
calls io:format("DEBUG: chip info -> ~p", [Debug]) and delete that io:format
call (or guard it behind an explicit debug flag if logging is required), leaving
the return value {?DEVICE_2_4GHZ_CHANNELS, 5000} unchanged; ensure no other
side-effecting prints remain in get_device_total_channels/0.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ec93b3ee-e4cd-425d-ae10-bf3c540ab85e
📒 Files selected for processing (9)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (4)
- examples/erlang/esp32/CMakeLists.txt
- examples/erlang/esp32/wifi_scan.erl
- src/platforms/esp32/test/main/test_main.c
- src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl
592914c to
cc88432
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
doc/src/network-programming-guide.md (1)
157-158:⚠️ Potential issue | 🟡 MinorWiFi scan docs are still inconsistent with the implemented API shape.
This section still mixes old tuple/proplist wording with the new map payload, includes a malformed result tuple example, has a wrong
wifi_scan/0anchor, and includes typos (discoverd,using and ESP32-C6).Suggested doc updates
-... [`network:wifi_scan/0`](./apidocs/erlang/eavmlib/network.md#wifi_scan1) ... +... [`network:wifi_scan/0`](./apidocs/erlang/eavmlib/network.md#wifi_scan0) ... @@ -... `Results = {FoundAPs [NetworkList]}` ... +... `Results = {FoundAPs, NetworkList}` ... @@ -... discoverd ... +... discovered ... @@ -For example to do a passive scan using and ESP32-C6 ... +For example, to do a passive scan using an ESP32-C6 ...Also align the prose with map entries like:
#{ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number}.Also applies to: 180-186
libs/eavmlib/src/network.erl (1)
224-226:⚠️ Potential issue | 🟠 Major
network_properties()typespec does not match the actual scan map entries.Current type wraps entries in an extra list, omits
ssid, and usesauth_type(atom) instead ofauth_type(). This makes thewifi_scan/*specs inaccurate.In Erlang typespecs for map fields, should a local type alias be written as `auth_type()` (with parentheses), and does writing `auth_type` mean only the literal atom `auth_type`?Suggested fix
--type network_properties() :: [ - #{rssi := dbm(), authmode := auth_type, bssid := bssid_t(), channel := wifi_channel()} -]. +-type network_properties() :: + #{ssid := ssid(), rssi := dbm(), authmode := auth_type(), bssid := bssid_t(), channel := wifi_channel()}.Also applies to: 509-511
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 224 - 226, The typespec network_properties() is wrong: remove the extra list wrapper so it describes a single map (not a list-of-maps), add the missing ssid key, and use the local type alias with parentheses (auth_type() not auth_type) — i.e. change the typedef to a map like #{rssi := dbm(), authmode := auth_type(), bssid := bssid_t(), channel := wifi_channel(), ssid := ssid()} and update any wifi_scan/* specs and the duplicate occurrence (lines around 509-511) to reference this corrected network_properties() type.
🧹 Nitpick comments (1)
libs/eavmlib/src/network.erl (1)
926-932: Remove the unconditional debug print fromget_device_total_channels/0.
io:format("DEBUG: chip info -> ~p", [Debug])is noisy in normal operation and leaks internal runtime details on every scan timeout computation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 926 - 932, get_device_total_channels/0 currently prints chip info unconditionally; remove the io:format("DEBUG: chip info -> ~p", [Debug]) call and return the 2.4GHz tuple directly in that clause. Locate the second clause in get_device_total_channels/0 (the one binding Debug) and delete the debug print, optionally rename the variable to _Other or _ to avoid an unused-variable warning, ensuring the clause still returns {?DEVICE_2_4GHZ_CHANNELS, 5000}.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 597-605: handle_call/3's current scan cancellation allows
overlapping in-flight scans to share a single Ref and scan_receiver, which can
cause replies to be delivered to the wrong caller
(gen_server:reply(undefined,...)). Fix by serializing scan requests or tying
replies to the originating ref: when accepting a new {scan, ScanOpts} in
handle_call, do not overwrite scan_receiver with just From; instead store the
tuple {From, Ref} (or maintain a queue of pending {From,ScanOpts}) and ensure
incoming results from network_port include the Ref and are only delivered when
Ref matches the stored {From,Ref}; only start the next scan from the queue after
the current scan’s Ref has been replied to (or explicitly increment/rotate Ref
on cancellation so stale in-flight results are ignored). Update all code paths
that set/compare scan_receiver and Ref (including the network_port result
handler and places referenced around handle_call and the code at lines ~649-657)
to use the new {From,Ref} pairing or queue semantics so replies cannot be
misrouted.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 427-437: The early-return error paths inside send_scan_results
currently free data and return without unregistering the scan_done_handler,
leaving a stale callback to freed memory; before freeing data (in the
IS_NULL_PTR(ap_records) path and the other error-paths around lines 439–457)
call the appropriate unregister function to remove the registered
scan_done_handler (e.g., wlan_unregister_scan_done_handler or the module's
unregister_scan_done_handler) using the stored handler reference on data
(scan_done_handler or equivalent) or data->scan_done_handler, then proceed to
free(data) and return.
---
Duplicate comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 224-226: The typespec network_properties() is wrong: remove the
extra list wrapper so it describes a single map (not a list-of-maps), add the
missing ssid key, and use the local type alias with parentheses (auth_type() not
auth_type) — i.e. change the typedef to a map like #{rssi := dbm(), authmode :=
auth_type(), bssid := bssid_t(), channel := wifi_channel(), ssid := ssid()} and
update any wifi_scan/* specs and the duplicate occurrence (lines around 509-511)
to reference this corrected network_properties() type.
---
Nitpick comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 926-932: get_device_total_channels/0 currently prints chip info
unconditionally; remove the io:format("DEBUG: chip info -> ~p", [Debug]) call
and return the 2.4GHz tuple directly in that clause. Locate the second clause in
get_device_total_channels/0 (the one binding Debug) and delete the debug print,
optionally rename the variable to _Other or _ to avoid an unused-variable
warning, ensuring the clause still returns {?DEVICE_2_4GHZ_CHANNELS, 5000}.
ℹ️ Review info
Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0a0057b3-3a7b-4372-aee4-acf026e60264
📒 Files selected for processing (9)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (5)
- CHANGELOG.md
- src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl
- examples/erlang/esp32/CMakeLists.txt
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- src/platforms/esp32/test/main/test_main.c
There was a problem hiding this comment.
♻️ Duplicate comments (4)
libs/eavmlib/src/network.erl (2)
597-605:⚠️ Potential issue | 🔴 CriticalScan cancellation logic can misroute replies and leave unsafe reply targets.
Line 597 cancels the previous caller but still overwrites
scan_receiverbefore the prior in-flight scan result is consumed. With a sharedRef, results can go to the wrong caller, and subsequent messages may hitgen_server:reply/2withundefined.Suggested stabilization (serialize scans)
-handle_call({scan, ScanOpts}, From, `#state`{scan_receiver = Scanning, ref = Ref} = State) -> - case Scanning of - {Pid, _Tag} = Previous when is_pid(Pid) -> - gen_server:reply(Previous, {error, canceled}); - _ -> - ok - end, - network_port ! {self(), Ref, {scan, ScanOpts}}, - {noreply, State#state{scan_receiver = From}}; +handle_call({scan, _ScanOpts}, _From, `#state`{scan_receiver = Scanning} = State) when is_tuple(Scanning) -> + {reply, {error, busy}, State}; +handle_call({scan, ScanOpts}, From, `#state`{scan_receiver = undefined, ref = Ref} = State) -> + network_port ! {self(), Ref, {scan, ScanOpts}}, + {noreply, State#state{scan_receiver = From}}; @@ -handle_info({Ref, {scan_results, Results}} = _Msg, `#state`{ref = Ref, scan_receiver = From} = State) -> +handle_info({Ref, {scan_results, Results}} = _Msg, `#state`{ref = Ref, scan_receiver = From} = State) when is_tuple(From) -> gen_server:reply(From, {ok, Results}), {noreply, State#state{scan_receiver = undefined}}; +handle_info({Ref, {scan_results, _Results}} = _Msg, `#state`{ref = Ref, scan_receiver = undefined} = State) -> + {noreply, State};Also applies to: 649-657
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 597 - 605, The scan cancellation currently replies to the previous caller and immediately overwrites scan_receiver, risking misrouted in-flight results (shared Ref) and gen_server:reply/2 on undefined; fix by serializing scan requests: generate/attach a fresh unique Ref per scan, send the cancel reply and ensure the previous scan result is consumed or ignored before setting State#state{scan_receiver = From} (e.g. wait for acknowledgement or clear Ref comparison), or alternatively stash incoming scan responses until the new scan_receiver is set and Ref matches; apply the same pattern to the other scan handling block referenced (lines 649-657) to avoid races on scan_receiver, Ref, and gen_server:reply calls.
224-227:⚠️ Potential issue | 🟠 Major
wifi_scantypes/docs are still out of sync with runtime payload.
network_properties()and the function docs/spec text still describe tuple/proplist-shaped entries, but scan results are map entries includingssid. This weakens type guarantees and API clarity.Suggested alignment
--type network_properties() :: [ - #{rssi := dbm(), authmode := auth_type, bssid := bssid_t(), channel := wifi_channel()} -]. -%% A proplist of network properties with the keys: `rssi', `authmode' and `channel' +-type network_properties() :: #{ + ssid := string() | binary(), + rssi := dbm(), + authmode := auth_type(), + bssid := bssid_t(), + channel := wifi_channel() +}. +%% A map describing one discovered network.-%% `{ok, {NumberResults, [{SSID, [{rssi, DBm}, {authmode, Mode}, {bssid, BSSID}, {channel, Number}]}, ...]}}' +%% `{ok, {NumberResults, [#{ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number}, ...]}}'Also applies to: 460-476, 509-511
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 224 - 227, The declared network_properties() type and related `@spec/docs` describe proplist/tuple entries but runtime scan results are maps containing an ssid key; update network_properties() to a list of maps (e.g. list(map())) with explicit map keys rssi :: dbm(), authmode :: auth_type, bssid :: bssid_t(), channel :: wifi_channel(), ssid :: binary() (or appropriate ssid type), and then update any function specs/docs that reference wifi_scan/network_properties (including functions named wifi_scan, parse_scan_result, or similar at the other affected locations) to use the new map-based type; also change code that pattern-matches or treats entries as tuples/proplists to match maps (e.g., #{ssid := S, rssi := R, ...}) so runtime and types/docs are consistent.src/platforms/esp32/components/avm_builtins/network_driver.c (1)
427-437:⚠️ Potential issue | 🔴 CriticalUnregister
scan_done_handleron early scan-result error exits before freeing callback data.Line 435 and Line 456 free
datawhile the scan-done handler may remain registered on these branches. This can leave a stale callback argument and trigger use-after-free on a laterWIFI_EVENT_SCAN_DONE.Suggested fix
if (IS_NULL_PTR(ap_records)) { BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2), heap); term error = port_heap_create_tuple2(&heap, ERROR_ATOM, OUT_OF_MEMORY_ATOM); term error_reply = port_heap_create_tuple2(&heap, make_atom(data->global, ATOM_STR("\xc", "scan_results")), error); @@ port_send_message_from_task(data->global, term_from_local_process_id(data->owner_process_id), msg); END_WITH_STACK_HEAP(heap, data->global); + esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); free(data); return; } @@ if (UNLIKELY_NOT_ESP_OK(err)) { // the ap_list must be cleared on failures to prevent a memory leak esp_wifi_clear_ap_list(); @@ port_send_message_from_task(data->global, term_from_local_process_id(data->owner_process_id), msg); END_WITH_STACK_HEAP(heap, data->global); + esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); free(ap_records); free(data); return; }Also applies to: 440-457
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 427 - 437, The error-handling branches that send an OUT_OF_MEMORY reply (the block that creates error_reply and calls port_send_message_from_task) free the callback data without unregistering the scan_done_handler, which can leave a stale callback and cause use-after-free on WIFI_EVENT_SCAN_DONE; before calling free(data) in these early-exit branches, call the codepath that unregisters the handler (e.g., the same logic used to remove scan_done_handler or deregister the callback that was registered when scheduling the scan) so the handler no longer holds the freed pointer, then proceed to free(data) and return.doc/src/network-programming-guide.md (1)
157-158:⚠️ Potential issue | 🟡 MinorFix wifi_scan docs to match the actual API and payload shape.
Line 157 still has multiple mismatches:
wifi_scan/0points to#wifi_scan1, tuple syntax is malformed, and entries are described as tuple/proplist while current scan results are map-based. This section should be updated to avoid incorrect usage.Suggested doc patch
-After the network has been configured for STA mode and started, as long as no connection has been initiated or associated, you may scan for available access points using [`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 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 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, `bssid` (a.k.a MAC address) of the access point, and the `channel` key for obtaining the primary channel for the network. +After the network has been configured for STA mode and started, as long as no connection has been initiated or associated, 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 active connections to be dropped. With no options, a default active scan with a per-channel dwell time of 120 ms is used and returns details for up to 6 access points. The return value is `{ok, Results}`, where `Results = {FoundAPs, NetworkList}`. `FoundAPs` may be larger than the length of `NetworkList` if more access points were discovered than requested. Entries in `NetworkList` are maps like `#{ssid := SSID, rssi := DBm, authmode := Mode, bssid := BSSID, channel := Number}`.🤖 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 157 - 158, The wifi_scan docs are inconsistent with the actual API and payload: fix the broken apidoc anchor references (ensure wifi_scan/0 links to the `#wifi_scan0` anchor and wifi_scan/1 to `#wifi_scan1`), correct the malformed tuple syntax around the {ok, Results} description, and update the payload description to reflect that scan results are map-based (not tuple/proplist). Specifically, describe the return as {ok, Results} where Results includes a total count and a list of AP maps (each map containing keys like ssid, rssi, authmode, bssid, channel), and remove references to tuple/proplist entry forms such as `{SSID, [AP_Properties]}` in favor of the actual map shape used by wifi_scan.
🧹 Nitpick comments (1)
libs/eavmlib/src/network.erl (1)
926-932: Use_for the fallback channel-case variable.Line 930 binds
Debugbut never uses it; replace with_(or_ChipInfo) to avoid compiler warnings.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 926 - 932, The function get_device_total_channels uses a case binding named Debug that is not used; update the fallback clause in the case on erlang:system_info(esp32_chip_info) to replace the unused variable Debug with an underscore (or a named underscore like _ChipInfo) so the pattern becomes #{model := esp32_c5} -> ...; _ -> ... and remove the unused binding to eliminate the compiler warning while keeping the same return values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 157-158: The wifi_scan docs are inconsistent with the actual API
and payload: fix the broken apidoc anchor references (ensure wifi_scan/0 links
to the `#wifi_scan0` anchor and wifi_scan/1 to `#wifi_scan1`), correct the malformed
tuple syntax around the {ok, Results} description, and update the payload
description to reflect that scan results are map-based (not tuple/proplist).
Specifically, describe the return as {ok, Results} where Results includes a
total count and a list of AP maps (each map containing keys like ssid, rssi,
authmode, bssid, channel), and remove references to tuple/proplist entry forms
such as `{SSID, [AP_Properties]}` in favor of the actual map shape used by
wifi_scan.
In `@libs/eavmlib/src/network.erl`:
- Around line 597-605: The scan cancellation currently replies to the previous
caller and immediately overwrites scan_receiver, risking misrouted in-flight
results (shared Ref) and gen_server:reply/2 on undefined; fix by serializing
scan requests: generate/attach a fresh unique Ref per scan, send the cancel
reply and ensure the previous scan result is consumed or ignored before setting
State#state{scan_receiver = From} (e.g. wait for acknowledgement or clear Ref
comparison), or alternatively stash incoming scan responses until the new
scan_receiver is set and Ref matches; apply the same pattern to the other scan
handling block referenced (lines 649-657) to avoid races on scan_receiver, Ref,
and gen_server:reply calls.
- Around line 224-227: The declared network_properties() type and related
`@spec/docs` describe proplist/tuple entries but runtime scan results are maps
containing an ssid key; update network_properties() to a list of maps (e.g.
list(map())) with explicit map keys rssi :: dbm(), authmode :: auth_type, bssid
:: bssid_t(), channel :: wifi_channel(), ssid :: binary() (or appropriate ssid
type), and then update any function specs/docs that reference
wifi_scan/network_properties (including functions named wifi_scan,
parse_scan_result, or similar at the other affected locations) to use the new
map-based type; also change code that pattern-matches or treats entries as
tuples/proplists to match maps (e.g., #{ssid := S, rssi := R, ...}) so runtime
and types/docs are consistent.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 427-437: The error-handling branches that send an OUT_OF_MEMORY
reply (the block that creates error_reply and calls port_send_message_from_task)
free the callback data without unregistering the scan_done_handler, which can
leave a stale callback and cause use-after-free on WIFI_EVENT_SCAN_DONE; before
calling free(data) in these early-exit branches, call the codepath that
unregisters the handler (e.g., the same logic used to remove scan_done_handler
or deregister the callback that was registered when scheduling the scan) so the
handler no longer holds the freed pointer, then proceed to free(data) and
return.
---
Nitpick comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 926-932: The function get_device_total_channels uses a case
binding named Debug that is not used; update the fallback clause in the case on
erlang:system_info(esp32_chip_info) to replace the unused variable Debug with an
underscore (or a named underscore like _ChipInfo) so the pattern becomes #{model
:= esp32_c5} -> ...; _ -> ... and remove the unused binding to eliminate the
compiler warning while keeping the same return values.
ℹ️ Review info
Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4de74472-916f-485a-8a90-26adb50538c6
📒 Files selected for processing (9)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (1)
- examples/erlang/esp32/CMakeLists.txt
fc07f45 to
706f93d
Compare
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
doc/src/network-programming-guide.md (1)
157-158:⚠️ Potential issue | 🟡 MinorA few doc typos remain in the updated scan section.
Examples:
- Line 157:
equivalent toa→equivalent to a- Line 206:
per channel→per-channel- Line 218:
using and ESP32-C6→using an ESP32-C6Also applies to: 206-207, 218-220
🤖 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 157 - 158, Fix the remaining typos in the scan section of the doc: change "equivalent toa" to "equivalent to a" (near the sentence about 10 dBm decrease), change "per channel" to "per-channel" in the occurrence covering lines around 206-207, and change "using and ESP32-C6" to "using an ESP32-C6" (and verify similar phrasing in the nearby 218-220 range); update those exact phrases in the document text to correct spelling and hyphenation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@doc/src/network-programming-guide.md`:
- Around line 79-81: Update the documented callback signature for scan_done to
show a single-argument function (it currently lists fun(() -> term())); change
it to fun((term) -> term()) or fun((Result) -> term()) to reflect that the
callback receives the scan results as one argument, and adjust any nearby
example callback code to match the single-argument form; ensure the symbol
referenced is scan_done so readers can find the corrected signature.
In `@examples/erlang/esp32/wifi_scan_callback.erl`:
- Around line 28-34: Check and handle the result of network:start(Config) before
proceeding to scanning: call network:start(Config) and pattern-match its return
(e.g., {ok, State} | {error, Reason}), log/return/exit on {error, Reason}
instead of ignoring it, then only call network:wifi_scan/0 or
network:wifi_scan(ScanOptions) with an explicit scan options map/list (not the
full startup Config) to avoid ambiguity; update the code around the
network:start/1 and network:wifi_scan/1 calls (references: network:start/1,
network:wifi_scan/1, network:wifi_scan/0, Config, Reason) so failures are
handled deterministically and scan arguments are explicit.
In `@libs/eavmlib/src/network.erl`:
- Around line 618-626: The call handler currently always stores From into
scan_receiver causing gen_server:call to block when a scan_done callback
(callback-mode) is configured and the reply is never sent; modify
handle_call({scan, ScanOpts}, From, State) so that when the
configuration/ScanOpts indicate callback-mode (a scan_done pid/fun is set) you
immediately gen_server:reply(From, ok) (or another simple acknowledgement)
instead of leaving From waiting, and do not overwrite scan_receiver with that
blocking From; keep the existing cancellation behavior that
gen_server:reply(Previous, {error, canceled}) for any previous {Pid,_Tag} stored
in scan_receiver, and ensure the network_port ! {self(), Ref, {scan, ScanOpts}}
send still occurs so callback routing (scan_done) continues to deliver results
to the configured callback.
- Around line 83-86: The type scan_done_config() is declared as {scan_done,
fun(() -> term())} but callbacks are invoked with one argument; update the type
to {scan_done, fun((term()) -> term())} (or fun((term()) -> any())) so the spec
matches runtime usage, and ensure references such as scan_done, wifi_scan and
sta_connect use the revised type in their specs/docs.
- Around line 865-867: There are two adjacent EDoc `@private` tags before
get_port/0 causing a "multiple `@private` tags" error; remove the duplicate
comment so only a single "%% `@private`" remains immediately associated with the
get_port/0 function (look for the duplicate tags surrounding get_port/0 in
network.erl and delete the extra one).
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1465-1467: The code calls esp_wifi_scan_start(scan_config, true),
which blocks and prevents the WIFI_EVENT_SCAN_DONE from being posted to your
registered scan_done_handler; change the call to
esp_wifi_scan_start(scan_config, false) so the scan is non-blocking and the
scan_done_handler will receive WIFI_EVENT_SCAN_DONE and deliver results to
Erlang; ensure the existing scan_done_handler registration remains intact and
only free or NULL scan_config if/when it is safe after starting a non-blocking
scan (or after the scan_done_handler has processed results) to avoid freeing
memory still needed by the driver.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 157-158: Fix the remaining typos in the scan section of the doc:
change "equivalent toa" to "equivalent to a" (near the sentence about 10 dBm
decrease), change "per channel" to "per-channel" in the occurrence covering
lines around 206-207, and change "using and ESP32-C6" to "using an ESP32-C6"
(and verify similar phrasing in the nearby 218-220 range); update those exact
phrases in the document text to correct spelling and hyphenation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c7bbc217-b222-4e17-b293-ae2b7c0b034b
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (4)
- src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl
- src/platforms/esp32/test/main/test_main.c
- examples/erlang/esp32/CMakeLists.txt
- examples/erlang/esp32/wifi_scan.erl
706f93d to
afecd18
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (6)
doc/src/network-programming-guide.md (2)
79-81:⚠️ Potential issue | 🟠 Major
scan_donecallback signature is documented with the wrong arity.Line 79 should document a one-argument callback; scan callbacks receive results/error payload.
📝 Suggested doc fix
-* `{scan_done, fun(() -> term())}` A callback function which will be called once a network scan is +* `{scan_done, fun((Result) -> 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.🤖 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 79 - 81, The documented scan_done callback currently shows a zero-argument fun; update the signature for scan_done to a one-argument callback that accepts the scan result/error payload and adjust the explanatory text accordingly so it states that scan callbacks receive a single argument containing results or an error payload (refer to the scan_done callback symbol when making the change).
157-158:⚠️ Potential issue | 🟡 MinorFix remaining typos in the new scan section.
There are a few text issues that should be cleaned up for clarity.
📝 Suggested doc fix
- power of 1 milliwatt. A 10 dBm decrease in level is equivalent toa ten-fold decrease in signal + power of 1 milliwatt. A 10 dBm decrease in level is equivalent to a ten-fold decrease in signal - a proplist. The per channel scan time can be changed with the `dwell` key, the channel dwell time + a proplist. The per-channel scan time can be changed with the `dwell` key, the channel dwell time - For example, to do a passive scan using and ESP32-C6, including hidden networks, using the longest - allowed scan time and showing the maximum number of networks available use the following: + 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:Also applies to: 206-206, 218-220
🤖 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 157 - 158, Fix the typos in the "new scan" section text: replace "toa" with "to a" and ensure spacing and sentence flow around "power of 1 milliwatt. A 10 dBm decrease in level is equivalent toa ten-fold decrease in signal power." so it reads cleanly (e.g., "power of 1 milliwatt. A 10 dBm decrease in level is equivalent to a ten-fold decrease in signal power."). Also scan and correct the similar wording issues referenced at the other occurrences (lines noted around 206 and 218–220) to keep consistent phrasing and spacing.src/platforms/esp32/components/avm_builtins/network_driver.c (1)
1465-1467:⚠️ Potential issue | 🔴 CriticalStart scans in non-blocking mode to preserve event-driven completion.
Line 1465 currently starts scans with
block=truewhile this implementation depends onWIFI_EVENT_SCAN_DONE+scan_done_handlerfor result delivery. This can break or stall the reply lifecycle.🔧 Suggested fix
- err = esp_wifi_scan_start(scan_config, true); + err = esp_wifi_scan_start(scan_config, false);ESP-IDF `esp_wifi_scan_start` behavior: when `block=true`, is `WIFI_EVENT_SCAN_DONE` emitted? Compare with `block=false` and confirm recommended mode for event-handler-driven result delivery.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1465 - 1467, The scan is started in blocking mode which prevents the implementation from relying on the WIFI_EVENT_SCAN_DONE + scan_done_handler flow; change the esp_wifi_scan_start call to use non-blocking mode by passing false (esp_wifi_scan_start(scan_config, false)) so results are delivered via WIFI_EVENT_SCAN_DONE and handled by scan_done_handler, and keep the existing free(scan_config); scan_config = NULL pattern after the call to avoid leaks.examples/erlang/esp32/wifi_scan_callback.erl (1)
27-38:⚠️ Potential issue | 🟠 MajorCheck startup result and call
wifi_scanwith scan options (orwifi_scan/0).This sample ignores
network:start/1errors and passes full startup config intowifi_scan/1, which makes the example ambiguous.🔧 Suggested fix
start() -> Config = [{sta, [managed, {scan_done, fun display_scan_results/1}]}], - 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(Config) of - {error, Reason} -> - io:format("wifi_scan failed for reason ~p", [Reason]); - ok -> - timer:sleep(infinity) - end. + case network:start(Config) of + {ok, _Pid} -> + 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~n", [Reason]); + ok -> + timer:sleep(infinity) + end; + {error, Reason} -> + io:format("network:start failed for reason ~p~n", [Reason]) + end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/erlang/esp32/wifi_scan_callback.erl` around lines 27 - 38, Check the result of network:start(Config) and handle {ok, _} vs {error, Reason} before calling network:wifi_scan; do not pass the full startup Config into network:wifi_scan/1—call network:wifi_scan/0 or call network:wifi_scan(ScanOptions) with explicit scan options instead. Specifically, update the flow around network:start/1 to pattern-match its return, log or return the error on failure, and on success invoke either network:wifi_scan() or network:wifi_scan(ScanOptions) (keeping display_scan_results/1 as the scan_done callback) so the example is unambiguous and startup failures are not ignored.libs/eavmlib/src/network.erl (2)
83-86:⚠️ Potential issue | 🟠 Major
scan_done_config()type is wrong for actual callback usage.The callback is invoked with one argument (
Results/Error) but declared as arity-0.🔧 Suggested fix
--type scan_done_config() :: {scan_done, fun(() -> term())}. +-type scan_done_config() :: {scan_done, fun((term()) -> term())}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 83 - 86, The declared type scan_done_config() currently specifies a zero-arity fun but the actual callback is called with one argument (Results/Error); update the type to a one-arity function (e.g., {scan_done, fun((term()) -> term())} or fun((term()) -> any())) so it matches how the callback is invoked, and ensure references like sta_connect/1 and wifi_scan/0,1 use the updated scan_done_config() type.
618-626:⚠️ Potential issue | 🔴 Critical
wifi_scan/0,1can timeout whenscan_donecallback is configured.
handle_call({scan, ...})always storesFrom, but callback-enabled branches route results to callback and skipgen_server:reply(From, ...), leaving callers waiting until timeout.🔧 Suggested fix (preserves preemption semantics)
-handle_call({scan, ScanOpts}, From, `#state`{scan_receiver = Scanning, ref = Ref} = State) -> +handle_call({scan, ScanOpts}, From, `#state`{ + scan_receiver = Scanning, ref = Ref, config = Config +} = State) -> case Scanning of {Pid, _Tag} = Previous when is_pid(Pid) -> gen_server:reply(Previous, {error, canceled}); _ -> ok end, network_port ! {self(), Ref, {scan, ScanOpts}}, - {noreply, State#state{scan_receiver = From}}; + case proplists:get_value(scan_done, proplists:get_value(sta, Config, [])) of + undefined -> + {noreply, State#state{scan_receiver = From}}; + _ -> + {reply, ok, State#state{scan_receiver = undefined}} + end; @@ scan_reply_or_callback(Results, `#state`{scan_receiver = From, config = Config} = State) -> case proplists:get_value(scan_done, proplists:get_value(sta, Config, [])) of undefined -> gen_server:reply(From, {ok, Results}); Pid when is_pid(Pid) -> - Pid ! {ok, Results}; + Pid ! {scan_done, Results}; Fun when is_function(Fun) -> spawn(fun() -> Fun(Results) end) end, State#state{scan_receiver = undefined}.Based on learnings: ESP-IDF does not support concurrent scans; initiating a second scan cancels the first and overwriting
scan_receiveris intentional, so this fix should preserve that preemption behavior.Also applies to: 734-753
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 618 - 626, The handler handle_call({scan, ScanOpts}, From, `#state`{scan_receiver = Scanning, ref = Ref} = State) stores From into scan_receiver but if a scan_done callback is configured the result is routed to the callback and no gen_server:reply(From, ...) is ever sent, leaving the caller to timeout; fix by detecting when a callback is configured (the scan_done callback in ScanOpts or the module/state that indicates callback usage), and in that branch do not leave the caller waiting — either reply immediately to From (e.g. gen_server:reply(From, {ok, Ref})) and avoid storing scan_receiver, or ensure the code that invokes the callback also calls gen_server:reply(From, ...); update the logic around scan_receiver, network_port ! {self(), Ref, {scan, ScanOpts}} and the subsequent callback/result dispatch so callers are always replied to even when a callback is used, preserving the preemption semantics that overwrite previous scan_receiver.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/erlang/esp32/wifi_scan.erl`:
- Around line 26-29: The start/0 function calls network:start([{sta,
[managed]}]) but ignores its return value so scans run even if startup fails;
update start/0 to pattern-match or case on the result of network:start/1 (e.g.,
{ok, Pid} or {error, Reason}) and only call scan_passive/1 and scan_active/1
when network:start/1 succeeds, otherwise log or return the error and abort
further scanning (referencing network:start/1, start/0, scan_passive/1, and
scan_active/1 to locate the change).
In `@libs/eavmlib/src/network.erl`:
- Around line 534-543: The current case on Dwell can crash if Dwell is
non-integer; update the clauses to guard with is_integer/1 for the numeric
comparisons and arithmetic: use a clause like Millis when is_integer(Millis) and
Millis =< ?MAX_SHORT_DWELL to handle short dwells, a clause like ChanDwellMs
when is_integer(ChanDwellMs) to compute Timeout = (ChanDwellMs * NumChannels) +
?GEN_RESPONSE_MS and call gen_server:call(?SERVER, {scan, Options}, Timeout),
and add a fallback clause for any non-integer Dwell (e.g., else ->
gen_server:call(?SERVER, {scan, Options}, DefaultTimeout)) so badarith is
avoided; reference symbols: Dwell, proplists:get_value(dwell, Options),
get_device_total_channels(), ?MAX_SHORT_DWELL, ?GEN_RESPONSE_MS,
gen_server:call.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 79-81: The documented scan_done callback currently shows a
zero-argument fun; update the signature for scan_done to a one-argument callback
that accepts the scan result/error payload and adjust the explanatory text
accordingly so it states that scan callbacks receive a single argument
containing results or an error payload (refer to the scan_done callback symbol
when making the change).
- Around line 157-158: Fix the typos in the "new scan" section text: replace
"toa" with "to a" and ensure spacing and sentence flow around "power of 1
milliwatt. A 10 dBm decrease in level is equivalent toa ten-fold decrease in
signal power." so it reads cleanly (e.g., "power of 1 milliwatt. A 10 dBm
decrease in level is equivalent to a ten-fold decrease in signal power."). Also
scan and correct the similar wording issues referenced at the other occurrences
(lines noted around 206 and 218–220) to keep consistent phrasing and spacing.
In `@examples/erlang/esp32/wifi_scan_callback.erl`:
- Around line 27-38: Check the result of network:start(Config) and handle {ok,
_} vs {error, Reason} before calling network:wifi_scan; do not pass the full
startup Config into network:wifi_scan/1—call network:wifi_scan/0 or call
network:wifi_scan(ScanOptions) with explicit scan options instead. Specifically,
update the flow around network:start/1 to pattern-match its return, log or
return the error on failure, and on success invoke either network:wifi_scan() or
network:wifi_scan(ScanOptions) (keeping display_scan_results/1 as the scan_done
callback) so the example is unambiguous and startup failures are not ignored.
In `@libs/eavmlib/src/network.erl`:
- Around line 83-86: The declared type scan_done_config() currently specifies a
zero-arity fun but the actual callback is called with one argument
(Results/Error); update the type to a one-arity function (e.g., {scan_done,
fun((term()) -> term())} or fun((term()) -> any())) so it matches how the
callback is invoked, and ensure references like sta_connect/1 and wifi_scan/0,1
use the updated scan_done_config() type.
- Around line 618-626: The handler handle_call({scan, ScanOpts}, From,
`#state`{scan_receiver = Scanning, ref = Ref} = State) stores From into
scan_receiver but if a scan_done callback is configured the result is routed to
the callback and no gen_server:reply(From, ...) is ever sent, leaving the caller
to timeout; fix by detecting when a callback is configured (the scan_done
callback in ScanOpts or the module/state that indicates callback usage), and in
that branch do not leave the caller waiting — either reply immediately to From
(e.g. gen_server:reply(From, {ok, Ref})) and avoid storing scan_receiver, or
ensure the code that invokes the callback also calls gen_server:reply(From,
...); update the logic around scan_receiver, network_port ! {self(), Ref, {scan,
ScanOpts}} and the subsequent callback/result dispatch so callers are always
replied to even when a callback is used, preserving the preemption semantics
that overwrite previous scan_receiver.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1465-1467: The scan is started in blocking mode which prevents the
implementation from relying on the WIFI_EVENT_SCAN_DONE + scan_done_handler
flow; change the esp_wifi_scan_start call to use non-blocking mode by passing
false (esp_wifi_scan_start(scan_config, false)) so results are delivered via
WIFI_EVENT_SCAN_DONE and handled by scan_done_handler, and keep the existing
free(scan_config); scan_config = NULL pattern after the call to avoid leaks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 57d1a69b-be8f-4a28-8985-9c7d268effb6
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (1)
- src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl
afecd18 to
6aa0090
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
examples/erlang/esp32/wifi_scan_callback.erl (1)
33-33:⚠️ Potential issue | 🟡 MinorUse scan options (or
wifi_scan/0) instead of startup config.Line 33 passes
Config([{sta, ...}]) towifi_scan/1, but that argument is intended for scan options. This makes the example API usage ambiguous.Proposed fix
- case network:wifi_scan(Config) of + case network:wifi_scan() of🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/erlang/esp32/wifi_scan_callback.erl` at line 33, The example is calling network:wifi_scan(Config) where Config is the startup [{sta,...}] list but wifi_scan/1 expects scan options; change the call to use wifi_scan/0 or build a proper ScanOptions term instead of reusing the startup Config. Locate the network:wifi_scan(Config) invocation and either replace it with network:wifi_scan() or construct a dedicated ScanOptions variable (e.g., ScanOpts) and pass that to network:wifi_scan(ScanOpts), leaving the original Config untouched.doc/src/network-programming-guide.md (1)
79-81:⚠️ Potential issue | 🟠 Major
scan_donecallback signature is still documented with wrong arity.Line 79 documents
fun(() -> term()), but callback is invoked with one argument (scan result or error). This will mislead users.Proposed fix
-* `{scan_done, fun(() -> term())}` A callback function which will be called once a network scan is +* `{scan_done, fun((Result) -> 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.🤖 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 79 - 81, The documentation for the scan_done callback incorrectly shows the arity as fun(() -> term()) even though the callback is invoked with one argument; update the description of scan_done to show a unary function type (e.g., fun((ScanResult | Error) -> term())) and mention that the single argument will contain the scan result or an error, so readers looking at the scan_done callback (symbol: scan_done) understand it receives one parameter rather than none.
🧹 Nitpick comments (1)
libs/eavmlib/src/network.erl (1)
217-230:auth_type()is narrower than values emitted by scan results.The driver mapping includes additional auth atoms (e.g., enterprise/DPP variants), but
auth_type()does not include them. This can make documented/typespec return values diverge from runtime payloads.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 217 - 230, The typespec auth_type() in network.erl is too narrow compared to runtime scan results (it omits additional auth atoms like enterprise/DPP variants emitted by the driver mapping); update the auth_type() type definition to include the missing atoms used by the mapping (e.g., enterprise, dpp, dpp_sa, dpp_proto or whatever exact atoms the driver emits) so the documented return values match runtime payloads (ensure you edit the auth_type() union in libs/eavmlib/src/network.erl to include those symbols and re-run Dialyzer/tests to confirm no mismatches remain).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 204-208: The typespec scan_options() is too narrow: code in
wifi_scan/0 passes tuple boolean forms like {show_hidden, Hidden} and {passive,
Passive}; update scan_options() to include those tuple forms (e.g.,
{show_hidden, boolean()} and {passive, boolean()}) alongside the existing
{results, ...} and {dwell, ...} clauses so the declared type matches actual
usage; locate and modify the scan_options() type declaration in
libs/eavmlib/src/network.erl to add these tuple entries.
- Around line 531-533: The specs for wifi_scan need to allow the atom ok for
callback mode responses; update the -spec declarations for wifi_scan/0 and
wifi_scan/1 (the spec using scan_options() and returning {ok, scan_results()} |
{error, term()}) to also accept ok as a valid return (i.e., return type should
include ok | {ok, scan_results()} | {error, term()}); apply the same change to
the other wifi_scan specs in this module that mirror the same signatures so
callback-mode returns are properly typed.
- Around line 83-86: The public type scan_done_config currently defines the
callback as fun((scan_results()) -> term()), but the scan callback flow can pass
error tuples like {error, Reason}; update the type to accept either successful
scan_results() or error tuples (e.g. fun((scan_results() | {error, term()}) ->
term())) so callers and dialyzer see the correct contract; adjust any related
specs/comments for scan_done_config, and ensure references such as scan_done,
scan_results(), sta_connect/1 and wifi_scan/0,1 reflect the widened input type.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Line 1322: Update the error message emitted by ESP_LOGE(TAG, ...) so it
references the correct exposed API arities: replace "network:wifi_scan/2" with
"network:wifi_scan/0" and "network:wifi_scan/1" (e.g. "network:wifi_scan/0 or
network:wifi_scan/1") so the log accurately reflects the available functions;
locate the ESP_LOGE call near the WiFi configuration check in network_driver.c
and change the string accordingly.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 79-81: The documentation for the scan_done callback incorrectly
shows the arity as fun(() -> term()) even though the callback is invoked with
one argument; update the description of scan_done to show a unary function type
(e.g., fun((ScanResult | Error) -> term())) and mention that the single argument
will contain the scan result or an error, so readers looking at the scan_done
callback (symbol: scan_done) understand it receives one parameter rather than
none.
In `@examples/erlang/esp32/wifi_scan_callback.erl`:
- Line 33: The example is calling network:wifi_scan(Config) where Config is the
startup [{sta,...}] list but wifi_scan/1 expects scan options; change the call
to use wifi_scan/0 or build a proper ScanOptions term instead of reusing the
startup Config. Locate the network:wifi_scan(Config) invocation and either
replace it with network:wifi_scan() or construct a dedicated ScanOptions
variable (e.g., ScanOpts) and pass that to network:wifi_scan(ScanOpts), leaving
the original Config untouched.
---
Nitpick comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 217-230: The typespec auth_type() in network.erl is too narrow
compared to runtime scan results (it omits additional auth atoms like
enterprise/DPP variants emitted by the driver mapping); update the auth_type()
type definition to include the missing atoms used by the mapping (e.g.,
enterprise, dpp, dpp_sa, dpp_proto or whatever exact atoms the driver emits) so
the documented return values match runtime payloads (ensure you edit the
auth_type() union in libs/eavmlib/src/network.erl to include those symbols and
re-run Dialyzer/tests to confirm no mismatches remain).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c95d1cbd-933b-44fc-9af2-2862568ade61
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (4)
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- src/platforms/esp32/test/main/test_main.c
- examples/erlang/esp32/CMakeLists.txt
- CHANGELOG.md
6aa0090 to
7f4bec5
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/platforms/esp32/components/avm_builtins/network_driver.c (1)
1322-1322:⚠️ Potential issue | 🟡 MinorFix incorrect API arity in the scan-mode error log.
Line 1322 references
network:wifi_scan/2, but the exposed API isnetwork:wifi_scan/0andnetwork:wifi_scan/1. This can mislead debugging and support.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` at line 1322, The error log message using "network:wifi_scan/2" is incorrect; update the ESP_LOGE call (the one using TAG in network driver error path) to reference the correct API arities "network:wifi_scan/0" and "network:wifi_scan/1" (or a neutral message listing both valid forms) so debugging reflects the actual exposed functions.libs/eavmlib/src/network.erl (2)
204-208:⚠️ Potential issue | 🟡 Minor
scan_options()is narrower than actual call sites.
wifi_scan/0passes{show_hidden, Hidden}and{passive, Passive}(Lines 566-568), butscan_options()only declares atom forms (show_hidden,passive). This mismatch should be aligned to avoid inaccurate specs.Also applies to: 566-568
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 204 - 208, The spec for scan_options() is too narrow compared to how wifi_scan/0 calls it: update the type definition in network.erl (scan_options()) to accept the tuple forms used at call sites—e.g., include {show_hidden, boolean()} and {passive, boolean()} in addition to the existing {results, ...} and {dwell, ...} forms—so the type matches the actual arguments passed by wifi_scan/0.
83-86:⚠️ Potential issue | 🟡 MinorWiden
scan_donecallback type to include error tuples.The
scan_done_config()type only allowsscan_results(), but runtime callback paths can pass{error, Reason}(e.g., Line 746 onward). The type contract is currently too narrow.Also applies to: 746-753
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/eavmlib/src/network.erl` around lines 83 - 86, The type scan_done_config() is too narrow because the scan_done callback can receive either scan_results() or an error tuple; update the typedef for scan_done_config() (and any related specs) to accept a callback of type fun((scan_results() | {error, term()}) -> term()), and adjust any specs/usages around scan_done, wifi_scan/0, wifi_scan/1 and sta_connect/1 to reflect the broadened callback type so callbacks handling {error, Reason} validate against the contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 736-765: The code currently re-reads the routing target from
Config inside scan_reply_or_callback/2 which allows a changing config to divert
or drop replies; instead capture the routing target at the time the scan is
requested and store that snapshot in the state (the existing scan_receiver field
or a new scan_callback field) so delivery uses the saved value rather than
proplists:get_value(scan_done, proplists:get_value(sta, Config, [])) at reply
time. Update the scan initiation path (where scan_receiver is set) to compute
and store the actual callback target (Pid | Fun | {reply, From}) and then change
all clauses of scan_reply_or_callback/2 to dispatch to that stored target and
then clear scan_receiver/scan_callback when finished.
---
Duplicate comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 204-208: The spec for scan_options() is too narrow compared to how
wifi_scan/0 calls it: update the type definition in network.erl (scan_options())
to accept the tuple forms used at call sites—e.g., include {show_hidden,
boolean()} and {passive, boolean()} in addition to the existing {results, ...}
and {dwell, ...} forms—so the type matches the actual arguments passed by
wifi_scan/0.
- Around line 83-86: The type scan_done_config() is too narrow because the
scan_done callback can receive either scan_results() or an error tuple; update
the typedef for scan_done_config() (and any related specs) to accept a callback
of type fun((scan_results() | {error, term()}) -> term()), and adjust any
specs/usages around scan_done, wifi_scan/0, wifi_scan/1 and sta_connect/1 to
reflect the broadened callback type so callbacks handling {error, Reason}
validate against the contract.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Line 1322: The error log message using "network:wifi_scan/2" is incorrect;
update the ESP_LOGE call (the one using TAG in network driver error path) to
reference the correct API arities "network:wifi_scan/0" and
"network:wifi_scan/1" (or a neutral message listing both valid forms) so
debugging reflects the actual exposed functions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d4ce8b2d-46b0-432a-8495-c4740c0cd5ce
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (2)
- CHANGELOG.md
- examples/erlang/esp32/wifi_scan_callback.erl
7f4bec5 to
b037f0d
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
doc/src/network-programming-guide.md (1)
146-147: Section heading is duplicated later; consider renaming this one.Using a distinct title here will make the document structure easier to scan.
📝 Suggested tweak
-### STA (or AP+STA) mode functions +### STA (or AP+STA) scan-related functions🤖 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 146 - 147, The heading "STA (or AP+STA) mode functions" is duplicated later; rename this instance to a distinct title (e.g., "STA (or AP+STA) mode — overview" or "STA (or AP+STA) mode (intro)") to avoid duplicate section headings and improve scanability, and update any internal anchors or references that rely on this heading text if present.examples/erlang/esp32/wifi_scan.erl (1)
41-53: Optional cleanup: extract duplicated network-print logic.Both scan paths format networks identically; a small helper would reduce repetition without changing behavior.
♻️ Proposed refactor
+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 + ). scan_active(Config) -> @@ - 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 - ). + format_networks(Networks). scan_passive(Config) -> @@ - 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 - ). + format_networks(Networks).Also applies to: 67-79
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/erlang/esp32/wifi_scan.erl` around lines 41 - 53, Extract the duplicated network-printing anonymous function into a named helper (e.g., print_network/1 or format_and_print_network/1) that accepts a Network map and performs the existing io:format with binary:encode_hex(BSSID) and the same field order; then replace the two lists:foreach anonymous fun usages (the one shown and the similar block at the other occurrence) with lists:foreach(fun print_network/1, Networks) (or equivalent calls) so behavior is unchanged but duplication removed.src/platforms/esp32/components/avm_builtins/network_driver.c (1)
1333-1353: Refactor repeated scan error-reply assembly into one helper.The same
{scan_results, {error, Reason}}construction is repeated in multiple branches; extracting a helper will reduce drift and simplify future changes.♻️ Proposed refactor sketch
+static void send_scan_error_reply(Context *ctx, term pid, term ref, term reason) +{ + term error = port_create_error_tuple(ctx, reason); + term scan_results_atom = make_atom(ctx->global, ATOM_STR("\xc", "scan_results")); + term ret = port_create_tuple2(ctx, scan_results_atom, error); + port_send_reply(ctx, pid, ref, ret); +} @@ - term error = port_create_error_tuple(ctx, BADARG_ATOM); - term scan_results_atom = make_atom(ctx->global, ATOM_STR("\xc", "scan_results")); - term ret = port_create_tuple2(ctx, scan_results_atom, error); - port_send_reply(ctx, pid, ref, ret); + send_scan_error_reply(ctx, pid, ref, BADARG_ATOM); return;Also applies to: 1373-1390, 1412-1418, 1456-1462, 1468-1476
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1333 - 1353, Extract the repeated construction and send of the `{scan_results, {error, Reason}}` reply into a single helper (e.g., send_scan_results_error(ctx, pid, ref, term reason_atom) or send_scan_results_error_badarg(ctx, pid, ref) depending on whether you want generic/reason-specific helpers) and replace every repeated block that calls port_ensure_available, port_create_error_tuple, make_atom("scan_results"), port_create_tuple2 and port_send_reply with a single call to that helper; the helper should take the context, pid and ref, call port_ensure_available(ctx, error_size) as needed, build the error tuple with port_create_error_tuple(ctx, Reason) and make the scan_results atom via make_atom(ctx->global, ATOM_STR("\xc","scan_results")), assemble the final tuple using port_create_tuple2, and call port_send_reply—then update all occurrences around term validation (uses of cfg_results/num_results and the other listed ranges) to use this new helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/eavmlib/src/network.erl`:
- Around line 685-691: handle_info/2 spawns scan_reply_or_callback/2 with a
message tagged {scan_results, ...} but scan_reply_or_callback/2 only matches
{scan_done, Results}, causing a function_clause and timeouts; change
scan_reply_or_callback/2 (or its clause) to accept the {scan_results, Results}
form (or accept both {scan_results, Results} and {scan_done, Results}) so the
spawned process can handle the message and reply/callback correctly; ensure you
update any pattern matches or clauses inside scan_reply_or_callback/2 (and any
callers/guards that assume {scan_done, ...}) to use the {scan_results, ...} tag
consistent with handle_info/2 and the driver.
- Line 251: The scan cancellation/invocation logic is inconsistent: code
sometimes records scan_receiver as the tuple {callback, Fun} but later stores
only FunOrPid (raw fun or pid), so cancellation misses callback cases and
handle_call({scan, ...}) may directly call a fun and crash the server; update
the logic so that scan_receiver is consistently stored as the tuple form
({callback, Fun} or {pid, Pid} or undefined), adjust the
preemption/cancel-on-second-scan path in handle_call({scan, ...}) to match and
compare against the full {callback, Fun} tuple, and when invoking a callback in
handle_call use a protected call (wrap the fun invocation in try...catch or
spawn a monitor/safe apply) so any exception from the callback is caught and
converted to an error reply instead of crashing the server; ensure the
intentional preemption behavior is preserved (do not serialize/queue cancelled
scans).
- Around line 542-546: The short-dwell branch uses a fixed DefaultTimeout which
can be too small for multi-channel scans; change the logic so that even when
Millis =< ?MAX_SHORT_DWELL you compute a channel-based timeout (Timeout =
(Millis * NumChannels) + ?GEN_RESPONSE_MS) and use max(Timeout, DefaultTimeout)
for the gen_server:call instead of always passing DefaultTimeout; update the
clause handling Millis and the clause using ChanDwellMs to both compute and use
this channel-aware timeout (referencing ?MAX_SHORT_DWELL, Millis/ChanDwellMs,
NumChannels, ?GEN_RESPONSE_MS, and the gen_server:call(?SERVER, {scan, Options},
...)).
- Line 757: Replace the invalid erlang:spawn(?MODULE, Fun, Results) call (which
passes a fun where an atom is expected) with the same pattern used later in the
file: wrap the fun invocation in an anonymous zero-arg function and call spawn/1
(or erlang:spawn/1) with fun() -> Fun(Results) end; locate the offending call by
the symbol Fun at the spawn site and mirror the approach used in the block
referenced around lines 833–862.
---
Nitpick comments:
In `@doc/src/network-programming-guide.md`:
- Around line 146-147: The heading "STA (or AP+STA) mode functions" is
duplicated later; rename this instance to a distinct title (e.g., "STA (or
AP+STA) mode — overview" or "STA (or AP+STA) mode (intro)") to avoid duplicate
section headings and improve scanability, and update any internal anchors or
references that rely on this heading text if present.
In `@examples/erlang/esp32/wifi_scan.erl`:
- Around line 41-53: Extract the duplicated network-printing anonymous function
into a named helper (e.g., print_network/1 or format_and_print_network/1) that
accepts a Network map and performs the existing io:format with
binary:encode_hex(BSSID) and the same field order; then replace the two
lists:foreach anonymous fun usages (the one shown and the similar block at the
other occurrence) with lists:foreach(fun print_network/1, Networks) (or
equivalent calls) so behavior is unchanged but duplication removed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1333-1353: Extract the repeated construction and send of the
`{scan_results, {error, Reason}}` reply into a single helper (e.g.,
send_scan_results_error(ctx, pid, ref, term reason_atom) or
send_scan_results_error_badarg(ctx, pid, ref) depending on whether you want
generic/reason-specific helpers) and replace every repeated block that calls
port_ensure_available, port_create_error_tuple, make_atom("scan_results"),
port_create_tuple2 and port_send_reply with a single call to that helper; the
helper should take the context, pid and ref, call port_ensure_available(ctx,
error_size) as needed, build the error tuple with port_create_error_tuple(ctx,
Reason) and make the scan_results atom via make_atom(ctx->global,
ATOM_STR("\xc","scan_results")), assemble the final tuple using
port_create_tuple2, and call port_send_reply—then update all occurrences around
term validation (uses of cfg_results/num_results and the other listed ranges) to
use this new helper.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 227b0d5c-5c26-4d0b-93c5-2d24cedf78c7
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
🚧 Files skipped from review as they are similar to previous changes (1)
- src/platforms/esp32/test/main/test_main.c
fbf0d4d to
44e29c1
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
doc/src/network-programming-guide.md (1)
79-81:⚠️ Potential issue | 🟡 MinorMinor grammar fix: hyphenate "event-driven".
Suggested fix
-* `{scan_done, fun((scan_results()) -> 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 +* `{scan_done, fun((scan_results()) -> 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🤖 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 79 - 81, The phrase describing the callback for `{scan_done, fun((scan_results()) -> term())}` should hyphenate "event-driven" for correct grammar; update the sentence to read "...this allows for event-driven connection management..." so the documentation for the scan callback uses the hyphenated form.
🧹 Nitpick comments (1)
src/platforms/esp32/components/avm_builtins/network_driver.c (1)
232-234: Missingbreakstatement inWIFI_AUTH_MAXcase.The
WIFI_AUTH_MAXcase setsauthmode = ERROR_ATOMbut lacks abreakstatement. While there's no subsequent case to fall through to, adding an explicitbreakmaintains consistency and prevents future issues if cases are reordered.Suggested fix
case WIFI_AUTH_MAX: authmode = ERROR_ATOM; + break; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 232 - 234, The switch case for WIFI_AUTH_MAX sets authmode = ERROR_ATOM but lacks an explicit break; update the switch in network_driver.c to add a break after the WIFI_AUTH_MAX case (i.e., after the assignment to authmode) so the case uses an explicit "break" to prevent unintended fall-through and future reorder errors; reference symbols: WIFI_AUTH_MAX, authmode, ERROR_ATOM.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 79-81: The phrase describing the callback for `{scan_done,
fun((scan_results()) -> term())}` should hyphenate "event-driven" for correct
grammar; update the sentence to read "...this allows for event-driven connection
management..." so the documentation for the scan callback uses the hyphenated
form.
---
Nitpick comments:
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 232-234: The switch case for WIFI_AUTH_MAX sets authmode =
ERROR_ATOM but lacks an explicit break; update the switch in network_driver.c to
add a break after the WIFI_AUTH_MAX case (i.e., after the assignment to
authmode) so the case uses an explicit "break" to prevent unintended
fall-through and future reorder errors; reference symbols: WIFI_AUTH_MAX,
authmode, ERROR_ATOM.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 71a3a552-5d1e-4429-b818-dc70a63a12bb
📒 Files selected for processing (11)
.github/workflows/esp32-build.yamlCHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/eavmlib/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
💤 Files with no reviewable changes (1)
- .github/workflows/esp32-build.yaml
🚧 Files skipped from review as they are similar to previous changes (5)
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- CHANGELOG.md
- examples/erlang/esp32/CMakeLists.txt
- examples/erlang/esp32/wifi_scan.erl
- src/platforms/esp32/test/main/test_main.c
🧩 Analysis chain🏁 Script executed: Length of output: 3773 🏁 Script executed: Length of output: 717 🏁 Script executed: Length of output: 2642
The preceding 37 commits are all upstream authors (bettio, pguyot, petermm) from the rebase. I'll start a fresh full review now, focusing exclusively on the changes introduced by your 4 commits. Please review only the changes introduced by the 4 commits authored by UncleGrumpy (SHAs: e23ab55, 3cddad5, 9ccd687, 9e6ded9). Do not flag or review any changes that originate from the upstream rebase commits by other authors (bettio, pguyot, petermm). 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/erlang/system_info_server.erl (1)
102-103:⚠️ Potential issue | 🟡 Minor
json:encode/1does not support proplists as objects.The error response
[{error, <<"Process not found.">>}]is a proplist, butjson:encode/1treats lists as JSON arrays. This will encode as[["error","Process not found."]](nested arrays) rather than{"error":"Process not found."}.Similarly,
proc_info_list/1returns a list of tuples fromerlang:process_info/2, which will also encode as nested arrays rather than an object.Use maps for JSON object encoding
try_proc_info_list(PidString) -> try proc_info_list(PidString) of Res -> {200, Res} catch - _:_ -> {404, [{error, <<"Process not found.">>}]} + _:_ -> {404, #{error => <<"Process not found.">>}} end. proc_info_list(PidString) -> PidInteger = erlang:list_to_integer(PidString), Procs = erlang:processes(), Pid = lists:nth(PidInteger, Procs), io:format("pid: ~p~n", [Pid]), - [ - erlang:process_info(Pid, heap_size), - erlang:process_info(Pid, stack_size), - erlang:process_info(Pid, message_queue_len), - erlang:process_info(Pid, memory) - ]. + #{ + heap_size => element(2, erlang:process_info(Pid, heap_size)), + stack_size => element(2, erlang:process_info(Pid, stack_size)), + message_queue_len => element(2, erlang:process_info(Pid, message_queue_len)), + memory => element(2, erlang:process_info(Pid, memory)) + }.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/erlang/system_info_server.erl` around lines 102 - 103, The response uses proplists which json:encode/1 will serialize as arrays; replace the proplist error and any proc_info_list/1 output with maps so objects are encoded correctly: change the 404 clause returning [{error, <<"Process not found.">>}] to return a map (e.g. #{error => <<"Process not found.">>}), and update proc_info_list/1 to convert the list of {Key,Val} tuples from erlang:process_info/2 into a map (e.g. via maps:from_list/1 or a fold) before passing to json:encode/1; ensure all code paths that build response bodies use maps not proplists so json:encode/1 emits JSON objects.
♻️ Duplicate comments (5)
tests/libs/estdlib/tests.erl (1)
59-60:⚠️ Potential issue | 🟠 MajorGuard the OTP-only clause against the
atomvmatom.
get_otp_version/0returnsatomvmoutside BEAM, and this guard still matches there, sotest_setsandtest_jsonare pulled into the AtomVM run as well.🛠️ Suggested fix
-get_non_networking_tests(OTPVersion) when OTPVersion >= 27 -> +get_non_networking_tests(OTPVersion) when is_integer(OTPVersion), OTPVersion >= 27 ->In Erlang term ordering, does the atom `atomvm` compare greater than the integer `27`, and would the guard `OTPVersion >= 27` match when `OTPVersion = atomvm`?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/tests.erl` around lines 59 - 60, The guard on get_non_networking_tests/1 currently uses "OTPVersion >= 27" which will match when get_otp_version/0 returns the atom atomvm (since term ordering can make that comparison true); change the guard to ensure OTPVersion is an integer and at least 27 (e.g. "is_integer(OTPVersion), OTPVersion >= 27") so that atomvm (or other non-integer returns) won't satisfy the clause; update the clause for get_non_networking_tests/1 accordingly..github/workflows/build-and-test.yaml (1)
445-450:⚠️ Potential issue | 🟠 MajorThe
CFLAGSemptiness check is broken.Line 447 uses
$"CFLAGS"which is POSIX localized string syntax, not variable expansion. This causes the condition to always be false whenCFLAGSis empty, preventing jobs withoutmatrix.cflagsfrom inheritingDEFAULT_CFLAGS.Suggested fix
- name: "Set CFLAGS" run: | - if [ $"CFLAGS" = "" ]; then + if [ -z "${CFLAGS:-}" ]; then echo "CFLAGS=$DEFAULT_CFLAGS" >> $GITHUB_ENV echo "CXXFLAGS=$DEFAULT_CFLAGS" >> $GITHUB_ENV fi🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/build-and-test.yaml around lines 445 - 450, The shell condition used to detect an empty CFLAGS uses the localized string syntax ($"CFLAGS") which never expands the variable; update the conditional in the "Set CFLAGS" step to use proper variable expansion and an emptiness test (use ${CFLAGS} and the -z test or compare to an empty string) so that when CFLAGS is unset/empty the block writes DEFAULT_CFLAGS into GITHUB_ENV for both CFLAGS and CXXFLAGS.libs/jit/src/jit_dwarf.erl (2)
353-355:⚠️ Potential issue | 🟠 MajorClose the last
.debug_locrange with the code end.Line 354 still builds
.debug_locwithout a code-end bound, andmerge_loc_entries/1at Lines 1057-1064 still terminates the final run as{Off, Off, ...}. That drops the x-register location for the tail of the generated code and filters single-snapshot runs away entirely.Also applies to: 970-1064
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_dwarf.erl` around lines 353 - 355, The final .debug_loc range is not being closed with the code end: update generate_debug_loc_section (the call site using LocListOffsets and the function that assembles the .debug_loc) to obtain the code end offset from State (or the same source used to finalize code emission) and append/merge a final location entry that uses that code end as the range end; then modify merge_loc_entries/1 so it does not terminate the final run as {Off, Off, ...} but instead emits the final entry with the proper start and the code end (e.g., {StartOffset, CodeEnd, LocData,...}) so single-snapshot runs and the tail-of-code x-registers are preserved. Ensure references to LocListOffsets, generate_debug_loc_section, merge_loc_entries, and State are updated consistently.
463-539:⚠️ Potential issue | 🟠 MajorExclude the leading format byte from
.ARM.attributeslength fields.Both generators still compute
TotalLength = 1 + 4 + VendorLength. In AAELF, that 4-byte length starts at the length field itself and does not include the leading$Aformat byte, so these sections are emitted one byte too long.Also applies to: 541-590
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_dwarf.erl` around lines 463 - 539, The section lengths currently include the leading format/tag/length bytes, causing the emitted .ARM.attributes to be one byte too long; in generate_arm_attributes_section adjust the length fields so the 4-byte length values count only the bytes following the length field: set FileAttributesLength to byte_size(TagValuePairs) (not 1+4+...), and set TotalLength to VendorLength (not 1+4+VendorLength); apply the same fix in the other ARM attributes generator referenced around lines 541-590 so all subsection/section length fields exclude the leading format/tag/length bytes.src/platforms/esp32/components/avm_builtins/network_driver.c (1)
598-599:⚠️ Potential issue | 🔴 CriticalSynchronize the
ctx->platform_datahandoff before dereferencing it.
cancel_scan()readsctx->platform_dataintodataand only then tries to claim ownership with the CAS ondata->canceled. The event-task paths can clear and free that same object concurrently, so this still leaves a use-after-free window on the CAS itself. The pointer needs a single atomic/critical-section claim path; the per-objectcanceledflag alone is not enough.Also applies to: 627-628, 656-657, 680-681, 713-714, 893-894, 1711-1711, 1760-1773, 1807-1811
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 598 - 599, cancel_scan currently reads ctx->platform_data into local data and then operates on data->canceled with a CAS, which can race with event-task paths that clear and free the same object; fix by making the pointer handoff atomic: claim ownership of ctx->platform_data itself before dereferencing (e.g., use an atomic_exchange or atomic compare-exchange on ctx->platform_data to swap in NULL or otherwise guard via the same critical section used when freeing), then operate on data->canceled and free data; ensure all other locations mentioned (lines around 627-628, 656-657, 680-681, 713-714, 893-894, 1711, 1760-1773, 1807-1811) follow the same pattern so the pointer is atomically claimed/cleared before any dereference or free.
🧹 Nitpick comments (11)
libs/exavmlib/lib/Keyword.ex (1)
10-12: Pin the upstream reference to a tag/commit for reproducibility.Using
.../blob/main/...can drift over time. Prefer a fixed tag or commit URL so provenance/audits stay stable.Suggested comment tweak
-# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/keyword.ex +# https://github.com/elixir-lang/elixir/blob/v1.16.0/lib/elixir/lib/keyword.ex +# (or a specific commit SHA)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/exavmlib/lib/Keyword.ex` around lines 10 - 12, The comment in Keyword.ex references the upstream put_new/3 implementation using a "blob/main" URL which can drift; update that inline comment to point to a fixed upstream tag or commit SHA instead (replace the current "https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/keyword.ex" link with the corresponding tag or commit URL for put_new/3) so the provenance is reproducible; ensure the comment still names put_new/3 and retains the same explanatory text but uses the stable URL.libs/estdlib/src/timer.erl (1)
88-95: Spawned process not cleaned up on timer cancellation.If the timer is canceled (e.g., via
timer:cancel/1), the spawned helper process will wait indefinitely for anapply_nowmessage that never arrives, causing a process leak. This matches OTP'stimer:apply_after/4behavior, but it's worth noting.Consider whether this is acceptable for AtomVM's resource-constrained environments, or if a timeout or monitor-based cleanup should be added.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/estdlib/src/timer.erl` around lines 88 - 95, The helper process spawned in apply_after (spawn(fun() -> receive apply_now -> apply(Module, Function, Arguments) end end)) is leaked when the timer is canceled because it blocks forever waiting for apply_now; update the spawned process logic in apply_after to handle cancellation by either (a) adding a receive timeout (after <reasonable_delay> -> exit(normal)) so the helper self-terminates if apply_now never arrives, or (b) monitoring the timer/manager and exit on the monitor 'DOWN' or explicit cancel notification; ensure you still call apply(Module, Function, Arguments) on apply_now and pass the same TRef from timer_manager:send_after to any monitor/cancellation logic so the helper can detect cancellation and clean up Pid.libs/estdlib/src/json.erl (2)
631-639:normalize_float/1assumes 'e'/'E' always exists — could infinite loop.The
normalize_float/2function recursively scans for$eor$Ebut has no base case for when the binary is exhausted. If called on a binary without an exponent marker, it will crash with a function clause error whenRestbecomes empty.However, looking at the call site (Line 625),
normalize_floatis only called whenType = float_exp, which is set only when an 'e'/'E' was actually parsed (Lines 554-555, 566-567). So this is safe in practice, but a defensive clause would improve robustness:Add defensive clause for safety
normalize_float(<<E, _/binary>>, Orig, Pos) when E =:= $e; E =:= $E -> IntPart = binary_part(Orig, 0, Pos), ExpPart = binary_part(Orig, Pos, byte_size(Orig) - Pos), iolist_to_binary([IntPart, ".0", ExpPart]); normalize_float(<<_, Rest/binary>>, Orig, Pos) -> - normalize_float(Rest, Orig, Pos + 1). + normalize_float(Rest, Orig, Pos + 1); +normalize_float(<<>>, Orig, _Pos) -> + %% Should never happen if called correctly (float_exp type implies 'e' present) + Orig.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/estdlib/src/json.erl` around lines 631 - 639, The recursive helper normalize_float(<<_, Rest/binary>>, Orig, Pos) can reach an empty binary and crash; add a defensive base clause (e.g., normalize_float(<<>>, Orig, _Pos) -> Orig or similar) to return the original binary when no 'e'/'E' is found so normalize_float/1 and normalize_float/3 never fail on exhausted input; update the clause for normalize_float/3 to handle the empty-remaining case and keep existing behavior when an exponent byte is found.
183-186: Overly broad error catch masks non-parsing errors.The
catch error:_clause catches ALL errors, not justunexpected_end. This could mask bugs like arithmetic errors, badarg, etc. during the scan/parse phase.Consider catching only the expected errors:
Catch only expected parsing errors
decode_continue(end_of_input, {Buf, Acc, Callbacks}) -> try Bin1 = skip_whitespace(<<Buf/binary, $\s>>), case scan_value(Bin1, Acc, Callbacks, []) of {Result, Acc1, <<>>} -> {Result, Acc1, <<>>}; {Result, Acc1, <<$\s>>} -> {Result, Acc1, <<>>}; _ -> error(unexpected_end) end catch - error:_ -> + error:unexpected_end -> + error(unexpected_end); + error:{invalid_byte, _} -> error(unexpected_end) end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/estdlib/src/json.erl` around lines 183 - 186, The current broad clause "catch error:_" that returns error(unexpected_end) masks all runtime errors; change the catch so it only matches the expected parsing error (e.g. catch error:unexpected_end -> error(unexpected_end)) and re-raise or propagate any other errors instead of converting them to unexpected_end (for example rethrow the original error or call erlang:error/1). Locate the "catch error:_" clause in json.erl and replace it with a try/catch or specific catch arms that handle only the parsing-related error and let other errors bubble up.tests/libs/estdlib/file_uart_hal.erl (1)
46-49: Spec mismatch:open/1always returns a pid, never{error, term()}.The spec claims
pid() | {error, term()}but the implementation always succeeds (crashes on error via{ok, Pid} = gen_server:start(...)). Ifgen_server:startfails, this will crash with a badmatch rather than returning an error tuple.Consider either updating the spec to just
pid()(and document that it crashes on failure), or handling the error case:Option 1: Fix spec to match implementation
--spec open(list()) -> pid() | {error, term()}. +-spec open(list()) -> pid(). open(Opts) -> {ok, Pid} = gen_server:start(?MODULE, Opts, []), Pid.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/file_uart_hal.erl` around lines 46 - 49, The spec for open/1 is incorrect: the implementation always crashes on start failure and returns a pid, so update the function spec to -spec open(list()) -> pid(). Alternatively, if you want to return errors instead of crashing, change the implementation of open/1 to pattern-match the gen_server:start/3 result (case gen_server:start(?MODULE, Opts, []) of {ok, Pid} -> Pid; {error, Reason} -> {error, Reason} end) and keep the current spec; adjust either the spec or the body (referencing open/1 and gen_server:start in file_uart_hal.erl).tests/libs/estdlib/mock_uart_hal.erl (1)
112-114: Potential crash ifwrite/2is called beforeset_peer.If
write/2is called on an endpoint beforeset_peerhas been called (i.e.,peerisundefined), the expressionPeer ! {mock_uart_data, Data}will crash withbadarg.Since this is a test mock and
create_pair/0always callsset_peerbefore returning, this is likely fine in practice. However, ifopen/1is used directly without going throughcreate_pair/0, the crash could be confusing.Optional: Add a guard or error for undefined peer
handle_call({write, Data}, _From, `#state`{peer = Peer} = State) -> + case Peer of + undefined -> {reply, {error, no_peer}, State}; + _ -> Peer ! {mock_uart_data, Data}, - {reply, ok, State}. + {reply, ok, State} + end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/mock_uart_hal.erl` around lines 112 - 114, The handle_call clause for {write, Data} currently assumes `#state`{peer = Peer} is set and does a raw send (Peer ! {mock_uart_data, Data}), which will crash if Peer is undefined; update the clause in handle_call/3 (the {write, Data} clause in mock_uart_hal) to explicitly handle the undefined peer case—either by adding a separate pattern match for `#state`{peer = undefined} that returns a safe error reply (e.g., {reply, {error, no_peer}, State}) or by adding a guard to ensure Peer =/= undefined before sending, and otherwise return an informative error instead of crashing; reference create_pair/0 and set_peer when documenting/justifying the guard.examples/erlang/serial_disterl.erl (1)
97-101: Dead code branch inbasename/1.The
[] -> Pathclause will never match becauselists:last/1returns an element from the list, not a list itself. For a trailing slash (e.g.,"foo/"),lists:last(["foo", ""])returns""(empty string), not[].This is a minor issue in an example file, so no action required, but worth noting.
basename(Path) -> case lists:last(string:split(Path, "/", all)) of - [] -> Path; + "" -> Path; Name -> Name end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/erlang/serial_disterl.erl` around lines 97 - 101, The basename/1 function contains a dead branch because lists:last(...) returns an element (e.g., "" for a trailing slash) not an empty list, so the `[] -> Path` clause never matches; to fix, simplify the function by removing the unreachable `[] -> Path` clause or change the match to handle an empty string explicitly (e.g., match on ""), updating the clause in basename/1 accordingly and keeping the Name -> Name behavior for non-empty names.doc/src/distributed-erlang.md (1)
140-142: Add language specifier to fenced code block.The static analysis tool flagged this code block as missing a language specifier. Since this shows a binary format schema rather than executable code, consider using
textorplaintext:-``` +```text <<16#AA, 16#55, Length:LenBits/big, Payload:Length/binary, CRC32:32/big>><details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@doc/src/distributed-erlang.mdaround lines 140 - 142, The fenced code block
showing the binary format line<<16#AA, 16#55, Length:LenBits/big, Payload:Length/binary, CRC32:32/big>>is missing a language specifier; update
that fence to include a plaintext specifier (e.g., changetotext orwhile preserving the exact schema string.
tests/libs/estdlib/CMakeLists.txt (1)
78-93: Consider using CMake's cross-platform directory creation.The
mkdir -pcommand is POSIX-specific. If Windows CI or cross-platform builds are ever needed, consider using CMake's built-in command:COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/beam_beams - && erlc +debug_info -DBEAM_INTERFACE + COMMAND erlc +debug_info -DBEAM_INTERFACEOr use
${CMAKE_COMMAND} -E make_directoryfor the mkdir. However, if these tests are strictly Unix-only (which appears to be the case for serial distribution tests using socat), this is fine as-is.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/CMakeLists.txt` around lines 78 - 93, The add_custom_command uses a POSIX-only "mkdir -p" in its COMMAND list; replace that with CMake's cross-platform directory creation by invoking ${CMAKE_COMMAND} -E make_directory for ${CMAKE_CURRENT_BINARY_DIR}/beam_beams before running erlc (update the COMMAND sequence in the add_custom_command that produces serial_dist_controller.beam and serial_dist.beam), ensuring the rest of the pipeline (erlc, ${CMAKE_COMMAND} -E copy) remains unchanged so Windows and other non-POSIX environments can create the target directory.
tests/libs/estdlib/test_serial_dist.erl (1)
234-245: This doesn't actually test the accepted-side of the boundary.The comment says the exact 8192-byte handshake frame should be accepted, but the test only checks the 8193-byte oversize case. A fencepost bug at the real max would still pass here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/test_serial_dist.erl` around lines 234 - 245, The test only checks the oversize 8193 case but not that a frame length exactly equal to MAX_HANDSHAKE_FRAME_SIZE is accepted; update test_scan_frame_max_handshake_boundary to also build a fake handshake length of 8192 (e.g. create LenAndPayload with 8192:16 and a small payload, compute CRC the same way and assemble Buffer) and call serial_dist_controller:scan_frame(Buffer, 16) expecting the accepted result (the successful frame/ok tuple the scanner returns for valid frames) to verify the fence‑post boundary handling.
libs/estdlib/src/serial_dist.erl (1)
160-171: Consider logging or returning which ports failed to open.Currently
lists:filtermapsilently filters out failed ports after printing to stdout. In production, it might be useful to return the list of failures alongside successful opens, or at least log with a severity level.case OpenPorts of [] -> {error, no_uart_ports}; _ when length(OpenPorts) < length(UartConfigs) -> %% Some ports failed - continue with available ones ...This is minor since the current behavior is reasonable for robustness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/estdlib/src/serial_dist.erl` around lines 160 - 171, Modify the port-opening logic so failures are captured instead of only printed: change the lists:filtermap block that builds OpenPorts (calling UartMod:open for each UartOpts from UartConfigs) to also accumulate failed attempts (e.g., a list of {UartOpts, Reason}) and log them via the project's logger at an appropriate severity; return or propagate a tuple such as {ok, OpenPorts, FailedPorts} or {error, no_uart_ports} when none opened, and update callers that rely on OpenPorts accordingly so callers can detect when some or all ports failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@doc/src/atomvm-internals.md`:
- Line 172: The sentence mislabels Pico 2 / Cortex-M33 as ARMv7-M; update the
text in doc/src/atomvm-internals.md to reflect the correct architecture family
by replacing "ARMv7-M devices using the Thumb-2 variant (Pico 2 and STM32 with
Cortex-M3/M4/M7/M33)" with wording that includes ARMv8-M (Thumb-2) — for
example: "ARMv7-M and ARMv8-M devices using the Thumb-2 variant (Pico 2 and
STM32 with Cortex-M3/M4/M7/M33)". Ensure the revised sentence keeps the same
structure as the original but corrects the architecture family name.
In `@libs/jit/src/jit_dwarf.erl`:
- Around line 395-398: The ELF e_flags returned by backend_to_elf_flags/1 must
match the ARM variant used to build the .ARM.attributes section; currently
backend_to_elf_flags/1 hardcodes ?EF_ARM_ARCH_V6M for the jit_armv6m backend and
omits architecture bits for jit_arm32, causing a mismatch with
generate_arm_attributes_section(State#dwarf.variant). Update
backend_to_elf_flags/1 to inspect the same variant parameter (or the backend
identifier) and set the correct architecture flag (e.g., use the v7-M
architecture flag for Thumb-2/v7-M builds instead of ?EF_ARM_ARCH_V6M, and
include the appropriate ?EF_ARM_ARCH_* bits alongside ?EF_ARM_EABI_VER5 for
jit_arm32) so the ELF header e_flags align with the values produced by
generate_arm_attributes_section/1.
- Line 24: Replace the invalid -include_lib("jit.hrl") with an
application-relative include in the form "app_name/include/jit.hrl" so the
compiler can find the header and resolve macros like ?JIT_VARIANT_THUMB2 and
others used in jit_dwarf.erl; locate the -include_lib directive in jit_dwarf.erl
and change it to use the providing OTP application's name
(app_name/include/jit.hrl) so the header is correctly located at compile time.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1629-1643: The code currently uses term_to_uint32(cfg_dwell) which
can truncate large Erlang integers before the range check; change the flow in
the scan config path (the cfg_dwell handling that uses term_is_integer,
term_to_uint32, dwell_config, dwell_ms) to first read the Erlang integer into a
wider signed type (e.g., int64_t or int64) using the appropriate term-to-int64
helper, validate that it is an integer and within 1..1500, and only then cast to
uint32_t for dwell_ms; on validation failure call send_scan_error_reason(ctx,
pid, ref, BADARG_ATOM) and keep the same log messages (ESP_LOGE/ESP_LOGD) and
control flow.
- Around line 296-299: The code currently constructs and ensures the internal
atom scan_canceled (symbol scan_canceled) for scan callback errors; change it to
use the public canceled atom instead (the atom named "canceled") so callbacks
receive {error, canceled} as expected: replace uses of the internal
scan_canceled atom creation/ensure with the public canceled atom
(ensure_atom(global, canceled) or equivalent), and update the same pattern also
at the other occurrence around the similar block (the one noted at the 887-890
region) so both scan callback and scan_done paths emit {error, canceled} rather
than {error, scan_canceled}.
In `@tests/libs/estdlib/file_uart_hal.erl`:
- Around line 75-79: The code builds a shell command by concatenating Peripheral
into Cmd in init/1, which risks command injection; instead validate Peripheral
(e.g., allow only characters like letters, digits, / _ . -) and call open_port
with spawn_executable to avoid a shell. Concretely: in init/1 validate the
Peripheral value (reject or error on characters like ; & | $ ` or whitespace),
build args such as ["-b","65536","-", Peripheral ++ ",rawer"] and call
open_port({spawn_executable, "socat"}, [binary, stream, exit_status, {args,
Args}]) so you stop interpolating Peripheral into a shell string and remove Cmd.
In `@tests/libs/estdlib/test_serial_dist_socat.erl`:
- Around line 200-217: The test never closes the peer subprocess file
descriptors PeerFdB and PeerFdC after use; mirror run_serial_test/2 by closing
them when done (e.g. call the cleanup helper used for peers such as
close_peer_node(PeerFdB) and close_peer_node(PeerFdC) or the appropriate
file/socket close function immediately after the <<"pong">> = ResultB/ResultC
assertions) and also add defensive closes in the after block (e.g. catch
close_peer_node(PeerFdB) and catch close_peer_node(PeerFdC)) so descriptors are
released on both success and failure paths.
- Around line 167-179: The current try in test_multi_port_ping() wraps both the
second start_socat() call and the entire test body, causing real test failures
to be misreported as "not enough PTYs" and leaving OsPid2/SocatFd2 uncleaned;
change the flow so only the second start_socat() is protected (e.g., use a
separate try/catch or case to only handle failure to allocate PTYs), then call
test_multi_port_ping/8 normally so test failures propagate; also ensure proper
cleanup: on failure to allocate the second socat stop_socat(OsPid1, SocatFd1)
and return skip, and if the test body raises ensure both stop_socat(OsPid1,
SocatFd1) and stop_socat(OsPid2, SocatFd2) are called (use try..after or
surrounding try..catch with after) so OsPid2/SocatFd2 are always cleaned up.
In `@tests/libs/estdlib/test_serial_dist.erl`:
- Around line 429-434: stop_controller currently unlinks and calls exit(Ctrl,
shutdown) then sleeps 10ms, which is racy; instead create a monitor for Ctrl
(erlang:monitor(process, Ctrl)), send exit(Ctrl, shutdown), and block waiting
for the {'DOWN', Ref, process, Ctrl, _Reason} message (then demonitor if needed)
so teardown waits for the controller to actually die; update the stop_controller
function to use monitor/2 and match on the DOWN tuple rather than a timed
receive.
---
Outside diff comments:
In `@examples/erlang/system_info_server.erl`:
- Around line 102-103: The response uses proplists which json:encode/1 will
serialize as arrays; replace the proplist error and any proc_info_list/1 output
with maps so objects are encoded correctly: change the 404 clause returning
[{error, <<"Process not found.">>}] to return a map (e.g. #{error => <<"Process
not found.">>}), and update proc_info_list/1 to convert the list of {Key,Val}
tuples from erlang:process_info/2 into a map (e.g. via maps:from_list/1 or a
fold) before passing to json:encode/1; ensure all code paths that build response
bodies use maps not proplists so json:encode/1 emits JSON objects.
---
Duplicate comments:
In @.github/workflows/build-and-test.yaml:
- Around line 445-450: The shell condition used to detect an empty CFLAGS uses
the localized string syntax ($"CFLAGS") which never expands the variable; update
the conditional in the "Set CFLAGS" step to use proper variable expansion and an
emptiness test (use ${CFLAGS} and the -z test or compare to an empty string) so
that when CFLAGS is unset/empty the block writes DEFAULT_CFLAGS into GITHUB_ENV
for both CFLAGS and CXXFLAGS.
In `@libs/jit/src/jit_dwarf.erl`:
- Around line 353-355: The final .debug_loc range is not being closed with the
code end: update generate_debug_loc_section (the call site using LocListOffsets
and the function that assembles the .debug_loc) to obtain the code end offset
from State (or the same source used to finalize code emission) and append/merge
a final location entry that uses that code end as the range end; then modify
merge_loc_entries/1 so it does not terminate the final run as {Off, Off, ...}
but instead emits the final entry with the proper start and the code end (e.g.,
{StartOffset, CodeEnd, LocData,...}) so single-snapshot runs and the
tail-of-code x-registers are preserved. Ensure references to LocListOffsets,
generate_debug_loc_section, merge_loc_entries, and State are updated
consistently.
- Around line 463-539: The section lengths currently include the leading
format/tag/length bytes, causing the emitted .ARM.attributes to be one byte too
long; in generate_arm_attributes_section adjust the length fields so the 4-byte
length values count only the bytes following the length field: set
FileAttributesLength to byte_size(TagValuePairs) (not 1+4+...), and set
TotalLength to VendorLength (not 1+4+VendorLength); apply the same fix in the
other ARM attributes generator referenced around lines 541-590 so all
subsection/section length fields exclude the leading format/tag/length bytes.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 598-599: cancel_scan currently reads ctx->platform_data into local
data and then operates on data->canceled with a CAS, which can race with
event-task paths that clear and free the same object; fix by making the pointer
handoff atomic: claim ownership of ctx->platform_data itself before
dereferencing (e.g., use an atomic_exchange or atomic compare-exchange on
ctx->platform_data to swap in NULL or otherwise guard via the same critical
section used when freeing), then operate on data->canceled and free data; ensure
all other locations mentioned (lines around 627-628, 656-657, 680-681, 713-714,
893-894, 1711, 1760-1773, 1807-1811) follow the same pattern so the pointer is
atomically claimed/cleared before any dereference or free.
In `@tests/libs/estdlib/tests.erl`:
- Around line 59-60: The guard on get_non_networking_tests/1 currently uses
"OTPVersion >= 27" which will match when get_otp_version/0 returns the atom
atomvm (since term ordering can make that comparison true); change the guard to
ensure OTPVersion is an integer and at least 27 (e.g. "is_integer(OTPVersion),
OTPVersion >= 27") so that atomvm (or other non-integer returns) won't satisfy
the clause; update the clause for get_non_networking_tests/1 accordingly.
---
Nitpick comments:
In `@doc/src/distributed-erlang.md`:
- Around line 140-142: The fenced code block showing the binary format line
`<<16#AA, 16#55, Length:LenBits/big, Payload:Length/binary, CRC32:32/big>>` is
missing a language specifier; update that fence to include a plaintext specifier
(e.g., change ``` to ```text or ```plaintext) so static analysis and renderers
treat it as non-executable text while preserving the exact schema string.
In `@examples/erlang/serial_disterl.erl`:
- Around line 97-101: The basename/1 function contains a dead branch because
lists:last(...) returns an element (e.g., "" for a trailing slash) not an empty
list, so the `[] -> Path` clause never matches; to fix, simplify the function by
removing the unreachable `[] -> Path` clause or change the match to handle an
empty string explicitly (e.g., match on ""), updating the clause in basename/1
accordingly and keeping the Name -> Name behavior for non-empty names.
In `@libs/estdlib/src/json.erl`:
- Around line 631-639: The recursive helper normalize_float(<<_, Rest/binary>>,
Orig, Pos) can reach an empty binary and crash; add a defensive base clause
(e.g., normalize_float(<<>>, Orig, _Pos) -> Orig or similar) to return the
original binary when no 'e'/'E' is found so normalize_float/1 and
normalize_float/3 never fail on exhausted input; update the clause for
normalize_float/3 to handle the empty-remaining case and keep existing behavior
when an exponent byte is found.
- Around line 183-186: The current broad clause "catch error:_" that returns
error(unexpected_end) masks all runtime errors; change the catch so it only
matches the expected parsing error (e.g. catch error:unexpected_end ->
error(unexpected_end)) and re-raise or propagate any other errors instead of
converting them to unexpected_end (for example rethrow the original error or
call erlang:error/1). Locate the "catch error:_" clause in json.erl and replace
it with a try/catch or specific catch arms that handle only the parsing-related
error and let other errors bubble up.
In `@libs/estdlib/src/serial_dist.erl`:
- Around line 160-171: Modify the port-opening logic so failures are captured
instead of only printed: change the lists:filtermap block that builds OpenPorts
(calling UartMod:open for each UartOpts from UartConfigs) to also accumulate
failed attempts (e.g., a list of {UartOpts, Reason}) and log them via the
project's logger at an appropriate severity; return or propagate a tuple such as
{ok, OpenPorts, FailedPorts} or {error, no_uart_ports} when none opened, and
update callers that rely on OpenPorts accordingly so callers can detect when
some or all ports failed.
In `@libs/estdlib/src/timer.erl`:
- Around line 88-95: The helper process spawned in apply_after (spawn(fun() ->
receive apply_now -> apply(Module, Function, Arguments) end end)) is leaked when
the timer is canceled because it blocks forever waiting for apply_now; update
the spawned process logic in apply_after to handle cancellation by either (a)
adding a receive timeout (after <reasonable_delay> -> exit(normal)) so the
helper self-terminates if apply_now never arrives, or (b) monitoring the
timer/manager and exit on the monitor 'DOWN' or explicit cancel notification;
ensure you still call apply(Module, Function, Arguments) on apply_now and pass
the same TRef from timer_manager:send_after to any monitor/cancellation logic so
the helper can detect cancellation and clean up Pid.
In `@libs/exavmlib/lib/Keyword.ex`:
- Around line 10-12: The comment in Keyword.ex references the upstream put_new/3
implementation using a "blob/main" URL which can drift; update that inline
comment to point to a fixed upstream tag or commit SHA instead (replace the
current
"https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/keyword.ex" link
with the corresponding tag or commit URL for put_new/3) so the provenance is
reproducible; ensure the comment still names put_new/3 and retains the same
explanatory text but uses the stable URL.
In `@tests/libs/estdlib/CMakeLists.txt`:
- Around line 78-93: The add_custom_command uses a POSIX-only "mkdir -p" in its
COMMAND list; replace that with CMake's cross-platform directory creation by
invoking ${CMAKE_COMMAND} -E make_directory for
${CMAKE_CURRENT_BINARY_DIR}/beam_beams before running erlc (update the COMMAND
sequence in the add_custom_command that produces serial_dist_controller.beam and
serial_dist.beam), ensuring the rest of the pipeline (erlc, ${CMAKE_COMMAND} -E
copy) remains unchanged so Windows and other non-POSIX environments can create
the target directory.
In `@tests/libs/estdlib/file_uart_hal.erl`:
- Around line 46-49: The spec for open/1 is incorrect: the implementation always
crashes on start failure and returns a pid, so update the function spec to -spec
open(list()) -> pid(). Alternatively, if you want to return errors instead of
crashing, change the implementation of open/1 to pattern-match the
gen_server:start/3 result (case gen_server:start(?MODULE, Opts, []) of {ok, Pid}
-> Pid; {error, Reason} -> {error, Reason} end) and keep the current spec;
adjust either the spec or the body (referencing open/1 and gen_server:start in
file_uart_hal.erl).
In `@tests/libs/estdlib/mock_uart_hal.erl`:
- Around line 112-114: The handle_call clause for {write, Data} currently
assumes `#state`{peer = Peer} is set and does a raw send (Peer ! {mock_uart_data,
Data}), which will crash if Peer is undefined; update the clause in
handle_call/3 (the {write, Data} clause in mock_uart_hal) to explicitly handle
the undefined peer case—either by adding a separate pattern match for
`#state`{peer = undefined} that returns a safe error reply (e.g., {reply, {error,
no_peer}, State}) or by adding a guard to ensure Peer =/= undefined before
sending, and otherwise return an informative error instead of crashing;
reference create_pair/0 and set_peer when documenting/justifying the guard.
In `@tests/libs/estdlib/test_serial_dist.erl`:
- Around line 234-245: The test only checks the oversize 8193 case but not that
a frame length exactly equal to MAX_HANDSHAKE_FRAME_SIZE is accepted; update
test_scan_frame_max_handshake_boundary to also build a fake handshake length of
8192 (e.g. create LenAndPayload with 8192:16 and a small payload, compute CRC
the same way and assemble Buffer) and call
serial_dist_controller:scan_frame(Buffer, 16) expecting the accepted result (the
successful frame/ok tuple the scanner returns for valid frames) to verify the
fence‑post boundary handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8ead83d7-d646-43d4-98e5-986d318c3fa7
⛔ Files ignored due to path filters (1)
src/platforms/esp32/partitions.csvis excluded by!**/*.csv
📒 Files selected for processing (90)
.github/workflows/build-and-test-macos.yaml.github/workflows/build-and-test.yaml.github/workflows/build-libraries.yaml.github/workflows/build-linux-artifacts.yaml.github/workflows/esp32-mkimage.yaml.github/workflows/pico-build.yaml.github/workflows/run-tests-with-beam.yaml.github/workflows/wasm-build.yamlCHANGELOG.mdCMakeLists.txtCMakeModules/BuildElixir.cmakeCMakeModules/BuildErlang.cmakeUPDATING.mddoc/src/atomvm-internals.mddoc/src/atomvm-tooling.mddoc/src/build-instructions.mddoc/src/distributed-erlang.mddoc/src/getting-started-guide.mddoc/src/jit.mddoc/src/network-programming-guide.mdexamples/erlang/CMakeLists.txtexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erlexamples/erlang/serial_disterl.erlexamples/erlang/system_info_server.erllibs/avm_network/src/network.erllibs/eavmlib/src/CMakeLists.txtlibs/eavmlib/src/json_encoder.erllibs/estdlib/src/CMakeLists.txtlibs/estdlib/src/binary.erllibs/estdlib/src/json.erllibs/estdlib/src/net_kernel.erllibs/estdlib/src/net_kernel_sup.erllibs/estdlib/src/serial_dist.erllibs/estdlib/src/serial_dist_controller.erllibs/estdlib/src/timer.erllibs/exavmlib/lib/CMakeLists.txtlibs/exavmlib/lib/Keyword.exlibs/exavmlib/lib/json.exlibs/jit/include/jit.hrllibs/jit/src/CMakeLists.txtlibs/jit/src/jit_armv6m.erllibs/jit/src/jit_armv6m_asm.erllibs/jit/src/jit_armv7m_asm.erllibs/jit/src/jit_dwarf.erllibs/jit/src/jit_precompile.erlsrc/libAtomVM/CMakeLists.txtsrc/libAtomVM/context.csrc/libAtomVM/defaultatoms.defsrc/libAtomVM/jit.csrc/libAtomVM/jit.hsrc/libAtomVM/module.csrc/libAtomVM/nifs.csrc/libAtomVM/opcodesswitch.hsrc/libAtomVM/stacktrace.csrc/platforms/esp32/GetBootAVM.cmakesrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.csrc/platforms/rp2/CMakeLists.txtsrc/platforms/stm32/CMakeLists.txttests/CMakeLists.txttests/erlang_tests/CMakeLists.txttests/erlang_tests/binary_at_test.erltests/erlang_tests/binary_first_test.erltests/erlang_tests/binary_last_test.erltests/erlang_tests/test_binary_split.erltests/erlang_tests/test_raise_built_stacktrace.erltests/libs/estdlib/CMakeLists.txttests/libs/estdlib/file_uart_hal.erltests/libs/estdlib/mock_uart_hal.erltests/libs/estdlib/test_binary.erltests/libs/estdlib/test_json.erltests/libs/estdlib/test_serial_dist.erltests/libs/estdlib/test_serial_dist_beam_peer.erltests/libs/estdlib/test_serial_dist_socat.erltests/libs/estdlib/test_serial_dist_socat_peer.erltests/libs/estdlib/test_timer.erltests/libs/estdlib/tests.erltests/libs/exavmlib/Tests.extests/libs/jit/CMakeLists.txttests/libs/jit/jit_armv7m_asm_tests.erltests/libs/jit/jit_armv7m_tests.erltests/libs/jit/jit_tests_common.erltests/libs/jit/tests.erltests/test.ctools/test/smoke-test/README.mdversion.cmake
💤 Files with no reviewable changes (2)
- libs/eavmlib/src/CMakeLists.txt
- libs/eavmlib/src/json_encoder.erl
✅ Files skipped from review due to trivial changes (15)
- version.cmake
- libs/jit/src/jit_armv6m_asm.erl
- tools/test/smoke-test/README.md
- libs/jit/src/CMakeLists.txt
- src/platforms/esp32/test/main/test_main.c
- tests/libs/jit/tests.erl
- examples/erlang/CMakeLists.txt
- libs/jit/include/jit.hrl
- doc/src/atomvm-tooling.md
- doc/src/jit.md
- examples/erlang/esp32/CMakeLists.txt
- CHANGELOG.md
- doc/src/getting-started-guide.md
- UPDATING.md
- doc/src/build-instructions.md
🚧 Files skipped from review as they are similar to previous changes (3)
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- src/platforms/esp32/GetBootAVM.cmake
- doc/src/network-programming-guide.md
👮 Files not reviewed due to content moderation or server errors (2)
- libs/avm_network/src/network.erl
- src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl
| -module(jit_dwarf). | ||
|
|
||
| -include("jit_dwarf.hrl"). | ||
| -include_lib("jit.hrl"). |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Looking for the header and current include usage..."
fd -a '^jit\.hrl$' .
rg -n --context 2 -- '-include_lib\("jit\.hrl"\)|-include_lib\("jit/include/jit\.hrl"\)|JIT_VARIANT_THUMB2' .Repository: UncleGrumpy/AtomVM
Length of output: 5662
🏁 Script executed:
# Check the app name and structure
cat libs/jit/src/jit.app.src 2>/dev/null || echo "jit.app.src not found"
# Check for rebar or other build configuration
fd -a '(rebar\.config|rebar\.lock|mix\.exs)$' . | head -20
# Look at the directory structure of libs/jit
tree -L 3 libs/jit/ 2>/dev/null || find libs/jit -type f -o -type d | head -30Repository: UncleGrumpy/AtomVM
Length of output: 2259
🏁 Script executed:
# Check the rebar.conf file for libs/jit
cat libs/jit/rebar.conf
# Also check if there's a root rebar.config that might set ERL_LIBS
cat rebar.config 2>/dev/null || echo "No root rebar.config"Repository: UncleGrumpy/AtomVM
Length of output: 1055
🏁 Script executed:
# Search for any compilation or build documentation
find . -name "*.md" -o -name "CMakeLists.txt" | xargs grep -l "erl_" 2>/dev/null | head -5
# Check if there's a top-level CMakeLists.txt that shows how Erlang compilation is configured
head -100 CMakeLists.txt 2>/dev/null || echo "Not found at root"
# Look for any erlang compiler invocation or documentation
find . -type f \( -name "CMakeLists.txt" -o -name "*.cmake" \) -path "*/libs/jit/*" | xargs cat 2>/dev/null | grep -A5 -B5 "erl_" | head -30Repository: UncleGrumpy/AtomVM
Length of output: 4098
🏁 Script executed:
# Check libs/jit CMakeLists.txt
cat libs/jit/src/CMakeLists.txt
# Also check libs/CMakeLists.txt
cat libs/CMakeLists.txt 2>/dev/null | head -100Repository: UncleGrumpy/AtomVM
Length of output: 6385
🏁 Script executed:
# Find and check the BuildErlang.cmake file
find . -name "BuildErlang.cmake" -exec cat {} \;
# Also check if there's any ERL_LIBS configuration
grep -r "ERL_LIBS\|include_lib" CMakeModules/ 2>/dev/null | head -20Repository: UncleGrumpy/AtomVM
Length of output: 24170
🏁 Script executed:
# Search for any compilation errors related to include_lib in the codebase or CI logs
find . -name "*.log" -o -name "*.txt" | xargs grep -l "include_lib\|jit.hrl" 2>/dev/null | head -5
# Check if there's any build history or CI configuration
find . -name ".github" -o -name ".gitlab-ci.yml" -o -name "*.yml" -path "*/.github/*" 2>/dev/null | head -5Repository: UncleGrumpy/AtomVM
Length of output: 71
🏁 Script executed:
# Check the GitHub Actions workflows
find .github/workflows -type f -name "*.yml" | head -5
# Read one of the workflow files to understand the build process
cat .github/workflows/erlang.yml 2>/dev/null || ls -la .github/workflows/Repository: UncleGrumpy/AtomVM
Length of output: 1373
🏁 Script executed:
# Check the build-libraries.yaml to see if jit modules are built
cat .github/workflows/build-libraries.yaml | head -100
# Also check if there are any error logs or test results
find . -name "*.log" -type f 2>/dev/null | head -10Repository: UncleGrumpy/AtomVM
Length of output: 2424
🏁 Script executed:
# Let's verify the actual Erlang include_lib semantics by checking documentation
# and testing if the current syntax would work
web_search "Erlang include_lib syntax documentation"Repository: UncleGrumpy/AtomVM
Length of output: 112
🌐 Web query:
Erlang include_lib syntax "app/path/file.hrl" documentation
💡 Result:
In Erlang, -include_lib("app/path/file.hrl"). is documented as:
-include_lib(File).works like-include(File)., but the first path component is treated as an OTP application name (after any$VARenv-var substitution). (erlang.org)- The compiler/preprocessor then uses the code server to find that application’s installed directory (for example via
code:lib_dir(App)), and looks for the header relative to that location—most commonly using the application’sinclude/directory, hence the conventional form:
-include_lib("mnesia/include/mnemosyne.hrl").(erlang.org) - Example from the official docs:
-include_lib("kernel/include/file.hrl").(erlang.org)
Citations:
- 1: https://www.erlang.org/doc/system/macros
- 2: https://www.erlang.org/doc/system/macros
- 3: https://www.erlang.org/doc/system/macros
🏁 Script executed:
# Let's check the git history to see if these files have been compiled/tested recently
git log --oneline -20 -- libs/jit/src/jit_dwarf.erl 2>/dev/null | head -10
# Check if there are any recent changes to the include statements
git log -p --all -S '-include_lib("jit.hrl")' -- libs/jit/src/ 2>/dev/null | head -100Repository: UncleGrumpy/AtomVM
Length of output: 98
🏁 Script executed:
# Check if the jit modules are actually compiled in the CMakeLists.txt
grep -n "jit_dwarf\|jit.erl" libs/jit/src/CMakeLists.txt
# Verify that the standard form is NOT used anywhere (check if there are any files using the correct syntax)
rg -- '-include_lib\("jit/include/jit\.hrl"\)' libs/jit/src/Repository: UncleGrumpy/AtomVM
Length of output: 255
Use an application-relative path in -include_lib.
Line 24 uses invalid -include_lib syntax. Erlang's -include_lib expects "app/include/file.hrl" format; with just "jit.hrl" the compiler cannot resolve the header, breaking compilation of ?JIT_VARIANT_THUMB2 and other macros from jit.hrl.
Suggested fix
--include_lib("jit.hrl").
+-include_lib("jit/include/jit.hrl").📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| -include_lib("jit.hrl"). | |
| -include_lib("jit/include/jit.hrl"). |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/jit/src/jit_dwarf.erl` at line 24, Replace the invalid
-include_lib("jit.hrl") with an application-relative include in the form
"app_name/include/jit.hrl" so the compiler can find the header and resolve
macros like ?JIT_VARIANT_THUMB2 and others used in jit_dwarf.erl; locate the
-include_lib directive in jit_dwarf.erl and change it to use the providing OTP
application's name (app_name/include/jit.hrl) so the header is correctly located
at compile time.
| static const char *const scan_canceled = ATOM_STR("\xD", "scan_canceled"); | ||
| if (UNLIKELY(!ensure_atom(global, scan_canceled))) { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Use the public canceled atom in scan callback errors.
This path currently emits {Ref, {scan_results, {error, scan_canceled}}}, but scan_canceled is the internal control atom used by cancel_scan/4. Callback consumers are supposed to receive {error, canceled} here, so this will miss existing matches/tests.
🔧 Minimal fix
@@
static const char *const scan_canceled = ATOM_STR("\xD", "scan_canceled");
if (UNLIKELY(!ensure_atom(global, scan_canceled))) {
return false;
}
+ static const char *const canceled = ATOM_STR("\x8", "canceled");
+ if (UNLIKELY(!ensure_atom(global, canceled))) {
+ return false;
+ }
@@
BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2), heap);
send_scan_error_from_task(global, data->owner_process_id,
- globalcontext_existing_term_from_atom_string(global, ATOM_STR("\xD", "scan_canceled")),
+ globalcontext_existing_term_from_atom_string(global, ATOM_STR("\x8", "canceled")),
data->ref_ticks, heap);
END_WITH_STACK_HEAP(heap, global);Based on learnings: cancel_scan now delivers {error, canceled} to the scan_done callback as well as to direct blocking callers.
Also applies to: 887-890
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines
296 - 299, The code currently constructs and ensures the internal atom
scan_canceled (symbol scan_canceled) for scan callback errors; change it to use
the public canceled atom instead (the atom named "canceled") so callbacks
receive {error, canceled} as expected: replace uses of the internal
scan_canceled atom creation/ensure with the public canceled atom
(ensure_atom(global, canceled) or equivalent), and update the same pattern also
at the other occurrence around the similar block (the one noted at the 887-890
region) so both scan callback and scan_done paths emit {error, canceled} rather
than {error, scan_canceled}.
| } else { | ||
| if (UNLIKELY(!term_is_integer(cfg_dwell))) { | ||
| ESP_LOGE(TAG, "Channel dwell time milliseconds must be an integer (example: {dwell, 250})"); | ||
| send_scan_error_reason(ctx, pid, ref, BADARG_ATOM); | ||
| return; | ||
| } | ||
| int32_t dwell_config = term_to_uint32(cfg_dwell); | ||
| if (UNLIKELY((dwell_config < 1lu) || (dwell_config > 1500lu))) { | ||
| ESP_LOGE(TAG, "Per channel dwell time milliseconds must be between 1 and 1500"); | ||
| send_scan_error_reason(ctx, pid, ref, BADARG_ATOM); | ||
| return; | ||
| } | ||
|
|
||
| dwell_ms = (uint32_t) dwell_config; | ||
| ESP_LOGD(TAG, "Scan will spend %lu ms per channel", dwell_ms); |
There was a problem hiding this comment.
Validate dwell before narrowing it.
term_to_uint32(cfg_dwell) truncates boxed integers before the range check. A very large Erlang integer can wrap into 1..1500 and start a scan instead of returning badarg. Read the original integer into a signed/wider type, validate it, then cast to uint32_t.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines
1629 - 1643, The code currently uses term_to_uint32(cfg_dwell) which can
truncate large Erlang integers before the range check; change the flow in the
scan config path (the cfg_dwell handling that uses term_is_integer,
term_to_uint32, dwell_config, dwell_ms) to first read the Erlang integer into a
wider signed type (e.g., int64_t or int64) using the appropriate term-to-int64
helper, validate that it is an integer and within 1..1500, and only then cast to
uint32_t for dwell_ms; on validation failure call send_scan_error_reason(ctx,
pid, ref, BADARG_ATOM) and keep the same log messages (ESP_LOGE/ESP_LOGD) and
control flow.
| init(Opts) -> | ||
| Peripheral = proplists:get_value(peripheral, Opts), | ||
| Cmd = "socat -b 65536 - " ++ Peripheral ++ ",rawer", | ||
| Port = open_port({spawn, Cmd}, [binary, stream, exit_status]), | ||
| {ok, #state{port = Port, buffer = <<>>, waiter = undefined}}. |
There was a problem hiding this comment.
Command injection risk in shell spawn.
The Peripheral value from options is directly interpolated into the shell command string without sanitization. If Peripheral contained shell metacharacters (e.g., ; rm -rf /), they would be executed.
Since this is test-only code and the Peripheral value comes from test configuration, the risk is low. However, for defense in depth, consider validating the peripheral path.
Optional: Basic path validation
init(Opts) ->
Peripheral = proplists:get_value(peripheral, Opts),
+ %% Basic sanity check - peripheral should look like a device path
+ case Peripheral of
+ "/dev/" ++ _ -> ok;
+ _ -> error({invalid_peripheral, Peripheral})
+ end,
Cmd = "socat -b 65536 - " ++ Peripheral ++ ",rawer",
Port = open_port({spawn, Cmd}, [binary, stream, exit_status]),
{ok, `#state`{port = Port, buffer = <<>>, waiter = undefined}}.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/libs/estdlib/file_uart_hal.erl` around lines 75 - 79, The code builds a
shell command by concatenating Peripheral into Cmd in init/1, which risks
command injection; instead validate Peripheral (e.g., allow only characters like
letters, digits, / _ . -) and call open_port with spawn_executable to avoid a
shell. Concretely: in init/1 validate the Peripheral value (reject or error on
characters like ; & | $ ` or whitespace), build args such as ["-b","65536","-",
Peripheral ++ ",rawer"] and call open_port({spawn_executable, "socat"}, [binary,
stream, exit_status, {args, Args}]) so you stop interpolating Peripheral into a
shell string and remove Cmd.
| PeerFdB = start_peer_node(atomvm, PtyB, "ping"), | ||
| handle_peer_request("ping", PeerFdB), | ||
| ResultB = read_peer_line(PeerFdB), | ||
| io:format("multi_port peer B result: ~s~n", [ResultB]), | ||
| <<"pong">> = ResultB, | ||
| %% Peer C on second UART (keep B alive to avoid disturbing A) | ||
| PeerFdC = start_peer_node(atomvm, PtyC, "ping"), | ||
| handle_peer_request("ping", PeerFdC), | ||
| ResultC = read_peer_line(PeerFdC), | ||
| io:format("multi_port peer C result: ~s~n", [ResultC]), | ||
| <<"pong">> = ResultC, | ||
| catch unregister(test_serial), | ||
| ok | ||
| after | ||
| catch net_kernel:stop(), | ||
| stop_socat(OsPid1, SocatFd1), | ||
| stop_socat(OsPid2, SocatFd2) | ||
| end. |
There was a problem hiding this comment.
Close the peer subprocess FDs in the multi-port path.
Unlike run_serial_test/2, this code never closes PeerFdB or PeerFdC. That leaks descriptors even on success and makes failure paths keep peer I/O alive longer than intended.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/libs/estdlib/test_serial_dist_socat.erl` around lines 200 - 217, The
test never closes the peer subprocess file descriptors PeerFdB and PeerFdC after
use; mirror run_serial_test/2 by closing them when done (e.g. call the cleanup
helper used for peers such as close_peer_node(PeerFdB) and
close_peer_node(PeerFdC) or the appropriate file/socket close function
immediately after the <<"pong">> = ResultB/ResultC assertions) and also add
defensive closes in the after block (e.g. catch close_peer_node(PeerFdB) and
catch close_peer_node(PeerFdC)) so descriptors are released on both success and
failure paths.
| stop_controller(Ctrl) -> | ||
| unlink(Ctrl), | ||
| exit(Ctrl, shutdown), | ||
| receive | ||
| after 10 -> ok | ||
| end. |
There was a problem hiding this comment.
Wait for the controller to die instead of sleeping.
exit(Ctrl, shutdown) is asynchronous, so the 10 ms delay does not guarantee teardown before the UART endpoints are closed or the next test starts. This can make the controller tests flaky.
Suggested fix
stop_controller(Ctrl) ->
+ Ref = erlang:monitor(process, Ctrl),
unlink(Ctrl),
exit(Ctrl, shutdown),
receive
- after 10 -> ok
+ {'DOWN', Ref, process, Ctrl, _} -> ok
+ after 5000 ->
+ exit({controller_stop_timeout, Ctrl})
end.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| stop_controller(Ctrl) -> | |
| unlink(Ctrl), | |
| exit(Ctrl, shutdown), | |
| receive | |
| after 10 -> ok | |
| end. | |
| stop_controller(Ctrl) -> | |
| Ref = erlang:monitor(process, Ctrl), | |
| unlink(Ctrl), | |
| exit(Ctrl, shutdown), | |
| receive | |
| {'DOWN', Ref, process, Ctrl, _} -> ok | |
| after 5000 -> | |
| exit({controller_stop_timeout, Ctrl}) | |
| end. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/libs/estdlib/test_serial_dist.erl` around lines 429 - 434,
stop_controller currently unlinks and calls exit(Ctrl, shutdown) then sleeps
10ms, which is racy; instead create a monitor for Ctrl (erlang:monitor(process,
Ctrl)), send exit(Ctrl, shutdown), and block waiting for the {'DOWN', Ref,
process, Ctrl, _Reason} message (then demonitor if needed) so teardown waits for
the controller to actually die; update the stop_controller function to use
monitor/2 and match on the DOWN tuple rather than a timed receive.
|
🧠 Learnings used✅ Actions performedReviews paused. |
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
doc/src/build-instructions.md (1)
530-542:⚠️ Potential issue | 🟠 MajorUpdate the remaining application-flash example.
These lines move
main.avmto0x250000, but Line 663 in the same guide still shows an app being written to0x00210000. That leaves the page self-contradictory and can send users to the wrong flash offset.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/build-instructions.md` around lines 530 - 542, The document currently states that main.avm starts at 0x250000 but an application-flash example still writes to 0x00210000; update that example so any commands or references that flash main.avm use 0x250000 (replace occurrences of 0x00210000 / 0x210000 with 0x250000) and ensure the surrounding text (the application-flash example and its caption) consistently refers to main.avm at 0x250000; look for the example that mentions main.avm, boot.avm, or the flash write command and change the offset accordingly.
♻️ Duplicate comments (18)
src/platforms/esp32/test/main/test_main.c (1)
622-626:⚠️ Potential issue | 🟠 MajorGate
test_wifi_scanfrom default[test_run]until bounded scan timing is guaranteed.On Line 622, this test is enabled in the default run despite prior simulator evidence of a 180s timeout path. Unless
test_wifi_scan.beamis now bounded (short internal timeout and/or active/channel-limited scan), this keeps CI fragile.#!/bin/bash # Verify whether wifi scan test has bounded timing behavior. # Expected: explicit short timeout behavior and/or scan options that bound duration. set -euo pipefail fd -i test_wifi_scan.erl # Inspect scan invocation options and timeout handling in the test module. rg -n -C4 --type=erlang '\bwifi_scan\s*\(|timeout|after\s+[0-9]+|channel|active|get_num_channels_timeout' src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl || true # Inspect network module timeout helper that scan test may rely on. rg -n -C4 --type=erlang '\bget_num_channels_timeout\s*\(' libs/avm_network/src/network.erl || true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/test/main/test_main.c` around lines 622 - 626, The test test_wifi_scan is currently in the default TEST_CASE("[test_run]") and must be gated until its internal scan duration is proven bounded; change the test metadata or invocation so it no longer runs by default (for example, move TEST_CASE("test_wifi_scan", "[test_run]") to a non-default tag like "[slow]" or "[wifi_scan]"), or alternatively wrap the avm_test_case("test_wifi_scan.beam") call with a deterministic timeout/assertion so the test cannot hang CI; update the TEST_CASE declaration and/or the avm_test_case usage accordingly to reference the new tag or include an explicit timeout guard.tests/libs/estdlib/tests.erl (1)
59-60:⚠️ Potential issue | 🟠 MajorGuard the OTP-27 clause with an integer check.
OTPVersioncan beatomvm, and this guard can still match unintentionally. Restrict this clause to numeric OTP versions.Suggested fix
-get_non_networking_tests(OTPVersion) when OTPVersion >= 27 -> +get_non_networking_tests(OTPVersion) when is_integer(OTPVersion), OTPVersion >= 27 -> [test_sets, test_json | get_non_networking_tests(26)];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/tests.erl` around lines 59 - 60, The clause for get_non_networking_tests(OTPVersion) currently uses a numeric comparison OTPVersion >= 27 but can match non-numeric values like the atom 'atomvm'; update the guard to require an integer (e.g., use is_integer(OTPVersion) and OTPVersion >= 27) so the clause only matches numeric OTP versions and leaves atoms to other clauses; update the guard on get_non_networking_tests/1 accordingly..github/workflows/build-libraries.yaml (1)
122-129:⚠️ Potential issue | 🟠 MajorSanitize tag names before embedding in artifact paths.
Using
github.ref_namedirectly in filenames still breaks tags containing/, which can fail rename and release file matching.🛠️ Proposed fix
- name: "Rename and write sha256sum" working-directory: build run: | + SAFE_REF="${GITHUB_REF_NAME//\//-}" for variant in atomvmlib atomvmlib-esp32 atomvmlib-rp2 atomvmlib-stm32 atomvmlib-emscripten; do - VARIANT_FILE="${variant}-${{ github.ref_name }}.avm" + VARIANT_FILE="${variant}-${SAFE_REF}.avm" mv "libs/${variant}.avm" "libs/${VARIANT_FILE}" && sha256sum "libs/${VARIANT_FILE}" > "libs/${VARIANT_FILE}.sha256" done - HELLO_WORLD_FILE=hello_world-${{ github.ref_name }}.avm + HELLO_WORLD_FILE=hello_world-${SAFE_REF}.avm mv examples/erlang/hello_world.avm "examples/erlang/${HELLO_WORLD_FILE}" sha256sum "examples/erlang/${HELLO_WORLD_FILE}" > "examples/erlang/${HELLO_WORLD_FILE}.sha256" @@ with: draft: true fail_on_unmatched_files: true files: | - build/libs/atomvmlib-${{ github.ref_name }}.avm - build/libs/atomvmlib-${{ github.ref_name }}.avm.sha256 - build/libs/atomvmlib-esp32-${{ github.ref_name }}.avm - build/libs/atomvmlib-esp32-${{ github.ref_name }}.avm.sha256 - build/libs/atomvmlib-rp2-${{ github.ref_name }}.avm - build/libs/atomvmlib-rp2-${{ github.ref_name }}.avm.sha256 - build/libs/atomvmlib-stm32-${{ github.ref_name }}.avm - build/libs/atomvmlib-stm32-${{ github.ref_name }}.avm.sha256 - build/libs/atomvmlib-emscripten-${{ github.ref_name }}.avm - build/libs/atomvmlib-emscripten-${{ github.ref_name }}.avm.sha256 - build/examples/erlang/hello_world-${{ github.ref_name }}.avm - build/examples/erlang/hello_world-${{ github.ref_name }}.avm.sha256 + build/libs/atomvmlib-${{ env.SAFE_REF }}.avm + build/libs/atomvmlib-${{ env.SAFE_REF }}.avm.sha256 + build/libs/atomvmlib-esp32-${{ env.SAFE_REF }}.avm + build/libs/atomvmlib-esp32-${{ env.SAFE_REF }}.avm.sha256 + build/libs/atomvmlib-rp2-${{ env.SAFE_REF }}.avm + build/libs/atomvmlib-rp2-${{ env.SAFE_REF }}.avm.sha256 + build/libs/atomvmlib-stm32-${{ env.SAFE_REF }}.avm + build/libs/atomvmlib-stm32-${{ env.SAFE_REF }}.avm.sha256 + build/libs/atomvmlib-emscripten-${{ env.SAFE_REF }}.avm + build/libs/atomvmlib-emscripten-${{ env.SAFE_REF }}.avm.sha256 + build/examples/erlang/hello_world-${{ env.SAFE_REF }}.avm + build/examples/erlang/hello_world-${{ env.SAFE_REF }}.avm.sha256Also applies to: 138-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/build-libraries.yaml around lines 122 - 129, Sanitize the tag name before embedding it in artifact filenames: instead of using github.ref_name directly when constructing VARIANT_FILE and HELLO_WORLD_FILE, normalize the ref (e.g., replace "/" with "-" or otherwise URL-safe/shell-safe transform) and use that sanitized variable in the mv and sha256sum commands; update the same pattern used later (the block referenced around lines 138-149) so all artifact paths use the sanitized ref to avoid failures when tags contain slashes.tests/libs/estdlib/test_serial_dist_socat.erl (2)
200-211:⚠️ Potential issue | 🟡 MinorPeer subprocess FDs are not closed.
Unlike
run_serial_test/2which closesPeerFdin itsafterblock (line 133),PeerFdBandPeerFdCare never closed here. This leaks file descriptors on both success and failure paths.🔧 Suggested fix
%% Peer B on first UART PeerFdB = start_peer_node(atomvm, PtyB, "ping"), - handle_peer_request("ping", PeerFdB), - ResultB = read_peer_line(PeerFdB), - io:format("multi_port peer B result: ~s~n", [ResultB]), - <<"pong">> = ResultB, + try + handle_peer_request("ping", PeerFdB), + ResultB = read_peer_line(PeerFdB), + io:format("multi_port peer B result: ~s~n", [ResultB]), + <<"pong">> = ResultB + after + catch atomvm:posix_close(PeerFdB) + end, %% Peer C on second UART (keep B alive to avoid disturbing A) PeerFdC = start_peer_node(atomvm, PtyC, "ping"), - handle_peer_request("ping", PeerFdC), - ResultC = read_peer_line(PeerFdC), - io:format("multi_port peer C result: ~s~n", [ResultC]), - <<"pong">> = ResultC, + try + handle_peer_request("ping", PeerFdC), + ResultC = read_peer_line(PeerFdC), + io:format("multi_port peer C result: ~s~n", [ResultC]), + <<"pong">> = ResultC + after + catch atomvm:posix_close(PeerFdC) + end, catch unregister(test_serial),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/test_serial_dist_socat.erl` around lines 200 - 211, The test leaks peer subprocess file descriptors PeerFdB and PeerFdC because they are not closed; update this test to mirror run_serial_test/2 by ensuring PeerFdB and PeerFdC are closed in an after/cleanup block (or use try...after) so both success and failure paths close the FDs, and keep the catch unregister(test_serial) call for cleanup; locate start_peer_node/3, handle_peer_request/2, read_peer_line/1 and ensure their returned FDs are closed with file:close/1 (or the existing close helper) in the cleanup.
167-179:⚠️ Potential issue | 🟡 MinorReal test failures may be masked as PTY allocation failures.
The
try...ofconstruct wraps both the secondstart_socat()call and the call totest_multi_port_ping/8. If the test body fails, the catch at lines 174-179 reports it as "not enough PTYs" instead of propagating the actual failure.🔧 Suggested fix
test_multi_port_ping() -> {OsPid1, SocatFd1, PtyA1, PtyB} = start_socat(), - try start_socat() of - {OsPid2, SocatFd2, PtyA2, PtyC} -> - test_multi_port_ping(OsPid1, SocatFd1, PtyA1, PtyB, OsPid2, SocatFd2, PtyA2, PtyC) - catch - _:_ -> - stop_socat(OsPid1, SocatFd1), - io:format("test_multi_port_ping: not enough PTYs, skipping~n"), - ok + case catch start_socat() of + {OsPid2, SocatFd2, PtyA2, PtyC} -> + test_multi_port_ping(OsPid1, SocatFd1, PtyA1, PtyB, OsPid2, SocatFd2, PtyA2, PtyC); + _ -> + stop_socat(OsPid1, SocatFd1), + io:format("test_multi_port_ping: not enough PTYs, skipping~n"), + ok end.libs/jit/src/jit_dwarf.erl (4)
24-24:⚠️ Potential issue | 🔴 CriticalUse an application-relative path in
-include_lib.
-include_lib("jit.hrl").does not follow Erlang’s"app/path/file.hrl"form, so the compiler cannot resolve this header through the application code path.Suggested fix
--include_lib("jit.hrl"). +-include_lib("jit/include/jit.hrl").What is the correct Erlang `-include_lib` syntax? Does it require `"app_name/path/to/header.hrl"` rather than just `"header.hrl"`?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_dwarf.erl` at line 24, Replace the bare header include with an application-relative path so the compiler can resolve it; change the -include_lib usage in jit_dwarf.erl from -include_lib("jit.hrl"). to the app/path form, e.g. -include_lib("jit/include/jit.hrl"). ensuring the string uses the "app_name/path/to/header.hrl" format.
443-446:⚠️ Potential issue | 🟠 MajorKeep ARM ELF
e_flagsin sync with the selected variant.
.ARM.attributesis now variant-aware, butbackend_to_elf_flags/1still hardcodes v6-M for everyjit_armv6mbuild and leavesjit_arm32without matching architecture bits. A Thumb-2/v7-M object can therefore advertise different architectures in the ELF header and in.ARM.attributes.For ARM ELF objects, should `e_flags` architecture bits align with the selected `.ARM.attributes` CPU architecture (for example v6-M vs v7-M/Thumb-2)?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_dwarf.erl` around lines 443 - 446, The backend_to_elf_flags/1 function currently hardcodes v6-M for jit_armv6m and leaves jit_arm32 without architecture bits, causing mismatches with the variant-aware .ARM.attributes; update backend_to_elf_flags/1 so each ARM variant returns matching EF_ARM_ARCH_* bits (e.g., include ?EF_ARM_ARCH_V6M for jit_armv6m and the correct ?EF_ARM_ARCH_V7 or Thumb-2 equivalent for jit_arm32 or other variants) while preserving the other flags (?EF_ARM_EABI_VER5, ?EF_ARM_ABI_FLOAT_SOFT), ensuring ELF e_flags align with the chosen .ARM.attributes CPU architecture.
528-529:⚠️ Potential issue | 🟠 MajorExclude the format byte from the
.ARM.attributeslength field.The 32-bit length field starts at itself; counting the leading
$Amakes the encoded subsection one byte too large and can break consumers that parse.ARM.attributesstrictly.Suggested fix
- TotalLength = 1 + 4 + VendorLength, + TotalLength = 4 + VendorLength,In ARM ELF `.ARM.attributes`, does the 4-byte length field exclude the leading format-version byte `A`?Also applies to: 585-585
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_dwarf.erl` around lines 528 - 529, The length calculation for the `.ARM.attributes` subsection currently includes the leading format/version byte by computing TotalLength = 1 + 4 + VendorLength; update the calculation so the 4-byte length field excludes the format byte (i.e., set TotalLength to 4 + VendorLength) and make the same change at the other occurrence around the other TotalLength calculation noted (line ~585) so the encoded subsection size written by the code matches the ARM spec.
1004-1064:⚠️ Potential issue | 🟠 MajorThe last x-register location range is still dropped.
merge_loc_entries/1closes the terminal run as{Off, Off, Loc}, andbuild_x_reg_loc_list/6immediately filters zero-length ranges out. Any x-register location that remains valid through the end of the generated code therefore disappears from.debug_loc.tests/libs/estdlib/file_uart_hal.erl (1)
75-79:⚠️ Potential issue | 🟠 MajorAvoid shell interpolation for
Peripheralwhen spawningsocat.Line 77 concatenates
Peripheraldirectly into a shell command string. This is command-injection prone and should usespawn_executable+ argument vector (with basic peripheral validation).Suggested fix
init(Opts) -> Peripheral = proplists:get_value(peripheral, Opts), - Cmd = "socat -b 65536 - " ++ Peripheral ++ ",rawer", - Port = open_port({spawn, Cmd}, [binary, stream, exit_status]), + ok = validate_peripheral(Peripheral), + Socat = os:find_executable("socat"), + true = (Socat =/= false), + Args = ["-b", "65536", "-", Peripheral ++ ",rawer"], + Port = open_port({spawn_executable, Socat}, [binary, stream, exit_status, {args, Args}]), {ok, `#state`{port = Port, buffer = <<>>, waiter = undefined}}. + +validate_peripheral(Peripheral) -> + case re:run(Peripheral, "^[A-Za-z0-9_./-]+$", [{capture, none}]) of + match -> ok; + nomatch -> error({invalid_peripheral, Peripheral}) + end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/file_uart_hal.erl` around lines 75 - 79, The init/1 function builds a shell command by concatenating Peripheral into Cmd which risks command injection; change open_port({spawn, Cmd}, ...) to open_port({spawn_executable, SocatPath}, [Args...]) where SocatPath is the socat executable and Args is a list like ["-b", "65536", "-", Peripheral ++ ",rawer"] supplied as separate args, and add a small validation step for Peripheral (e.g., allow only expected device name pattern or whitelist) before using it; update references to `#state`{port = Port} to use the new Port from open_port with spawn_executable.doc/src/network-programming-guide.md (1)
337-339:⚠️ Potential issue | 🟡 MinorAP paragraph still has unresolved emphasis/style and grammar issues.
Line 337 still uses
__open network__, and Line 339 still reads “is operation is AP mode only.”Suggested fix
-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 channel 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. +If the channel is omitted the default channel for esp32 is `1`. This setting is only used while a device is operating in 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.🤖 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 337 - 339, Replace the markdown emphasis and fix the grammar in the AP paragraph: remove the literal underscores around "__open network__" and render it as "open network", correct the phrase "is operation is AP mode only" to "is only used while a device is operating in AP mode", and keep the rest of the sentence about ap_channel behavior (including the symbol ap_channel and the explanation about AP+STA channel switching) intact so it reads clearly and grammatically.libs/avm_network/src/network.erl (2)
820-822:⚠️ Potential issue | 🟠 MajorDon't clear
scan_receiverbefore the follow-up unregister error is routed.For callback/pid scans, this clears the dispatch target immediately after the first success. If the driver then sends the intentional
{scan_results, {error, {unregister_handler, Reason}}}message, the later handler seesundefinedand silently drops it. Preserve the callback metadata until that follow-up path is processed.Based on learnings:
send_scan_resultsintentionally sends a second{scan_results, {error, {unregister_handler, Reason}}}after successful results so callback users can decide whether future scans are still viable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 820 - 822, The handler handle_info({Ref, {scan_results, _Results} = Msg}, `#state`{ref = Ref} = State) prematurely clears State#state.scan_receiver causing the follow-up unregister error message to be dropped; instead, stop setting scan_receiver = undefined here and let scan_reply_or_callback/2 (or the code path that processes the follow-up {scan_results, {error, {unregister_handler, Reason}}}) clear the scan_receiver once it has processed both the successful results and any subsequent unregister error; update handle_info to only spawn scan_reply_or_callback(Msg, State) and preserve State#state.scan_receiver, and move the scan_receiver = undefined assignment into scan_reply_or_callback (or into the explicit unregister-error branch) so callback/pid scans receive the follow-up unregister message.
455-472:⚠️ Potential issue | 🟠 MajorCatch all
gen_server:call/2exits here.
stop/0still lets timeout and server-death exits escape fromgen_server:call/2, so callers can crash instead of receiving the advertised{error, Reason}result.Suggested fix
catch exit:{noproc, _} -> erlang:demonitor(Monitor, [flush]), - ok + ok; + exit:{timeout, _} -> + erlang:demonitor(Monitor, [flush]), + {error, timeout}; + exit:Reason -> + erlang:demonitor(Monitor, [flush]), + {error, Reason} end;In Erlang/OTP, does gen_server:call/2 exit the caller on timeout or if the target gen_server dies before replying, rather than returning an error tuple?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 455 - 472, The gen_server:call(Pid, stop_network) can exit the caller on timeout or when the target dies, so update the try/catch around that call to catch exit reasons and convert them to {error, Reason} instead of letting them propagate; specifically, in the block that currently does try gen_server:call(Pid, stop_network) of ... catch exit:{noproc,_} -> ... end, add a generic catch clause like catch exit:Reason -> erlang:demonitor(Monitor, [flush]), {error, Reason} (and ensure existing {'DOWN', ...} handling and erlang:demonitor calls remain) so stop/0 returns {error, Reason} for all gen_server:call exits.tests/libs/estdlib/test_serial_dist.erl (1)
429-434:⚠️ Potential issue | 🟠 MajorWait for
Ctrlto terminate instead of sleeping.
exit(Ctrl, shutdown)is asynchronous. The fixed 10 ms delay does not guarantee the controller and its linked reader are gone before the UART endpoints are closed, so teardown can still race and make this suite flaky.🛠️ Suggested teardown fix
stop_controller(Ctrl) -> + Ref = erlang:monitor(process, Ctrl), unlink(Ctrl), exit(Ctrl, shutdown), receive - after 10 -> ok + {'DOWN', Ref, process, Ctrl, _Reason} -> ok + after 5000 -> + exit({controller_stop_timeout, Ctrl}) end.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/test_serial_dist.erl` around lines 429 - 434, The teardown currently in stop_controller(Ctrl) unlinks and calls exit(Ctrl, shutdown) then sleeps 10ms, which races; instead create a monitor for Ctrl (erlang:monitor/2), call exit(Ctrl, shutdown), and block in receive waiting for the corresponding {'DOWN', Ref, process, Ctrl, Reason} message (with an appropriate timeout) so the function only returns after the controller process has actually terminated; keep the unlink(Ctrl) and reference the stop_controller/1 function, the exit(Ctrl, shutdown) call, and the monitor/DOWN receive to locate where to change the code.src/platforms/esp32/components/avm_builtins/network_driver.c (4)
296-299:⚠️ Potential issue | 🟠 MajorUse the public
canceledatom in callback errors.This path still emits
{error, scan_canceled}to the scan callback. Existing callback consumers and tests accept{error, canceled}, so this branch will miss those matches. Please preload/usecanceledhere instead of the internal control atom.Based on learnings:
cancel_scannow delivers{error, canceled}to thescan_donecallback as well as to direct blocking callers.Also applies to: 887-890
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 296 - 299, Replace the internal "scan_canceled" control atom with the public "canceled" atom when preloading and passing error tuples so callbacks receive {error, canceled}; specifically, change the ATOM_STR instantiation and the ensure_atom call that currently creates scan_canceled (the scan_canceled variable and ensure_atom(global, scan_canceled)) to use the public canceled atom identifier instead (and do the same change at the other occurrence around the scan_done/cancel_scan path). This ensures the scan callback and blocking callers see {error, canceled} rather than {error, scan_canceled}.
865-874:⚠️ Potential issue | 🔴 Critical
ScanDatacan be freed whilescan_done_handlerstill holds the same pointer.
cancel_scan()readsctx->platform_data, wins the CAS, and freesdataon Lines 1807-1811. Butscan_done_handler()receives that same pointer as itsargand dereferences it immediately on Lines 867-876. IfWIFI_EVENT_SCAN_DONEis already queued or in-flight when cancel wins, the callback will run against freed memory. The CAS only decides who cleans up; it does not make the allocation lifetime safe. Please move theplatform_dataclaim/free behind a synchronized handoff and keep the struct alive until the handler/completion path has definitively dropped its last reference.Also applies to: 1757-1812
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 865 - 874, The handler scan_done_handler currently dereferences struct ScanData *data passed in arg without ensuring it remains allocated; update the lifetime management so cancel_scan() cannot free ctx->platform_data while the handler is executing: introduce a reference-count or atomic ownership handoff on ScanData (e.g., increment a refcount or set a held flag) at the start of scan_done_handler before any dereference, and decrement/release that reference at the end of the handler/completion path; change cancel_scan() to check/decrement the same refcount (or block until the handler releases) instead of freeing the struct immediately when it wins the CAS on ctx->platform_data; ensure both scan_done_handler and cancel_scan use the same atomic refcount/holding API so the allocation is kept alive until the last reference is dropped (apply same fix to the other occurrence around lines ~1757-1812).
1571-1578:⚠️ Potential issue | 🟠 MajorPre-creating
scan_resultshere still leaves one bare-error path.If
globalcontext_make_atom(..., "scan_results")fails in this branch, the reply falls back to a bare{error, out_of_memory}. The scan flow elsewhere assumes failures come back wrapped as{scan_results, ...}, so this rare request-time path can bypass the normal scan completion/error handling and leave the scan state stuck. Preloadscan_resultsbefore enteringwifi_scan()and treat its absence here as impossible instead of emitting a differently shaped reply.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1571 - 1578, Preload the "scan_results" atom before entering wifi_scan and remove the runtime fallback here: instead of calling globalcontext_make_atom(ctx->global, scan_results) and handling term_invalid_term() by sending a bare {error, out_of_memory}, treat that condition as impossible (e.g., assert or UNREACHABLE) because the atom must have been created at module/init time; update the code path around the term results_atom and the error branch so this function assumes results_atom exists and the rare failure cannot emit a differently shaped reply that breaks the scan flow.
1629-1642:⚠️ Potential issue | 🟡 MinorValidate
dwellbefore narrowing it.
term_to_uint32(cfg_dwell)on Line 1635 truncates the Erlang integer before the range check. Oversized boxed integers can wrap into1..1500and start a scan instead of returningbadarg. Read the original value into a signed/wider type first, validate it, and only then cast touint32_t.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1629 - 1642, The code currently calls term_to_uint32(cfg_dwell) before validating the original Erlang integer which can truncate/wrap large boxed integers; change to read the value into a wider signed type (e.g., int64_t or intptr_t) using a safe extractor for Erlang terms (use term_to_int64 or equivalent) into a variable like int64_t raw_dwell, validate that raw_dwell is within 1..1500 and only then cast to uint32_t to assign dwell_ms; keep existing error logging and send_scan_error_reason(ctx, pid, ref, BADARG_ATOM) branches when validation fails and continue to use cfg_dwell/type checks (term_is_integer(cfg_dwell)) as currently done.
🟡 Minor comments (8)
tools/test/smoke-test/README.md-189-190 (1)
189-190:⚠️ Potential issue | 🟡 MinorUpdate the sample flash log addresses to match the new
0x250000command.Line 189 now flashes at
0x250000, but the sample output still reports erase/write at0x00210000(Line 208-Line 210), which is contradictory.✏️ Suggested doc fix
- Flash will be erased from 0x00210000 to 0x00210fff... - Writing at 0x00210000... (100 %) - Wrote 16384 bytes at 0x00210000 in 0.2 seconds (557.7 kbit/s)... + Flash will be erased from 0x00250000 to 0x00250fff... + Writing at 0x00250000... (100 %) + Wrote 16384 bytes at 0x00250000 in 0.2 seconds (557.7 kbit/s)...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tools/test/smoke-test/README.md` around lines 189 - 190, The README sample is inconsistent: the esptool.py invocation uses address 0x250000 but the sample output still shows erase/write at 0x00210000; update the sample output to reflect the actual flashed address 0x250000 (replace occurrences of 0x00210000 in the esptool output block with 0x250000) so the displayed "Erasing" / "Writing" / "Hash of data" lines match the esptool.py command string shown.doc/src/atomvm-tooling.md-150-157 (1)
150-157:⚠️ Potential issue | 🟡 MinorKeep ESP32 partition log examples consistent across this page.
This block shows
boot.avmsize as524288, but the later ESP32 log example in the same file still shows262144(Line 449). Please align both examples to avoid conflicting flashing/layout guidance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/atomvm-tooling.md` around lines 150 - 157, The ESP32 partition size values in the example logs are inconsistent (boot.avm shows size=524288 while another example shows 262144); update the example log lines so both show the same partition size for boot.avm (and any matching main.avm references) to keep flashing/layout guidance consistent—locate the log strings containing "Loaded BEAM partition boot.avm" and "Loaded BEAM partition main.avm" and change the size value in the later example to match the chosen canonical value used in the first example (or vice versa) so both examples use the same byte size.doc/src/build-instructions.md-585-585 (1)
585-585:⚠️ Potential issue | 🟡 MinorUse
main.avmhere, notmain.app.
main.appis a typo; the partition name everywhere else in this guide ismain.avm.✏️ Suggested fix
-Users can then use the `esptool.py` directly to flash the entire image to the ESP32 device, and then flash their applications to the `main.app` partition at address `0x250000`. +Users can then use the `esptool.py` directly to flash the entire image to the ESP32 device, and then flash their applications to the `main.avm` partition at address `0x250000`.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/build-instructions.md` at line 585, The sentence references the wrong partition name: change the partition reference from `main.app` to `main.avm` so it matches the rest of the guide; update the line that says to flash applications to the `main.app` partition at address `0x250000` to instead say flash to the `main.avm` partition at `0x250000` (ensure any nearby mentions of `main.app` are corrected to `main.avm` as well).doc/src/network-programming-guide.md-470-473 (1)
470-473:⚠️ Potential issue | 🟡 MinorUse the correct admonition fence syntax for this note block.
Line 470 uses
```note; elsewhere the guide uses MyST directives (e.g.,```{warning}). This likely renders as plain code instead of a note.Suggested fix
-```note -If `network:stop/0` called while a WiFi scan is in progress, the scan caller or callback may +```{note} +If `network:stop/0` is called while a WiFi scan is in progress, the scan caller or callback may receive either the final scan result or `{error, canceled}`.</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@doc/src/network-programming-guide.mdaround lines 470 - 473, Replace the
incorrect code-fence "note" with a MyST admonition fence "{note}" and fix
the sentence to "Ifnetwork:stop/0is called while a WiFi scan is in
progress..." so the note renders correctly; ensure the block uses the opening
"{note}" and a matching closing "" and reference thenetwork:stop/0
symbol in the revised text.</details> </blockquote></details> <details> <summary>libs/jit/src/jit_armv6m.erl-156-156 (1)</summary><blockquote> `156-156`: _⚠️ Potential issue_ | _🟡 Minor_ **Broaden `#state.branches` to include reference labels.** `set_continuation_to_offset/1` pushes `{make_ref(), Offset, {adr, Temp}}` into this field, so the first tuple element is not always a `non_neg_integer()`. The record type is now narrower than the real data shape. <details> <summary>Proposed fix</summary> ```diff - branches :: [{non_neg_integer(), non_neg_integer(), branch_type()}], + branches :: [{integer() | reference(), non_neg_integer(), branch_type()}], ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_armv6m.erl` at line 156, The spec for the record field `#state.branches` is too narrow because set_continuation_to_offset/1 pushes tuples whose first element can be a reference (e.g. {make_ref(), Offset, {adr, Temp}}), not always a non_neg_integer; update the type for branches to reflect this real shape (for example change the tuple type to {non_neg_integer() | reference(), non_neg_integer(), branch_type()} or a union that includes reference() as the first element) and adjust any related type specs that reference branches so functions like set_continuation_to_offset/1 and any callers compile cleanly. ``` </details> </blockquote></details> <details> <summary>libs/avm_network/src/network.erl-646-652 (1)</summary><blockquote> `646-652`: _⚠️ Potential issue_ | _🟡 Minor_ **Keep `wifi_scan/0` equivalent to `wifi_scan/1` when the server is down.** This wrapper returns `{error, network_not_started}`, while `wifi_scan/1` returns `{error, not_started}` for the same condition. That breaks the stated equivalence and forces callers to special-case the convenience API. <details> <summary>Suggested fix</summary> ```diff catch - exit:{noproc, _} -> {error, network_not_started} + exit:{noproc, _} -> {error, not_started} end, ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 646 - 652, The wrapper function wifi_scan/0 currently returns {error, network_not_started} when the gen_server call fails, which differs from wifi_scan/1's {error, not_started}; change the catch branch in the Config retrieval (the try around gen_server:call(?SERVER, get_config) and proplists:get_value(sta, NetConfig)) so that it returns {error, not_started} instead of {error, network_not_started}, ensuring wifi_scan/0 and wifi_scan/1 are equivalent when the server is down. ``` </details> </blockquote></details> <details> <summary>libs/estdlib/src/serial_dist_controller.erl-187-195 (1)</summary><blockquote> `187-195`: _⚠️ Potential issue_ | _🟡 Minor_ **Only increment `sent` after a successful handshake write.** This updates the counter even when `send_framed/3` returns `{error, Reason}`, so `getstat/1` can report packets that never made it onto the wire. <details> <summary>📉 Suggested counter fix</summary> ```diff handle_call( {send, Data}, _From, `#state`{uart = Uart, uart_module = UartMod, sent = Sent} = State ) -> DataBin = iolist_to_binary(Data), DataSize = byte_size(DataBin), - Result = send_framed(UartMod, Uart, <<DataSize:16, DataBin/binary>>), - {reply, Result, State#state{sent = Sent + 1}}; + case send_framed(UartMod, Uart, <<DataSize:16, DataBin/binary>>) of + ok -> + {reply, ok, State#state{sent = Sent + 1}}; + {error, _} = Error -> + {reply, Error, State} + end; ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@libs/estdlib/src/serial_dist_controller.erl` around lines 187 - 195, The current handle_call in serial_dist_controller increments state.sent unconditionally after calling send_framed/3; change it to only increment when send_framed returns success by pattern-matching Result (e.g. match {ok, ...} vs {error, Reason}) and update State#state{sent = Sent + 1} only in the success branch, while returning the original State on error; locate the handle_call clause and the send_framed call to implement this conditional update so getstat/1 reflects only successful writes. ``` </details> </blockquote></details> <details> <summary>tests/libs/estdlib/test_serial_dist.erl-234-245 (1)</summary><blockquote> `234-245`: _⚠️ Potential issue_ | _🟡 Minor_ **Exercise the inclusive boundary, not just the overflow case.** This test name says `8192` is accepted, but the body only checks that `8193` is rejected. A regression on the exact upper bound would still pass here. <details> <summary>🧪 Suggested test adjustment</summary> ```diff test_scan_frame_max_handshake_boundary() -> - %% Frame with length exactly at MAX_HANDSHAKE_FRAME_SIZE (8192) should - %% be accepted; length at 8193 should be treated as false sync. - %% We don't actually build an 8192-byte payload (too large for test), - %% but we can verify that a length of 8193 is rejected as false sync. + %% Frame with length exactly at MAX_HANDSHAKE_FRAME_SIZE (8192) should + %% be accepted; length at 8193 should be treated as false sync. + Payload = binary:copy(<<0>>, 8192), + {ok, Payload, <<>>} = + serial_dist_controller:scan_frame(make_handshake_frame(Payload), 16), + LenAndPayload = <<8193:16, 0>>, CRC = erlang:crc32(LenAndPayload), Buffer = <<16#AA, 16#55, LenAndPayload/binary, CRC:32>>, %% 8193 > 8192 so this sync is treated as false; scanner looks for %% another sync marker and finds none. {need_more, _} = serial_dist_controller:scan_frame(Buffer, 16), ok. ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/test_serial_dist.erl` around lines 234 - 245, The test only checks the overflow case (8193) but doesn't exercise the inclusive boundary; update test_scan_frame_max_handshake_boundary to also construct a Buffer where the length field is exactly MAX_HANDSHAKE_FRAME_SIZE (8192) using the same LenAndPayload/CRC pattern and call serial_dist_controller:scan_frame(Buffer, 16) expecting a successful match (e.g., match {ok, _} or the module's success tuple) for the 8192 case before keeping the existing 8193 {need_more, _} assertion; reference serial_dist_controller:scan_frame and MAX_HANDSHAKE_FRAME_SIZE in the update so the exact upper-bound behavior is asserted. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🧹 Nitpick comments (4)</summary><blockquote> <details> <summary>src/libAtomVM/opcodesswitch.h (1)</summary><blockquote> `5248-5252`: **Keep the helper name in the comment aligned with the actual macro behavior.** Line [5251] references `stacktrace_create_raw_mfa`, but `HANDLE_ERROR()` currently routes through `stacktrace_create_raw`. Updating the comment text will avoid reader confusion. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@src/libAtomVM/opcodesswitch.h` around lines 5248 - 5252, The comment incorrectly names stacktrace_create_raw_mfa while the macro path uses stacktrace_create_raw; update the comment in opcodesswitch.h (near the HANDLE_ERROR() mention) to reference stacktrace_create_raw (or clarify that HANDLE_ERROR() routes via stacktrace_create_raw) so the helper name matches actual macro behavior; keep the rest of the explanatory text intact and ensure HANDLE_ERROR() and OP_RAISE are mentioned so readers can correlate the flow. ``` </details> </blockquote></details> <details> <summary>tests/libs/jit/jit_armv7m_asm_tests.erl (1)</summary><blockquote> `25-27`: **Minor: Unused macro parameter in name.** The macro is named `_assertAsmEqual` but it's actually doing more than just comparing - it's computing the expected value via `jit_tests_common:asm/3` and comparing against the provided `Value`. Consider renaming to better reflect the dual-purpose nature, or this is fine as-is since it's a test helper. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@tests/libs/jit/jit_armv7m_asm_tests.erl` around lines 25 - 27, The macro name _assertAsmEqual is misleading because it both assembles via jit_tests_common:asm(arm_thumb2, Bin, Str) and compares to Value; rename the macro to something clearer (e.g., _assertAsmCompiledEqual or _assertAsmAndCompare) and update its definition and every call site to use the new name so callers reflect that it assembles then asserts; ensure you only change the macro identifier and keep the body invoking jit_tests_common:asm/3 and the ?_assertEqual comparison intact. ``` </details> </blockquote></details> <details> <summary>tests/libs/estdlib/test_json.erl (1)</summary><blockquote> `88-107`: **Use the recorded expected error from the generated negative vectors.** `run_n_test/1` and the error branch of `run_i_test/1` only check that decoding fails, even though the tuple data already carries a concrete expected error. That leaves most of this generated corpus unable to catch regressions in error classification or tagging. <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@tests/libs/estdlib/test_json.erl` around lines 88 - 107, run_n_test/1 and the error branch of run_i_test/1 only assert that json:decode/1 fails instead of verifying the specific expected error encoded in the test tuple; update run_n_test/1 to accept the third element (rename {_} to Expected0), resolve or decode that Expected0 (using resolve/1 if necessary) to obtain the concrete expected error term, then capture the thrown error from json:decode/1 in the catch and compare it to the expected error (fail if it differs). Similarly, in run_i_test/1’s {error,_} branch, use resolve/1 on the provided Expected0 to get the expected error and assert that the caught error from json:decode(base64:decode(B64)) equals that expected error instead of just accepting any error. Ensure comparisons use the same shape/types as produced by json:decode/1 so error classification/tagging regressions are detected. ``` </details> </blockquote></details> <details> <summary>libs/jit/src/jit_armv6m.erl (1)</summary><blockquote> `3200-3216`: **Use `jump_table_start` here instead of `offset`.** `add_label/3` patches Thumb-2 jump-table entries from `State#state.jump_table_start`, but this address calculation still keys off `State#state.offset`. Keeping both paths on the same base field avoids a silent mismatch if the jump table ever stops starting at the initial stream offset. <details> <summary>Proposed fix</summary> ```diff set_continuation_to_label( `#state`{ stream_module = StreamModule, stream = Stream0, - offset = JumpTableOffset, + jump_table_start = JumpTableOffset, available_regs = Avail } = State, Label ) -> ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@libs/jit/src/jit_armv6m.erl` around lines 3200 - 3216, The jump-table entry offset is being calculated from JumpTableOffset (derived from State#state.offset) but add_label/3 and Thumb-2 patching use State#state.jump_table_start; change the base to use State#state.jump_table_start instead of the current offset so both paths use the same base. Locate the calculation that sets JumpTableEntryOffset in the function containing add_label/3 (variables State, JumpTableOffset, EntrySize, JumpTableEntryOffset) and replace the base with State#state.jump_table_start (or update JumpTableOffset to be assigned from State#state.jump_table_start) so the computed (Label * EntrySize) + <jump_table_start> matches add_label/3. ``` </details> </blockquote></details> </blockquote></details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Organization UI **Review profile**: CHILL **Plan**: Pro **Run ID**: `6c11833c-0778-45ec-bb27-c9946dd3a6d5` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between b80dae25a04a9a53098804af2d2400c010c88d90 and 9e6ded9a14f2b9e5d2f0415616903c12d1068b25. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `src/platforms/esp32/partitions.csv` is excluded by `!**/*.csv` </details> <details> <summary>📒 Files selected for processing (90)</summary> * `.github/workflows/build-and-test-macos.yaml` * `.github/workflows/build-and-test.yaml` * `.github/workflows/build-libraries.yaml` * `.github/workflows/build-linux-artifacts.yaml` * `.github/workflows/esp32-mkimage.yaml` * `.github/workflows/pico-build.yaml` * `.github/workflows/run-tests-with-beam.yaml` * `.github/workflows/wasm-build.yaml` * `CHANGELOG.md` * `CMakeLists.txt` * `CMakeModules/BuildElixir.cmake` * `CMakeModules/BuildErlang.cmake` * `UPDATING.md` * `doc/src/atomvm-internals.md` * `doc/src/atomvm-tooling.md` * `doc/src/build-instructions.md` * `doc/src/distributed-erlang.md` * `doc/src/getting-started-guide.md` * `doc/src/jit.md` * `doc/src/network-programming-guide.md` * `examples/erlang/CMakeLists.txt` * `examples/erlang/esp32/CMakeLists.txt` * `examples/erlang/esp32/wifi_scan.erl` * `examples/erlang/esp32/wifi_scan_callback.erl` * `examples/erlang/serial_disterl.erl` * `examples/erlang/system_info_server.erl` * `libs/avm_network/src/network.erl` * `libs/eavmlib/src/CMakeLists.txt` * `libs/eavmlib/src/json_encoder.erl` * `libs/estdlib/src/CMakeLists.txt` * `libs/estdlib/src/binary.erl` * `libs/estdlib/src/json.erl` * `libs/estdlib/src/net_kernel.erl` * `libs/estdlib/src/net_kernel_sup.erl` * `libs/estdlib/src/serial_dist.erl` * `libs/estdlib/src/serial_dist_controller.erl` * `libs/estdlib/src/timer.erl` * `libs/exavmlib/lib/CMakeLists.txt` * `libs/exavmlib/lib/Keyword.ex` * `libs/exavmlib/lib/json.ex` * `libs/jit/include/jit.hrl` * `libs/jit/src/CMakeLists.txt` * `libs/jit/src/jit_armv6m.erl` * `libs/jit/src/jit_armv6m_asm.erl` * `libs/jit/src/jit_armv7m_asm.erl` * `libs/jit/src/jit_dwarf.erl` * `libs/jit/src/jit_precompile.erl` * `src/libAtomVM/CMakeLists.txt` * `src/libAtomVM/context.c` * `src/libAtomVM/defaultatoms.def` * `src/libAtomVM/jit.c` * `src/libAtomVM/jit.h` * `src/libAtomVM/module.c` * `src/libAtomVM/nifs.c` * `src/libAtomVM/opcodesswitch.h` * `src/libAtomVM/stacktrace.c` * `src/platforms/esp32/GetBootAVM.cmake` * `src/platforms/esp32/components/avm_builtins/network_driver.c` * `src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt` * `src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl` * `src/platforms/esp32/test/main/test_main.c` * `src/platforms/rp2/CMakeLists.txt` * `src/platforms/stm32/CMakeLists.txt` * `tests/CMakeLists.txt` * `tests/erlang_tests/CMakeLists.txt` * `tests/erlang_tests/binary_at_test.erl` * `tests/erlang_tests/binary_first_test.erl` * `tests/erlang_tests/binary_last_test.erl` * `tests/erlang_tests/test_binary_split.erl` * `tests/erlang_tests/test_raise_built_stacktrace.erl` * `tests/libs/estdlib/CMakeLists.txt` * `tests/libs/estdlib/file_uart_hal.erl` * `tests/libs/estdlib/mock_uart_hal.erl` * `tests/libs/estdlib/test_binary.erl` * `tests/libs/estdlib/test_json.erl` * `tests/libs/estdlib/test_serial_dist.erl` * `tests/libs/estdlib/test_serial_dist_beam_peer.erl` * `tests/libs/estdlib/test_serial_dist_socat.erl` * `tests/libs/estdlib/test_serial_dist_socat_peer.erl` * `tests/libs/estdlib/test_timer.erl` * `tests/libs/estdlib/tests.erl` * `tests/libs/exavmlib/Tests.ex` * `tests/libs/jit/CMakeLists.txt` * `tests/libs/jit/jit_armv7m_asm_tests.erl` * `tests/libs/jit/jit_armv7m_tests.erl` * `tests/libs/jit/jit_tests_common.erl` * `tests/libs/jit/tests.erl` * `tests/test.c` * `tools/test/smoke-test/README.md` * `version.cmake` </details> <details> <summary>💤 Files with no reviewable changes (2)</summary> * libs/eavmlib/src/CMakeLists.txt * libs/eavmlib/src/json_encoder.erl </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| handle_req("GET", ["processes", PidString, "info"], Conn) -> | ||
| {Code, ProcInfo} = try_proc_info_list(PidString), | ||
| Body = json_encoder:encode(ProcInfo), | ||
| Body = json:encode(ProcInfo), |
There was a problem hiding this comment.
json:encode/1 will crash on the process_info/2 result shape.
Line 76 now passes a list of {Key, Value} tuples into json:encode/1, but the new encoder only handles maps, lists, binaries, numbers, and atoms. That makes /processes/<pid>/info fail for both the normal process_info/2 response and the not-found branch from try_proc_info_list/1.
🩹 Minimal fix
handle_req("GET", ["processes", PidString, "info"], Conn) ->
{Code, ProcInfo} = try_proc_info_list(PidString),
- Body = json:encode(ProcInfo),
+ Body = json:encode(maps:from_list(ProcInfo)),
http_server:reply(Code, Body, Conn);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| handle_req("GET", ["processes", PidString, "info"], Conn) -> | |
| {Code, ProcInfo} = try_proc_info_list(PidString), | |
| Body = json_encoder:encode(ProcInfo), | |
| Body = json:encode(ProcInfo), | |
| handle_req("GET", ["processes", PidString, "info"], Conn) -> | |
| {Code, ProcInfo} = try_proc_info_list(PidString), | |
| Body = json:encode(maps:from_list(ProcInfo)), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/erlang/system_info_server.erl` around lines 74 - 76, handle_req/3
currently passes the raw result from try_proc_info_list (a proplist/list of
{Key,Value} tuples or an error tuple from process_info/2) into json:encode/1
which will crash because the encoder expects maps/lists/binaries/etc.; update
the code that builds Body to convert the proplist into a JSON-friendly map
(e.g., use maps:from_list/1 or normalize nested proplists to maps) and ensure
the not-found branch (returned by try_proc_info_list/1) is also converted to a
map or tagged structure before calling json:encode/1 so json:encode/1 always
receives a supported shape; change references in handle_req/3 where Body is set
and keep try_proc_info_list/process_info/2 behavior unchanged.
| {Ref, {scan_canceled, {Next, ReplyTo}, ok}}, #state{ref = Ref, scan_receiver = Receiver} = State | ||
| ) -> | ||
| ScanMsg = {scan_results, {error, scan_canceled}}, | ||
| case Receiver of | ||
| {reply, _} -> | ||
| scan_reply_or_callback(ScanMsg, State); | ||
| _ -> | ||
| spawn(fun() -> scan_reply_or_callback(ScanMsg, State) end) | ||
| end, | ||
| gen_server:reply(ReplyTo, ok), |
There was a problem hiding this comment.
Use {error, canceled} here, not {error, scan_canceled}.
This rewrites a successful cancellation to a different atom than the documented/tested callback contract. Callers matching {error, canceled} will miss the cancellation path.
Suggested fix
- ScanMsg = {scan_results, {error, scan_canceled}},
+ ScanMsg = {scan_results, {error, canceled}},Based on learnings: network_stop_while_scanning_test/0 correctly accepts {error, canceled} from the scan callback, and the driver behavior for canceled scans is intentionally defined around that atom.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/avm_network/src/network.erl` around lines 824 - 833, The scan
cancellation path currently builds ScanMsg as {scan_results, {error,
scan_canceled}} which violates the documented/tested callback contract; update
the message so the cancellation uses the expected atom by changing ScanMsg to
{scan_results, {error, canceled}} in the clause that matches {Ref,
{scan_canceled, {Next, ReplyTo}, ok}} (the code that invokes
scan_reply_or_callback and gen_server:reply), so callers receiving the scan
callback will match the `{error, canceled}` case.
| handle_call( | ||
| {recv, _Length, Timeout}, | ||
| _From, | ||
| #state{uart = Uart, uart_module = UartMod, buffer = Buffer} = State | ||
| ) -> | ||
| case recv_handshake_packet(Uart, UartMod, Buffer, Timeout) of | ||
| {ok, Packet, NewBuffer} -> | ||
| {reply, {ok, ?POST_PROCESS(Packet)}, State#state{buffer = NewBuffer}}; | ||
| {error, _} = Error -> | ||
| {reply, Error, State} | ||
| end; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n libs/estdlib/src/serial_dist_controller.erl | sed -n '170,200p'Repository: UncleGrumpy/AtomVM
Length of output: 1367
🏁 Script executed:
cat -n libs/estdlib/src/serial_dist_controller.erl | sed -n '240,275p'Repository: UncleGrumpy/AtomVM
Length of output: 1539
🏁 Script executed:
wc -l libs/estdlib/src/serial_dist_controller.erlRepository: UncleGrumpy/AtomVM
Length of output: 109
Carry the partial frame and one timeout budget through handshake reads.
The recv_handshake_packet/4 function restarts the full Timeout on every fragmented read (line 258), allowing a slow peer sending partial frames to exceed the requested overall timeout. Additionally, when recv_handshake_packet returns an error (lines 252–253 for crc_error or 260–261 for read errors), it does not return the already-consumed buffer bytes (Rest or Trimmed). The handle_call error case (lines 184–185) then preserves the original state buffer, losing the partial frame data. This leaves the next recv/3 call unable to reconstruct the frame from bytes that were already read from UART.
🧭 Suggested shape of the fix
handle_call(
{recv, _Length, Timeout},
_From,
`#state`{uart = Uart, uart_module = UartMod, buffer = Buffer} = State
) ->
- case recv_handshake_packet(Uart, UartMod, Buffer, Timeout) of
+ Deadline = erlang:monotonic_time(millisecond) + Timeout,
+ case recv_handshake_packet(Uart, UartMod, Buffer, Deadline) of
{ok, Packet, NewBuffer} ->
{reply, {ok, ?POST_PROCESS(Packet)}, State#state{buffer = NewBuffer}};
- {error, _} = Error ->
- {reply, Error, State}
+ {{error, _} = Error, NewBuffer} ->
+ {reply, Error, State#state{buffer = NewBuffer}}
end;-recv_handshake_packet(Uart, UartMod, Buffer, Timeout) ->
+recv_handshake_packet(Uart, UartMod, Buffer, Deadline) ->
case scan_frame(Buffer, 16) of
{ok, Packet, Rest} ->
{ok, Packet, Rest};
- {crc_error, _Rest} ->
- {error, crc_error};
+ {crc_error, Rest} ->
+ {{error, crc_error}, Rest};
{need_more, Trimmed} ->
- case UartMod:read(Uart, Timeout) of
+ Remaining =
+ erlang:max(0, Deadline - erlang:monotonic_time(millisecond)),
+ case UartMod:read(Uart, Remaining) of
{ok, Data} ->
recv_handshake_packet(
- Uart, UartMod, <<Trimmed/binary, Data/binary>>, Timeout
+ Uart, UartMod, <<Trimmed/binary, Data/binary>>, Deadline
);
{error, _} = Error ->
- Error
+ {Error, Trimmed}
end
end.Also applies to: 248–263
| accept(Ports) when is_list(Ports) -> | ||
| Kernel = self(), | ||
| spawn_link(fun() -> | ||
| register(serial_dist_link_manager, self()), |
There was a problem hiding this comment.
Terminate the coordinator when the last UART manager dies.
close/1 only closes the UART handles. After that, each link manager exits, but the registered coordinator stays alive with no managers behind it. A later accept/1 then hits register(serial_dist_link_manager, ...), and setup/5 can keep targeting a stale coordinator.
🔧 One way to make shutdown self-cleaning
{'EXIT', ManagerPid, _Reason} ->
NewStates = maps:remove(ManagerPid, ManagerStates),
- coordinator_loop(Kernel, NewStates, PendingAccepts, SetupAwards)
+ case map_size(NewStates) of
+ 0 ->
+ exit(normal);
+ _ ->
+ coordinator_loop(Kernel, NewStates, PendingAccepts, SetupAwards)
+ endAlso applies to: 297-299, 484-489
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/estdlib/src/serial_dist.erl` at line 200, The coordinator process is
left running after all UART link managers exit because close/1 only closes
handles; modify the coordinator lifecycle so it terminates itself when the last
link manager dies: have the coordinator track registered link managers (or
maintain a manager count) and monitor each manager (use monitor or link) so that
on DOWN it decrements the count and when count reaches zero it calls
exit(self(), normal) (or otherwise terminate/unregister the coordinator). Update
places that call register(serial_dist_link_manager, self()) and setup/5 to rely
on this self-termination behavior (and ensure accept/1 handles the coordinator
not existing by spawning a fresh coordinator when needed). Ensure the changes
touch the coordinator process code that registers as serial_dist_link_manager,
close/1, accept/1, and setup/5 to keep lifecycle consistent.
| {setup_abort} -> | ||
| exit(Ctrl, shutdown), | ||
| flush_setup_messages(), | ||
| link_manager(Coordinator, UartPort, UartMod, <<>>) |
There was a problem hiding this comment.
Wait for the aborted controller to die before reusing the UART.
exit(Ctrl, shutdown) is asynchronous. Re-entering link_manager/4 immediately after it means the old controller can still be unwinding while the new loop starts reading and writing the same UART.
🔧 Minimal teardown ordering fix
{setup_abort} ->
- exit(Ctrl, shutdown),
+ Ref = monitor(process, Ctrl),
+ exit(Ctrl, shutdown),
+ receive
+ {'DOWN', Ref, process, Ctrl, _Reason} ->
+ ok
+ end,
flush_setup_messages(),
link_manager(Coordinator, UartPort, UartMod, <<>>)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {setup_abort} -> | |
| exit(Ctrl, shutdown), | |
| flush_setup_messages(), | |
| link_manager(Coordinator, UartPort, UartMod, <<>>) | |
| {setup_abort} -> | |
| Ref = monitor(process, Ctrl), | |
| exit(Ctrl, shutdown), | |
| receive | |
| {'DOWN', Ref, process, Ctrl, _Reason} -> | |
| ok | |
| end, | |
| flush_setup_messages(), | |
| link_manager(Coordinator, UartPort, UartMod, <<>>) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/estdlib/src/serial_dist.erl` around lines 399 - 402, The shutdown path
currently calls exit(Ctrl, shutdown) and immediately re-enters link_manager/4,
risking concurrent access to the UART because exit/2 is asynchronous; change the
{setup_abort} clause to monitor the controller (Ref = erlang:monitor(process,
Ctrl)), call exit(Ctrl, shutdown), then wait for the {'DOWN', Ref, process,
Ctrl, _Reason} message (and demonitor/flush the monitor) before calling
flush_setup_messages() and link_manager(Coordinator, UartPort, UartMod, <<>>);
reference setup_abort, exit(Ctrl, shutdown), and link_manager/4.
| handle_call({read, infinity}, From, #state{buffer = <<>>} = State) -> | ||
| {noreply, State#state{waiter = {From, undefined}}}; | ||
| handle_call({read, Timeout}, From, #state{buffer = <<>>} = State) -> | ||
| Ref = erlang:send_after(Timeout, self(), read_timeout), | ||
| {noreply, State#state{waiter = {From, Ref}}}; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd tests && find . -name "file_uart_hal.erl" -type fRepository: UncleGrumpy/AtomVM
Length of output: 94
🏁 Script executed:
cat -n tests/libs/estdlib/file_uart_hal.erl | head -120Repository: UncleGrumpy/AtomVM
Length of output: 5237
🏁 Script executed:
cat -n tests/libs/estdlib/file_uart_hal.erl | sed -n '80,120p'Repository: UncleGrumpy/AtomVM
Length of output: 2033
Add waiter conflict detection and correlate timeouts with unique tags to prevent stale timeout messages from incorrectly timing out later reads.
The current implementation has two bugs:
-
Overlapping reads: Lines 85-89 lack a guard to check if a waiter already exists. A second
readcall can overwrite the first caller'sFromreference in the waiter tuple, abandoning the first request. -
Stale timeout messages: The
read_timeoutatom (line 88) is not request-specific. A timeout message from an earlier read can remain queued and incorrectly match a later read's timeout handler (lines 105, 108-109), causing the wrong caller to receive a timeout error.
The suggested fix correctly addresses both issues by:
- Adding a rejection clause for existing waiters
- Using
make_ref()to generate uniqueTimeoutTagper request - Changing the timeout message format to
{read_timeout, TimeoutTag}for proper correlation - Updating all timeout handlers to match the tagged format and validate the tag matches the current waiter
Suggested fix
-handle_call({read, infinity}, From, `#state`{buffer = <<>>} = State) ->
+handle_call({read, _}, _From, `#state`{waiter = {_From0, _Timer0}} = State) ->
+ {reply, {error, busy}, State};
+handle_call({read, infinity}, From, `#state`{buffer = <<>>, waiter = undefined} = State) ->
{noreply, State#state{waiter = {From, undefined}}};
-handle_call({read, Timeout}, From, `#state`{buffer = <<>>} = State) ->
- Ref = erlang:send_after(Timeout, self(), read_timeout),
- {noreply, State#state{waiter = {From, Ref}}};
+handle_call({read, Timeout}, From, `#state`{buffer = <<>>, waiter = undefined} = State) ->
+ TimeoutTag = make_ref(),
+ TimerRef = erlang:send_after(Timeout, self(), {read_timeout, TimeoutTag}),
+ {noreply, State#state{waiter = {From, {TimeoutTag, TimerRef}}}};
@@
-handle_info(
- {Port, {data, Data}}, `#state`{port = Port, waiter = {From, TimerRef}, buffer = Buffer} = State
-) ->
- cancel_timer(TimerRef),
+handle_info(
+ {Port, {data, Data}}, `#state`{port = Port, waiter = {From, {_Tag, TimerRef}}, buffer = Buffer} = State
+) ->
+ cancel_timer(TimerRef),
gen_server:reply(From, {ok, <<Buffer/binary, Data/binary>>}),
{noreply, State#state{buffer = <<>>, waiter = undefined}};
-handle_info(read_timeout, `#state`{waiter = {From, _}} = State) ->
+handle_info({read_timeout, TimeoutTag}, `#state`{waiter = {From, {TimeoutTag, _TimerRef}}} = State) ->
gen_server:reply(From, {error, timeout}),
{noreply, State#state{waiter = undefined}};
-handle_info(read_timeout, State) ->
+handle_info({read_timeout, _OldTag}, State) ->
{noreply, State};Also applies to: 100-109
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/libs/estdlib/file_uart_hal.erl` around lines 85 - 89, Prevent
overlapping reads and stale timeout delivery by rejecting new read requests when
a waiter already exists and by correlating timeouts with unique tags: in the
handle_call clauses that set waiter = {From, Ref} (the {read, infinity} and
{read, Timeout} branches) add a clause that returns an error when
State#state.waiter is non-empty, generate a unique TimeoutTag via make_ref()
instead of a plain atom, store waiter as {From, TimeoutTag} and call
erlang:send_after(Timeout, self(), {read_timeout, TimeoutTag}); then update all
timeout handlers that currently match read_timeout to match {read_timeout, Tag}
and verify Tag equals the current waiter’s stored tag in State#state.waiter
before treating it as a valid timeout (ignore or drop mismatched/stale tags).
| -spec close(pid()) -> ok. | ||
| close(Pid) -> | ||
| gen_server:stop(Pid). | ||
|
|
There was a problem hiding this comment.
close/1 does not unblock a waiting reader cleanly.
If a caller is blocked in read/1 or read/2, gen_server:stop/1 will terminate the server without replying to that call, so the reader exits instead of getting a {error, ...} result. That violates this module’s own read contract and can turn shutdown paths into unexpected crashes.
Also applies to: 134-135
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/libs/estdlib/mock_uart_hal.erl` around lines 72 - 75, The close/1
implementation currently calls gen_server:stop/1 which kills the server without
replying to blocked read/1 or read/2 callers; change close/1 to perform a
synchronous close request (e.g., gen_server:call(Pid, close)) so the mock UART
server can transition to a closed state and explicitly reply to any waiting
readers with an error tuple (e.g., {error,closed}) before terminating; make the
same change for the other close clause referenced (lines ~134-135) and ensure
the server's handle_call/handle_info that handles the close request sends
replies to pending read callers and then stops cleanly.
| handle_call({read, Timeout}, From, #state{buffer = <<>>} = State) -> | ||
| Ref = erlang:send_after(Timeout, self(), read_timeout), | ||
| {noreply, State#state{waiter = {From, Ref}}}; | ||
| handle_call({write, Data}, _From, #state{peer = Peer} = State) -> | ||
| Peer ! {mock_uart_data, Data}, | ||
| {reply, ok, State}. | ||
|
|
||
| handle_cast(_Msg, State) -> | ||
| {noreply, State}. | ||
|
|
||
| handle_info({mock_uart_data, Data}, #state{waiter = undefined, buffer = Buffer} = State) -> | ||
| {noreply, State#state{buffer = <<Buffer/binary, Data/binary>>}}; | ||
| handle_info({mock_uart_data, Data}, #state{waiter = {From, TimerRef}, buffer = Buffer} = State) -> | ||
| cancel_timer(TimerRef), | ||
| gen_server:reply(From, {ok, <<Buffer/binary, Data/binary>>}), | ||
| {noreply, State#state{buffer = <<>>, waiter = undefined}}; | ||
| handle_info(read_timeout, #state{waiter = {From, _}} = State) -> | ||
| gen_server:reply(From, {error, timeout}), | ||
| {noreply, State#state{waiter = undefined}}; | ||
| handle_info(read_timeout, State) -> | ||
| %% Stale timer after data arrived — ignore | ||
| {noreply, State}; |
There was a problem hiding this comment.
Tag timeout messages with the waiter ref.
Using send_after(Timeout, self(), read_timeout) means a stale timeout from an earlier read can fire against a later read and reply {error, timeout} to the wrong caller. Because the state only keeps one waiter, a second pending read can also overwrite the first completely. Send a ref-tagged message like {read_timeout, Ref} and reject or queue a second waiter instead of reusing the bare atom.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/libs/estdlib/mock_uart_hal.erl` around lines 109 - 130, The read
timeout uses an untagged atom and a single mutable waiter so stale timers or
concurrent reads can reply to the wrong caller; change the read path in
handle_call({read, Timeout}, From, State) to create the timer and send a
ref-tagged timeout message (use the erlang:send_after return ref and send
{read_timeout, Ref} as the message) and update handle_info clauses to match
{read_timeout, Ref} and only act when the Ref matches the current state.waiter
ref; additionally, prevent overwriting an existing waiter by rejecting or
queueing a second read (e.g. in handle_call({read, Timeout}, From, `#state`{waiter
= undefined} = State) allow starting the timer, but when waiter != undefined
reply immediately with an error/busy) and keep using cancel_timer(TimerRef) and
waiter state field to correlate timers.
| if (chdir(AVM_JIT_TARGET_ARCH_DIR) != 0) { | ||
| perror("Error: cannot find " AVM_JIT_TARGET_ARCH_DIR " directory"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Direct references =="
rg -n "AVM_JIT_TARGET_ARCH_DIR"
echo
echo "== Build-system definitions =="
rg -n "AVM_JIT_TARGET_ARCH_DIR|target_compile_definitions|add_compile_definitions|add_definitions"
echo
echo "== JIT-related guards around test binary build =="
rg -n "AVM_NO_JIT|JIT_ARCH_TARGET|test-erlang|tests/test\\.c"Repository: UncleGrumpy/AtomVM
Length of output: 14744
🏁 Script executed:
sed -n '95,115p' tests/CMakeLists.txtRepository: UncleGrumpy/AtomVM
Length of output: 1431
🏁 Script executed:
echo "=== AVM_DISABLE_JIT definition ===" && \
rg -n "AVM_DISABLE_JIT" -B 2 -A 2 | head -50 && \
echo && \
echo "=== AVM_JIT_TARGET_ARCH definition ===" && \
rg -n "AVM_JIT_TARGET_ARCH[^_]" -B 2 -A 2 | head -80Repository: UncleGrumpy/AtomVM
Length of output: 9935
🏁 Script executed:
rg -n "AVM_NO_JIT" src/libAtomVM/CMakeLists.txt -B 3 -A 1Repository: UncleGrumpy/AtomVM
Length of output: 421
AVM_JIT_TARGET_ARCH_DIR is undefined in hybrid mode builds (precompiled JIT with AVM_DISABLE_JIT=ON and AVM_ENABLE_PRECOMPILED=ON).
The macro is only defined when NOT AVM_DISABLE_JIT (tests/CMakeLists.txt:106), but the code at line 743 executes whenever AVM_NO_JIT is not defined. In hybrid mode, AVM_NO_JIT remains undefined (set only when both AVM_DISABLE_JIT and NOT AVM_ENABLE_PRECOMPILED are true), so the code block executes despite the macro being unavailable, causing a compilation error.
🧰 Tools
🪛 Cppcheck (2.20.0)
[error] 744-744: There is an unknown macro here somewhere. Configuration is required. If AVM_JIT_TARGET_ARCH_DIR is a macro then please configure it.
(unknownMacro)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/test.c` around lines 743 - 744, The chdir/perror block uses
AVM_JIT_TARGET_ARCH_DIR which is undefined in hybrid builds; guard that block
with a preprocessor check that matches where the macro is defined (e.g. wrap the
chdir(...) and perror(...) calls with `#ifdef` AVM_JIT_TARGET_ARCH_DIR or `#if`
!defined(AVM_DISABLE_JIT) && defined(AVM_JIT_TARGET_ARCH_DIR)) so the code
referencing AVM_JIT_TARGET_ARCH_DIR (the chdir and perror calls) is compiled
only when the macro is available.
| # Please, keep also in sync src/libAtomVM/atomvm_version.h | ||
| set(ATOMVM_BASE_VERSION "0.7.0-alpha.0") | ||
| set(ATOMVM_BASE_VERSION "0.7.0-beta.0") | ||
| set(ATOMVM_DEV TRUE) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'ATOMVM_BASE_VERSION|ATOMVM_DEV' version.cmake
echo
rg -n 'ATOMVM|0\.7\.0|alpha|beta' src/libAtomVM/atomvm_version.hRepository: UncleGrumpy/AtomVM
Length of output: 293
Update src/libAtomVM/atomvm_version.h to match the CMake version.
Line 21 instructs to keep src/libAtomVM/atomvm_version.h in sync. Currently, the header defines ATOMVM_VERSION_MAJOR=0, ATOMVM_VERSION_MINOR=6, ATOMVM_VERSION_PATCH=999 (version 0.6.999), while version.cmake now specifies 0.7.0-beta.0. Update the header macros to ATOMVM_VERSION_MAJOR=0, ATOMVM_VERSION_MINOR=7, ATOMVM_VERSION_PATCH=0 to avoid runtime version mismatch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@version.cmake` around lines 21 - 23, Update the macros in
src/libAtomVM/atomvm_version.h to match version.cmake: change
ATOMVM_VERSION_MAJOR to 0, ATOMVM_VERSION_MINOR to 7 and ATOMVM_VERSION_PATCH to
0 (so the header represents 0.7.0), ensuring the constants ATOMVM_VERSION_MAJOR,
ATOMVM_VERSION_MINOR, ATOMVM_VERSION_PATCH are updated to stay in sync with
ATOMVM_BASE_VERSION set in version.cmake to avoid runtime version mismatch.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (7)
doc/src/network-programming-guide.md (1)
339-339:⚠️ Potential issue | 🟡 MinorFix AP-mode sentence typo.
The phrase “is operation is AP mode only” reads incorrectly and should be corrected for clarity.
Suggested fix
-If the channel is omitted the default channel 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. +If the channel is omitted the default channel for esp32 is `1`. This setting is only used while a device is operating in 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.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 339, Fix the typo in the sentence containing "is operation is AP mode only" in the network programming guide: change it to read "This setting is only used while a device is operating in AP mode." so the sentence reads clearly and grammatically (the surrounding sentence about default channel for esp32 and behavior when ap_channel is configured should remain unchanged).libs/avm_network/src/network.erl (4)
633-635:⚠️ Potential issue | 🟠 MajorRestore the documented
unsupported_platformexception.Both scan entry points now return
{error, unsupported_platform}on non-ESP32 targets, but this API is intentionally documented to raiseunsupported_platforminstead. That changes observable behavior for callers relying on the exception path.Suggested fix
{true, _} -> - {error, unsupported_platform} + error(unsupported_platform). @@ _ -> - {error, unsupported_platform} + error(unsupported_platform) end.Based on learnings:
wifi_scan/1andwifi_scan/0intentionally raise anunsupported_platformerror on non-ESP32 platforms.Also applies to: 675-676
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 633 - 635, The current implementations of wifi_scan/1 and wifi_scan/0 return {error, unsupported_platform} on non-ESP32 targets; change them to raise the documented unsupported_platform exception instead of returning an error tuple. Locate the branches in network.erl that return {error, unsupported_platform} (the clauses for wifi_scan/1 and wifi_scan/0) and replace the return with a raise (use erlang:error(unsupported_platform) or simply throw the documented unsupported_platform exception) so callers observe the exception path as documented.
455-473:⚠️ Potential issue | 🟠 MajorCatch all
gen_server:call/2exits instop/0.This still only translates
noproc.gen_server:call/2also exits on timeout and when the target dies before replying, sostop/0can still crash callers instead of returning{error, Reason}.Suggested fix
catch exit:{noproc, _} -> erlang:demonitor(Monitor, [flush]), - ok + ok; + exit:{timeout, _} -> + erlang:demonitor(Monitor, [flush]), + {error, timeout}; + exit:Reason -> + erlang:demonitor(Monitor, [flush]), + {error, Reason} end;In Erlang/OTP, does gen_server:call/2 exit the caller on timeout or when the target process dies before replying, rather than returning an error tuple?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 455 - 473, The stop/0 implementation only catches exit:{noproc, _} from gen_server:call(Pid, stop_network) but gen_server:call/2 can also exit for timeout or if the target dies before replying; update the try/catch around gen_server:call(Pid, stop_network) in stop/0 to catch all exit reasons (catch exit:Reason -> erlang:demonitor(Monitor, [flush]), {error, Reason}) so non-noproc exits return {error, Reason} (and keep the existing noproc handling if you want to treat it specially), ensuring you still demonitor the Monitor in every exit path.
824-839:⚠️ Potential issue | 🟠 MajorUse
{error, canceled}for canceled scans.The cancellation path still emits
scan_canceled, but the public contract here iscanceled. Callers matching{error, canceled}will miss the cancellation case, and the new ESP32 test is currently asserting the wrong atom downstream because of this.Suggested fix
- ScanMsg = {scan_results, {error, scan_canceled}}, + ScanMsg = {scan_results, {error, canceled}},Based on learnings:
network_stop_while_scanning_test/0correctly accepts{error, canceled}from the scan_done callback when cancel wins the race.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 824 - 839, The clause handling cancellation currently builds ScanMsg as {scan_results, {error, scan_canceled}} which violates the public contract; change the constructed error tuple to {error, canceled} so callers receive {error, canceled}. Update the ScanMsg construction in the clause that matches {Ref, {scan_canceled, {Next, ReplyTo}, ok}} (the code that calls scan_reply_or_callback and later gen_server:reply) to use {scan_results, {error, canceled}}, leaving the rest of the clause (spawn vs direct call to scan_reply_or_callback, gen_server:reply, and state updates) unchanged.
816-822:⚠️ Potential issue | 🟠 MajorKeep callback routing alive for the unregister-handler follow-up.
This branch clears
scan_receiveras soon as the first successful callback/pid result arrives. If the driver then sends the intentional trailing{scan_results, {error, {unregister_handler, Reason}}}message, Line 813 seesundefinedand drops it instead of forwarding it to the configured callback/pid.Based on learnings:
send_scan_resultsintentionally emits a second{scan_results, {error, {unregister_handler, Reason}}}message after successful results when handler unregister fails.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 816 - 822, The handler clears state#state.scan_receiver too early after the first successful scan result, so the intentional trailing `{scan_results, {error, {unregister_handler, Reason}}}` sent by send_scan_results is dropped; in the clause that calls scan_reply_or_callback(Msg, State) (the branch matching `#state`{ref = Ref, scan_receiver = {reply, _}}) stop setting scan_receiver = undefined there and instead let scan_reply_or_callback or the other clause (which spawns) perform the clear when it actually processes the unregister_handler error; update the clause in handle_info that currently returns `{noreply, State#state{scan_receiver = undefined}}` to return `{noreply, State}` (or only clear when Msg indicates an unregister_handler error) so the trailing error message is forwarded to the configured callback/pid.src/platforms/esp32/components/avm_builtins/network_driver.c (2)
1711-1713:⚠️ Potential issue | 🔴 CriticalGuard
ctx->platform_datawith a critical section.Line 1711 publishes the active
ScanData, while Line 1760 and Line 1821 read/clear the same slot from another FreeRTOS task. Those accesses are currently unsynchronized, so cancel/completion can race on a stale or already-freed pointer. Please restore a singleportMUX_TYPE-guarded handoff around every read/write of this slot.Based on learnings: the correct fix in this file is a file-static
portMUX_TYPE scan_ctx_mux = portMUX_INITIALIZER_UNLOCKEDguarding the scan-data handoff.Also applies to: 1760-1760, 1820-1828
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1711 - 1713, Add a file-static mutex and use it to guard all handoffs to ctx->platform_data (the ScanData slot): declare a file-static portMUX_TYPE scan_ctx_mux = portMUX_INITIALIZER_UNLOCKED and wrap every write (e.g. the ctx->platform_data = (void *) data assignment) and every read/clear (the accesses in scan_done_handler and the code paths around the reads/clears at the other referenced locations) with portENTER_CRITICAL(&scan_ctx_mux) / portEXIT_CRITICAL(&scan_ctx_mux) to ensure the ScanData pointer is never raced or freed concurrently.
887-890:⚠️ Potential issue | 🟠 MajorSend
{error, canceled}here, not{error, scan_canceled}.This branch still emits the internal control atom. Callback consumers and the updated tests now match on
canceled, so this payload will miss the expected receive clauses.Based on learnings:
cancel_scanis expected to deliver{error, canceled}to thescan_donecallback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 887 - 890, The branch that sends the scan cancellation currently constructs the tuple with the internal atom "scan_canceled" — change the atom to the public "canceled" so the payload becomes {error, canceled}. Specifically, in the call to send_scan_error_from_task (the call site using globalcontext_existing_term_from_atom_string with ATOM_STR and "scan_canceled"), replace the atom string with "canceled" (the same atom used by the scan_done callback/tests) so consumers receive the expected {error, canceled} message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@doc/src/network-programming-guide.md`:
- Around line 470-472: Change the incorrect admonition fence and fix the missing
verb in the note: replace the inline fence starting with ```note with the
directive fence ```{note} and update the sentence "If `network:stop/0` called
while a WiFi scan is in progress" to "If `network:stop/0` is called while a WiFi
scan is in progress" so the note reads correctly and matches the other
admonitions; locate the block containing the `network:stop/0` mention to apply
this change.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1820-1823: The busy-wait in send_scan_results() on
ctx->platform_data can spin forever when the unregister_failed: path
deliberately leaves platform_data non-NULL; change the wait to avoid an infinite
spin by making the unregister_failed condition observable and breaking out: set
a flag (e.g. ctx->unregister_failed) in the unregister_failed path, then replace
the tight while (ctx->platform_data != NULL) loop with a bounded wait that
yields (sched_yield or a short sleep), checks both ctx->platform_data == NULL
and ctx->unregister_failed, and returns an appropriate error/reply if the
unregister_failed flag is set or a timeout elapses; reference
ctx->platform_data, ctx->unregister_failed (new flag), send_scan_results(), and
the unregister_failed label so the loop can reliably stop and not wedge
network:stop/0.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Line 339: Fix the typo in the sentence containing "is operation is AP mode
only" in the network programming guide: change it to read "This setting is only
used while a device is operating in AP mode." so the sentence reads clearly and
grammatically (the surrounding sentence about default channel for esp32 and
behavior when ap_channel is configured should remain unchanged).
In `@libs/avm_network/src/network.erl`:
- Around line 633-635: The current implementations of wifi_scan/1 and
wifi_scan/0 return {error, unsupported_platform} on non-ESP32 targets; change
them to raise the documented unsupported_platform exception instead of returning
an error tuple. Locate the branches in network.erl that return {error,
unsupported_platform} (the clauses for wifi_scan/1 and wifi_scan/0) and replace
the return with a raise (use erlang:error(unsupported_platform) or simply throw
the documented unsupported_platform exception) so callers observe the exception
path as documented.
- Around line 455-473: The stop/0 implementation only catches exit:{noproc, _}
from gen_server:call(Pid, stop_network) but gen_server:call/2 can also exit for
timeout or if the target dies before replying; update the try/catch around
gen_server:call(Pid, stop_network) in stop/0 to catch all exit reasons (catch
exit:Reason -> erlang:demonitor(Monitor, [flush]), {error, Reason}) so
non-noproc exits return {error, Reason} (and keep the existing noproc handling
if you want to treat it specially), ensuring you still demonitor the Monitor in
every exit path.
- Around line 824-839: The clause handling cancellation currently builds ScanMsg
as {scan_results, {error, scan_canceled}} which violates the public contract;
change the constructed error tuple to {error, canceled} so callers receive
{error, canceled}. Update the ScanMsg construction in the clause that matches
{Ref, {scan_canceled, {Next, ReplyTo}, ok}} (the code that calls
scan_reply_or_callback and later gen_server:reply) to use {scan_results, {error,
canceled}}, leaving the rest of the clause (spawn vs direct call to
scan_reply_or_callback, gen_server:reply, and state updates) unchanged.
- Around line 816-822: The handler clears state#state.scan_receiver too early
after the first successful scan result, so the intentional trailing
`{scan_results, {error, {unregister_handler, Reason}}}` sent by
send_scan_results is dropped; in the clause that calls
scan_reply_or_callback(Msg, State) (the branch matching `#state`{ref = Ref,
scan_receiver = {reply, _}}) stop setting scan_receiver = undefined there and
instead let scan_reply_or_callback or the other clause (which spawns) perform
the clear when it actually processes the unregister_handler error; update the
clause in handle_info that currently returns `{noreply,
State#state{scan_receiver = undefined}}` to return `{noreply, State}` (or only
clear when Msg indicates an unregister_handler error) so the trailing error
message is forwarded to the configured callback/pid.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1711-1713: Add a file-static mutex and use it to guard all
handoffs to ctx->platform_data (the ScanData slot): declare a file-static
portMUX_TYPE scan_ctx_mux = portMUX_INITIALIZER_UNLOCKED and wrap every write
(e.g. the ctx->platform_data = (void *) data assignment) and every read/clear
(the accesses in scan_done_handler and the code paths around the reads/clears at
the other referenced locations) with portENTER_CRITICAL(&scan_ctx_mux) /
portEXIT_CRITICAL(&scan_ctx_mux) to ensure the ScanData pointer is never raced
or freed concurrently.
- Around line 887-890: The branch that sends the scan cancellation currently
constructs the tuple with the internal atom "scan_canceled" — change the atom to
the public "canceled" so the payload becomes {error, canceled}. Specifically, in
the call to send_scan_error_from_task (the call site using
globalcontext_existing_term_from_atom_string with ATOM_STR and "scan_canceled"),
replace the atom string with "canceled" (the same atom used by the scan_done
callback/tests) so consumers receive the expected {error, canceled} message.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d9f0c7cf-56d0-480b-9056-5e532a82662d
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/avm_network/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
✅ Files skipped from review due to trivial changes (2)
- CHANGELOG.md
- examples/erlang/esp32/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (2)
- src/platforms/esp32/test/main/test_main.c
- examples/erlang/esp32/wifi_scan.erl
|
@coderabbitai with the updated code ctx->platform_data no longer needs a portMUX_TYPE-guard. We use the atomic CAS to control ownership. |
|
Tip For best results, initiate chat on the files or code changes.
I'll remove the now-stale ✏️ Learnings added
⛔ Learnings removed🧠 Learnings used |
|
@coderabbitai. tests and the contract has been updated to use |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
⛔ Learnings removed🧠 Learnings used |
|
@coderabbitai the contract had been updated to return |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
⛔ Learnings removed🧠 Learnings used |
There was a problem hiding this comment.
♻️ Duplicate comments (3)
doc/src/network-programming-guide.md (2)
471-472:⚠️ Potential issue | 🟡 MinorMissing verb in stop-while-scan note at Line 471.
The sentence is missing “is” and currently reads ungrammatically.
Proposed doc fix
-If `network:stop/0` called while a WiFi scan is in progress, the scan caller or callback may +If `network:stop/0` is called while a WiFi scan is in progress, the scan caller or callback may receive either the final scan result or `{error, canceled}`.🤖 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 471 - 472, The sentence describing behavior when network:stop/0 is invoked during a WiFi scan is missing the verb "is"; update the text so it reads "If `network:stop/0` is called while a WiFi scan is in progress, the scan caller or callback may receive either the final scan result or `{error, canceled}`." to correct the grammar and preserve the intended meaning.
339-339:⚠️ Potential issue | 🟡 MinorFix AP-mode sentence grammar at Line 339.
The phrase “is operation is AP mode only” is malformed and should be corrected for readability.
Proposed doc fix
-If the channel is omitted the default channel 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. +If the channel is omitted the default channel for esp32 is `1`. This setting is only used while a device is operating in 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.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 339, The sentence "This setting is only used while a device is operation is AP mode only" is malformed; update the doc text around the esp32 ap_channel description to read something like: "This setting is only used while a device is operating in AP mode." Keep the rest of the paragraph intact (references to esp32, ap_channel behavior when AP+STA mode is used, and the hardware limitation of the modem radio) and ensure only the grammar is corrected.libs/avm_network/src/network.erl (1)
829-845:⚠️ Potential issue | 🟡 MinorUse
{error, canceled}here, not{error, scan_canceled}.Line 832 uses
{error, scan_canceled}but the documented and tested callback contract expects{error, canceled}. The testnetwork_stop_while_scanning_test/0accepts{error, canceled}from the scan callback, and this inconsistency will cause callers matching{error, canceled}to miss the cancellation path.Suggested fix
ScanMsg = {scan_results, {error, scan_canceled}}, + ScanMsg = {scan_results, {error, canceled}},Based on learnings:
network_stop_while_scanning_test/0correctly accepts{error, canceled}from the scan_done callback, and the driver behavior for canceled scans is defined around that atom.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 829 - 845, The scan cancellation message uses the wrong error atom; in handle_info (the clause matching {Ref, {scan_canceled, {Next, ReplyTo}, ok}}) change the ScanMsg from {scan_results, {error, scan_canceled}} to {scan_results, {error, canceled}} so the scan_reply_or_callback/2 and callers receive the documented `{error, canceled}` shape (leave the surrounding logic that replies to ReplyTo and clears State#state.scan_receiver intact).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 471-472: The sentence describing behavior when network:stop/0 is
invoked during a WiFi scan is missing the verb "is"; update the text so it reads
"If `network:stop/0` is called while a WiFi scan is in progress, the scan caller
or callback may receive either the final scan result or `{error, canceled}`." to
correct the grammar and preserve the intended meaning.
- Line 339: The sentence "This setting is only used while a device is operation
is AP mode only" is malformed; update the doc text around the esp32 ap_channel
description to read something like: "This setting is only used while a device is
operating in AP mode." Keep the rest of the paragraph intact (references to
esp32, ap_channel behavior when AP+STA mode is used, and the hardware limitation
of the modem radio) and ensure only the grammar is corrected.
In `@libs/avm_network/src/network.erl`:
- Around line 829-845: The scan cancellation message uses the wrong error atom;
in handle_info (the clause matching {Ref, {scan_canceled, {Next, ReplyTo}, ok}})
change the ScanMsg from {scan_results, {error, scan_canceled}} to {scan_results,
{error, canceled}} so the scan_reply_or_callback/2 and callers receive the
documented `{error, canceled}` shape (leave the surrounding logic that replies
to ReplyTo and clears State#state.scan_receiver intact).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: efd977ec-3e19-4dbf-be3c-46bf415032f6
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/avm_network/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
✅ Files skipped from review due to trivial changes (2)
- CHANGELOG.md
- examples/erlang/esp32/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (2)
- src/platforms/esp32/test/main/test_main.c
- examples/erlang/esp32/wifi_scan.erl
👮 Files not reviewed due to content moderation or server errors (1)
- src/platforms/esp32/components/avm_builtins/network_driver.c
Please update the learnings to reflect that the contract is now |
|
Tip For best results, initiate chat on the files or code changes.
The stale review comment you quoted was generated before that update — future reviews will use the correct 🧠 Learnings used |
Update the use of deprecated `term_from_int32` to `term_from_int11` for encoding gpio pin values. The values will fit in an int4, but the type is uint16_t, so `term_to_int11` is used for correctness. Signed-off-by: Winford <winford@object.stream>
Change all uses of the `term_from_int32` to `term_from_int11`, which is of adequate size to contain all of the encoded terms. Signed-off-by: Winford <winford@object.stream>
Replace use of `term_from_int32` other safe term_from_int* variants. Signed-off-by: Winford <winford@object.stream>
Updates use of deprecated `term_from_int32` to `term_from_int` when encoding the `send_timeout_ms` value. Signed-off-by: Winford <winford@object.stream>
Update the use of `term_from_int32` to encode `port` (`u16_t`) values to the new `term_from_int28`. Signed-off-by: Winford <winford@object.stream>
Update the use of deprecated `term_from_int32` to `term_from_int11` for encoding gpio pin values. The 11-bit value is more than enough to allow for the number of pins available on any ESP32 for many years. Signed-off-by: Winford <winford@object.stream>
Update deprecated `term_from_int32` to use `term_from_int28`. See also: atomvm#1897 Signed-off-by: Winford <winford@object.stream>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (4)
doc/src/network-programming-guide.md (3)
339-339:⚠️ Potential issue | 🟡 MinorFix AP-mode sentence grammar.
“This setting is only used while a device is operation in AP mode only” reads incorrectly; use “operating in AP mode” wording.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 339, Edit the sentence that currently reads “This setting is only used while a device is operation in AP mode only” to correct the grammar and redundancy: change it to “This setting is only used while a device is operating in AP mode” (or simply “This setting is used while a device is operating in AP mode”); ensure the surrounding text referencing ap_channel and AP + STA mode remains unchanged.
337-337:⚠️ Potential issue | 🟡 MinorUse project-consistent strong emphasis style.
__open network__should use**open network**for consistency with the rest of the guide/linting style.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 337, The guide uses double-underscore emphasis for "open network" which is inconsistent with the project's bold style; replace the occurrence of "__open network__" with "**open network**" in the sentence that reads "If the password is omitted, then an __open network__ will be created..." to match the rest of the document's emphasis convention.
223-225:⚠️ Potential issue | 🟡 MinorUse
{error, scan_canceled}(not{error, canceled}) in scan-cancel docs.These sections document the old cancellation atom and now conflict with the current scan-cancel contract.
📝 Proposed doc fix
-If the network is stopped while a scan is in progress, the callback or caller may receive either a successful scan -result, or `{error, canceled}`. +If the network is stopped while a scan is in progress, the callback or caller may receive either a successful scan +result, or `{error, scan_canceled}`. @@ -If `network:stop/0` is called while a WiFi scan is in progress, the scan caller or callback may -receive either the final scan result or `{error, canceled}`. +If `network:stop/0` is called while a WiFi scan is in progress, the scan caller or callback may +receive either the final scan result or `{error, scan_canceled}`.Based on learnings,
cancel_scandelivers{error, scan_canceled}to scan receivers/callback paths, and{error, canceled}is obsolete in this PR.Also applies to: 470-472
🤖 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 223 - 225, Update the docs to reflect the current scan-cancel contract: replace the obsolete `{error, canceled}` with `{error, scan_canceled}` in the passages describing scan cancellation (mentions around the current paragraph and the other occurrence noted), and ensure any text referencing cancel_scan or scan callback/caller behavior explicitly states that cancel_scan delivers `{error, scan_canceled}` to receivers/callbacks.src/platforms/esp32/components/avm_builtins/network_driver.c (1)
1630-1645:⚠️ Potential issue | 🟡 MinorValidate
dwellbefore narrowing it.Line 1644 converts the Erlang integer with
term_to_uint32()before the bounds check. Oversized boxed integers can wrap into1..1500, so an invalid{dwell, ...}can start a scan instead of returningbadarg.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1630 - 1645, The code narrows the Erlang integer with term_to_uint32(cfg_dwell) before validating bounds, allowing oversized boxed integers to wrap; instead, obtain the integer using a wider/checked conversion (e.g., term_to_int64 or term_to_uint64 / a checked conversion API), validate that the value is within 1..1500, handle conversion failure by sending BADARG (same as current error path), and only after the bounds check cast/assign to uint32_t (update references to cfg_dwell, term_is_integer, dwell_config/ dwell_ms, and term_to_uint32 accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1735-1751: The branch that handles the esp_wifi_scan_start failure
currently skips clearing ctx->platform_data and freeing data if
esp_event_handler_unregister() fails, leaving stale scan ownership; modify the
error path so that regardless of the return value of
esp_event_handler_unregister(scan_done_handler), you still set
ctx->platform_data = NULL and free(data) (and set data = NULL) before
returning/sending the error reply, while still logging the unregister failure
via ESP_LOGE; locate the block around esp_event_handler_unregister,
scan_done_handler, ctx->platform_data and data and ensure cleanup (free +
nulling) runs in both the success and failure branches.
- Around line 883-895: The handler collapses driver failures into the
user-cancellation atom by always sending send_scan_error_from_task with
ATOM_STR("scan_canceled"); instead, detect scan_done->status != 0 in the
WIFI_EVENT_SCAN_DONE branch and send a dedicated failure reason (e.g.,
ATOM_STR("scan_failed") or a tuple containing the numeric status) via
send_scan_error_from_task so callers can distinguish driver errors from
cancellations; update the call that currently uses
ATOM_STR("\xD","scan_canceled") (and any related error construction) to use the
new atom or include scan_done->status, leaving esp_wifi_clear_ap_list() and
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE,
&scan_done_handler) behavior unchanged.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Line 339: Edit the sentence that currently reads “This setting is only used
while a device is operation in AP mode only” to correct the grammar and
redundancy: change it to “This setting is only used while a device is operating
in AP mode” (or simply “This setting is used while a device is operating in AP
mode”); ensure the surrounding text referencing ap_channel and AP + STA mode
remains unchanged.
- Line 337: The guide uses double-underscore emphasis for "open network" which
is inconsistent with the project's bold style; replace the occurrence of "__open
network__" with "**open network**" in the sentence that reads "If the password
is omitted, then an __open network__ will be created..." to match the rest of
the document's emphasis convention.
- Around line 223-225: Update the docs to reflect the current scan-cancel
contract: replace the obsolete `{error, canceled}` with `{error, scan_canceled}`
in the passages describing scan cancellation (mentions around the current
paragraph and the other occurrence noted), and ensure any text referencing
cancel_scan or scan callback/caller behavior explicitly states that cancel_scan
delivers `{error, scan_canceled}` to receivers/callbacks.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1630-1645: The code narrows the Erlang integer with
term_to_uint32(cfg_dwell) before validating bounds, allowing oversized boxed
integers to wrap; instead, obtain the integer using a wider/checked conversion
(e.g., term_to_int64 or term_to_uint64 / a checked conversion API), validate
that the value is within 1..1500, handle conversion failure by sending BADARG
(same as current error path), and only after the bounds check cast/assign to
uint32_t (update references to cfg_dwell, term_is_integer, dwell_config/
dwell_ms, and term_to_uint32 accordingly).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c211f71b-7a3d-437b-814d-26965bd33873
📒 Files selected for processing (10)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/avm_network/src/network.erlsrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.c
✅ Files skipped from review due to trivial changes (2)
- examples/erlang/esp32/wifi_scan_callback.erl
- src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl
🚧 Files skipped from review as they are similar to previous changes (5)
- src/platforms/esp32/test/main/test_main.c
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- examples/erlang/esp32/CMakeLists.txt
- examples/erlang/esp32/wifi_scan.erl
- libs/avm_network/src/network.erl
| if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { | ||
| wifi_event_sta_scan_done_t *scan_done = (wifi_event_sta_scan_done_t *) event_data; | ||
| if (scan_done->status == 0) { | ||
| ESP_LOGD(TAG, "Scan complete."); | ||
| send_scan_results(data); | ||
| } else { | ||
| ESP_LOGW(TAG, "Scan ended with status: failure"); | ||
| esp_wifi_clear_ap_list(); | ||
| esp_err_t err = esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); | ||
| BEGIN_WITH_STACK_HEAP(PORT_REPLY_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2), heap); | ||
| send_scan_error_from_task(global, data->owner_process_id, | ||
| globalcontext_existing_term_from_atom_string(global, ATOM_STR("\xD", "scan_canceled")), | ||
| data->ref_ticks, heap); |
There was a problem hiding this comment.
Don't collapse scan failures into scan_canceled.
Line 889 logs a driver failure, but Lines 893-895 send {error, scan_canceled}. That makes genuine scan failures indistinguishable from user-initiated cancellation and pushes callers down the wrong recovery path. Return a dedicated failure reason here instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines
883 - 895, The handler collapses driver failures into the user-cancellation atom
by always sending send_scan_error_from_task with ATOM_STR("scan_canceled");
instead, detect scan_done->status != 0 in the WIFI_EVENT_SCAN_DONE branch and
send a dedicated failure reason (e.g., ATOM_STR("scan_failed") or a tuple
containing the numeric status) via send_scan_error_from_task so callers can
distinguish driver errors from cancellations; update the call that currently
uses ATOM_STR("\xD","scan_canceled") (and any related error construction) to use
the new atom or include scan_done->status, leaving esp_wifi_clear_ap_list() and
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE,
&scan_done_handler) behavior unchanged.
MCUs: update deprecated use of term_from_int32 See also: atomvm#1897 These changes are made under both the "Apache 2.0" and the "GNU Lesser General Public License 2.1 or later" license terms (dual license). SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
Avoid a possible crash when connecting to an open network, by not de-refernencing a NULL pointer. Fix incorrect cast that may lead to crashes or unexpected behaviors during client connection events when AP mode is enabled. Signed-off-by: Winford <winford@object.stream>
Corrects the type name db() to the more correct `dbm()`, and adds a brief edoc explanation for the value. Signed-off-by: Winford <winford@object.stream>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/platforms/esp32/components/avm_builtins/adc_driver.c (1)
598-605:⚠️ Potential issue | 🟡 MinorUpdate the validation message to match
MAX_SAMPLES.The code accepts up to
100000, but the error text still says1..1024. That will send users looking in the wrong place when sample-count validation fails.Suggested fix
- ESP_LOGE(TAG, "samples value must be an integer from 1 to 1024."); + ESP_LOGE(TAG, "samples value must be an integer from 1 to %d.", MAX_SAMPLES); @@ - ESP_LOGE(TAG, "invalid samples value: %i, out of range (1..1024)", samples_val); + ESP_LOGE(TAG, "invalid samples value: %i, out of range (1..%d)", samples_val, MAX_SAMPLES);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/adc_driver.c` around lines 598 - 605, The error message for samples validation is hardcoded to "1..1024" while the code uses MAX_SAMPLES (e.g., up to 100000); update the ESP_LOGE call in the block that checks samples_val (after term_to_int32(samples) and the UNLIKELY range check) to print the actual MAX_SAMPLES instead of 1024 (for example: "invalid samples value: %i, out of range (1..%i)" and pass samples_val, MAX_SAMPLES) so the runtime message matches the validation logic involving MAX_SAMPLES.
♻️ Duplicate comments (6)
doc/src/network-programming-guide.md (2)
223-225:⚠️ Potential issue | 🟡 MinorUse
scan_canceledin both stop-while-scan notes.These two spots still document the old
{error, canceled}atom. The current contract is{error, scan_canceled}, with{Num, Networks}still possible if the scan finishes before cancellation wins.Based on learnings:
cancel_scandelivers{error, scan_canceled}(not{error, canceled}) to thescan_donecallback, and{Num, Networks}is still possible if the scan finishes first.Also applies to: 470-472
🤖 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 223 - 225, Update the two occurrences that mention the old `{error, canceled}` return value to the current contract `{error, scan_canceled}`; specifically change the sentence describing what happens when the network is stopped during a scan to state that `cancel_scan` will deliver `{error, scan_canceled}` to the `scan_done` callback (while keeping that `{Num, Networks}` remains possible if the scan completes before cancellation). Make the same change in the other occurrence noted (around the `scan_done` / cancel behavior) so both spots consistently document `{error, scan_canceled}`.
337-339:⚠️ Potential issue | 🟡 MinorPolish the AP-mode wording in this paragraph.
__open network__doesn't match the rest of the guide's emphasis style, and “is operation in AP mode only” still reads like a typo.Suggested wording
-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 channel for esp32 is `1`. This setting is only used while a device is operation in 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. +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 channel for ESP32 is `1`. This setting is only used while a device is operating in 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.🤖 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 337 - 339, Revise the paragraph to use consistent emphasis (replace "__open network__" with the guide's standard emphasis style, e.g., italic or inline code), correct the typo "is operation in AP mode only" to "is used only when a device is operating in AP mode", and tighten the AP+STA sentence to say: if ap_channel is set it will be temporarily changed to match the associated access point when the device is in AP+STA mode and associated, noting the hardware limitation that the modem can only use one channel at a time; ensure the symbol ap_channel and phrases "AP+STA mode" and "open network" remain present and styled consistently with the rest of the guide.libs/avm_network/src/network.erl (1)
817-828:⚠️ Potential issue | 🟠 MajorKeep the callback target alive until the trailing unregister error is delivered.
Line 826 clears
scan_receiveras soon as the first successful callback-mode result arrives. If the driver then sends its intentional follow-up{scan_results, {error, {unregister_handler, Reason}}}, Line 819 no longer has the original callback/pid target, so that signal gets dropped instead of reaching the registered scan consumer. Preserve the callback dispatch context until that second message is handled, and route it through the same asynchronous callback path used for normal callback results.Based on learnings:
send_scan_resultsintentionally emits a second{scan_results, {error, {unregister_handler, Reason}}}message so callback users can decide whether future scans are viable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 817 - 828, The first successful callback-path handle_info clause clears scan_receiver too early so the follow-up `{scan_results, {error, {unregister_handler, Reason}}}` is dropped; update the logic so callback dispatch context (scan_receiver) is preserved until the trailing unregister error is handled and ensure the unregister message is routed through the same asynchronous dispatch path used for normal callback results: leave `scan_receiver` intact (do not set it to `undefined`) in the clause matching `#state{scan_receiver = {reply, _}}` and use the same `spawn(fun() -> scan_reply_or_callback(Msg, State) end)` approach (as in the third clause) for delivering the callback and only clear `scan_receiver` when an unregister error is actually processed by the handler that matches `{scan_results, {error, {unregister_handler, _}}}` so the original callback PID stays alive until that message is delivered.src/platforms/esp32/components/avm_builtins/network_driver.c (3)
1715-1722:⚠️ Potential issue | 🔴 CriticalDon't publish a new scan while the previous completion still owns
ctx->platform_data.Line 1720 stores the next
ScanDataunconditionally, but the previous completion path only clearsctx->platform_dataafter it has already queued the{scan_results, ...}reply. That lets a follow-up scan overwrite the slot just before the previous teardown nulls it, orphaning the new request state. Reject or defer the new scan until the slot is alreadyNULL.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1715 - 1722, The code stores a new ScanData into ctx->platform_data unconditionally, which can overwrite an existing active scan; before allocating/storing the new ScanData in ctx->platform_data inside the function that sets data->ctx (the scan start path), check if ctx->platform_data is non-NULL and if so reject or defer the new scan (return an error or queue it) rather than overwriting; ensure the check references ctx->platform_data and the ScanData type and that scan_done_handler still clears ctx->platform_data only after its completion, so the start path must guard against replacing the slot.
883-895:⚠️ Potential issue | 🟠 MajorReturn a driver-failure reason instead of
scan_canceled.This branch logs
scan_done->status != 0as a scan failure but still delivers{error, scan_canceled}. That makes genuine driver failures indistinguishable from user cancellation on the Erlang side, so callers can take the wrong recovery path; use a dedicated failure reason here (for examplescan_failed, or includescan_done->status).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 883 - 895, The failure branch for WIFI_EVENT_SCAN_DONE treats any nonzero scan_done->status as a user cancellation by sending the atom scan_canceled; instead create and send a distinct driver-failure reason (e.g. scan_failed or include the numeric status) from send_scan_error_from_task so Erlang can distinguish driver failures from cancellations. Locate the branch where scan_done->status != 0 (inside the WIFI_EVENT && WIFI_EVENT_SCAN_DONE handler) and change the globalcontext_existing_term_from_atom_string(...) call that currently uses ATOM_STR("\xD","scan_canceled") to instead use a new atom like "scan_failed" or build a tuple containing {"scan_failed", Status} (using scan_done->status) before calling send_scan_error_from_task so the error reason reflects the driver failure.
1732-1751:⚠️ Potential issue | 🟠 Major
esp_wifi_scan_start()failure can leave a phantom scan behind.If
esp_wifi_scan_start()fails andesp_event_handler_unregister()also fails, this branch only logs and returns. No scan will ever complete to reclaimdata, soctx->platform_dataand the stalescan_done_handlerregistration can survive into the next request.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1732 - 1751, When esp_wifi_scan_start() fails, ensure we always clear ctx->platform_data and free(data) so a phantom scan and stale scan_done_handler don't persist: after building and sending the error reply (the existing port_send_reply(ctx, pid, ref, ret) call), call esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler) but regardless of its return value set ctx->platform_data = NULL and free(data) (and null it) so resources are reclaimed; still log if esp_event_handler_unregister returns an error but don't skip freeing/clearing (symbols: esp_wifi_scan_start, esp_event_handler_unregister, scan_done_handler, ctx->platform_data, data).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1779-1814: The CAS winner should keep cleanup ownership: currently
after __atomic_compare_exchange sets data->canceled true (claimed == true) the
error branches reset data->canceled back to false, relinquishing ownership;
change both branches (after esp_wifi_scan_stop() error handling) to only store
false when we did NOT win the CAS (i.e., replace "if (claimed) {
__atomic_store(&data->canceled, &released, ...); }" with "if (!claimed) {
__atomic_store(&data->canceled, &released, ...); }") so the CAS winner (the
cancel_scan path that flipped data->canceled) remains responsible for
unregistering/freeing the ScanData and scan_done_handler can still clean up.
In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl`:
- Around line 207-209: The teardown in bad_options_test/0 currently ignores
failures from network:stop/0; update bad_options_test/0 to assert that
network:stop() succeeds by checking its return value (e.g., assert equal to ok
or match {ok, _} as appropriate) instead of discarding it so any cleanup failure
is reported and won't leak state into later tests; locate the call to
network:stop() in bad_options_test/0 and replace the ignored call with an
assertion on its result.
- Around line 229-236: The receive block that matches {Num, Networks}, {error,
Reason}, or other Error must include an after clause to avoid hanging; update
the receive in the pid-results receiver test (the clause handling {Num,
Networks} / {error, Reason} / Error) to add an after <timeout_ms> clause (e.g.,
5_000 or 15_000) that calls erlang:error({pid_results_receiver_test, timeout})
(or similar timeout diagnostic) so the test fails fast instead of blocking
indefinitely.
---
Outside diff comments:
In `@src/platforms/esp32/components/avm_builtins/adc_driver.c`:
- Around line 598-605: The error message for samples validation is hardcoded to
"1..1024" while the code uses MAX_SAMPLES (e.g., up to 100000); update the
ESP_LOGE call in the block that checks samples_val (after term_to_int32(samples)
and the UNLIKELY range check) to print the actual MAX_SAMPLES instead of 1024
(for example: "invalid samples value: %i, out of range (1..%i)" and pass
samples_val, MAX_SAMPLES) so the runtime message matches the validation logic
involving MAX_SAMPLES.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Around line 223-225: Update the two occurrences that mention the old `{error,
canceled}` return value to the current contract `{error, scan_canceled}`;
specifically change the sentence describing what happens when the network is
stopped during a scan to state that `cancel_scan` will deliver `{error,
scan_canceled}` to the `scan_done` callback (while keeping that `{Num,
Networks}` remains possible if the scan completes before cancellation). Make the
same change in the other occurrence noted (around the `scan_done` / cancel
behavior) so both spots consistently document `{error, scan_canceled}`.
- Around line 337-339: Revise the paragraph to use consistent emphasis (replace
"__open network__" with the guide's standard emphasis style, e.g., italic or
inline code), correct the typo "is operation in AP mode only" to "is used only
when a device is operating in AP mode", and tighten the AP+STA sentence to say:
if ap_channel is set it will be temporarily changed to match the associated
access point when the device is in AP+STA mode and associated, noting the
hardware limitation that the modem can only use one channel at a time; ensure
the symbol ap_channel and phrases "AP+STA mode" and "open network" remain
present and styled consistently with the rest of the guide.
In `@libs/avm_network/src/network.erl`:
- Around line 817-828: The first successful callback-path handle_info clause
clears scan_receiver too early so the follow-up `{scan_results, {error,
{unregister_handler, Reason}}}` is dropped; update the logic so callback
dispatch context (scan_receiver) is preserved until the trailing unregister
error is handled and ensure the unregister message is routed through the same
asynchronous dispatch path used for normal callback results: leave
`scan_receiver` intact (do not set it to `undefined`) in the clause matching
`#state{scan_receiver = {reply, _}}` and use the same `spawn(fun() ->
scan_reply_or_callback(Msg, State) end)` approach (as in the third clause) for
delivering the callback and only clear `scan_receiver` when an unregister error
is actually processed by the handler that matches `{scan_results, {error,
{unregister_handler, _}}}` so the original callback PID stays alive until that
message is delivered.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1715-1722: The code stores a new ScanData into ctx->platform_data
unconditionally, which can overwrite an existing active scan; before
allocating/storing the new ScanData in ctx->platform_data inside the function
that sets data->ctx (the scan start path), check if ctx->platform_data is
non-NULL and if so reject or defer the new scan (return an error or queue it)
rather than overwriting; ensure the check references ctx->platform_data and the
ScanData type and that scan_done_handler still clears ctx->platform_data only
after its completion, so the start path must guard against replacing the slot.
- Around line 883-895: The failure branch for WIFI_EVENT_SCAN_DONE treats any
nonzero scan_done->status as a user cancellation by sending the atom
scan_canceled; instead create and send a distinct driver-failure reason (e.g.
scan_failed or include the numeric status) from send_scan_error_from_task so
Erlang can distinguish driver failures from cancellations. Locate the branch
where scan_done->status != 0 (inside the WIFI_EVENT && WIFI_EVENT_SCAN_DONE
handler) and change the globalcontext_existing_term_from_atom_string(...) call
that currently uses ATOM_STR("\xD","scan_canceled") to instead use a new atom
like "scan_failed" or build a tuple containing {"scan_failed", Status} (using
scan_done->status) before calling send_scan_error_from_task so the error reason
reflects the driver failure.
- Around line 1732-1751: When esp_wifi_scan_start() fails, ensure we always
clear ctx->platform_data and free(data) so a phantom scan and stale
scan_done_handler don't persist: after building and sending the error reply (the
existing port_send_reply(ctx, pid, ref, ret) call), call
esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE,
&scan_done_handler) but regardless of its return value set ctx->platform_data =
NULL and free(data) (and null it) so resources are reclaimed; still log if
esp_event_handler_unregister returns an error but don't skip freeing/clearing
(symbols: esp_wifi_scan_start, esp_event_handler_unregister, scan_done_handler,
ctx->platform_data, data).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 8b8765c3-b84e-4ff8-ba79-cb0dd49c5870
📒 Files selected for processing (17)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/avm_network/src/network.erlsrc/platforms/esp32/components/avm_builtins/adc_driver.csrc/platforms/esp32/components/avm_builtins/gpio_driver.csrc/platforms/esp32/components/avm_builtins/i2c_resource.csrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/components/avm_builtins/socket_driver.csrc/platforms/esp32/components/avm_sys/sys.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.csrc/platforms/rp2/src/lib/networkdriver.csrc/platforms/stm32/src/lib/gpio_driver.c
✅ Files skipped from review due to trivial changes (3)
- CHANGELOG.md
- examples/erlang/esp32/CMakeLists.txt
- examples/erlang/esp32/wifi_scan_callback.erl
🚧 Files skipped from review as they are similar to previous changes (3)
- src/platforms/esp32/test/main/test_main.c
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- examples/erlang/esp32/wifi_scan.erl
| // Claim cleanup ownership atomically. If we lose the CAS, send_scan_results already has ownership. | ||
| bool expected = false; | ||
| bool desired = true; | ||
| bool claimed = __atomic_compare_exchange(&data->canceled, &expected, &desired, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); | ||
|
|
||
| esp_err_t err = esp_wifi_scan_stop(); | ||
| if (UNLIKELY(err == ESP_ERR_WIFI_STATE)) { | ||
| ESP_LOGE(TAG, "Unable to stop wifi scan, wifi is negotiating a connection to an access point"); | ||
|
|
||
| // Release ownership so handler can clean up when scan completes | ||
| if (claimed) { | ||
| bool released = false; | ||
| __atomic_store(&data->canceled, &released, __ATOMIC_SEQ_CST); | ||
| } | ||
|
|
||
| size_t error_size = PORT_REPLY_SIZE + TUPLE_SIZE(3) + TUPLE_SIZE(2); | ||
| port_ensure_available(ctx, error_size); | ||
|
|
||
| term reason = make_atom(ctx->global, ATOM_STR("\xE", "sta_connecting")); | ||
| if (UNLIKELY(reason == term_invalid_term())) { | ||
| reason = OUT_OF_MEMORY_ATOM; | ||
| } | ||
| term error_tuple = port_create_error_tuple(ctx, reason); | ||
| term ret = port_create_tuple3(ctx, scan_canceled_atom, reply_config, error_tuple); | ||
| port_send_reply(ctx, pid, ref, ret); | ||
| return; | ||
|
|
||
| } else if (UNLIKELY(err != ESP_OK)) { | ||
| const char *err_str = esp_err_to_name(err); | ||
| size_t error_len = strlen(err_str); | ||
|
|
||
| // Release ownership so handler can clean up when scan completes | ||
| if (claimed) { | ||
| bool released = false; | ||
| __atomic_store(&data->canceled, &released, __ATOMIC_SEQ_CST); | ||
| } |
There was a problem hiding this comment.
Keep the CAS winner responsible for cleanup.
After cancel_scan() successfully flips data->canceled to true, both error branches store false back to hand ownership to scan_done_handler(). If the handler already observed true and returned, nobody is left to unregister/free the scan state, and the scan can get stuck permanently. Based on learnings: ownership of ScanData between cancel and completion is controlled exclusively by an atomic CAS on the ScanData.canceled field.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines
1779 - 1814, The CAS winner should keep cleanup ownership: currently after
__atomic_compare_exchange sets data->canceled true (claimed == true) the error
branches reset data->canceled back to false, relinquishing ownership; change
both branches (after esp_wifi_scan_stop() error handling) to only store false
when we did NOT win the CAS (i.e., replace "if (claimed) {
__atomic_store(&data->canceled, &released, ...); }" with "if (!claimed) {
__atomic_store(&data->canceled, &released, ...); }") so the CAS winner (the
cancel_scan path that flipped data->canceled) remains responsible for
unregistering/freeing the ScanData and scan_done_handler can still clean up.
| after | ||
| network:stop() | ||
| end; |
There was a problem hiding this comment.
Assert teardown success in bad_options_test/0.
Line 208 currently ignores network:stop/0 failures, which can mask cleanup problems and leak state into later tests.
💡 Minimal fix
after
- network:stop()
+ ok = network:stop()
end;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| after | |
| network:stop() | |
| end; | |
| after | |
| ok = network:stop() | |
| end; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl` around
lines 207 - 209, The teardown in bad_options_test/0 currently ignores failures
from network:stop/0; update bad_options_test/0 to assert that network:stop()
succeeds by checking its return value (e.g., assert equal to ok or match {ok, _}
as appropriate) instead of discarding it so any cleanup failure is reported and
won't leak state into later tests; locate the call to network:stop() in
bad_options_test/0 and replace the ignored call with an assertion on its result.
| receive | ||
| {Num, Networks} when is_integer(Num) andalso is_list(Networks) -> | ||
| ok; | ||
| {error, Reason} -> | ||
| erlang:error({pid_results_receiver_test, {failed, Reason}}); | ||
| Error -> | ||
| erlang:error({pid_results_receiver_test, {unexpected_message, Error}}) | ||
| end; |
There was a problem hiding this comment.
Add a timeout to avoid indefinite hang in pid-receiver test.
This receive has no after clause. If scan results are never delivered, the test can block until external CI watchdog termination.
💡 Minimal fix
receive
{Num, Networks} when is_integer(Num) andalso is_list(Networks) ->
ok;
{error, Reason} ->
erlang:error({pid_results_receiver_test, {failed, Reason}});
Error ->
erlang:error({pid_results_receiver_test, {unexpected_message, Error}})
+ after 15000 ->
+ erlang:error({pid_results_receiver_test, scan_results_timeout})
end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl` around
lines 229 - 236, The receive block that matches {Num, Networks}, {error,
Reason}, or other Error must include an after clause to avoid hanging; update
the receive in the pid-results receiver test (the clause handling {Num,
Networks} / {error, Reason} / Error) to add an after <timeout_ms> clause (e.g.,
5_000 or 15_000) that calls erlang:error({pid_results_receiver_test, timeout})
(or similar timeout diagnostic) so the test fails fast instead of blocking
indefinitely.
Closes atomvm#2024 Signed-off-by: Winford <winford@object.stream>
Adds documentation for `network:wifi_scan/0,1` and updates details for `network:sta_rssi/0`. Signed-off-by: Winford <winford@object.stream>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (8)
src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl (2)
229-236:⚠️ Potential issue | 🟠 MajorBound the pid-receiver wait so the suite fails fast.
This
receivehas noafterclause. If pid-basedscan_donedelivery never arrives,test_wifi_scan.beamblocks here until the outer CI watchdog kills the whole ESP32 test run.💡 Suggested fix
receive {Num, Networks} when is_integer(Num) andalso is_list(Networks) -> ok; {error, Reason} -> erlang:error({pid_results_receiver_test, {failed, Reason}}); Error -> erlang:error({pid_results_receiver_test, {unexpected_message, Error}}) + after 15000 -> + erlang:error({pid_results_receiver_test, scan_results_timeout}) end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl` around lines 229 - 236, The receive in the pid results handling (the clause that matches {Num, Networks}, {error, Reason}, and Error) in test_wifi_scan.erl has no timeout and can block indefinitely; add an after clause (e.g., a short timeout like a few seconds) to fail fast and raise a deterministic error (use erlang:error({pid_results_receiver_test, {timeout, TimeoutMs}}) or similar) so the test suite fails promptly if the scan_done PID message never arrives; update the receive block that references pid_results_receiver_test to include this after branch and ensure any cleanup or logging is preserved.
207-208:⚠️ Potential issue | 🟠 MajorMake
bad_options_test/0fail if teardown fails.Ignoring
network:stop/0here can leak a livenetworkserver into the next subtest, which then makes the opening{error, network_not_started}assertion fail for the wrong reason.💡 Suggested fix
after - network:stop() + ok = network:stop() end;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl` around lines 207 - 208, The after block in bad_options_test/0 currently ignores the result of network:stop(), which can leak a running network server into subsequent subtests; update the teardown to check the return value of network:stop/0 and make the test fail if it does not return ok (e.g., assert or call ?fail with the error), so any teardown failure in bad_options_test/0 surfaces as a test failure instead of being silently ignored.doc/src/network-programming-guide.md (2)
339-339:⚠️ Potential issue | 🟡 MinorFix remaining grammar error in AP channel description.
Line 339 still reads "while a device is operation in AP mode" instead of "is operating in AP mode". This was flagged previously but remains unfixed.
📝 Suggested fix
-If the channel is omitted the default channel for esp32 is `1`. This setting is only used while a device is operation in AP mode only. +If the channel is omitted the default channel for esp32 is `1`. This setting is only used while a device is operating in AP mode only.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 339, Replace the incorrect phrase "while a device is operation in AP mode" with "while a device is operating in AP mode" in the AP channel description (search for the exact string "while a device is operation in AP mode" in doc/src/network-programming-guide.md) to correct the grammar.
337-337:⚠️ Potential issue | 🟡 MinorUse consistent Markdown strong-emphasis style.
Line 337 still uses
__open network__instead of the project-preferred**open network**. This was flagged in a previous review but appears unresolved.📝 Suggested fix
-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.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@doc/src/network-programming-guide.md` at line 337, Replace the inconsistent Markdown strong-emphasis `__open network__` in the sentence inside network-programming-guide.md with the project-preferred `**open network**`; locate the sentence that begins "If the password is omitted, then an __open network__ will be created..." and update the emphasis style so it uses double asterisks instead of double underscores.libs/avm_network/src/network.erl (2)
818-822:⚠️ Potential issue | 🟠 MajorDon't drop the late unregister failure for callback/pid scans.
Clearing
scan_receiverimmediately after scheduling the success callback means the intentional follow-up{scan_results, {error, {unregister_handler, Reason}}}arrives with no dispatch target, so callback/pid consumers never see the error that explains why future scans are now blocked.Based on learnings: the driver intentionally sends a second
{scan_results, {error, {unregister_handler, Reason}}}message after successful results so callback users can decide whether future scans are viable.Also applies to: 828-830
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/avm_network/src/network.erl` around lines 818 - 822, The handle_info clause matching {Ref, {scan_results, {error, {unregister_handler, _}}} = Msg} currently calls scan_reply_or_callback(Msg, State) then immediately clears State#state.scan_receiver to blocked, which drops the intentional follow-up unregister error for callback/pid scans; modify the logic in the handle_info clauses (including the similar block around lines 828-830) so that you do not clear scan_receiver until after any potential follow-up unregister_handler message is delivered—either by returning a state that preserves scan_receiver until an explicit unregister message is observed or by deferring setting scan_receiver = blocked until you detect the unregister error in the message processing done by scan_reply_or_callback; locate the clause using the Ref pattern and the scan_reply_or_callback call to implement the change.
848-855:⚠️ Potential issue | 🟠 MajorTreat a shutdown cancel miss as success once the scan already finished.
If normal scan results win the race and clear
scan_receiverfirst, replying{error, cancel_scan_failed}here makesstop/0fail even though there is no active scan left to cancel and the scan consumer may already have received{Num, Networks}.🛑 One way to make the shutdown path idempotent
handle_info( - {Ref, {scan_canceled, {shutdown, ReplyTo}, {error, _Reason}}}, `#state`{ref = Ref} = State + {Ref, {scan_canceled, {shutdown, ReplyTo}, {error, _Reason}}}, + `#state`{ref = Ref, scan_receiver = undefined} = State +) -> + gen_server:reply(ReplyTo, ok), + {stop, normal, State}; +handle_info( + {Ref, {scan_canceled, {shutdown, ReplyTo}, {error, _Reason}}}, `#state`{ref = Ref} = State ) -> gen_server:reply(ReplyTo, {error, cancel_scan_failed}), {noreply, State};Based on learnings: stop-while-scanning may legitimately complete with either
{Num, Networks}or{error, scan_canceled}depending on whether the scan finished before cancellation.src/platforms/esp32/components/avm_builtins/network_driver.c (2)
1842-1845:⚠️ Potential issue | 🟠 MajorBound this wait loop to avoid indefinite wedging.
Line 1842 spins forever if
ctx->platform_datanever transitions toNULL(e.g., abnormal callback/unregister path). This can wedgecancel_scan/network:stop.🔧 Suggested fix
- while (__atomic_load_n((void **)&ctx->platform_data, __ATOMIC_ACQUIRE) != NULL) { - // Spin until handler sets ctx->platform_data = NULL after free - } + uint32_t spin_count = 0; + while (__atomic_load_n((void **)&ctx->platform_data, __ATOMIC_ACQUIRE) != NULL) { + if (++spin_count > 5000) { + ESP_LOGE(TAG, "Timed out waiting for scan cleanup"); + break; + } + vTaskDelay(1); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1842 - 1845, The spin-wait on ctx->platform_data (inside the block guarded by if (!claimed)) can hang forever; change it to a bounded wait by replacing the infinite spin with a loop that checks ctx->platform_data with a timeout (use a counter or timestamp via esp_timer_get_time()/gettimeofday and a small sleep/yield like vTaskDelay/yield or esp_rom_delay_us between checks) and if the timeout expires abort the wait and return an error/cleanup so cancel_scan/network:stop can't wedge; update the surrounding code handling (the caller or this function) to handle the failure path when the bounded wait gives up.
1801-1804:⚠️ Potential issue | 🔴 CriticalKeep CAS ownership once cancel wins; don’t flip
canceledback tofalse.Line 1801 and Line 1823 release ownership after a successful CAS claim. If
scan_done_handleralready sawcanceled=trueand returned, resetting it tofalseleaves no active owner to clean upScanData.🔧 Suggested fix
- if (claimed) { - bool released = false; - __atomic_store(&data->canceled, &released, __ATOMIC_SEQ_CST); - } + // Keep ownership with cancel path once CAS is won. + // Do not release canceled back to false here.Based on learnings: ownership between cancel and completion paths is controlled exclusively by an atomic CAS on
ScanData.canceled; the CAS winner should retain cleanup responsibility.Also applies to: 1823-1826
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines 1801 - 1804, The CAS winner must keep ownership and not flip ScanData.canceled back to false; in both the cancel path and the completion path (look for the local variable named "claimed" and updates to ScanData.canceled in scan_done_handler and the cancel routine), remove the __atomic_store that sets canceled to false after a successful CAS claim so the owner retains responsibility for cleanup of ScanData; ensure only the true->false transition is performed by the cancel CAS loser logic and that the CAS winner performs cleanup without resetting ScanData.canceled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/avm_network/src/network.erl`:
- Around line 634-636: The timeout calculation can still be below the documented
floor; modify the Timeout computation in the block using
get_num_channels_timeout/0 so the final timeout uses the documented floor per
band: compute ComputedTimeout = Dwell * NumChannels as before, then pick FloorMs
= if NumChannels > 14 -> 25000; true -> 15000 end (5GHz vs 2.4GHz), and set
Timeout = erlang:max(DefaultTimeout, ComputedTimeout, FloorMs) +
?GEN_RESPONSE_MS; ensure these changes touch the variables
get_num_channels_timeout, Dwell, NumChannels, DefaultTimeout, ComputedTimeout
and Timeout so wifi_scan uses the enforced 15s/25s floor.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1847-1854: The code frees `data` and clears `ctx->platform_data`
even if `esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE,
&scan_done_handler)` failed, which can leave a registered callback with a stale
arg; change the logic so that when `esp_event_handler_unregister` returns
non-ESP_OK you set `unregister_failed = true` and do NOT clear
`ctx->platform_data` or call `free(data)` (i.e., only call
`esp_wifi_clear_ap_list()`, `ctx->platform_data = NULL`, and `free(data)` when
unregister succeeded); use the existing `unregister_failed` flag around the
`ctx->platform_data = NULL` and `free(data)` operations so the handler’s arg
remains valid if unregister failed.
In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl`:
- Line 50: The test currently calls network:wifi_scan([{results, 3}]) which
truncates Networks to 3 entries so the later assertion that ?FIND_NETWORK is in
Networks (using Variables Networks and Num) can fail if the target AP was
discovered but truncated out; fix by removing or increasing the results cap so
the full returned list is used (e.g., call network:wifi_scan() or omit the
{results, 3} option) and then assert membership of ?FIND_NETWORK against the
full scan list (or alternatively assert based on DiscoveredCount instead of the
truncated Networks), updating the references in the test where Networks, Num,
and the ?FIND_NETWORK check occur.
---
Duplicate comments:
In `@doc/src/network-programming-guide.md`:
- Line 339: Replace the incorrect phrase "while a device is operation in AP
mode" with "while a device is operating in AP mode" in the AP channel
description (search for the exact string "while a device is operation in AP
mode" in doc/src/network-programming-guide.md) to correct the grammar.
- Line 337: Replace the inconsistent Markdown strong-emphasis `__open network__`
in the sentence inside network-programming-guide.md with the project-preferred
`**open network**`; locate the sentence that begins "If the password is omitted,
then an __open network__ will be created..." and update the emphasis style so it
uses double asterisks instead of double underscores.
In `@libs/avm_network/src/network.erl`:
- Around line 818-822: The handle_info clause matching {Ref, {scan_results,
{error, {unregister_handler, _}}} = Msg} currently calls
scan_reply_or_callback(Msg, State) then immediately clears
State#state.scan_receiver to blocked, which drops the intentional follow-up
unregister error for callback/pid scans; modify the logic in the handle_info
clauses (including the similar block around lines 828-830) so that you do not
clear scan_receiver until after any potential follow-up unregister_handler
message is delivered—either by returning a state that preserves scan_receiver
until an explicit unregister message is observed or by deferring setting
scan_receiver = blocked until you detect the unregister error in the message
processing done by scan_reply_or_callback; locate the clause using the Ref
pattern and the scan_reply_or_callback call to implement the change.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c`:
- Around line 1842-1845: The spin-wait on ctx->platform_data (inside the block
guarded by if (!claimed)) can hang forever; change it to a bounded wait by
replacing the infinite spin with a loop that checks ctx->platform_data with a
timeout (use a counter or timestamp via esp_timer_get_time()/gettimeofday and a
small sleep/yield like vTaskDelay/yield or esp_rom_delay_us between checks) and
if the timeout expires abort the wait and return an error/cleanup so
cancel_scan/network:stop can't wedge; update the surrounding code handling (the
caller or this function) to handle the failure path when the bounded wait gives
up.
- Around line 1801-1804: The CAS winner must keep ownership and not flip
ScanData.canceled back to false; in both the cancel path and the completion path
(look for the local variable named "claimed" and updates to ScanData.canceled in
scan_done_handler and the cancel routine), remove the __atomic_store that sets
canceled to false after a successful CAS claim so the owner retains
responsibility for cleanup of ScanData; ensure only the true->false transition
is performed by the cancel CAS loser logic and that the CAS winner performs
cleanup without resetting ScanData.canceled.
In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl`:
- Around line 229-236: The receive in the pid results handling (the clause that
matches {Num, Networks}, {error, Reason}, and Error) in test_wifi_scan.erl has
no timeout and can block indefinitely; add an after clause (e.g., a short
timeout like a few seconds) to fail fast and raise a deterministic error (use
erlang:error({pid_results_receiver_test, {timeout, TimeoutMs}}) or similar) so
the test suite fails promptly if the scan_done PID message never arrives; update
the receive block that references pid_results_receiver_test to include this
after branch and ensure any cleanup or logging is preserved.
- Around line 207-208: The after block in bad_options_test/0 currently ignores
the result of network:stop(), which can leak a running network server into
subsequent subtests; update the teardown to check the return value of
network:stop/0 and make the test fail if it does not return ok (e.g., assert or
call ?fail with the error), so any teardown failure in bad_options_test/0
surfaces as a test failure instead of being silently ignored.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0f5666aa-8811-4346-8864-da50d2e7aad6
📒 Files selected for processing (17)
CHANGELOG.mddoc/src/network-programming-guide.mdexamples/erlang/esp32/CMakeLists.txtexamples/erlang/esp32/wifi_scan.erlexamples/erlang/esp32/wifi_scan_callback.erllibs/avm_network/src/network.erlsrc/platforms/esp32/components/avm_builtins/adc_driver.csrc/platforms/esp32/components/avm_builtins/gpio_driver.csrc/platforms/esp32/components/avm_builtins/i2c_resource.csrc/platforms/esp32/components/avm_builtins/network_driver.csrc/platforms/esp32/components/avm_builtins/socket_driver.csrc/platforms/esp32/components/avm_sys/sys.csrc/platforms/esp32/test/main/test_erl_sources/CMakeLists.txtsrc/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erlsrc/platforms/esp32/test/main/test_main.csrc/platforms/rp2/src/lib/networkdriver.csrc/platforms/stm32/src/lib/gpio_driver.c
✅ Files skipped from review due to trivial changes (5)
- src/platforms/stm32/src/lib/gpio_driver.c
- src/platforms/esp32/components/avm_builtins/gpio_driver.c
- src/platforms/esp32/test/main/test_erl_sources/CMakeLists.txt
- src/platforms/esp32/components/avm_builtins/i2c_resource.c
- examples/erlang/esp32/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (4)
- src/platforms/esp32/components/avm_sys/sys.c
- src/platforms/esp32/components/avm_builtins/adc_driver.c
- examples/erlang/esp32/wifi_scan.erl
- CHANGELOG.md
| {NumChannels, DefaultTimeout} = get_num_channels_timeout(), | ||
| ComputedTimeout = (Dwell * NumChannels), | ||
| Timeout = erlang:max(DefaultTimeout, ComputedTimeout) + ?GEN_RESPONSE_MS, |
There was a problem hiding this comment.
Enforce the documented 15s/25s timeout floor.
With passive = true and the default 360 ms dwell, the 2.4 GHz path computes max(5000, 360 * 14) + 5000 = 10040 ms. The PR notes passive scans around 14088 ms on real hardware, so blocking wifi_scan/0,1 can still time out on a successful default passive scan.
⏱️ Minimal adjustment that preserves the current formula
get_num_channels_timeout() ->
case erlang:system_info(esp32_chip_info) of
#{model := esp32_c5} ->
- {?DEVICE_2G5G_CHANNELS, 15000};
+ {?DEVICE_2G5G_CHANNELS, 20000};
_ ->
- {?DEVICE_2G_CHANNELS, 5000}
+ {?DEVICE_2G_CHANNELS, 10000}
end.Also applies to: 1161-1166
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@libs/avm_network/src/network.erl` around lines 634 - 636, The timeout
calculation can still be below the documented floor; modify the Timeout
computation in the block using get_num_channels_timeout/0 so the final timeout
uses the documented floor per band: compute ComputedTimeout = Dwell *
NumChannels as before, then pick FloorMs = if NumChannels > 14 -> 25000; true ->
15000 end (5GHz vs 2.4GHz), and set Timeout = erlang:max(DefaultTimeout,
ComputedTimeout, FloorMs) + ?GEN_RESPONSE_MS; ensure these changes touch the
variables get_num_channels_timeout, Dwell, NumChannels, DefaultTimeout,
ComputedTimeout and Timeout so wifi_scan uses the enforced 15s/25s floor.
| err = esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); | ||
| if (UNLIKELY(err != ESP_OK)) { | ||
| unregister_failed = true; | ||
| } | ||
| esp_wifi_clear_ap_list(); | ||
| ctx->platform_data = NULL; | ||
| free(data); | ||
| } |
There was a problem hiding this comment.
Avoid freeing data when handler unregister fails.
If esp_event_handler_unregister fails at Line 1848, freeing data at Line 1853 can leave a registered callback holding a stale arg pointer, which is a UAF risk on later scan-done delivery.
🔧 Suggested fix
- if (UNLIKELY(err != ESP_OK)) {
- unregister_failed = true;
- }
- esp_wifi_clear_ap_list();
- ctx->platform_data = NULL;
- free(data);
+ if (UNLIKELY(err != ESP_OK)) {
+ unregister_failed = true;
+ // Keep data alive if unregister failed; block future scans via error path.
+ } else {
+ esp_wifi_clear_ap_list();
+ ctx->platform_data = NULL;
+ free(data);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| err = esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); | |
| if (UNLIKELY(err != ESP_OK)) { | |
| unregister_failed = true; | |
| } | |
| esp_wifi_clear_ap_list(); | |
| ctx->platform_data = NULL; | |
| free(data); | |
| } | |
| err = esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE, &scan_done_handler); | |
| if (UNLIKELY(err != ESP_OK)) { | |
| unregister_failed = true; | |
| // Keep data alive if unregister failed; block future scans via error path. | |
| } else { | |
| esp_wifi_clear_ap_list(); | |
| ctx->platform_data = NULL; | |
| free(data); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/components/avm_builtins/network_driver.c` around lines
1847 - 1854, The code frees `data` and clears `ctx->platform_data` even if
`esp_event_handler_unregister(WIFI_EVENT, WIFI_EVENT_SCAN_DONE,
&scan_done_handler)` failed, which can leave a registered callback with a stale
arg; change the logic so that when `esp_event_handler_unregister` returns
non-ESP_OK you set `unregister_failed = true` and do NOT clear
`ctx->platform_data` or call `free(data)` (i.e., only call
`esp_wifi_clear_ap_list()`, `ctx->platform_data = NULL`, and `free(data)` when
unregister succeeded); use the existing `unregister_failed` flag around the
`ctx->platform_data = NULL` and `free(data)` operations so the handler’s arg
remains valid if unregister failed.
| case network:start([{sta, [managed]}]) of | ||
| {ok, _Pid} -> | ||
| try | ||
| case network:wifi_scan([{results, 3}]) of |
There was a problem hiding this comment.
Don't assert ?FIND_NETWORK against a truncated scan result list.
Line 50 caps Networks at 3 entries, but Lines 71-73 then require the expected SSID to be present in that list. On boards that discover more than 3 APs, the target network can be counted in Num and still be legitimately omitted from Networks, so this test becomes RF-environment dependent.
💡 Suggested fix
- case network:wifi_scan([{results, 3}]) of
+ case network:wifi_scan() ofBased on learnings: the return_results option can cause the returned list to be shorter than DiscoveredCount due to truncation, even when the scan discovered more networks.
Also applies to: 71-73
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platforms/esp32/test/main/test_erl_sources/test_wifi_scan.erl` at line
50, The test currently calls network:wifi_scan([{results, 3}]) which truncates
Networks to 3 entries so the later assertion that ?FIND_NETWORK is in Networks
(using Variables Networks and Num) can fail if the target AP was discovered but
truncated out; fix by removing or increasing the results cap so the full
returned list is used (e.g., call network:wifi_scan() or omit the {results, 3}
option) and then assert membership of ?FIND_NETWORK against the full scan list
(or alternatively assert based on DiscoveredCount instead of the truncated
Networks), updating the references in the test where Networks, Num, and the
?FIND_NETWORK check occur.
|
Merged into atomvm release-0.7 branch. |
These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).
SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later