Skip to content

Commit 0c98c60

Browse files
committed
tools/mpremote: Add romfs query, build and deploy commands.
These commands use the `vfs.rom_ioctl()` function to manage the ROM partitions on a device, and create and deploy ROMFS images. Signed-off-by: Damien George <damien@micropython.org>
1 parent 840b641 commit 0c98c60

File tree

4 files changed

+385
-1
lines changed

4 files changed

+385
-1
lines changed

docs/reference/mpremote.rst

+24
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ The full list of supported commands are:
7878
- `mip <mpremote_command_mip>`
7979
- `mount <mpremote_command_mount>`
8080
- `unmount <mpremote_command_unmount>`
81+
- `romfs <mpremote_command_romfs>`
8182
- `rtc <mpremote_command_rtc>`
8283
- `sleep <mpremote_command_sleep>`
8384
- `reset <mpremote_command_reset>`
@@ -347,6 +348,29 @@ The full list of supported commands are:
347348
This happens automatically when ``mpremote`` terminates, but it can be used
348349
in a sequence to unmount an earlier mount before subsequent command are run.
349350

351+
.. _mpremote_command_romfs:
352+
353+
- **romfs** -- manage ROMFS partitions on the device:
354+
355+
.. code-block:: bash
356+
357+
$ mpremote romfs <sub-command>
358+
359+
``<sub-command>`` may be:
360+
361+
- ``romfs query`` to list all the available ROMFS partitions and their size
362+
- ``romfs [-o <output>] build <source>`` to create a ROMFS image from the given
363+
source directory; the default output file is the source appended by ``.romfs``
364+
- ``romfs [-p <partition>] deploy <source>`` to deploy a ROMFS image to the device;
365+
will also create a temporary ROMFS image if the source is a directory
366+
367+
The ``build`` and ``deploy`` sub-commands both support the ``-m``/``--mpy`` option
368+
to automatically compile ``.py`` files to ``.mpy`` when creating the ROMFS image.
369+
This option is enabled by default, but only works if the ``mpy_cross`` Python
370+
package has been installed (eg via ``pip install mpy_cross``). If the package is
371+
not installed then a warning is printed and ``.py`` files remain as is. Compiling
372+
of ``.py`` files can be disabled with the ``--no-mpy`` option.
373+
350374
.. _mpremote_command_rtc:
351375

352376
- **rtc** -- set/get the device clock (RTC):

tools/mpremote/mpremote/commands.py

+182-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import binascii
12
import hashlib
23
import os
34
import sys
45
import tempfile
6+
import zlib
57

68
import serial.tools.list_ports
79

8-
from .transport import TransportError, stdout_write_bytes
10+
from .transport import TransportError, TransportExecError, stdout_write_bytes
911
from .transport_serial import SerialTransport
12+
from .romfs import make_romfs
1013

1114

1215
class CommandError(Exception):
@@ -478,3 +481,181 @@ def do_rtc(state, args):
478481
state.transport.exec("machine.RTC().datetime({})".format(timetuple))
479482
else:
480483
print(state.transport.eval("machine.RTC().datetime()"))
484+
485+
486+
def _do_romfs_query(state, args):
487+
state.ensure_raw_repl()
488+
state.did_action()
489+
490+
# Detect the romfs and get its associated device.
491+
state.transport.exec("import vfs")
492+
if not state.transport.eval("hasattr(vfs,'rom_ioctl')"):
493+
print("ROMFS is not enabled on this device")
494+
return
495+
num_rom_partitions = state.transport.eval("vfs.rom_ioctl(1)")
496+
if num_rom_partitions <= 0:
497+
print("No ROMFS partitions available")
498+
return
499+
500+
for rom_id in range(num_rom_partitions):
501+
state.transport.exec(f"dev=vfs.rom_ioctl(2,{rom_id})")
502+
has_object = state.transport.eval("hasattr(dev,'ioctl')")
503+
if has_object:
504+
rom_block_count = state.transport.eval("dev.ioctl(4,0)")
505+
rom_block_size = state.transport.eval("dev.ioctl(5,0)")
506+
rom_size = rom_block_count * rom_block_size
507+
print(
508+
f"ROMFS{rom_id} partition has size {rom_size} bytes ({rom_block_count} blocks of {rom_block_size} bytes each)"
509+
)
510+
else:
511+
rom_size = state.transport.eval("len(dev)")
512+
print(f"ROMFS{rom_id} partition has size {rom_size} bytes")
513+
romfs = state.transport.eval("bytes(memoryview(dev)[:12])")
514+
print(f" Raw contents: {romfs.hex(':')} ...")
515+
if not romfs.startswith(b"\xd2\xcd\x31"):
516+
print(" Not a valid ROMFS")
517+
else:
518+
size = 0
519+
for value in romfs[3:]:
520+
size = (size << 7) | (value & 0x7F)
521+
if not value & 0x80:
522+
break
523+
print(f" ROMFS image size: {size}")
524+
525+
526+
def _do_romfs_build(state, args):
527+
state.did_action()
528+
529+
if args.path is None:
530+
raise CommandError("romfs build: source path not given")
531+
532+
input_directory = args.path
533+
534+
if args.output is None:
535+
output_file = input_directory + ".romfs"
536+
else:
537+
output_file = args.output
538+
539+
romfs = make_romfs(input_directory, mpy_cross=args.mpy)
540+
541+
print(f"Writing {len(romfs)} bytes to output file {output_file}")
542+
with open(output_file, "wb") as f:
543+
f.write(romfs)
544+
545+
546+
def _do_romfs_deploy(state, args):
547+
state.ensure_raw_repl()
548+
state.did_action()
549+
transport = state.transport
550+
551+
if args.path is None:
552+
raise CommandError("romfs deploy: source path not given")
553+
554+
rom_id = args.partition
555+
romfs_filename = args.path
556+
557+
# Read in or create the ROMFS filesystem image.
558+
if romfs_filename.endswith(".romfs"):
559+
with open(romfs_filename, "rb") as f:
560+
romfs = f.read()
561+
else:
562+
romfs = make_romfs(romfs_filename, mpy_cross=args.mpy)
563+
print(f"Image size is {len(romfs)} bytes")
564+
565+
# Detect the ROMFS partition and get its associated device.
566+
state.transport.exec("import vfs")
567+
if not state.transport.eval("hasattr(vfs,'rom_ioctl')"):
568+
raise CommandError("ROMFS is not enabled on this device")
569+
transport.exec(f"dev=vfs.rom_ioctl(2,{rom_id})")
570+
if transport.eval("isinstance(dev,int) and dev<0"):
571+
raise CommandError(f"ROMFS{rom_id} partition not found on device")
572+
has_object = transport.eval("hasattr(dev,'ioctl')")
573+
if has_object:
574+
rom_block_count = transport.eval("dev.ioctl(4,0)")
575+
rom_block_size = transport.eval("dev.ioctl(5,0)")
576+
rom_size = rom_block_count * rom_block_size
577+
print(
578+
f"ROMFS{rom_id} partition has size {rom_size} bytes ({rom_block_count} blocks of {rom_block_size} bytes each)"
579+
)
580+
else:
581+
rom_size = transport.eval("len(dev)")
582+
print(f"ROMFS{rom_id} partition has size {rom_size} bytes")
583+
584+
# Check if ROMFS filesystem image will fit in the target partition.
585+
if len(romfs) > rom_size:
586+
print("ROMFS image is too big for the target partition")
587+
sys.exit(1)
588+
589+
# Prepare ROMFS partition for writing.
590+
print(f"Preparing ROMFS{rom_id} partition for writing")
591+
transport.exec("import vfs\ntry:\n vfs.umount('/rom')\nexcept:\n pass")
592+
chunk_size = 4096
593+
if has_object:
594+
for offset in range(0, len(romfs), rom_block_size):
595+
transport.exec(f"dev.ioctl(6,{offset // rom_block_size})")
596+
chunk_size = min(chunk_size, rom_block_size)
597+
else:
598+
rom_min_write = transport.eval(f"vfs.rom_ioctl(3,{rom_id},{len(romfs)})")
599+
chunk_size = max(chunk_size, rom_min_write)
600+
601+
# Detect capabilities of the device to use the fastest method of transfer.
602+
has_bytes_fromhex = transport.eval("hasattr(bytes,'fromhex')")
603+
try:
604+
transport.exec("from binascii import a2b_base64")
605+
has_a2b_base64 = True
606+
except TransportExecError:
607+
has_a2b_base64 = False
608+
try:
609+
transport.exec("from io import BytesIO")
610+
transport.exec("from deflate import DeflateIO,RAW")
611+
has_deflate_io = True
612+
except TransportExecError:
613+
has_deflate_io = False
614+
615+
# Deploy the ROMFS filesystem image to the device.
616+
for offset in range(0, len(romfs), chunk_size):
617+
romfs_chunk = romfs[offset : offset + chunk_size]
618+
romfs_chunk += bytes(chunk_size - len(romfs_chunk))
619+
if has_deflate_io:
620+
# Needs: binascii.a2b_base64, io.BytesIO, deflate.DeflateIO.
621+
romfs_chunk_compressed = zlib.compress(romfs_chunk, wbits=-9)
622+
buf = binascii.b2a_base64(romfs_chunk_compressed).strip()
623+
transport.exec(f"buf=DeflateIO(BytesIO(a2b_base64({buf})),RAW,9).read()")
624+
elif has_a2b_base64:
625+
# Needs: binascii.a2b_base64.
626+
buf = binascii.b2a_base64(romfs_chunk)
627+
transport.exec(f"buf=a2b_base64({buf})")
628+
elif has_bytes_fromhex:
629+
# Needs: bytes.fromhex.
630+
buf = romfs_chunk.hex()
631+
transport.exec(f"buf=bytes.fromhex('{buf}')")
632+
else:
633+
# Needs nothing special.
634+
transport.exec("buf=" + repr(romfs_chunk))
635+
print(f"\rWriting at offset {offset}", end="")
636+
if has_object:
637+
transport.exec(
638+
f"dev.writeblocks({offset // rom_block_size},buf,{offset % rom_block_size})"
639+
)
640+
else:
641+
transport.exec(f"vfs.rom_ioctl(4,{rom_id},{offset},buf)")
642+
643+
# Complete writing.
644+
if not has_object:
645+
transport.eval(f"vfs.rom_ioctl(5,{rom_id})")
646+
647+
print()
648+
print("ROMFS image deployed")
649+
650+
651+
def do_romfs(state, args):
652+
if args.command[0] == "query":
653+
_do_romfs_query(state, args)
654+
elif args.command[0] == "build":
655+
_do_romfs_build(state, args)
656+
elif args.command[0] == "deploy":
657+
_do_romfs_deploy(state, args)
658+
else:
659+
raise CommandError(
660+
f"romfs: '{args.command[0]}' is not a command; pass romfs --help for a list"
661+
)

tools/mpremote/mpremote/main.py

+31
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
do_resume,
3737
do_rtc,
3838
do_soft_reset,
39+
do_romfs,
3940
)
4041
from .mip import do_mip
4142
from .repl import do_repl
@@ -228,6 +229,32 @@ def argparse_mip():
228229
return cmd_parser
229230

230231

232+
def argparse_romfs():
233+
cmd_parser = argparse.ArgumentParser(description="manage ROM partitions")
234+
_bool_flag(
235+
cmd_parser,
236+
"mpy",
237+
"m",
238+
True,
239+
"automatically compile .py files to .mpy when building the ROMFS image (default)",
240+
)
241+
cmd_parser.add_argument(
242+
"--partition",
243+
"-p",
244+
type=int,
245+
default=0,
246+
help="ROMFS partition to use",
247+
)
248+
cmd_parser.add_argument(
249+
"--output",
250+
"-o",
251+
help="output file",
252+
)
253+
cmd_parser.add_argument("command", nargs=1, help="romfs command, one of: query, build, deploy")
254+
cmd_parser.add_argument("path", nargs="?", help="path to directory to deploy")
255+
return cmd_parser
256+
257+
231258
def argparse_none(description):
232259
return lambda: argparse.ArgumentParser(description=description)
233260

@@ -302,6 +329,10 @@ def argparse_none(description):
302329
do_version,
303330
argparse_none("print version and exit"),
304331
),
332+
"romfs": (
333+
do_romfs,
334+
argparse_romfs,
335+
),
305336
}
306337

307338
# Additional commands aliases.

0 commit comments

Comments
 (0)