From ca4e1bf8a461f4ed5813eeb62cc74066e3216604 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 1 Jul 2023 16:46:33 +0200 Subject: [PATCH 1/7] Add esp32init prototype esp32init purpose is to load the application from an application partition. Signed-off-by: Davide Bettio --- libs/CMakeLists.txt | 1 + libs/esp32boot/CMakeLists.txt | 29 +++ libs/esp32boot/esp32init.erl | 368 ++++++++++++++++++++++++++++++++++ 3 files changed, 398 insertions(+) create mode 100644 libs/esp32boot/CMakeLists.txt create mode 100644 libs/esp32boot/esp32init.erl diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 1236642b4..85b212fdf 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(estdlib/src) add_subdirectory(eavmlib/src) add_subdirectory(alisp/src) add_subdirectory(etest/src) +add_subdirectory(esp32boot) if (Elixir_FOUND) add_subdirectory(exavmlib/lib) diff --git a/libs/esp32boot/CMakeLists.txt b/libs/esp32boot/CMakeLists.txt new file mode 100644 index 000000000..18edf2646 --- /dev/null +++ b/libs/esp32boot/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# This file is part of AtomVM. +# +# Copyright 2023 Davide Bettio +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +project(esp32boot) + +include(BuildErlang) + +if (Elixir_FOUND) + pack_runnable(esp32boot esp32init eavmlib estdlib alisp exavmlib) +else() + pack_runnable(esp32boot esp32init eavmlib estdlib alisp) +endif() diff --git a/libs/esp32boot/esp32init.erl b/libs/esp32boot/esp32init.erl new file mode 100644 index 000000000..6cca91c0d --- /dev/null +++ b/libs/esp32boot/esp32init.erl @@ -0,0 +1,368 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Davide Bettio +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(esp32init). + +-export([ + start/0, + start_repl/1, + start_network/0, + handle_req/3, + erase_net_config/0, + save_net_config/2 +]). + +-record(nc_state, {socket, pending_pid, pending_ref}). + +-define(DEFAULT_AP_SSID, <<"AtomVM-ESP32">>). +-define(DEFAULT_AP_PSK, <<"esp32default">>). +-define(DEFAULT_CONSOLE_PORT, 2323). +-define(DEFAULT_WEB_SERVER_PORT, 8080). + +start() -> + console:print(<<"AtomVM init.\n">>), + + avm_pubsub:start(default_pubsub), + + spawn(fun maybe_start_network/0), + + io:format("Starting application...~n"), + + Exit = + try boot() of + Result -> {exit, Result} + catch + Error -> {crash, Error} + end, + erlang:display(Exit), + + io:format("Looping...~n"), + + loop(). + +loop() -> + receive + Msg -> + erlang:display({received_message, Msg}), + loop() + end. + +%% +%% Boot handling +%% + +% TODO: add support for multiple apps +% /dev/partition/by-name/app1.avm +% /dev/partition/by-name/app2.avm + +boot() -> + BootPath = get_boot_path(), + atomvm:add_avm_pack_file(BootPath, []), + + StartModule = get_start_module(), + StartModule:start(). + +get_boot_path() -> + case esp:nvs_get_binary(atomvm, boot_path) of + undefined -> + "/dev/partition/by-name/main.avm"; + Path -> + Path + end. + +get_start_module() -> + case esp:nvs_get_binary(atomvm, start_module) of + undefined -> + main; + Module -> + erlang:binary_to_atom(Module, latin1) + end. + +%% +%% Network management +%% + +erase_net_config() -> + io:format("Erasing net config.~n"), + esp:nvs_erase_key(atomvm, sta_ssid), + esp:nvs_erase_key(atomvm, sta_psk). + +save_net_config(SSID, Pass) -> + io:format("Saving config: SSID: ~p Pass: ~p.~n", [SSID, Pass]), + esp:nvs_set_binary(atomvm, sta_ssid, erlang:list_to_binary(SSID)), + esp:nvs_set_binary(atomvm, sta_psk, erlang:list_to_binary(Pass)). + +get_net_config() -> + case esp:nvs_get_binary(atomvm, sta_ssid) of + undefined -> + get_default_net_config(); + SSID -> + case esp:nvs_get_binary(atomvm, sta_psk) of + undefined -> + get_default_net_config(); + Psk -> + get_net_config(SSID, Psk) + end + end. + +get_default_net_config() -> + Creds = [ + {ssid, ?DEFAULT_AP_SSID}, + {psk, ?DEFAULT_AP_PSK} + ], + {wait_for_ap, Creds}. + +get_net_config(SSID, Psk) -> + Creds = [ + {ssid, SSID}, + {psk, Psk} + ], + {wait_for_sta, Creds}. + +maybe_start_network() -> + case esp:nvs_get_binary(atomvm, wlan_enabled) of + undefined -> + start_network(); + <<"always">> -> + start_network(); + <<"never">> -> + not_started + end. + +start_network() -> + io:format("Starting network...~n"), + {WaitFunc, Creds} = get_net_config(), + case network:WaitFunc(Creds) of + ok -> + io:format("WLAN AP ready. Waiting connections.~n"), + Event = #{ + event => wlan_ap_started + }, + avm_pubsub:pub(default_pubsub, [system, network, wlan, connected], Event), + maybe_start_web_server(), + maybe_start_console(), + started; + {ok, {Address, Netmask, Gateway}} -> + io:format( + "Acquired IP address: ~s Netmask: ~s Gateway: ~s~n", + [to_string(Address), to_string(Netmask), to_string(Gateway)] + ), + Event = #{ + event => wlan_connected, + address => Address, + netmask => Netmask, + gateway => Gateway + }, + avm_pubsub:pub(default_pubsub, [system, network, wlan, connected], Event), + maybe_start_web_server(), + maybe_start_console(), + started; + Error -> + io:format("An error occurred starting network: ~p~n", [Error]), + not_started + end. + +to_string({{A, B, C, D}, Port}) -> + io_lib:format("~p.~p.~p.~p:~p", [A, B, C, D, Port]); +to_string({A, B, C, D}) -> + io_lib:format("~p.~p.~p.~p", [A, B, C, D]). + +%% +%% LISP +%% + +maybe_start_console() -> + case get_console_config() of + {always, Port} -> + listen(Port); + _ -> + io:format("ALISP console not enabled: skipping.~n"), + not_started + end. + +listen(Port) -> + case gen_tcp:listen(Port, []) of + {ok, ListenSocket} -> + io:format("ALISP console listening on port ~p~n", [Port]), + spawn(fun() -> accept(ListenSocket) end), + started; + Error -> + io:format("An error occurred listening: ~p~n", [Error]), + {error, Error} + end. + +get_console_config() -> + Enable = + case esp:nvs_get_binary(atomvm, console_enable) of + undefined -> + always; + <<"always">> -> + always; + <<"never">> -> + never + end, + Port = + case esp:nvs_get_binary(atomvm, console_port) of + undefined -> + ?DEFAULT_CONSOLE_PORT; + PortBinary -> + try erlang:binary_to_integer(PortBinary) of + PortInt -> PortInt + catch + Error -> + io:format("Unable to read ALISP console port: ~p.~n", [Error]), + ?DEFAULT_CONSOLE_PORT + end + end, + {Enable, Port}. + +accept(ListenSocket) -> + io:format("Waiting to accept shell connection...~n"), + case gen_tcp:accept(ListenSocket) of + {ok, Socket} -> + spawn_opt(?MODULE, start_repl, [self()], [link]), + io:format("Accepted shell connection. local: ~s peer: ~s~n", [ + local_address(Socket), peer_address(Socket) + ]), + spawn(fun() -> accept(ListenSocket) end), + loop(#nc_state{socket = Socket}); + Error -> + io:format("An error occurred accepting connection: ~p~n", [Error]) + end. + +loop(State) -> + receive + {tcp_closed, _Socket} -> + io:format("Connection closed.~n"), + erlang:exit(connection_closed); + {tcp, _Socket, <<255, 244, 255, 253, 6>>} -> + io:format("Break.~n"), + gen_tcp:close(State#nc_state.socket), + erlang:exit(break); + {tcp, _Socket, Packet} -> + Reply = {io_reply, State#nc_state.pending_ref, Packet}, + State#nc_state.pending_pid ! Reply, + loop(State#nc_state{pending_pid = undefined, pending_ref = undefined}); + {io_request, FPid, FRef, Request} -> + {ok, NewState} = io_request(Request, FPid, FRef, State), + loop(NewState) + end. + +local_address(Socket) -> + {ok, SockName} = inet:sockname(Socket), + to_string(SockName). + +peer_address(Socket) -> + {ok, Peername} = inet:peername(Socket), + to_string(Peername). + +start_repl(SocketIOLeader) -> + erlang:group_leader(SocketIOLeader, self()), + arepl:start(). + +io_request({get_line, unicode, Data}, FPid, FRef, State) -> + gen_tcp:send(State#nc_state.socket, Data), + {ok, State#nc_state{pending_pid = FPid, pending_ref = FRef}}; +io_request({put_chars, unicode, Data}, FPid, FRef, State) -> + gen_tcp:send(State#nc_state.socket, Data), + FPid ! {io_reply, FRef, ok}, + {ok, State}. + +%% +%% Web Server +%% + +maybe_start_web_server() -> + case get_web_server_config() of + {always, Port} -> + Router = [ + {"*", ?MODULE, []} + ], + http_server:start_server(Port, Router), + io:format("Web server listening on port ~p~n", [Port]), + started; + _ -> + io:format("Web server not enabled: skipping.~n"), + not_started + end. + +get_web_server_config() -> + Enable = + case esp:nvs_get_binary(atomvm, web_server_enable) of + undefined -> + always; + <<"always">> -> + always; + <<"never">> -> + never + end, + Port = + case esp:nvs_get_binary(atomvm, web_server_port) of + undefined -> + ?DEFAULT_WEB_SERVER_PORT; + PortBinary -> + try erlang:binary_to_integer(PortBinary) of + PortInt -> PortInt + catch + Error -> + io:format("Unable to read web server port: ~p.~n", [Error]), + ?DEFAULT_WEB_SERVER_PORT + end + end, + {Enable, Port}. + +handle_req("GET", [], Conn) -> + Body = + << + "\n" + " \n" + "

Configuration

\n" + "
\n" + "

SSID:

\n" + "

Pass:

\n" + " \n" + "
\n" + " \n" + "" + >>, + http_server:reply(200, Body, Conn); +handle_req("POST", [], Conn) -> + ParamsBody = proplists:get_value(body_chunk, Conn), + Params = http_server:parse_query_string(ParamsBody), + + SSID = proplists:get_value("ssid", Params), + Pass = proplists:get_value("pass", Params), + save_net_config(SSID, Pass), + + Body = + << + "\n" + " \n" + "

Configuration

\n" + "

Configured.

\n" + " \n" + "" + >>, + http_server:reply(200, Body, Conn); +handle_req(Method, Path, Conn) -> + erlang:display(Conn), + erlang:display({Method, Path}), + Body = <<"

Not Found

">>, + http_server:reply(404, Body, Conn). From 097f8c9179eac44c19e3f25c11fb6e9a86ccfc73 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 1 Jul 2023 23:50:40 +0200 Subject: [PATCH 2/7] ESP32: add support to boot.avm partition Try to boot boot.avm partition first, which will take care of application loading. Signed-off-by: Davide Bettio --- src/platforms/esp32/main/main.c | 18 +++++++++++------- src/platforms/esp32/partitions.csv | 2 +- src/platforms/esp32/tools/mkimage.config.in | 4 ++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/platforms/esp32/main/main.c b/src/platforms/esp32/main/main.c index ef6d4599e..1c66858b2 100644 --- a/src/platforms/esp32/main/main.c +++ b/src/platforms/esp32/main/main.c @@ -65,7 +65,11 @@ void app_main() spi_flash_mmap_handle_t handle; int size; - const void *main_avm = esp32_sys_mmap_partition("main.avm", &handle, &size); + const void *startup_avm = esp32_sys_mmap_partition("boot.avm", &handle, &size); + if (IS_NULL_PTR(startup_avm)) { + ESP_LOGI(TAG, "Trying deprecated main.avm partition."); + startup_avm = esp32_sys_mmap_partition("main.avm", &handle, &size); + } uint32_t startup_beam_size; const void *startup_beam; @@ -76,12 +80,12 @@ void app_main() port_driver_init_all(glb); nif_collection_init_all(glb); - if (!avmpack_is_valid(main_avm, size)) { - ESP_LOGE(TAG, "Invalid main.avm packbeam. size=%u", size); + if (!avmpack_is_valid(startup_avm, size)) { + ESP_LOGE(TAG, "Invalid startup avmpack. size=%u", size); AVM_ABORT(); } - if (!avmpack_find_section_by_flag(main_avm, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { - ESP_LOGE(TAG, "Error: Failed to locate start module in main.avm packbeam. (Did you flash a library by mistake?)"); + if (!avmpack_find_section_by_flag(startup_avm, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name)) { + ESP_LOGE(TAG, "Error: Failed to locate start module in startup partition. (Did you flash a library by mistake?)"); AVM_ABORT(); } ESP_LOGI(TAG, "Found startup beam %s", startup_module_name); @@ -92,7 +96,7 @@ void app_main() } avmpack_data_init(&avmpack_data->base, &const_avm_pack_info); avmpack_data->base.in_use = true; - avmpack_data->base.data = main_avm; + avmpack_data->base.data = startup_avm; synclist_append(&glb->avmpack_data, &avmpack_data->base.avmpack_head); const void *lib_avm = esp32_sys_mmap_partition("lib.avm", &handle, &size); @@ -106,7 +110,7 @@ void app_main() avmpack_data->base.data = lib_avm; synclist_append(&glb->avmpack_data, &avmpack_data->base.avmpack_head); } else { - ESP_LOGW(TAG, "Unable to mount lib.avm partition. Hopefully the AtomVM core libraries are included in your application."); + ESP_LOGI(TAG, "Unable to mount lib.avm partition. Hopefully the AtomVM core libraries are included in your application."); } Module *mod = module_new_from_iff_binary(glb, startup_beam, startup_beam_size); diff --git a/src/platforms/esp32/partitions.csv b/src/platforms/esp32/partitions.csv index 5392c6f37..95c1cf74b 100644 --- a/src/platforms/esp32/partitions.csv +++ b/src/platforms/esp32/partitions.csv @@ -8,5 +8,5 @@ nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x1C0000, -lib.avm, data, phy, 0x1D0000, 0x40000, +boot.avm, data, phy, 0x1D0000, 0x40000, main.avm, data, phy, 0x210000, 0x100000 diff --git a/src/platforms/esp32/tools/mkimage.config.in b/src/platforms/esp32/tools/mkimage.config.in index 637b4214b..bd5d109c5 100644 --- a/src/platforms/esp32/tools/mkimage.config.in +++ b/src/platforms/esp32/tools/mkimage.config.in @@ -36,9 +36,9 @@ path => ["${BUILD_DIR}/atomvm-esp32.bin", "${ROOT_DIR}/src/platforms/esp32/build/atomvvm-esp32.bin"] }, #{ - name => "AtomVM Core BEAM Library", + name => "AtomVM Boot and Core BEAM Library", offset => "0x1D0000", - path => ["${BUILD_DIR}/../../../../build/libs/atomvmlib.avm"] + path => ["${BUILD_DIR}/../../../../build/libs/esp32boot/esp32boot.avm"] } ] }. From d62451afc577e1319e32028e156fa83ac1751cd7 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Mon, 3 Jul 2023 19:52:16 +0200 Subject: [PATCH 3/7] ESP32/init: use atomvm:get_start_beam/1 Use atomvm:get_start_beam/1 for finding startup module instead of relying on some static default. Signed-off-by: Davide Bettio --- libs/esp32boot/esp32init.erl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/esp32boot/esp32init.erl b/libs/esp32boot/esp32init.erl index 6cca91c0d..47940095c 100644 --- a/libs/esp32boot/esp32init.erl +++ b/libs/esp32boot/esp32init.erl @@ -74,7 +74,7 @@ loop() -> boot() -> BootPath = get_boot_path(), - atomvm:add_avm_pack_file(BootPath, []), + atomvm:add_avm_pack_file(BootPath, [{name, app}]), StartModule = get_start_module(), StartModule:start(). @@ -90,7 +90,14 @@ get_boot_path() -> get_start_module() -> case esp:nvs_get_binary(atomvm, start_module) of undefined -> - main; + case atomvm:get_start_beam(app) of + error -> + main; + {ok, ModuleNameWithExt} when is_binary(ModuleNameWithExt) -> + Len = byte_size(ModuleNameWithExt) - byte_size(<<".beam">>), + ModuleName = binary:part(ModuleNameWithExt, 0, Len), + erlang:binary_to_atom(ModuleName, latin1) + end; Module -> erlang:binary_to_atom(Module, latin1) end. From 73c84631b6296890f7aa9e2e27b2c9846da4ba4b Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 22 Jul 2023 14:49:20 +0200 Subject: [PATCH 4/7] ESP32/init: start web server only on app start fail Do not enable it by default. Signed-off-by: Davide Bettio --- libs/esp32boot/esp32init.erl | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/libs/esp32boot/esp32init.erl b/libs/esp32boot/esp32init.erl index 47940095c..7b985d153 100644 --- a/libs/esp32boot/esp32init.erl +++ b/libs/esp32boot/esp32init.erl @@ -39,10 +39,6 @@ start() -> console:print(<<"AtomVM init.\n">>), - avm_pubsub:start(default_pubsub), - - spawn(fun maybe_start_network/0), - io:format("Starting application...~n"), Exit = @@ -64,6 +60,10 @@ loop() -> loop() end. +start_dev_mode() -> + avm_pubsub:start(default_pubsub), + spawn(fun maybe_start_network/0). + %% %% Boot handling %% @@ -74,10 +74,14 @@ loop() -> boot() -> BootPath = get_boot_path(), - atomvm:add_avm_pack_file(BootPath, [{name, app}]), - - StartModule = get_start_module(), - StartModule:start(). + case atomvm:add_avm_pack_file(BootPath, [{name, app}]) of + ok -> + StartModule = get_start_module(), + StartModule:start(); + {error, Reason} -> + io:format("Failed app start: ~p.~n", [Reason]), + start_dev_mode() + end. get_boot_path() -> case esp:nvs_get_binary(atomvm, boot_path) of From 3a39b11d7e849568df2848063e538515bdfa15a1 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sat, 22 Jul 2023 15:57:30 +0200 Subject: [PATCH 5/7] ESP32: add option option for starting always dev mode Add (opt-in) NVS setting for enabling always ALISP console and web server. Signed-off-by: Davide Bettio --- libs/esp32boot/esp32init.erl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/libs/esp32boot/esp32init.erl b/libs/esp32boot/esp32init.erl index 7b985d153..4baeb7841 100644 --- a/libs/esp32boot/esp32init.erl +++ b/libs/esp32boot/esp32init.erl @@ -60,9 +60,22 @@ loop() -> loop() end. +maybe_start_dev_mode(SystemStatus) -> + case {SystemStatus, esp:nvs_get_binary(atomvm, dev_mode)} of + {_, <<"always">>} -> + start_dev_mode(); + {_, <<"never">>} -> + not_started; + {ok, undefined} -> + not_started; + {failed_app_start, undefined} -> + start_dev_mode() + end. + start_dev_mode() -> avm_pubsub:start(default_pubsub), - spawn(fun maybe_start_network/0). + spawn(fun maybe_start_network/0), + started. %% %% Boot handling @@ -77,10 +90,11 @@ boot() -> case atomvm:add_avm_pack_file(BootPath, [{name, app}]) of ok -> StartModule = get_start_module(), + maybe_start_dev_mode(ok), StartModule:start(); {error, Reason} -> io:format("Failed app start: ~p.~n", [Reason]), - start_dev_mode() + maybe_start_dev_mode(failed_app_start) end. get_boot_path() -> From 6fe8c61d58aef33cbdce1b1ed2482348eb10d6ec Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 28 Jul 2023 14:41:13 +0200 Subject: [PATCH 6/7] ESP32: move "dev mode" code to esp32devmode Keep esp32init as small as possible. Signed-off-by: Davide Bettio --- libs/CMakeLists.txt | 1 + libs/esp32boot/CMakeLists.txt | 4 +- libs/esp32boot/esp32init.erl | 301 +----------------------- libs/esp32devmode/src/CMakeLists.txt | 25 ++ libs/esp32devmode/src/esp32devmode.erl | 314 +++++++++++++++++++++++++ 5 files changed, 345 insertions(+), 300 deletions(-) create mode 100644 libs/esp32devmode/src/CMakeLists.txt create mode 100644 libs/esp32devmode/src/esp32devmode.erl diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt index 85b212fdf..2a089c359 100644 --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -26,6 +26,7 @@ add_subdirectory(eavmlib/src) add_subdirectory(alisp/src) add_subdirectory(etest/src) add_subdirectory(esp32boot) +add_subdirectory(esp32devmode/src) if (Elixir_FOUND) add_subdirectory(exavmlib/lib) diff --git a/libs/esp32boot/CMakeLists.txt b/libs/esp32boot/CMakeLists.txt index 18edf2646..571034859 100644 --- a/libs/esp32boot/CMakeLists.txt +++ b/libs/esp32boot/CMakeLists.txt @@ -23,7 +23,7 @@ project(esp32boot) include(BuildErlang) if (Elixir_FOUND) - pack_runnable(esp32boot esp32init eavmlib estdlib alisp exavmlib) + pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp exavmlib) else() - pack_runnable(esp32boot esp32init eavmlib estdlib alisp) + pack_runnable(esp32boot esp32init esp32devmode eavmlib estdlib alisp) endif() diff --git a/libs/esp32boot/esp32init.erl b/libs/esp32boot/esp32init.erl index 4baeb7841..5105f650c 100644 --- a/libs/esp32boot/esp32init.erl +++ b/libs/esp32boot/esp32init.erl @@ -20,21 +20,7 @@ -module(esp32init). --export([ - start/0, - start_repl/1, - start_network/0, - handle_req/3, - erase_net_config/0, - save_net_config/2 -]). - --record(nc_state, {socket, pending_pid, pending_ref}). - --define(DEFAULT_AP_SSID, <<"AtomVM-ESP32">>). --define(DEFAULT_AP_PSK, <<"esp32default">>). --define(DEFAULT_CONSOLE_PORT, 2323). --define(DEFAULT_WEB_SERVER_PORT, 8080). +-export([start/0]). start() -> console:print(<<"AtomVM init.\n">>), @@ -63,24 +49,15 @@ loop() -> maybe_start_dev_mode(SystemStatus) -> case {SystemStatus, esp:nvs_get_binary(atomvm, dev_mode)} of {_, <<"always">>} -> - start_dev_mode(); + ep32devmode:start_dev_mode(); {_, <<"never">>} -> not_started; {ok, undefined} -> not_started; {failed_app_start, undefined} -> - start_dev_mode() + esp32devmode:start_dev_mode() end. -start_dev_mode() -> - avm_pubsub:start(default_pubsub), - spawn(fun maybe_start_network/0), - started. - -%% -%% Boot handling -%% - % TODO: add support for multiple apps % /dev/partition/by-name/app1.avm % /dev/partition/by-name/app2.avm @@ -119,275 +96,3 @@ get_start_module() -> Module -> erlang:binary_to_atom(Module, latin1) end. - -%% -%% Network management -%% - -erase_net_config() -> - io:format("Erasing net config.~n"), - esp:nvs_erase_key(atomvm, sta_ssid), - esp:nvs_erase_key(atomvm, sta_psk). - -save_net_config(SSID, Pass) -> - io:format("Saving config: SSID: ~p Pass: ~p.~n", [SSID, Pass]), - esp:nvs_set_binary(atomvm, sta_ssid, erlang:list_to_binary(SSID)), - esp:nvs_set_binary(atomvm, sta_psk, erlang:list_to_binary(Pass)). - -get_net_config() -> - case esp:nvs_get_binary(atomvm, sta_ssid) of - undefined -> - get_default_net_config(); - SSID -> - case esp:nvs_get_binary(atomvm, sta_psk) of - undefined -> - get_default_net_config(); - Psk -> - get_net_config(SSID, Psk) - end - end. - -get_default_net_config() -> - Creds = [ - {ssid, ?DEFAULT_AP_SSID}, - {psk, ?DEFAULT_AP_PSK} - ], - {wait_for_ap, Creds}. - -get_net_config(SSID, Psk) -> - Creds = [ - {ssid, SSID}, - {psk, Psk} - ], - {wait_for_sta, Creds}. - -maybe_start_network() -> - case esp:nvs_get_binary(atomvm, wlan_enabled) of - undefined -> - start_network(); - <<"always">> -> - start_network(); - <<"never">> -> - not_started - end. - -start_network() -> - io:format("Starting network...~n"), - {WaitFunc, Creds} = get_net_config(), - case network:WaitFunc(Creds) of - ok -> - io:format("WLAN AP ready. Waiting connections.~n"), - Event = #{ - event => wlan_ap_started - }, - avm_pubsub:pub(default_pubsub, [system, network, wlan, connected], Event), - maybe_start_web_server(), - maybe_start_console(), - started; - {ok, {Address, Netmask, Gateway}} -> - io:format( - "Acquired IP address: ~s Netmask: ~s Gateway: ~s~n", - [to_string(Address), to_string(Netmask), to_string(Gateway)] - ), - Event = #{ - event => wlan_connected, - address => Address, - netmask => Netmask, - gateway => Gateway - }, - avm_pubsub:pub(default_pubsub, [system, network, wlan, connected], Event), - maybe_start_web_server(), - maybe_start_console(), - started; - Error -> - io:format("An error occurred starting network: ~p~n", [Error]), - not_started - end. - -to_string({{A, B, C, D}, Port}) -> - io_lib:format("~p.~p.~p.~p:~p", [A, B, C, D, Port]); -to_string({A, B, C, D}) -> - io_lib:format("~p.~p.~p.~p", [A, B, C, D]). - -%% -%% LISP -%% - -maybe_start_console() -> - case get_console_config() of - {always, Port} -> - listen(Port); - _ -> - io:format("ALISP console not enabled: skipping.~n"), - not_started - end. - -listen(Port) -> - case gen_tcp:listen(Port, []) of - {ok, ListenSocket} -> - io:format("ALISP console listening on port ~p~n", [Port]), - spawn(fun() -> accept(ListenSocket) end), - started; - Error -> - io:format("An error occurred listening: ~p~n", [Error]), - {error, Error} - end. - -get_console_config() -> - Enable = - case esp:nvs_get_binary(atomvm, console_enable) of - undefined -> - always; - <<"always">> -> - always; - <<"never">> -> - never - end, - Port = - case esp:nvs_get_binary(atomvm, console_port) of - undefined -> - ?DEFAULT_CONSOLE_PORT; - PortBinary -> - try erlang:binary_to_integer(PortBinary) of - PortInt -> PortInt - catch - Error -> - io:format("Unable to read ALISP console port: ~p.~n", [Error]), - ?DEFAULT_CONSOLE_PORT - end - end, - {Enable, Port}. - -accept(ListenSocket) -> - io:format("Waiting to accept shell connection...~n"), - case gen_tcp:accept(ListenSocket) of - {ok, Socket} -> - spawn_opt(?MODULE, start_repl, [self()], [link]), - io:format("Accepted shell connection. local: ~s peer: ~s~n", [ - local_address(Socket), peer_address(Socket) - ]), - spawn(fun() -> accept(ListenSocket) end), - loop(#nc_state{socket = Socket}); - Error -> - io:format("An error occurred accepting connection: ~p~n", [Error]) - end. - -loop(State) -> - receive - {tcp_closed, _Socket} -> - io:format("Connection closed.~n"), - erlang:exit(connection_closed); - {tcp, _Socket, <<255, 244, 255, 253, 6>>} -> - io:format("Break.~n"), - gen_tcp:close(State#nc_state.socket), - erlang:exit(break); - {tcp, _Socket, Packet} -> - Reply = {io_reply, State#nc_state.pending_ref, Packet}, - State#nc_state.pending_pid ! Reply, - loop(State#nc_state{pending_pid = undefined, pending_ref = undefined}); - {io_request, FPid, FRef, Request} -> - {ok, NewState} = io_request(Request, FPid, FRef, State), - loop(NewState) - end. - -local_address(Socket) -> - {ok, SockName} = inet:sockname(Socket), - to_string(SockName). - -peer_address(Socket) -> - {ok, Peername} = inet:peername(Socket), - to_string(Peername). - -start_repl(SocketIOLeader) -> - erlang:group_leader(SocketIOLeader, self()), - arepl:start(). - -io_request({get_line, unicode, Data}, FPid, FRef, State) -> - gen_tcp:send(State#nc_state.socket, Data), - {ok, State#nc_state{pending_pid = FPid, pending_ref = FRef}}; -io_request({put_chars, unicode, Data}, FPid, FRef, State) -> - gen_tcp:send(State#nc_state.socket, Data), - FPid ! {io_reply, FRef, ok}, - {ok, State}. - -%% -%% Web Server -%% - -maybe_start_web_server() -> - case get_web_server_config() of - {always, Port} -> - Router = [ - {"*", ?MODULE, []} - ], - http_server:start_server(Port, Router), - io:format("Web server listening on port ~p~n", [Port]), - started; - _ -> - io:format("Web server not enabled: skipping.~n"), - not_started - end. - -get_web_server_config() -> - Enable = - case esp:nvs_get_binary(atomvm, web_server_enable) of - undefined -> - always; - <<"always">> -> - always; - <<"never">> -> - never - end, - Port = - case esp:nvs_get_binary(atomvm, web_server_port) of - undefined -> - ?DEFAULT_WEB_SERVER_PORT; - PortBinary -> - try erlang:binary_to_integer(PortBinary) of - PortInt -> PortInt - catch - Error -> - io:format("Unable to read web server port: ~p.~n", [Error]), - ?DEFAULT_WEB_SERVER_PORT - end - end, - {Enable, Port}. - -handle_req("GET", [], Conn) -> - Body = - << - "\n" - " \n" - "

Configuration

\n" - "
\n" - "

SSID:

\n" - "

Pass:

\n" - " \n" - "
\n" - " \n" - "" - >>, - http_server:reply(200, Body, Conn); -handle_req("POST", [], Conn) -> - ParamsBody = proplists:get_value(body_chunk, Conn), - Params = http_server:parse_query_string(ParamsBody), - - SSID = proplists:get_value("ssid", Params), - Pass = proplists:get_value("pass", Params), - save_net_config(SSID, Pass), - - Body = - << - "\n" - " \n" - "

Configuration

\n" - "

Configured.

\n" - " \n" - "" - >>, - http_server:reply(200, Body, Conn); -handle_req(Method, Path, Conn) -> - erlang:display(Conn), - erlang:display({Method, Path}), - Body = <<"

Not Found

">>, - http_server:reply(404, Body, Conn). diff --git a/libs/esp32devmode/src/CMakeLists.txt b/libs/esp32devmode/src/CMakeLists.txt new file mode 100644 index 000000000..5a25c2567 --- /dev/null +++ b/libs/esp32devmode/src/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# This file is part of AtomVM. +# +# Copyright 2023 Davide Bettio +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +project(esp32devmode) + +include(BuildErlang) + +pack_archive(esp32devmode esp32devmode) diff --git a/libs/esp32devmode/src/esp32devmode.erl b/libs/esp32devmode/src/esp32devmode.erl new file mode 100644 index 000000000..b3c7bf6b4 --- /dev/null +++ b/libs/esp32devmode/src/esp32devmode.erl @@ -0,0 +1,314 @@ +% +% This file is part of AtomVM. +% +% Copyright 2023 Davide Bettio +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(esp32devmode). + +-export([ + start_dev_mode/0, + start_repl/1, + start_network/0, + handle_req/3, + erase_net_config/0, + save_net_config/2 +]). + +-record(nc_state, {socket, pending_pid, pending_ref}). + +-define(DEFAULT_AP_SSID, <<"AtomVM-ESP32">>). +-define(DEFAULT_AP_PSK, <<"esp32default">>). +-define(DEFAULT_CONSOLE_PORT, 2323). +-define(DEFAULT_WEB_SERVER_PORT, 8080). + +start_dev_mode() -> + avm_pubsub:start(default_pubsub), + spawn(fun maybe_start_network/0), + started. + +%% +%% Network management +%% + +erase_net_config() -> + io:format("Erasing net config.~n"), + esp:nvs_erase_key(atomvm, sta_ssid), + esp:nvs_erase_key(atomvm, sta_psk). + +save_net_config(SSID, Pass) -> + io:format("Saving config: SSID: ~p Pass: ~p.~n", [SSID, Pass]), + esp:nvs_set_binary(atomvm, sta_ssid, erlang:list_to_binary(SSID)), + esp:nvs_set_binary(atomvm, sta_psk, erlang:list_to_binary(Pass)). + +get_net_config() -> + case esp:nvs_get_binary(atomvm, sta_ssid) of + undefined -> + get_default_net_config(); + SSID -> + case esp:nvs_get_binary(atomvm, sta_psk) of + undefined -> + get_default_net_config(); + Psk -> + get_net_config(SSID, Psk) + end + end. + +get_default_net_config() -> + Creds = [ + {ssid, ?DEFAULT_AP_SSID}, + {psk, ?DEFAULT_AP_PSK} + ], + {wait_for_ap, Creds}. + +get_net_config(SSID, Psk) -> + Creds = [ + {ssid, SSID}, + {psk, Psk} + ], + {wait_for_sta, Creds}. + +maybe_start_network() -> + case esp:nvs_get_binary(atomvm, wlan_enabled) of + undefined -> + start_network(); + <<"always">> -> + start_network(); + <<"never">> -> + not_started + end. + +start_network() -> + io:format("Starting network...~n"), + {WaitFunc, Creds} = get_net_config(), + case network:WaitFunc(Creds) of + ok -> + io:format("WLAN AP ready. Waiting connections.~n"), + Event = #{ + event => wlan_ap_started + }, + avm_pubsub:pub(default_pubsub, [system, network, wlan, connected], Event), + maybe_start_web_server(), + maybe_start_console(), + started; + {ok, {Address, Netmask, Gateway}} -> + io:format( + "Acquired IP address: ~s Netmask: ~s Gateway: ~s~n", + [to_string(Address), to_string(Netmask), to_string(Gateway)] + ), + Event = #{ + event => wlan_connected, + address => Address, + netmask => Netmask, + gateway => Gateway + }, + avm_pubsub:pub(default_pubsub, [system, network, wlan, connected], Event), + maybe_start_web_server(), + maybe_start_console(), + started; + Error -> + io:format("An error occurred starting network: ~p~n", [Error]), + not_started + end. + +to_string({{A, B, C, D}, Port}) -> + io_lib:format("~p.~p.~p.~p:~p", [A, B, C, D, Port]); +to_string({A, B, C, D}) -> + io_lib:format("~p.~p.~p.~p", [A, B, C, D]). + +%% +%% LISP +%% + +maybe_start_console() -> + case get_console_config() of + {always, Port} -> + listen(Port); + _ -> + io:format("ALISP console not enabled: skipping.~n"), + not_started + end. + +listen(Port) -> + case gen_tcp:listen(Port, []) of + {ok, ListenSocket} -> + io:format("ALISP console listening on port ~p~n", [Port]), + spawn(fun() -> accept(ListenSocket) end), + started; + Error -> + io:format("An error occurred listening: ~p~n", [Error]), + {error, Error} + end. + +get_console_config() -> + Enable = + case esp:nvs_get_binary(atomvm, console_enable) of + undefined -> + always; + <<"always">> -> + always; + <<"never">> -> + never + end, + Port = + case esp:nvs_get_binary(atomvm, console_port) of + undefined -> + ?DEFAULT_CONSOLE_PORT; + PortBinary -> + try erlang:binary_to_integer(PortBinary) of + PortInt -> PortInt + catch + Error -> + io:format("Unable to read ALISP console port: ~p.~n", [Error]), + ?DEFAULT_CONSOLE_PORT + end + end, + {Enable, Port}. + +accept(ListenSocket) -> + io:format("Waiting to accept shell connection...~n"), + case gen_tcp:accept(ListenSocket) of + {ok, Socket} -> + spawn_opt(?MODULE, start_repl, [self()], [link]), + io:format("Accepted shell connection. local: ~s peer: ~s~n", [ + local_address(Socket), peer_address(Socket) + ]), + spawn(fun() -> accept(ListenSocket) end), + loop(#nc_state{socket = Socket}); + Error -> + io:format("An error occurred accepting connection: ~p~n", [Error]) + end. + +loop(State) -> + receive + {tcp_closed, _Socket} -> + io:format("Connection closed.~n"), + erlang:exit(connection_closed); + {tcp, _Socket, <<255, 244, 255, 253, 6>>} -> + io:format("Break.~n"), + gen_tcp:close(State#nc_state.socket), + erlang:exit(break); + {tcp, _Socket, Packet} -> + Reply = {io_reply, State#nc_state.pending_ref, Packet}, + State#nc_state.pending_pid ! Reply, + loop(State#nc_state{pending_pid = undefined, pending_ref = undefined}); + {io_request, FPid, FRef, Request} -> + {ok, NewState} = io_request(Request, FPid, FRef, State), + loop(NewState) + end. + +local_address(Socket) -> + {ok, SockName} = inet:sockname(Socket), + to_string(SockName). + +peer_address(Socket) -> + {ok, Peername} = inet:peername(Socket), + to_string(Peername). + +start_repl(SocketIOLeader) -> + erlang:group_leader(SocketIOLeader, self()), + arepl:start(). + +io_request({get_line, unicode, Data}, FPid, FRef, State) -> + gen_tcp:send(State#nc_state.socket, Data), + {ok, State#nc_state{pending_pid = FPid, pending_ref = FRef}}; +io_request({put_chars, unicode, Data}, FPid, FRef, State) -> + gen_tcp:send(State#nc_state.socket, Data), + FPid ! {io_reply, FRef, ok}, + {ok, State}. + +%% +%% Web Server +%% + +maybe_start_web_server() -> + case get_web_server_config() of + {always, Port} -> + Router = [ + {"*", ?MODULE, []} + ], + http_server:start_server(Port, Router), + io:format("Web server listening on port ~p~n", [Port]), + started; + _ -> + io:format("Web server not enabled: skipping.~n"), + not_started + end. + +get_web_server_config() -> + Enable = + case esp:nvs_get_binary(atomvm, web_server_enable) of + undefined -> + always; + <<"always">> -> + always; + <<"never">> -> + never + end, + Port = + case esp:nvs_get_binary(atomvm, web_server_port) of + undefined -> + ?DEFAULT_WEB_SERVER_PORT; + PortBinary -> + try erlang:binary_to_integer(PortBinary) of + PortInt -> PortInt + catch + Error -> + io:format("Unable to read web server port: ~p.~n", [Error]), + ?DEFAULT_WEB_SERVER_PORT + end + end, + {Enable, Port}. + +handle_req("GET", [], Conn) -> + Body = + << + "\n" + " \n" + "

Configuration

\n" + "
\n" + "

SSID:

\n" + "

Pass:

\n" + " \n" + "
\n" + " \n" + "" + >>, + http_server:reply(200, Body, Conn); +handle_req("POST", [], Conn) -> + ParamsBody = proplists:get_value(body_chunk, Conn), + Params = http_server:parse_query_string(ParamsBody), + + SSID = proplists:get_value("ssid", Params), + Pass = proplists:get_value("pass", Params), + save_net_config(SSID, Pass), + + Body = + << + "\n" + " \n" + "

Configuration

\n" + "

Configured.

\n" + " \n" + "" + >>, + http_server:reply(200, Body, Conn); +handle_req(Method, Path, Conn) -> + erlang:display(Conn), + erlang:display({Method, Path}), + Body = <<"

Not Found

">>, + http_server:reply(404, Body, Conn). From 47caf88556d959ae8a9187b147378c39a45d2063 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Fri, 28 Jul 2023 14:51:45 +0200 Subject: [PATCH 7/7] ESP32/init: `timer:sleep(infinity)` on exit when devmode is enabled Application is started inside of `try .. catch` block and `timer:sleep(infinity)` is used after application exit according to devmode settings. Signed-off-by: Davide Bettio --- libs/esp32boot/esp32init.erl | 66 ++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/libs/esp32boot/esp32init.erl b/libs/esp32boot/esp32init.erl index 5105f650c..58f3b0743 100644 --- a/libs/esp32boot/esp32init.erl +++ b/libs/esp32boot/esp32init.erl @@ -24,38 +24,26 @@ start() -> console:print(<<"AtomVM init.\n">>), + boot(). - io:format("Starting application...~n"), - - Exit = - try boot() of - Result -> {exit, Result} - catch - Error -> {crash, Error} - end, - erlang:display(Exit), - - io:format("Looping...~n"), - - loop(). - -loop() -> - receive - Msg -> - erlang:display({received_message, Msg}), - loop() - end. - -maybe_start_dev_mode(SystemStatus) -> +is_dev_mode_enabled(SystemStatus) -> case {SystemStatus, esp:nvs_get_binary(atomvm, dev_mode)} of {_, <<"always">>} -> - ep32devmode:start_dev_mode(); + true; {_, <<"never">>} -> - not_started; + false; {ok, undefined} -> - not_started; - {failed_app_start, undefined} -> - esp32devmode:start_dev_mode() + false; + {app_exit, undefined} -> + false; + {app_fail, undefined} -> + true + end. + +maybe_start_dev_mode(SystemStatus) -> + case is_dev_mode_enabled(SystemStatus) of + true -> esp32devmode:start_dev_mode(); + false -> not_started end. % TODO: add support for multiple apps @@ -67,11 +55,29 @@ boot() -> case atomvm:add_avm_pack_file(BootPath, [{name, app}]) of ok -> StartModule = get_start_module(), - maybe_start_dev_mode(ok), - StartModule:start(); + DevOnExit = is_dev_mode_enabled(app_exit), + StartedDevMode = maybe_start_dev_mode(ok), + + io:format("Starting application...~n"), + case DevOnExit of + true -> + try StartModule:start() of + Result -> io:format("Exited: ~p.~n", [Result]) + catch + Error -> io:format("Crashed: ~p.~n", [Error]) + end, + case StartedDevMode of + started -> ok; + _NotStarted -> maybe_start_dev_mode(app_exit) + end, + timer:sleep(infinity); + false -> + StartModule:start() + end; {error, Reason} -> io:format("Failed app start: ~p.~n", [Reason]), - maybe_start_dev_mode(failed_app_start) + maybe_start_dev_mode(app_fail), + timer:sleep(infinity) end. get_boot_path() ->