Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,33 @@ or CAN and radio injection, raises `simantic.NotSupported` and names the gap
instead of quietly doing nothing. You can follow what each engine covers in
[simantic-core#183](https://github.com/simantic-dev/simantic-core/issues/183).

## ESP32 images

The ESP32-C3, C6 and P4 boot the way silicon does: Espressif's mask ROM runs
first, then your bootloader, then your app. `sim --elf` therefore takes one ELF
that carries the ROM and your whole flash image. Build it from the files your
ESP-IDF or PlatformIO build already produced:

```bash
simantic esp-image --chip esp32c3 \
--part 0x0:bootloader.bin --part 0x8000:partitions.bin --part 0x10000:firmware.bin \
--flash-size 16MB -o image.elf
sim --elf image.elf ...
```

Already have a merged image from `esptool.py merge_bin`? Pass `--flash merged.bin`
instead of the parts. The same thing from Python is
`simantic.esp_image.build_image("esp32c3", flash, out="image.elf")`.

The mask ROM is Espressif's and is not bundled. On first use it is downloaded
from Espressif's own repositories, pinned by commit and SHA-256, and cached in
`~/.simantic/esp-rom`: the raw C3 and C6 dumps from
[espressif/qemu](https://github.com/espressif/qemu/tree/master/pc-bios), and
the P4 ROM from the [esp-rom-elfs](https://github.com/espressif/esp-rom-elfs)
release ESP-IDF installs. Offline, or with your own copy, pass `--rom`.
`simantic esp-rom --chip esp32c3` downloads it ahead of time and prints where it
went.

## Testing with pytest

Take the `sim` fixture and write ordinary tests:
Expand Down
69 changes: 66 additions & 3 deletions src/simantic/_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The `simantic` command: authenticate, install binaries, report status.
"""The `simantic` command: authenticate, install binaries, report status,
and build bootable ESP32 images.

Thin by design. It exists so `pip install simantic` is followed by two
obvious commands rather than a documentation hunt, not to become a third
Expand All @@ -11,7 +12,7 @@
import getpass
import sys

from . import auth, install, telemetry
from . import auth, esp_image, install, telemetry
from ._locate import BinaryNotFound, locate
from .mcu import BINARY as SIM_BINARY
from .mcu import ENV_VAR as SIM_ENV
Expand Down Expand Up @@ -81,6 +82,40 @@ def _status(args) -> int:
return 0


def _parse_part(text: str) -> tuple[int, bytes]:
offset, sep, path = text.partition(":")
if not sep or not path:
raise esp_image.EspImageError(f"--part expects OFFSET:PATH, e.g. 0x10000:firmware.bin (got {text!r})")
try:
return int(offset, 0), open(path, "rb").read()
except ValueError:
raise esp_image.EspImageError(f"--part offset {offset!r} is not a number") from None
except OSError as exc:
raise esp_image.EspImageError(f"--part {path}: {exc.strerror}") from None


def _esp_image(args) -> int:
size = esp_image.parse_size(args.flash_size) if args.flash_size else None
if args.flash:
flash = esp_image._read(args.flash, "flash image")
if size is not None:
flash = esp_image.merge_flash([(0, flash)], size=size)
else:
flash = esp_image.merge_flash([_parse_part(p) for p in args.part], size=size)
rom = args.rom
if rom is None:
lay = esp_image.layout(args.chip)
print(f"using Espressif's {lay.chip} mask ROM from {lay.source.url} (cached after the first download)")
elf = esp_image.build_image(args.chip, flash, out=args.out, rom=rom)
print(f"wrote {args.out}: {len(elf)} bytes, flash {len(flash):#x} bytes; run it with `sim --elf {args.out}`")
return 0


def _esp_rom(args) -> int:
print(esp_image.fetch_rom(args.chip, force=args.force))
return 0


def main(argv: list[str] | None = None) -> int:
# prog is left to argparse so usage reflects however it was invoked:
# `simantic`, the short `smtc`, or `python -m simantic`.
Expand Down Expand Up @@ -117,6 +152,34 @@ def main(argv: list[str] | None = None) -> int:
func=_status
)

p_img = sub.add_parser(
"esp-image",
help="build a bootable ESP32 image ELF (mask ROM + flash) for `sim --elf`",
)
p_img.add_argument("--chip", required=True, help=f"one of {', '.join(esp_image.chips())}")
src = p_img.add_mutually_exclusive_group(required=True)
src.add_argument("--flash", help="merged flash image (bootloader, partition table and app at their offsets)")
src.add_argument(
"--part",
action="append",
metavar="OFFSET:PATH",
help="a flash part at its offset, repeatable, e.g. 0x0:bootloader.bin 0x8000:partitions.bin "
"0x10000:firmware.bin (gaps are 0xFF)",
)
p_img.add_argument("--flash-size", help="pad the flash image to this size, e.g. 16MB")
p_img.add_argument(
"--rom",
help="use this mask ROM instead of downloading Espressif's pinned copy "
"(raw dump for C3/C6; esp32p4_rev0_rom.elf for P4)",
)
p_img.add_argument("-o", "--out", required=True, help="output ELF path")
p_img.set_defaults(func=_esp_image)

p_rom = sub.add_parser("esp-rom", help="download Espressif's pinned mask ROM for a chip and print its path")
p_rom.add_argument("--chip", required=True, help=f"one of {', '.join(esp_image.chips())}")
p_rom.add_argument("--force", action="store_true", help="download again even if cached")
p_rom.set_defaults(func=_esp_rom)

args = parser.parse_args(argv)
telemetry.record(f"cli.{args.command}")
try:
Expand All @@ -125,7 +188,7 @@ def main(argv: list[str] | None = None) -> int:
# waiting on a simulation, and the spool is due at most hourly.
telemetry.flush()
return result
except (auth.AuthError, install.InstallError) as exc:
except (auth.AuthError, install.InstallError, esp_image.EspImageError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
except KeyboardInterrupt:
Expand Down
Loading
Loading