micropython-microbit-fs 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- micropython_microbit_fs/.DS_Store +0 -0
- micropython_microbit_fs/__init__.py +59 -0
- micropython_microbit_fs/api.py +114 -0
- micropython_microbit_fs/cli.py +246 -0
- micropython_microbit_fs/device_info.py +121 -0
- micropython_microbit_fs/exceptions.py +37 -0
- micropython_microbit_fs/file.py +62 -0
- micropython_microbit_fs/filesystem.py +464 -0
- micropython_microbit_fs/flash_regions.py +276 -0
- micropython_microbit_fs/hex_utils.py +131 -0
- micropython_microbit_fs/hexes/.DS_Store +0 -0
- micropython_microbit_fs/hexes/microbitv1/.DS_Store +0 -0
- micropython_microbit_fs/hexes/microbitv1/v1.1.1/micropython-microbit-v1.1.1.hex +14455 -0
- micropython_microbit_fs/hexes/microbitv2/.DS_Store +0 -0
- micropython_microbit_fs/hexes/microbitv2/v2.1.2/micropython-microbit-v2.1.2.hex +28186 -0
- micropython_microbit_fs/hexes.py +166 -0
- micropython_microbit_fs/py.typed +0 -0
- micropython_microbit_fs/uicr.py +89 -0
- micropython_microbit_fs-0.1.0.dist-info/METADATA +272 -0
- micropython_microbit_fs-0.1.0.dist-info/RECORD +22 -0
- micropython_microbit_fs-0.1.0.dist-info/WHEEL +4 -0
- micropython_microbit_fs-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Filesystem reading and writing functionality.
|
|
4
|
+
|
|
5
|
+
This module provides functions to read and write files to the MicroPython
|
|
6
|
+
filesystem embedded in an Intel Hex file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from intelhex import IntelHex
|
|
10
|
+
|
|
11
|
+
from micropython_microbit_fs.device_info import DeviceInfo, DeviceVersion
|
|
12
|
+
from micropython_microbit_fs.exceptions import (
|
|
13
|
+
FilesystemError,
|
|
14
|
+
InvalidFileError,
|
|
15
|
+
StorageFullError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Chunk Constants
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
CHUNK_SIZE = 128
|
|
23
|
+
"""Size of a filesystem chunk in bytes."""
|
|
24
|
+
|
|
25
|
+
CHUNK_DATA_SIZE = 126
|
|
26
|
+
"""Size of data that can be stored in a chunk (128 - 1 marker - 1 tail)."""
|
|
27
|
+
|
|
28
|
+
CHUNK_MARKER_SIZE = 1
|
|
29
|
+
"""Size of the chunk marker byte."""
|
|
30
|
+
|
|
31
|
+
MAX_CHUNKS = 252
|
|
32
|
+
"""Maximum number of chunks (256 - 4 reserved markers)."""
|
|
33
|
+
|
|
34
|
+
MAX_FILENAME_LENGTH = 120
|
|
35
|
+
"""Maximum length of a filename in bytes."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ChunkMarker:
|
|
39
|
+
"""Chunk marker byte values."""
|
|
40
|
+
|
|
41
|
+
FREED = 0x00
|
|
42
|
+
"""Chunk has been freed but not erased."""
|
|
43
|
+
|
|
44
|
+
PERSISTENT_DATA = 0xFD
|
|
45
|
+
"""Persistent data page marker (first byte of last FS page)."""
|
|
46
|
+
|
|
47
|
+
FILE_START = 0xFE
|
|
48
|
+
"""Start of a file."""
|
|
49
|
+
|
|
50
|
+
UNUSED = 0xFF
|
|
51
|
+
"""Empty/unused chunk."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Address Utilities
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def chunk_index_to_address(chunk_index: int, fs_start: int) -> int:
|
|
60
|
+
"""Convert a 1-based chunk index to a flash address.
|
|
61
|
+
|
|
62
|
+
:param chunk_index: The 1-based chunk index (1-252).
|
|
63
|
+
:param fs_start: The filesystem start address in flash.
|
|
64
|
+
:returns: The flash address where this chunk begins.
|
|
65
|
+
"""
|
|
66
|
+
return fs_start + (chunk_index - 1) * CHUNK_SIZE
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def address_to_chunk_index(address: int, fs_start: int) -> int:
|
|
70
|
+
"""Convert a flash address to a 1-based chunk index.
|
|
71
|
+
|
|
72
|
+
:param address: The flash address.
|
|
73
|
+
:param fs_start: The filesystem start address in flash.
|
|
74
|
+
:returns: The 1-based chunk index.
|
|
75
|
+
"""
|
|
76
|
+
return ((address - fs_start) // CHUNK_SIZE) + 1
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_fs_start_address(device_info: DeviceInfo) -> int:
|
|
80
|
+
"""Get the effective filesystem start address.
|
|
81
|
+
|
|
82
|
+
The filesystem may have more space available than needed. The start
|
|
83
|
+
address is adjusted to ensure we don't exceed MAX_CHUNKS.
|
|
84
|
+
|
|
85
|
+
:param device_info: Device information from the hex file.
|
|
86
|
+
:returns: The effective filesystem start address.
|
|
87
|
+
"""
|
|
88
|
+
fs_max_size = CHUNK_SIZE * MAX_CHUNKS
|
|
89
|
+
fs_end = get_fs_end_address(device_info)
|
|
90
|
+
|
|
91
|
+
# There might be more free space than the filesystem needs
|
|
92
|
+
start_for_max_fs = fs_end - fs_max_size
|
|
93
|
+
return max(device_info.fs_start_address, start_for_max_fs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_fs_end_address(device_info: DeviceInfo) -> int:
|
|
97
|
+
"""Get the filesystem end address.
|
|
98
|
+
|
|
99
|
+
For V1, we subtract one page for the magnetometer calibration data.
|
|
100
|
+
|
|
101
|
+
:param device_info: Device information from the hex file.
|
|
102
|
+
:returns: The filesystem end address.
|
|
103
|
+
"""
|
|
104
|
+
end_address = device_info.fs_end_address
|
|
105
|
+
|
|
106
|
+
# In V1 the magnetometer calibration data takes one flash page
|
|
107
|
+
if device_info.device_version == DeviceVersion.V1:
|
|
108
|
+
end_address -= device_info.flash_page_size
|
|
109
|
+
|
|
110
|
+
return end_address
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_last_page_address(device_info: DeviceInfo) -> int:
|
|
114
|
+
"""Get the address of the last filesystem page (persistent page).
|
|
115
|
+
|
|
116
|
+
The last page is reserved for persistent data and not used for files.
|
|
117
|
+
|
|
118
|
+
:param device_info: Device information from the hex file.
|
|
119
|
+
:returns: The address where the last (persistent) page starts.
|
|
120
|
+
"""
|
|
121
|
+
return get_fs_end_address(device_info) - device_info.flash_page_size
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# =============================================================================
|
|
125
|
+
# Reading Files
|
|
126
|
+
# =============================================================================
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_chunk(ih: IntelHex, address: int) -> bytes:
|
|
130
|
+
"""Read a full chunk from the Intel Hex at the given address.
|
|
131
|
+
|
|
132
|
+
:param ih: The Intel Hex object.
|
|
133
|
+
:param address: The start address of the chunk.
|
|
134
|
+
:returns: The 128 bytes of the chunk.
|
|
135
|
+
"""
|
|
136
|
+
return bytes(ih[address + i] for i in range(CHUNK_SIZE))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_files_from_hex(ih: IntelHex, device_info: DeviceInfo) -> dict[str, bytes]:
|
|
140
|
+
"""Read all files from the MicroPython filesystem.
|
|
141
|
+
|
|
142
|
+
This scans the filesystem area for file start markers (0xFE), then
|
|
143
|
+
follows the chunk chain to extract the complete file content.
|
|
144
|
+
|
|
145
|
+
:param ih: The Intel Hex object containing MicroPython.
|
|
146
|
+
:param device_info: Device information from the hex file.
|
|
147
|
+
:returns: Dictionary mapping filenames to their content as bytes.
|
|
148
|
+
:raises FilesystemError: If the filesystem structure is corrupted.
|
|
149
|
+
"""
|
|
150
|
+
start_address = get_fs_start_address(device_info)
|
|
151
|
+
end_address = get_last_page_address(device_info)
|
|
152
|
+
|
|
153
|
+
# First pass: collect all used chunks and identify file starts
|
|
154
|
+
used_chunks: dict[int, bytes] = {}
|
|
155
|
+
start_chunk_indexes: list[int] = []
|
|
156
|
+
|
|
157
|
+
chunk_addr = start_address
|
|
158
|
+
chunk_index = 1
|
|
159
|
+
|
|
160
|
+
while chunk_addr < end_address:
|
|
161
|
+
chunk = read_chunk(ih, chunk_addr)
|
|
162
|
+
marker = chunk[0]
|
|
163
|
+
|
|
164
|
+
# Skip unused, freed, and persistent data chunks
|
|
165
|
+
if marker not in (
|
|
166
|
+
ChunkMarker.UNUSED,
|
|
167
|
+
ChunkMarker.FREED,
|
|
168
|
+
ChunkMarker.PERSISTENT_DATA,
|
|
169
|
+
):
|
|
170
|
+
used_chunks[chunk_index] = chunk
|
|
171
|
+
if marker == ChunkMarker.FILE_START:
|
|
172
|
+
start_chunk_indexes.append(chunk_index)
|
|
173
|
+
|
|
174
|
+
chunk_index += 1
|
|
175
|
+
chunk_addr += CHUNK_SIZE
|
|
176
|
+
|
|
177
|
+
# Second pass: follow chunk chains and extract file data
|
|
178
|
+
files: dict[str, bytes] = {}
|
|
179
|
+
seen_filenames: set[str] = set()
|
|
180
|
+
|
|
181
|
+
for start_idx in start_chunk_indexes:
|
|
182
|
+
start_chunk = used_chunks[start_idx]
|
|
183
|
+
|
|
184
|
+
# Parse file header:
|
|
185
|
+
# Byte 0: FILE_START marker (0xFE)
|
|
186
|
+
# Byte 1: End offset in last chunk
|
|
187
|
+
# Byte 2: Filename length
|
|
188
|
+
# Bytes 3+: Filename
|
|
189
|
+
end_offset = start_chunk[1]
|
|
190
|
+
name_len = start_chunk[2]
|
|
191
|
+
filename_bytes = start_chunk[3 : 3 + name_len]
|
|
192
|
+
filename = filename_bytes.decode("utf-8")
|
|
193
|
+
|
|
194
|
+
if filename in seen_filenames:
|
|
195
|
+
raise FilesystemError(f"Found multiple files named: {filename}")
|
|
196
|
+
seen_filenames.add(filename)
|
|
197
|
+
|
|
198
|
+
# Follow chunk chain and collect data
|
|
199
|
+
data = bytearray()
|
|
200
|
+
current_chunk = start_chunk
|
|
201
|
+
current_index = start_idx
|
|
202
|
+
# In first chunk, data starts after header
|
|
203
|
+
chunk_data_start = 3 + name_len
|
|
204
|
+
|
|
205
|
+
# Track iterations to detect infinite loops
|
|
206
|
+
max_iterations = len(used_chunks) + 1
|
|
207
|
+
|
|
208
|
+
while max_iterations > 0:
|
|
209
|
+
max_iterations -= 1
|
|
210
|
+
next_index = current_chunk[CHUNK_SIZE - 1] # Tail byte
|
|
211
|
+
|
|
212
|
+
if next_index == ChunkMarker.UNUSED:
|
|
213
|
+
# This is the last chunk - extract data up to end_offset
|
|
214
|
+
data.extend(current_chunk[chunk_data_start : 1 + end_offset])
|
|
215
|
+
break
|
|
216
|
+
else:
|
|
217
|
+
# Not the last chunk - extract all data bytes
|
|
218
|
+
data.extend(
|
|
219
|
+
current_chunk[chunk_data_start : CHUNK_SIZE - 1]
|
|
220
|
+
) # All but tail
|
|
221
|
+
|
|
222
|
+
# Move to next chunk
|
|
223
|
+
next_chunk = used_chunks.get(next_index)
|
|
224
|
+
if next_chunk is None:
|
|
225
|
+
raise FilesystemError(
|
|
226
|
+
f"Chunk {current_index} points to unused index {next_index}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Verify the back-link
|
|
230
|
+
if next_chunk[0] != current_index:
|
|
231
|
+
raise FilesystemError(
|
|
232
|
+
f"Chunk index {next_index} did not link to "
|
|
233
|
+
f"previous chunk index {current_index}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
current_chunk = next_chunk
|
|
237
|
+
current_index = next_index
|
|
238
|
+
# After first chunk, data starts after the marker byte
|
|
239
|
+
chunk_data_start = CHUNK_MARKER_SIZE
|
|
240
|
+
|
|
241
|
+
if max_iterations <= 0:
|
|
242
|
+
raise FilesystemError("Malformed file chunks did not link correctly")
|
|
243
|
+
|
|
244
|
+
files[filename] = bytes(data)
|
|
245
|
+
|
|
246
|
+
return files
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# =============================================================================
|
|
250
|
+
# Writing Files
|
|
251
|
+
# =============================================================================
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def calculate_file_size(filename: str, content: bytes) -> int:
|
|
255
|
+
"""Calculate the size in bytes a file will occupy in the filesystem.
|
|
256
|
+
|
|
257
|
+
This returns the total number of bytes the file will use, which is
|
|
258
|
+
always a multiple of the chunk size (128 bytes).
|
|
259
|
+
|
|
260
|
+
:param filename: The name of the file.
|
|
261
|
+
:param content: The file content as bytes.
|
|
262
|
+
:returns: Size in bytes (always a multiple of 128).
|
|
263
|
+
"""
|
|
264
|
+
filename_bytes = filename.encode("utf-8") if isinstance(filename, str) else filename
|
|
265
|
+
header_size = 2 + len(filename_bytes) # end_offset + name_len + name
|
|
266
|
+
|
|
267
|
+
# Total data: header + content + trailing 0xFF byte
|
|
268
|
+
total_data = header_size + len(content) + 1
|
|
269
|
+
|
|
270
|
+
chunks_needed = (total_data + CHUNK_DATA_SIZE - 1) // CHUNK_DATA_SIZE
|
|
271
|
+
return chunks_needed * CHUNK_SIZE
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_free_chunks(ih: IntelHex, device_info: DeviceInfo) -> list[int]:
|
|
275
|
+
"""Get a list of free chunk indices in the filesystem.
|
|
276
|
+
|
|
277
|
+
Scans the filesystem area and returns indices of chunks that are
|
|
278
|
+
either unused (0xFF) or freed (0x00).
|
|
279
|
+
|
|
280
|
+
:param ih: The Intel Hex object.
|
|
281
|
+
:param device_info: Device information from the hex file.
|
|
282
|
+
:returns: List of 1-based chunk indices that are free.
|
|
283
|
+
"""
|
|
284
|
+
start_address = get_fs_start_address(device_info)
|
|
285
|
+
end_address = get_last_page_address(device_info)
|
|
286
|
+
|
|
287
|
+
free_chunks: list[int] = []
|
|
288
|
+
chunk_addr = start_address
|
|
289
|
+
chunk_index = 1
|
|
290
|
+
|
|
291
|
+
while chunk_addr < end_address:
|
|
292
|
+
marker = ih[chunk_addr]
|
|
293
|
+
if marker == ChunkMarker.UNUSED or marker == ChunkMarker.FREED:
|
|
294
|
+
free_chunks.append(chunk_index)
|
|
295
|
+
chunk_index += 1
|
|
296
|
+
chunk_addr += CHUNK_SIZE
|
|
297
|
+
|
|
298
|
+
return free_chunks
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def set_persistent_page(ih: IntelHex, device_info: DeviceInfo) -> None:
|
|
302
|
+
"""Set the persistent page marker in the last filesystem page.
|
|
303
|
+
|
|
304
|
+
The last page of the filesystem is reserved for persistent data
|
|
305
|
+
and is marked with a special marker byte.
|
|
306
|
+
|
|
307
|
+
:param ih: The Intel Hex object to modify.
|
|
308
|
+
:param device_info: Device information from the hex file.
|
|
309
|
+
"""
|
|
310
|
+
last_page_addr = get_last_page_address(device_info)
|
|
311
|
+
ih[last_page_addr] = ChunkMarker.PERSISTENT_DATA
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def generate_file_header(filename: str, content: bytes) -> bytes:
|
|
315
|
+
"""Generate the file header bytes for the first chunk.
|
|
316
|
+
|
|
317
|
+
The header contains:
|
|
318
|
+
- Byte 0: end_offset (where data ends in last chunk)
|
|
319
|
+
- Byte 1: filename length
|
|
320
|
+
- Bytes 2+: filename
|
|
321
|
+
|
|
322
|
+
:param filename: The name of the file.
|
|
323
|
+
:param content: The file content.
|
|
324
|
+
:returns: The header bytes to put in the first chunk's data area.
|
|
325
|
+
"""
|
|
326
|
+
filename_bytes = filename.encode("utf-8")
|
|
327
|
+
header_size = 2 + len(filename_bytes) # end_offset + name_len + name
|
|
328
|
+
|
|
329
|
+
# Calculate end_offset: where data ends in the last chunk (within data area)
|
|
330
|
+
# This is (header_size + content_len) % CHUNK_DATA_SIZE
|
|
331
|
+
# But we also add 1 trailing 0xFF byte like MicroPython does
|
|
332
|
+
total_data = header_size + len(content)
|
|
333
|
+
end_offset = total_data % CHUNK_DATA_SIZE
|
|
334
|
+
|
|
335
|
+
header = bytearray(header_size)
|
|
336
|
+
header[0] = end_offset
|
|
337
|
+
header[1] = len(filename_bytes)
|
|
338
|
+
header[2:] = filename_bytes
|
|
339
|
+
|
|
340
|
+
return bytes(header)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def build_file_chunks(
|
|
344
|
+
filename: str, content: bytes, free_chunks: list[int]
|
|
345
|
+
) -> tuple[list[tuple[int, bytes]], int]:
|
|
346
|
+
"""Build the chunk data for a file.
|
|
347
|
+
|
|
348
|
+
Creates all the chunks needed to store the file, using the provided
|
|
349
|
+
free chunk indices.
|
|
350
|
+
|
|
351
|
+
:param filename: The name of the file.
|
|
352
|
+
:param content: The file content.
|
|
353
|
+
:param free_chunks: List of available chunk indices.
|
|
354
|
+
:returns: Tuple of (list of (chunk_index, chunk_bytes), chunks_used)
|
|
355
|
+
:raises InvalidFileError: If the filename is too long or empty.
|
|
356
|
+
:raises StorageFullError: If there aren't enough free chunks.
|
|
357
|
+
"""
|
|
358
|
+
# Validate file
|
|
359
|
+
if not filename:
|
|
360
|
+
raise InvalidFileError("File must have a filename")
|
|
361
|
+
|
|
362
|
+
filename_bytes = filename.encode("utf-8")
|
|
363
|
+
if len(filename_bytes) > MAX_FILENAME_LENGTH:
|
|
364
|
+
raise InvalidFileError(
|
|
365
|
+
f"Filename '{filename}' is too long "
|
|
366
|
+
f"(max {MAX_FILENAME_LENGTH} bytes, got {len(filename_bytes)})"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if len(content) == 0:
|
|
370
|
+
raise InvalidFileError(f"File '{filename}' must have content")
|
|
371
|
+
|
|
372
|
+
# Build the full data stream: header + content + trailing 0xFF
|
|
373
|
+
# MicroPython adds a trailing 0xFF when file fills exactly to chunk boundary
|
|
374
|
+
header = generate_file_header(filename, content)
|
|
375
|
+
fs_data = bytearray(len(header) + len(content) + 1)
|
|
376
|
+
fs_data[: len(header)] = header
|
|
377
|
+
fs_data[len(header) : len(header) + len(content)] = content
|
|
378
|
+
fs_data[-1] = 0xFF
|
|
379
|
+
|
|
380
|
+
# Calculate how many chunks we need
|
|
381
|
+
chunks_needed = (len(fs_data) + CHUNK_DATA_SIZE - 1) // CHUNK_DATA_SIZE
|
|
382
|
+
|
|
383
|
+
if chunks_needed > len(free_chunks):
|
|
384
|
+
raise StorageFullError(
|
|
385
|
+
f"Not enough space for file '{filename}'. "
|
|
386
|
+
f"Need {chunks_needed} chunks, have {len(free_chunks)} free."
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Build the chunks
|
|
390
|
+
result: list[tuple[int, bytes]] = []
|
|
391
|
+
data_index = 0
|
|
392
|
+
chunks_to_use = free_chunks[:chunks_needed]
|
|
393
|
+
|
|
394
|
+
for i, chunk_idx in enumerate(chunks_to_use):
|
|
395
|
+
chunk = bytearray(CHUNK_SIZE)
|
|
396
|
+
# Fill with 0xFF first
|
|
397
|
+
for j in range(CHUNK_SIZE):
|
|
398
|
+
chunk[j] = 0xFF
|
|
399
|
+
|
|
400
|
+
if i == 0:
|
|
401
|
+
# First chunk: FILE_START marker
|
|
402
|
+
chunk[0] = ChunkMarker.FILE_START
|
|
403
|
+
else:
|
|
404
|
+
# Continuation chunk: back-pointer to previous chunk
|
|
405
|
+
chunk[0] = chunks_to_use[i - 1]
|
|
406
|
+
|
|
407
|
+
# Copy data into the chunk's data area (bytes 1-126)
|
|
408
|
+
data_end = min(len(fs_data), data_index + CHUNK_DATA_SIZE)
|
|
409
|
+
data_to_copy = fs_data[data_index:data_end]
|
|
410
|
+
chunk[CHUNK_MARKER_SIZE : CHUNK_MARKER_SIZE + len(data_to_copy)] = data_to_copy
|
|
411
|
+
data_index = data_end
|
|
412
|
+
|
|
413
|
+
# Set tail byte (next chunk index or 0xFF if last)
|
|
414
|
+
if i < len(chunks_to_use) - 1:
|
|
415
|
+
chunk[CHUNK_SIZE - 1] = chunks_to_use[i + 1]
|
|
416
|
+
# else: already 0xFF
|
|
417
|
+
|
|
418
|
+
result.append((chunk_idx, bytes(chunk)))
|
|
419
|
+
|
|
420
|
+
return result, chunks_needed
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def add_files_to_hex(
|
|
424
|
+
ih: IntelHex,
|
|
425
|
+
device_info: DeviceInfo,
|
|
426
|
+
files: dict[str, bytes],
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Write files to the MicroPython filesystem in an Intel Hex.
|
|
429
|
+
|
|
430
|
+
Modifies the Intel Hex object in place to add the files to the
|
|
431
|
+
filesystem region.
|
|
432
|
+
|
|
433
|
+
:param ih: The Intel Hex object to modify.
|
|
434
|
+
:param device_info: Device information from the hex file.
|
|
435
|
+
:param files: Dictionary mapping filenames to their content as bytes.
|
|
436
|
+
:raises InvalidFileError: If any file has invalid name or content.
|
|
437
|
+
:raises StorageFullError: If the files don't fit in the filesystem.
|
|
438
|
+
"""
|
|
439
|
+
if not files:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
fs_start = get_fs_start_address(device_info)
|
|
443
|
+
|
|
444
|
+
# Get all free chunks initially
|
|
445
|
+
free_chunks = get_free_chunks(ih, device_info)
|
|
446
|
+
|
|
447
|
+
if not free_chunks:
|
|
448
|
+
raise StorageFullError("No storage space available in filesystem")
|
|
449
|
+
|
|
450
|
+
# Write each file
|
|
451
|
+
for filename, content in files.items():
|
|
452
|
+
chunks, chunks_used = build_file_chunks(filename, content, free_chunks)
|
|
453
|
+
|
|
454
|
+
# Write chunks to hex
|
|
455
|
+
for chunk_idx, chunk_data in chunks:
|
|
456
|
+
chunk_addr = chunk_index_to_address(chunk_idx, fs_start)
|
|
457
|
+
for i, byte in enumerate(chunk_data):
|
|
458
|
+
ih[chunk_addr + i] = byte
|
|
459
|
+
|
|
460
|
+
# Remove used chunks from free list
|
|
461
|
+
free_chunks = free_chunks[chunks_used:]
|
|
462
|
+
|
|
463
|
+
# Set persistent page marker
|
|
464
|
+
set_persistent_page(ih, device_info)
|