From e62d9dde8e2b69ab9b542ee41b97b667e1c9d485 Mon Sep 17 00:00:00 2001 From: Winford Date: Tue, 22 Jul 2025 22:27:05 -0700 Subject: [PATCH 1/2] Make esp32_flash include stack trace in diagnostic mode Changes error reporting for the esp32_flash task to include stack trace when rebar3 is executed with DIAGNOSTIC=1 or DEBUG=1. Signed-off-by: Winford --- src/atomvm_esp32_flash_provider.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/atomvm_esp32_flash_provider.erl b/src/atomvm_esp32_flash_provider.erl index 75bb63a..58e278a 100644 --- a/src/atomvm_esp32_flash_provider.erl +++ b/src/atomvm_esp32_flash_provider.erl @@ -87,10 +87,10 @@ do(State) -> catch C:E:S -> rebar_api:error( - "An error occurred in the ~p task. Class=~p Error=~p Stacktrace=~p~n", [ - ?PROVIDER, C, E, S - ] + "An error occurred in the ~p task. Error=~p", + [?PROVIDER, E] ), + rebar_api:debug("Class=~p, Error=~p~nSTACKTRACE:~n~p~n", [C, E, S]), {error, E} end. From fe79208ed048dcfb4ee0834b445b18afed8e72f5 Mon Sep 17 00:00:00 2001 From: Winford Date: Sun, 27 Jul 2025 20:46:08 -0700 Subject: [PATCH 2/2] Enhancements for the esp32_flash task The `offset` for the beam application partition is now read from the partition table on the attached esp32 device. When a custom partition table is used that does not use `main.avm` for the beam app partition name the `app_partition` parameter should be used to specify the application partition to be flashed. Valid application partition sub-types (the type is `data`) are `phy` or `0xAA`. If the `offset` parameter is specified it will be used to verify that the offset address of the application partition matched the expected value. This may be used to prevent flashing to a standard build of AtomMV for application that require a custom partition table. The `port` that the ESP32 is attached to is now auto discovered by default. When more than one ESP32 device is plugged into USB the port should be specified to control which device is flashed. Error reporting has been improved with descriptive error messages. Dialyzer warnings for the esp32_flash task have been fixed when analyzing atomvm_rebar3_plugin. Signed-off-by: Winford --- CHANGELOG.md | 13 + Makefile | 2 +- README.md | 54 +++- UPDATING.md | 11 + src/atomvm_esp32_flash_provider.erl | 227 ++++++++++++--- src/esp_part_dump.erl | 266 ++++++++++++++++++ test/driver/scripts/README.md | 41 +++ test/driver/scripts/esptool.sh | 26 ++ test/driver/scripts/partition.bin | Bin 0 -> 3072 bytes test/driver/scripts/partition.bin.license | 2 + test/driver/scripts/partition_elixir.bin | Bin 0 -> 3072 bytes .../scripts/partition_elixir.bin.license | 2 + test/driver/src/esp32_flash_tests.erl | 114 +++++++- test/driver/src/test.erl | 2 +- test/run.sh | 8 + 15 files changed, 713 insertions(+), 55 deletions(-) create mode 100644 src/esp_part_dump.erl create mode 100644 test/driver/scripts/README.md create mode 100755 test/driver/scripts/esptool.sh create mode 100644 test/driver/scripts/partition.bin create mode 100644 test/driver/scripts/partition.bin.license create mode 100644 test/driver/scripts/partition_elixir.bin create mode 100644 test/driver/scripts/partition_elixir.bin.license diff --git a/CHANGELOG.md b/CHANGELOG.md index ee5e997..7515f58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added dialyzer task to simplify running dialyzer on AtomVM applications. - Added support for rp2350 devices to allow for default detection of the device mount path. - Added configuration paramenter for setting the path to picotool for the pico_flash task. +- Added `app_partition` parameter to `esp32_flash` task. This is only needed to be provided for +custom partition tables that do not use `main.avm` for the beam application partition name, or to +flash to a custom alternate partition. ### Changed - The `uf2create` task now creates `universal` format uf2 files by default, suitable for both @@ -28,6 +31,16 @@ rp2040 or rp2350 devices. - The `pico_flash` task now checks that a device is an RP2 platform before resetting to `BOOTSEL` mode, preventing interference with other MCUs that may be attached to the host system. - The `pico_flash` task now aborts on all errors rather than trying to continue after a failure. +- The `offset` used by the `esp32_flash` task is now read from the partition table of the device. +When this parameter is provided it will be used to verify the offset of the application partition on +flash matches the expected value. +- The `esp32_flash` task now uses auto discovery for the `port` by default. +- Stacktraces are not shown by default if the `esp32_flash` fails, instead a descriptive error +message is displayed. To view the stacktrace use diagnostic mode. + +### Fixed +- The `esp32_flash` task aborts when an error occurs, rather than attempt to continue after a step +has failed. ## [0.7.5] (2025.05.27) diff --git a/Makefile b/Makefile index 2047f39..d03e6ff 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ doc: rebar3 as doc ex_doc etest: - cd test && ./run.sh + cd test && TEST=1 ./run.sh clean: rm -rf _build diff --git a/README.md b/README.md index 8763e38..614fff6 100644 --- a/README.md +++ b/README.md @@ -260,24 +260,32 @@ Running this AVM file will boot the `myapp` application automatically, without h You may use the `esp32_flash` task to flash the generated AtomVM packbeam application to the flash storage on an ESP32 device connected over a serial connection. - shell$ rebar3 help atomvm esp32_flash - - Use this plugin to flash an AtomVM packbeam file to an ESP32 device. - - Usage: rebar3 atomvm esp32_flash [-e ] [-c ] [-p ] - [-b ] [-o ] - - -e, --esptool Path to esptool.py - -c, --chip ESP chip (default auto) - -p, --port Device port (default /dev/ttyUSB0) - -b, --baud Baud rate (default 115200) - -o, --offset Offset (default 0x210000) +```shell +shell$ rebar3 help atomvm esp32_flash + +Use this plugin to flash an AtomVM packbeam file to an ESP32 device. + +Usage: rebar3 atomvm esp32_flash [-e ] [-c ] [-p ] + [-b ] [-o ] + [-a ] + + -e, --esptool Path to esptool.py + -c, --chip ESP chip (default auto) + -p, --port Device port (default auto discovery) + -b, --baud Baud rate (default 115200) + -o, --offset Offset (default read from device) *deprecated, use + app_partition. When given, a warning will be issued + if the partition name does not match the expected + name, as long as the offset aligns with a valid + application partition. + -a, --app_partition Application partition name (default main.avm) +``` The `esp32_flash` task will use the `esptool.py` command to flash the ESP32 device. This tool is available via the IDF SDK, or directly via github. The `esptool.py` command is also available via many package managers (e.g., MacOS Homebrew). -By default, the `esp32_flash` task will assume the `esptool.py` command is available on the user's executable path. Alternatively, you may specify the full path to the `esptool.py` command via the `-e` (or `--esptool`) option +By default, the `esp32_flash` task will assume the `esptool.py` command is available on the user's executable path. Alternatively, you may specify the full path to the `esptool.py` command via the `-e` (or `--esptool`) option. -By default, the `esp32_flash` task will write to port `/dev/ttyUSB0` at a baud rate of `115200`. You may control the port and baud settings for connecting to your ESP device via the `-port` and `-baud` options to the `esp32_flash` task, e.g., +By default, the `esp32_flash` task uses port auto discovery at a baud rate of `115200`. You may control the port and baud settings for connecting to your ESP device via the `--port` and `--baud` options to the `esp32_flash` task, e.g., shell$ rebar3 atomvm esp32_flash --port /dev/tty.SLAB_USBtoUART --baud 921600 ... @@ -306,7 +314,8 @@ The following table enumerates the properties that may be defined in your projec | `chip` | `string()` | ESP32 chip type | | `port` | `string()` | Device port on which the ESP32 can be located | | `baud` | `integer()` | Device BAUD rate | -| `offset` | `string()` | Offset into which to write AtomVM application | +| `offset` | `string()` | Optionally verify offset on flash matches expected value. Original behavior deprecated: use `app_partition` for custom images or to target a different partition | +| `app_partition` | `string()` | Name of application partition to write AtomVM application for custom partition tables | Example: @@ -319,9 +328,24 @@ Alternatively, the following environment variables may be used to control the ab * `ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_PORT` * `ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_BAUD` * `ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_OFFSET` +* `ATOMVM_REBAR3_PLUGIN_ESP32_APP_PARTITION` Any setting specified on the command line take precedence over settings in `rebar.config`, which in turn take precedence over environment variable settings, which in turn take precedence over the default values specified above. +```note +The behavior of the `offset` configuration option has changed, the correct offset for standard +AtomVM builds are determined by the partition table flashed to the device. Elixir supported builds +are recognized and the correct offset will be used. When using a custom partition table it is +necessary to supply the `app_partition` name. If an offset is given it will be compared to the +address of the discovered `app_partition` and an warning will be given if they do not match, but as +long as the offset aligns to the beginning of a `data` partition with a valid subtype it will be +used. Currently any subtype not reserved for another purpose (such as `fat`, `nvs`, or ESP-IDF +defined subtypes) are considered valid. AtomVM release images use subtype `phy` for the `main.avm` +BEAM application partition, but it is not necessary to supply the name when using a release image +or one of the standard partition tables, or custom tables that use the name `main.avm` for the app +partition. +``` + The `esp32_flash` task depends on the `packbeam` task, so the packbeam file will get automatically built if any changes have been made to its dependencies. ### The `stm32_flash` task diff --git a/UPDATING.md b/UPDATING.md index 8fdd24a..26189a4 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -6,6 +6,17 @@ # `atomvm_rebar3_plugin` Update Instructions +## (unreleased) + +- The `esp32_flash` task now reads the application `offset` from the partition table on the device. +If you are using a custom partition table that does not use `main.avm` for the application partition +name you should supply the name used with the `app_partition` parameter. An `offset` may optionally +be supplied to assure the offset of the application partition matches the expected offset, this may +be helpful to assure that specific applications are only flashed to devices with a custom build of +AtomVM. +- Pico 2 (RP2350) devices are recognized and now work with default parameters. Specifying device +path and uf2 flavor for these chipsets is no longer necessary. + ## 0.6.* -> 0.7.* - The `atomvm_rebar3_plugin` tasks have been moved into the `atomvm` namespace (from the [`rebar3`](https://rebar3.org) `default` namespace). The "legacy" tasks in the `default` namespace are deprecated, and users will be issued a warning when used. Be sure to use the `atomvm` namespace in any future usage of this plugin, as the deprecated tasks may be removed without warning. E.g., `rebar3 atomvm packbeam ...` diff --git a/src/atomvm_esp32_flash_provider.erl b/src/atomvm_esp32_flash_provider.erl index 58e278a..57604d3 100644 --- a/src/atomvm_esp32_flash_provider.erl +++ b/src/atomvm_esp32_flash_provider.erl @@ -30,17 +30,22 @@ -define(OPTS, [ {esptool, $e, "esptool", string, "Path to esptool.py"}, {chip, $c, "chip", string, "ESP chip (default auto)"}, - {port, $p, "port", string, "Device port (default /dev/ttyUSB0)"}, + {port, $p, "port", string, "Device port (default auto discovery)"}, {baud, $b, "baud", integer, "Baud rate (default 115200)"}, - {offset, $o, "offset", string, "Offset (default 0x210000)"} + {offset, $o, "offset", string, + "Offset (default read from device) *deprecated, use app_partition. " + "When given, a warning will be issued if the partition name does not match the " + "expected name, as long as the offset aligns with a valid application partition."}, + {app_partition, $a, "app_partition", string, "Application partition name (default main.avm)"} ]). -define(DEFAULT_OPTS, #{ - esptool => "esptool.py", + esptool => os:find_executable("esptool.py"), chip => "auto", - port => "/dev/ttyUSB0", - baud => 115200, - offset => "0x210000" + port => "auto", + baud => "115200", + offset => "auto", + app_partition => "main.avm" }). %% @@ -81,16 +86,25 @@ do(State) -> maps:get(chip, Opts), maps:get(port, Opts), maps:get(baud, Opts), - maps:get(offset, Opts) + integer_from_string(maps:get(offset, Opts)), + maps:get(app_partition, Opts), + State ), {ok, State} catch - C:E:S -> + C:rebar_abort:S -> rebar_api:error( - "An error occurred in the ~p task. Error=~p", - [?PROVIDER, E] + "A fatal error occurred in the ~p task.", + [?PROVIDER] ), + rebar_api:debug("Class=~p, Error=~p~nSTACKTRACE:~n~p~n", [C, rebar_abort, S]), + {error, rebar_abort}; + C:E:S -> rebar_api:debug("Class=~p, Error=~p~nSTACKTRACE:~n~p~n", [C, E, S]), + rebar_api:abort( + "An unhandled error occurred in the ~p task. Error=~p", + [?PROVIDER, E] + ), {error, E} end. @@ -105,6 +119,7 @@ format_error(Reason) -> %% @private get_opts(State) -> {ParsedArgs, _} = rebar_state:command_parsed_args(State), + rebar_api:debug("ParsedArgs = ~w", [ParsedArgs]), RebarOpts = atomvm_rebar3_plugin:get_atomvm_rebar_provider_config(State, ?PROVIDER), ParsedOpts = atomvm_rebar3_plugin:proplist_to_map(ParsedArgs), maps:merge( @@ -127,37 +142,71 @@ env_opts() -> "ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_PORT", maps:get(port, ?DEFAULT_OPTS) ), - baud => maybe_convert_string( + baud => integer_from_string( os:getenv( "ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_BAUD", maps:get(baud, ?DEFAULT_OPTS) ) ), - offset => os:getenv( - "ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_OFFSET", - maps:get(offset, ?DEFAULT_OPTS) + offset => integer_from_string( + os:getenv( + "ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_OFFSET", + maps:get(offset, ?DEFAULT_OPTS) + ) + ), + app_partition => os:getenv( + "ATOMVM_REBAR3_PLUGIN_ESP32_APP_PARTITION", + maps:get(app_partition, ?DEFAULT_OPTS) ) }. %% @private -maybe_convert_string(S) when is_list(S) -> - list_to_integer(S); -maybe_convert_string(I) -> - I. +-spec integer_from_string(S :: list() | integer()) -> Value :: integer(). +integer_from_string("auto") -> + -1; +integer_from_string(I) when is_integer(I) -> + I; +integer_from_string(S) when is_list(S) -> + S0 = string:trim(S), + case lists:prefix("0x", S0) of + true -> + list_to_integer(lists:subtract(S0, "0x"), 16); + false -> + list_to_integer(S0) + end. %% @private -do_flash(ProjectApps, EspTool, Chip, Port, Baud, Offset) -> - [ProjectAppAVM | _] = [get_avm_file(ProjectApp) || ProjectApp <- ProjectApps], - Portparam = - case Port of - "auto" -> ""; - _ -> ["--port ", Port] +do_flash(ProjectApps, EspTool, Chip, Port, Baud, Offset, Partition, State) -> + TempFile = get_part_tempfile(State), + Address = + case Offset of + -1 -> + try + read_flash_offset(EspTool, Port, Partition, TempFile) + catch + error:{partition_not_found, Partition} -> + rebar_api:abort("Partition ~s not found!", [Partition]); + error:invalid_partition_table -> + rebar_api:abort( + "Error parsing partition table, possible line noise reading from device.", + [] + ) + end; + Val -> + Offset0 = read_flash_offset(EspTool, Port, Partition, TempFile), + case Val =:= Offset0 of + true -> + Offset0; + false -> + {ok, Offset1} = validate_offset(EspTool, Port, Val, Partition, TempFile), + Offset1 + end end, - Cmd = lists:join(" ", [ - EspTool, + [ProjectAppAVM | _] = [get_avm_file(ProjectApp) || ProjectApp <- ProjectApps], + + StdArgs = [ "--chip", Chip, - Portparam, "--baud", integer_to_list(Baud), "--before", @@ -165,18 +214,55 @@ do_flash(ProjectApps, EspTool, Chip, Port, Baud, Offset) -> "--after", "hard_reset", "write_flash", - "-u", "--flash_mode", "keep", "--flash_freq", "keep", "--flash_size", "detect", - Offset, + "0x" ++ integer_to_list(Address, 16), ProjectAppAVM - ]), - rebar_api:info("~s~n", [Cmd]), - rebar_api:console("~s", [os:cmd(Cmd)]), + ], + Args = + case Port of + "auto" -> + StdArgs; + _ -> + ["--port", Port | StdArgs] + end, + + AVMApp = filename:basename(ProjectAppAVM), + rebar_api:info("Flashing ~s to device.", [AVMApp]), + + %% The following log output is parsed by the tests and should not be changed or removed. + case os:getenv("TEST") of + false -> + ok; + _ -> + rebar_api:info("~s ~s", [EspTool, lists:flatten(lists:join(" ", Args))]) + end, + + ToolPort = + try + open_port({spawn_executable, EspTool}, [ + {args, Args}, stderr_to_stdout, use_stdio, exit_status + ]) + catch + error:enoent -> + error("esptool.py not found"); + error:eacces -> + error("not executable", EspTool) + end, + receive + {ToolPort, {exit_status, 0}} -> + ok; + {ToolPort, {exit_status, Status}} -> + error({"esptool.py exit error", Status}) + after 120000 -> + error("esptool.py failed (2 minute timeout exceeded)") + end, + + rebar_api:info("Success!", []), ok. %% @private @@ -185,3 +271,80 @@ get_avm_file(App) -> Name = binary_to_list(rebar_app_info:name(App)), DirName = filename:dirname(OutDir), filename:join(DirName, Name ++ ".avm"). + +%% @private +read_flash_offset(Esptool, Port, PartName, TempFile) -> + rebar_api:info("Reading application partition offset from device...", []), + try esp_part_dump:read_app_offset(Esptool, Port, PartName, TempFile) of + Offset -> + Offset + catch + error:invalid_partition_table -> + rebar_api:abort("Invalid partition data!", []); + error:no_device -> + rebar_api:abort("No ESP32 device attached!", []); + error:no_device_dump -> + rebar_api:abort("No partition table retrieved from device.", []); + error:{partition_not_found, Partition} -> + rebar_api:error("The partition ~s was not found in device partition table!", [Partition]), + rebar_api:abort( + "When using a custom partition table always specify the 'app_partition' NAME.", [] + ); + error:{invalid_subtype, Type} -> + rebar_api:abort("The partition ~s was found, but used invalid subtype ~w.", [ + PartName, Type + ]); + error:invalid_partition_type -> + rebar_api:abort( + "The partition ~s is not a data partition!~nOnly data partitions may be used for AtomVM applications", + [ + PartName + ] + ); + error:corrupt_partition_data -> + rebar_api:abort("Fatal error! Corrupt partition table data!", []); + error:no_data_partitions -> + rebar_api:abort("The partition table on the device contains no data partitions"); + error:Error -> + rebar_api:abort("Unexpected error reading partition table from device, ~p.", [Error]) + end. + +validate_offset(Esptool, Port, Offset, Partition, TempFile) -> + rebar_api:debug("Checking that offset aligns with partition from table on device...", []), + try esp_part_dump:partition_at_offset(Esptool, Port, Offset, TempFile) of + {error, {Name, Reason}} -> + rebar_api:abort( + "The configured offset 0x~.16B contains partition ~s, which cannot be used for reason ~p.~n", + [Offset, Name, Reason] + ); + {Found, _Type} -> + rebar_api:warn( + "The discovered partition ~s at offset 0x~.16B will be used, not the expected partition named ~s.", + [Found, Offset, Partition] + ), + {ok, Offset} + catch + _:no_device -> + rebar_api:abort("No ESP32 device attached!", []); + _:corrupt_partition_data -> + rebar_api:abort("Fatal error! Corrupt partition table data!", []); + C:E:S -> + rebar_api:debug("Class=~p, Error=~p~nSTACKTRACE:~n~p~n", [C, E, S]), + rebar_api:abort( + "Unexpected error reading partition table from device. Error = ~p.", + [E] + ) + end. + +%% @private +get_part_tempfile(State) -> + OutDir = filename:absname(rebar_dir:base_dir(State)), + TempFile = filename:absname_join(OutDir, "part.tmp"), + case filelib:is_file(TempFile) of + true -> + rebar_api:debug("Removing possibly stale partition dump data ~s", [TempFile]), + file:delete(TempFile); + false -> + ok + end, + TempFile. diff --git a/src/esp_part_dump.erl b/src/esp_part_dump.erl new file mode 100644 index 0000000..ce3da87 --- /dev/null +++ b/src/esp_part_dump.erl @@ -0,0 +1,266 @@ +%% +%% Copyright (c) 2025 Winford (UncleGrumpy) +%% All rights reserved. +%% +%% 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(esp_part_dump). + +-export([read_app_offset/4, read_app_offset/2, partition_at_offset/4, partition_at_offset/2]). + +-spec read_app_offset( + Esptool :: string(), Port :: string(), PartName :: string(), TempFile :: string() +) -> Offset :: non_neg_integer(). +read_app_offset(Esptool, Port, PartName, TempFile) -> + Partitions = get_partition_data(Esptool, Port, TempFile), + Offset = lookup_partition_by_name(PartName, Partitions), + Offset. + +-spec read_app_offset( + PartName :: string(), File :: string() +) -> Offset :: non_neg_integer(). +read_app_offset(PartName, File) -> + Partitions = + case file:read_file(File) of + {ok, Data} -> + Data; + {error, enoent} -> + error(no_dump_file) + end, + Offset = lookup_partition_by_name(PartName, Partitions), + Offset. + +-spec lookup_partition_by_name(Name :: string(), Partitions :: binary()) -> + Offset :: non_neg_integer(). +lookup_partition_by_name(Name, Partitions) -> + PartitionsData = parse_data_partitions(Partitions), + case lists:keyfind(Name, 2, PartitionsData) of + false -> + error({partition_not_found, Name}); + {_Offset, _Name, protected} -> + error(invalid_partition_type); + {_Offset, _Name, {reserved, SubType}} -> + error({invalid_subtype, SubType}); + {Offset, _Name, _SubType} -> + Offset + end. + +-spec partition_at_offset( + Esptool :: string(), Port :: string(), Offset :: non_neg_integer(), TempFile :: string() +) -> + {Name :: string(), Type :: atom()} + | {error, {Name :: string(), {Reason :: any(), Type :: atom() | byte()}}}. +partition_at_offset(Esptool, Port, Offset, TempFile) -> + Partitions = get_partition_data(Esptool, Port, TempFile), + Partition = lookup_partition_by_offset(Offset, Partitions), + Partition. + +-spec partition_at_offset(Offset :: non_neg_integer(), TempFile :: string()) -> + {Name :: string(), Type :: atom()} + | {error, {Name :: string(), {Reason :: any(), Type :: atom() | byte()}}}. +partition_at_offset(Offset, File) -> + Partitions = + case file:read_file(File) of + {ok, Data} -> + Data; + {error, enoent} -> + error(no_dump_file) + end, + Partition = lookup_partition_by_offset(Offset, Partitions), + Partition. + +% @private +-spec lookup_partition_by_offset(Offset :: non_neg_integer(), Partitions :: binary()) -> + {Name :: string(), Type :: atom()} + | {error, {Name :: list(), {Reason :: any(), Type :: atom() | byte()}}}. +lookup_partition_by_offset(Offset, Partitions) -> + PartitionsData = parse_data_partitions(Partitions), + case lists:keyfind(Offset, 1, PartitionsData) of + false -> + {error, {"none", "No partition aligned to address"}}; + {Offset, Name, protected} -> + {error, {Name, {not_data_partition, protected}}}; + {Offset, Name, {reserved, Type}} -> + {error, {Name, {reserved, Type}}}; + {Offset, Name, Type} -> + {Name, Type} + end. + +% @private +-spec get_partition_data(Esptool :: string(), Port :: string(), TempFile :: string()) -> + PartitionData :: binary(). +get_partition_data(Esptool, Port, TempFile) -> + case file:read_file(TempFile) of + {ok, Data} -> + Data; + {error, enoent} -> + dump_device_partition(Esptool, Port, TempFile) + end. + +%% @private +-spec dump_device_partition(Esptool :: string(), Port :: string(), TempFile :: string()) -> + PartitionData :: binary(). +dump_device_partition(Esptool, Port, TempFile) -> + BaseArgs = [ + "read_flash", + "0x8000", + "0xC00", + TempFile + ], + Args = + case Port of + "auto" -> + BaseArgs; + _ -> + ["--port", Port | BaseArgs] + end, + case os:getenv("TEST") of + false -> + ok; + _ -> + rebar_api:info("~s ~s", [Esptool, lists:flatten(lists:join(" ", Args))]) + end, + + Tool = + try + open_port({spawn_executable, Esptool}, [ + {args, Args}, stderr_to_stdout, use_stdio, exit_status + ]) + catch + error:enoent -> + error("esptool.py not found"); + error:eacces -> + error(not_executable, Esptool) + end, + receive + {Tool, {exit_status, 0}} -> + ok; + {Tool, {exit_status, Status}} -> + error({"esptool.py failure", Status}) + after 120000 -> + error("esptool.py failed (2 minute timeout exceeded)") + end, + Partition_data = + case file:read_file(TempFile) of + {ok, Data} -> + Data; + {error, enoent} -> + error(no_device_dump); + Error -> + error(Error) + end, + Partition_data. + +%% @private +-spec parse_data_partitions(PartitionTable :: binary()) -> + [ + { + Offset :: non_neg_integer(), + Name :: string(), + Type :: + atom() | byte() | protected | {Type :: atom() | byte() | protected, Reason :: any()} + } + ]. +parse_data_partitions(PartitionTable) -> + parse_data_partitions(PartitionTable, []). + +parse_data_partitions(<>, PartitionData) -> + case Partition of + %% The default sub-type for AtomVM applications is phy + <<16#aa50:16, 16#01, 16#01, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), phy} | PartitionData + ]); + %% custom sub-type 0xAA can be used to recognize AtomVM applications + <<16#aa50:16, 16#01, 16#aa, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), avm_app} | PartitionData + ]); + %% ESP-IDF sub-type "undefined" is allowed + <<16#aa50:16, 16#01, 16#06, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), undefined} | PartitionData + ]); + %% Other data partition subtypes should not be used (including fat, nvs, coredump, efuse, etc...) + <<16#aa50:16, 16#01, 16#00, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, ota}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#02, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, nvs}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#03, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, coredump}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#04, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, nvs_keys}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#05, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, efuse}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#81, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, fat}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#82, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, spiffs}} + | PartitionData + ]); + <<16#aa50:16, 16#01, 16#83, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), {reserved, littlefs}} + | PartitionData + ]); + %% Catchall to allow any other sub-types + <<16#aa50:16, 16#01, SubType:8, 0, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), SubType} | PartitionData + ]); + %% Non data partitions are off-limits. BEAM applications should only be flashed to data partitions. + <<16#aa50:16, _NonDatatype:3/binary, Address:4/binary, _:3/binary, PName:16/binary, 0:32>> -> + [Name | _End] = binary:split(PName, <<0>>), + parse_data_partitions(Partitions, [ + {binary:decode_unsigned(Address), binary_to_list(Name), protected} | PartitionData + ]); + %% End of table marker + <<16#ebeb:16, 16#ffffffffffffffffffffffffffff:112, _:16/binary>> -> + lists:reverse(PartitionData); + _ -> + error(corrupt_partition_data) + end. diff --git a/test/driver/scripts/README.md b/test/driver/scripts/README.md new file mode 100644 index 0000000..ebd2610 --- /dev/null +++ b/test/driver/scripts/README.md @@ -0,0 +1,41 @@ + +# esptool.sh and partition binary files + +The partition binary files in this directory are used by esptool.sh to simulate the real esptool.py +dummping the partiton table from an AtomVM installed device. They were built using esp-idf and the +following partiton.csv contents were used to generate the partition tables: + +## partition.bin +This partition table starts with the standard Erlang only partiton table, but adds several extra +partitons used for tests to flash to an alternate partition and test failures for invalid partiton +tyes. + +```csv +# Name, Type, SubType, Offset, Size, Flags +# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild +nvs, data, nvs, 0x9000, 0x6000, \ +phy_init, data, phy, 0xf000, 0x1000, \ +factory, app, factory, 0x10000, 0x1C0000, > these are standard partitons +boot.avm, data, phy, 0x1D0000, 0x40000, / +main.avm, data, phy, 0x210000, 0x100000, / +app1.avm, data, 0xAA, 0x310000, 0x070000, \ +bad1, data, fat, 0x380000, 0x010000, > extra test partitons +bad2, app, test, 0x390000, 0x010000 / +``` + +## partition_elixir.bin +This partition talbe is just the standard Elixir supported build partiton table. + +```csv +# Name, Type, SubType, Offset, Size, Flags +# Note: if you change the phy_init or app partition offset, make sure to change the offset in Kconfig.projbuild +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x1C0000, +boot.avm, data, 0xAB, 0x1D0000, 0x80000, +main.avm, data, 0xAA, 0x250000, 0x100000 +``` diff --git a/test/driver/scripts/esptool.sh b/test/driver/scripts/esptool.sh new file mode 100755 index 0000000..e203e0d --- /dev/null +++ b/test/driver/scripts/esptool.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +## +## Copyright (c) Winford (UncleGrumpy) +## All rights reserved. +## +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + +binary=${ATOMVM_REBAR3_PLUGIN_PARTITION_DATA:=${ATOMVM_REBAR3_PLUGIN_ESP32_PARTITION_DUMP}} + +while [ "${#}" -gt 0 ]; do + case ${1} in + read_flash ) shift + if [ "${1}" = "0x8000" ] && [ "${2}" = "0xC00" ] || [ "${2}" = "0xc00" ]; then + target=${3} + cp "${binary}" "${target}" + unset ATOMVM_REBAR3_PLUGIN_PARTITION_DATA + exit 0 + fi ;; + write_flash ) exit 0 ;; + esac + shift +done + +echo "${@}" + +exit 0 diff --git a/test/driver/scripts/partition.bin b/test/driver/scripts/partition.bin new file mode 100644 index 0000000000000000000000000000000000000000..1f55e3a69e6232173cb568c6886267e973e099d7 GIT binary patch literal 3072 zcmZ1#z{tcffq{V`fq@~ftQg2Z1*-xW85uqR#RM1_3Nk9=GxIV_kX5V-0Pz@sScV}j zF}Wnas1jM40j!@v7AVHTkd&WaqL)~fi>v@5uLu+pV8~6(%)=tTih;opD9_H2SWsYy zMZS@N!2&4H$dHtn0#b+qfbLUZV6X&=f#r=*B!QgQum7Wfv{Q$dvrVkbH@Q%*^FL6M i6Gd{AGa3S;Aut*OqaiRF0;3@?8UmvsFd70wJOlvCL=1HR literal 0 HcmV?d00001 diff --git a/test/driver/scripts/partition.bin.license b/test/driver/scripts/partition.bin.license new file mode 100644 index 0000000..e23e315 --- /dev/null +++ b/test/driver/scripts/partition.bin.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: Apache-2.0 +SPDX-FileCopyrightText: Winford (Uncle Grumpy) diff --git a/test/driver/scripts/partition_elixir.bin b/test/driver/scripts/partition_elixir.bin new file mode 100644 index 0000000000000000000000000000000000000000..5888934b1cdaca9a9fb31aca3216730831629b73 GIT binary patch literal 3072 zcmZ1#z{tcffq{V`fq@~ftQg2Z1*-xW85uqR#RM1_3Nk9=GxIV_kX5V-0Pz@sScV}j zF}Wnas1jM40j!@v7AVHSkd&WaqL)~fi>v@5uL=|rV8~6(%tM!d{rW!&*tXzu+KR4a tOlx{W8|SfWenXKQ<&1{FXb6mkz-S1JhQMeDjE2By2#kinXb9j90RZkCVyplF literal 0 HcmV?d00001 diff --git a/test/driver/scripts/partition_elixir.bin.license b/test/driver/scripts/partition_elixir.bin.license new file mode 100644 index 0000000..e23e315 --- /dev/null +++ b/test/driver/scripts/partition_elixir.bin.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: Apache-2.0 +SPDX-FileCopyrightText: Winford (Uncle Grumpy) diff --git a/test/driver/src/esp32_flash_tests.erl b/test/driver/src/esp32_flash_tests.erl index 1c08287..d33fd6d 100644 --- a/test/driver/src/esp32_flash_tests.erl +++ b/test/driver/src/esp32_flash_tests.erl @@ -23,15 +23,17 @@ run(Opts) -> ok = test_flags(Opts), + ok = test_elixir_partition_table(Opts), ok = test_env_overrides(Opts), ok = test_rebar_overrides(Opts), + ok = test_warnings(Opts), + ok = test_errors(Opts), ok. %% @private test_flags(Opts) -> test_flags(Opts, [], [ {"--chip", "auto"}, - {"--port", "/dev/ttyUSB0"}, {"--baud", "115200"}, {"--offset", "0x210000"} ]), @@ -40,16 +42,28 @@ test_flags(Opts) -> Opts, [ {"-c", "esp32c3"}, - {"-p", "/dev/tty.usbserial-0001"} + {"-p", "/dev/tty.usbserial-0001"}, + {"-b", "921600"} ], [ {"--chip", "esp32c3"}, - {"--port", "tty.usbserial-0001"}, - {"--baud", "115200"}, + {"--port", "/dev/tty.usbserial-0001"}, + {"--baud", "921600"}, {"--offset", "0x210000"} ] ), + test_flags( + Opts, + [ + {"-a", "app1.avm"} + ], + [ + {"--chip", "auto"}, + {"--baud", "115200"}, + {"--offset", "0x310000"} + ] + ), ok. %% @private @@ -67,7 +81,23 @@ test_flags(Opts, Flags, FlagExpectList) -> end, FlagExpectList ), - ok = test:expect_contains("_build/default/lib/myapp.avm", Output), + ok = test:expect_contains("Flashing myapp.avm to device.", Output), + + test:tick(). + +test_elixir_partition_table(Opts) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myapp"]), + Offset = 16#250000, + + Cmd = create_esp32_flash_cmd(AppDir, [], [ + {"ATOMVM_REBAR3_PLUGIN_PARTITION_DATA", + os:getenv("ATOMVM_REBAR3_PLUGIN_ESP32_EX_PARTITION_DUMP")} + ]), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains(io_lib:format("~i", [Offset]), Output), test:tick(). @@ -123,6 +153,78 @@ test_rebar_overrides(Opts, Flags, EnvVar, Value, Flag, ExpectedValue) -> test:tick(). +%% @private +test_warnings(Opts) -> + test_expect( + Opts, + [ + {"--offset", "0x210000"}, + {"--app_partition", "app1.avm"} + ], + "The discovered partition main.avm at offset 0x210000 will be used, not the expected partition named app1.avm." + ), + ok. + +%% @private +test_errors(Opts) -> + test_expect( + Opts, + [ + {"-a", "fake"} + ], + "The partition fake was not found in device partition table!" + ), + + %% overrides the script that emulates dumping the partition data + %% simulating execution when no device is attached. + test_expect( + Opts, + [ + {"--esptool", "/usr/bin/echo"} + ], + "No partition table retrieved from device." + ), + + test_expect( + Opts, + [ + {"--app_partition", "app1.avm"}, + {"--offset", "0x210000"} + ], + "The discovered partition main.avm at offset 0x210000 will be used, not the expected partition named app1.avm." + ), + + test_expect( + Opts, + [ + {"-a", "bad1"} + ], + "The partition bad1 was found, but used invalid subtype fat." + ), + + test_expect( + Opts, + [ + {"-a", "bad2"} + ], + "The partition bad2 is not a data partition!" + ), + + ok. + +%% @private +test_expect(Opts, Flags, Expect) -> + AppsDir = maps:get(apps_dir, Opts), + AppDir = test:make_path([AppsDir, "myapp"]), + + Cmd = create_esp32_flash_cmd(AppDir, Flags, []), + Output = test:execute_cmd(Cmd, Opts), + test:debug(Output, Opts), + + ok = test:expect_contains(Expect, Output), + + test:tick(). + %% @private create_esp32_flash_cmd(AppDir, Opts, Env) -> - test:create_rebar3_cmd(AppDir, esp32_flash, [{"-e", "echo"} | Opts], Env). + test:create_rebar3_cmd(AppDir, esp32_flash, Opts, Env). diff --git a/test/driver/src/test.erl b/test/driver/src/test.erl index e6cde06..ce7f240 100644 --- a/test/driver/src/test.erl +++ b/test/driver/src/test.erl @@ -139,7 +139,7 @@ execute_cmd(Cmd, Opts) -> debug(Msg, Opts) -> case maps:get(debug, Opts) of true -> - io:format("~s~n", [Msg]); + io:format("~p~n", [Msg]); _ -> ok end. diff --git a/test/run.sh b/test/run.sh index 49a5b69..e880f2d 100755 --- a/test/run.sh +++ b/test/run.sh @@ -24,6 +24,14 @@ unset ATOMVM_REBAR3_PLUGIN_PICO_RESET_DEV unset ATOMVM_REBAR3_PLUGIN_UF2CREATE_START +export ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_ESPTOOL="${test_dir}/scripts/esptool.sh" +export ATOMVM_REBAR3_PLUGIN_ESP32_PARTITION_DUMP="${test_dir}/scripts/partition.bin" +export ATOMVM_REBAR3_PLUGIN_ESP32_EX_PARTITION_DUMP="${test_dir}/scripts/partition_elixir.bin" + cd "${test_dir}" rebar3 escriptize ./_build/default/bin/driver -r "$(pwd)" "$@" + +unset ATOMVM_REBAR3_PLUGIN_ESP32_PARTITION_DUMP +unset ATOMVM_REBAR3_PLUGIN_ESP32_EX_PARTITION_DUMP +unset ATOMVM_REBAR3_PLUGIN_ESP32_FLASH_ESPTOOL