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,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parse Flash Regions Table from MicroPython V2 hex files.
|
|
3
|
+
|
|
4
|
+
The micro:bit flash layout is divided in flash regions, each containing a
|
|
5
|
+
different type of data (Nordic SoftDevice, MicroPython, bootloader, etc).
|
|
6
|
+
One of the regions is dedicated to the micro:bit filesystem, and this info
|
|
7
|
+
is used by this library to add the user files into a MicroPython hex file.
|
|
8
|
+
|
|
9
|
+
The Flash Regions Table stores a data table at the end of the last flash page
|
|
10
|
+
used by the MicroPython runtime.
|
|
11
|
+
|
|
12
|
+
Table format (from end of page, reading backwards):
|
|
13
|
+
```
|
|
14
|
+
| MAGIC_1 (4) | VERSION (2) | TABLE_LEN (2) | REG_COUNT (2) | P_SIZE (2) | MAGIC_2 (4) |
|
|
15
|
+
| Row N: ID (1) | HT (1) | START_PAGE (2) | LENGTH (4) | HASH_DATA (8) |
|
|
16
|
+
| ...
|
|
17
|
+
| Row 1: ID (1) | HT (1) | START_PAGE (2) | LENGTH (4) | HASH_DATA (8) |
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
More information:
|
|
21
|
+
https://github.com/microbit-foundation/micropython-microbit-v2/blob/v2.0.0-beta.3/src/addlayouttable.py
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
from intelhex import IntelHex
|
|
29
|
+
|
|
30
|
+
from micropython_microbit_fs import hex_utils as ihex
|
|
31
|
+
from micropython_microbit_fs.device_info import DEVICE_SPECS, DeviceInfo, DeviceVersion
|
|
32
|
+
|
|
33
|
+
FLASH_REGIONS_MAGIC_1 = 0x597F30FE
|
|
34
|
+
"""First magic value for flash regions table."""
|
|
35
|
+
|
|
36
|
+
FLASH_REGIONS_MAGIC_2 = 0xC1B1D79D
|
|
37
|
+
"""Second magic value for flash regions table."""
|
|
38
|
+
|
|
39
|
+
FLASH_REGIONS_HEADER_SIZE = 8
|
|
40
|
+
"""Size of flash regions table header (2 magic words)."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FlashRegionId:
|
|
44
|
+
"""Flash region identifiers."""
|
|
45
|
+
|
|
46
|
+
SOFTDEVICE = 1
|
|
47
|
+
"""SoftDevice (Bluetooth stack) region."""
|
|
48
|
+
|
|
49
|
+
MICROPYTHON = 2
|
|
50
|
+
"""MicroPython runtime region."""
|
|
51
|
+
|
|
52
|
+
FILESYSTEM = 3
|
|
53
|
+
"""Filesystem region."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Header field sizes (bytes)
|
|
57
|
+
MAGIC_1_SIZE = 4
|
|
58
|
+
VERSION_SIZE = 2
|
|
59
|
+
TABLE_LEN_SIZE = 2
|
|
60
|
+
REG_COUNT_SIZE = 2
|
|
61
|
+
PAGE_SIZE_LOG2_SIZE = 2
|
|
62
|
+
MAGIC_2_SIZE = 4
|
|
63
|
+
|
|
64
|
+
# Header total size
|
|
65
|
+
HEADER_SIZE = (
|
|
66
|
+
MAGIC_1_SIZE
|
|
67
|
+
+ VERSION_SIZE
|
|
68
|
+
+ TABLE_LEN_SIZE
|
|
69
|
+
+ REG_COUNT_SIZE
|
|
70
|
+
+ PAGE_SIZE_LOG2_SIZE
|
|
71
|
+
+ MAGIC_2_SIZE
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Header field offsets from end of page (reading backwards)
|
|
75
|
+
OFFSET_MAGIC_2 = MAGIC_2_SIZE
|
|
76
|
+
OFFSET_PAGE_SIZE_LOG2 = OFFSET_MAGIC_2 + PAGE_SIZE_LOG2_SIZE
|
|
77
|
+
OFFSET_REG_COUNT = OFFSET_PAGE_SIZE_LOG2 + REG_COUNT_SIZE
|
|
78
|
+
OFFSET_TABLE_LEN = OFFSET_REG_COUNT + TABLE_LEN_SIZE
|
|
79
|
+
OFFSET_VERSION = OFFSET_TABLE_LEN + VERSION_SIZE
|
|
80
|
+
OFFSET_MAGIC_1 = OFFSET_VERSION + MAGIC_1_SIZE
|
|
81
|
+
|
|
82
|
+
# Region row field sizes (bytes)
|
|
83
|
+
ROW_ID_SIZE = 1
|
|
84
|
+
ROW_HASH_TYPE_SIZE = 1
|
|
85
|
+
ROW_START_PAGE_SIZE = 2
|
|
86
|
+
ROW_LENGTH_SIZE = 4
|
|
87
|
+
ROW_HASH_DATA_SIZE = 8
|
|
88
|
+
ROW_SIZE = (
|
|
89
|
+
ROW_ID_SIZE
|
|
90
|
+
+ ROW_HASH_TYPE_SIZE
|
|
91
|
+
+ ROW_START_PAGE_SIZE
|
|
92
|
+
+ ROW_LENGTH_SIZE
|
|
93
|
+
+ ROW_HASH_DATA_SIZE
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Region row field offsets from end of row (reading backwards)
|
|
97
|
+
ROW_OFFSET_HASH_DATA = ROW_HASH_DATA_SIZE
|
|
98
|
+
ROW_OFFSET_LENGTH = ROW_OFFSET_HASH_DATA + ROW_LENGTH_SIZE
|
|
99
|
+
ROW_OFFSET_START_PAGE = ROW_OFFSET_LENGTH + ROW_START_PAGE_SIZE
|
|
100
|
+
ROW_OFFSET_HASH_TYPE = ROW_OFFSET_START_PAGE + ROW_HASH_TYPE_SIZE
|
|
101
|
+
ROW_OFFSET_ID = ROW_OFFSET_HASH_TYPE + ROW_ID_SIZE
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class RegionHashType:
|
|
105
|
+
"""Hash type field values in region rows."""
|
|
106
|
+
|
|
107
|
+
EMPTY = 0
|
|
108
|
+
"""The hash data is empty."""
|
|
109
|
+
|
|
110
|
+
DATA = 1
|
|
111
|
+
"""The full hash data field is used as a hash of the region in flash."""
|
|
112
|
+
|
|
113
|
+
POINTER = 2
|
|
114
|
+
"""The 4 LSB bytes of the hash data field are used as a pointer."""
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class RegionRow:
|
|
119
|
+
"""Data from a region row in the Flash Regions Table."""
|
|
120
|
+
|
|
121
|
+
id: int
|
|
122
|
+
start_page: int
|
|
123
|
+
length_bytes: int
|
|
124
|
+
hash_type: int
|
|
125
|
+
hash_data: int
|
|
126
|
+
hash_pointer_data: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class TableHeader:
|
|
131
|
+
"""Flash Regions Table header data."""
|
|
132
|
+
|
|
133
|
+
page_size_log2: int
|
|
134
|
+
page_size: int
|
|
135
|
+
region_count: int
|
|
136
|
+
table_length: int
|
|
137
|
+
version: int
|
|
138
|
+
start_address: int
|
|
139
|
+
end_address: int
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _find_table_header(ih: IntelHex, page_size: int) -> TableHeader | None:
|
|
143
|
+
"""
|
|
144
|
+
Search for the Flash Regions Table header by scanning page boundaries.
|
|
145
|
+
|
|
146
|
+
:param ih: IntelHex object containing the hex data.
|
|
147
|
+
:param page_size: Flash page size to scan (default: 4096 for V2).
|
|
148
|
+
:returns: TableHeader if found, None otherwise.
|
|
149
|
+
"""
|
|
150
|
+
# Get the address range in the hex file
|
|
151
|
+
min_addr = ih.minaddr()
|
|
152
|
+
max_addr = ih.maxaddr()
|
|
153
|
+
|
|
154
|
+
# Check if hex file is empty or has no valid data
|
|
155
|
+
if min_addr is None or max_addr is None:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Scan pages from end to beginning, looking for magic values
|
|
159
|
+
# Start from the first page boundary after min_addr
|
|
160
|
+
start_page = (min_addr // page_size) * page_size
|
|
161
|
+
end_page = ((max_addr // page_size) + 1) * page_size
|
|
162
|
+
|
|
163
|
+
for page_start in range(start_page, end_page, page_size):
|
|
164
|
+
page_end = page_start + page_size
|
|
165
|
+
|
|
166
|
+
# Read magic values from end of page
|
|
167
|
+
magic_1_addr = page_end - OFFSET_MAGIC_1
|
|
168
|
+
magic_2_addr = page_end - OFFSET_MAGIC_2
|
|
169
|
+
|
|
170
|
+
# Check if addresses are in range
|
|
171
|
+
if magic_1_addr < min_addr or magic_2_addr + 4 > max_addr + 1:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
magic_1 = ihex.read_uint32(ih, magic_1_addr)
|
|
175
|
+
magic_2 = ihex.read_uint32(ih, magic_2_addr)
|
|
176
|
+
|
|
177
|
+
if magic_1 == FLASH_REGIONS_MAGIC_1 and magic_2 == FLASH_REGIONS_MAGIC_2:
|
|
178
|
+
# Found the table header
|
|
179
|
+
version = ihex.read_uint16(ih, page_end - OFFSET_VERSION)
|
|
180
|
+
table_length = ihex.read_uint16(ih, page_end - OFFSET_TABLE_LEN)
|
|
181
|
+
region_count = ihex.read_uint16(ih, page_end - OFFSET_REG_COUNT)
|
|
182
|
+
page_size_log2 = ihex.read_uint16(ih, page_end - OFFSET_PAGE_SIZE_LOG2)
|
|
183
|
+
|
|
184
|
+
return TableHeader(
|
|
185
|
+
page_size_log2=page_size_log2,
|
|
186
|
+
page_size=2**page_size_log2,
|
|
187
|
+
region_count=region_count,
|
|
188
|
+
table_length=table_length,
|
|
189
|
+
version=version,
|
|
190
|
+
start_address=page_end - OFFSET_MAGIC_1,
|
|
191
|
+
end_address=page_end,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _read_region_row(ih: IntelHex, row_end_address: int) -> RegionRow:
|
|
198
|
+
"""
|
|
199
|
+
Read a region row from the Flash Regions Table.
|
|
200
|
+
|
|
201
|
+
:param ih: IntelHex object.
|
|
202
|
+
:param row_end_address: Address where this row ends.
|
|
203
|
+
:returns: RegionRow with the parsed data.
|
|
204
|
+
"""
|
|
205
|
+
region_id = ihex.read_uint8(ih, row_end_address - ROW_OFFSET_ID)
|
|
206
|
+
hash_type = ihex.read_uint8(ih, row_end_address - ROW_OFFSET_HASH_TYPE)
|
|
207
|
+
start_page = ihex.read_uint16(ih, row_end_address - ROW_OFFSET_START_PAGE)
|
|
208
|
+
length_bytes = ihex.read_uint32(ih, row_end_address - ROW_OFFSET_LENGTH)
|
|
209
|
+
|
|
210
|
+
# Read hash data (8 bytes, but we only need the lower 4 bytes for pointer)
|
|
211
|
+
hash_data_addr = row_end_address - ROW_OFFSET_HASH_DATA
|
|
212
|
+
hash_data = ihex.read_uint32(ih, hash_data_addr)
|
|
213
|
+
|
|
214
|
+
# If hash type is pointer, read the string it points to
|
|
215
|
+
hash_pointer_data = ""
|
|
216
|
+
if hash_type == RegionHashType.POINTER:
|
|
217
|
+
hash_pointer_data = ihex.read_string(ih, hash_data)
|
|
218
|
+
|
|
219
|
+
return RegionRow(
|
|
220
|
+
id=region_id,
|
|
221
|
+
start_page=start_page,
|
|
222
|
+
length_bytes=length_bytes,
|
|
223
|
+
hash_type=hash_type,
|
|
224
|
+
hash_data=hash_data,
|
|
225
|
+
hash_pointer_data=hash_pointer_data,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_device_info_from_flash_regions(ih: IntelHex) -> DeviceInfo | None:
|
|
230
|
+
"""
|
|
231
|
+
Extract DeviceInfo from Flash Regions Table in an IntelHex object.
|
|
232
|
+
|
|
233
|
+
This is the primary detection method for micro:bit V2 MicroPython.
|
|
234
|
+
|
|
235
|
+
:param ih: IntelHex object containing the hex data.
|
|
236
|
+
:returns: DeviceInfo if valid Flash Regions Table is found, None otherwise.
|
|
237
|
+
"""
|
|
238
|
+
header = _find_table_header(ih, DEVICE_SPECS[DeviceVersion.V2].page_size)
|
|
239
|
+
if header is None:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
# Read all region rows
|
|
243
|
+
regions: dict[int, RegionRow] = {}
|
|
244
|
+
for i in range(header.region_count):
|
|
245
|
+
row_end_address = header.start_address - i * ROW_SIZE
|
|
246
|
+
row = _read_region_row(ih, row_end_address)
|
|
247
|
+
regions[row.id] = row
|
|
248
|
+
|
|
249
|
+
# Check for required regions
|
|
250
|
+
if FlashRegionId.MICROPYTHON not in regions:
|
|
251
|
+
return None
|
|
252
|
+
if FlashRegionId.FILESYSTEM not in regions:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
micropython_region = regions[FlashRegionId.MICROPYTHON]
|
|
256
|
+
fs_region = regions[FlashRegionId.FILESYSTEM]
|
|
257
|
+
|
|
258
|
+
# Calculate addresses
|
|
259
|
+
runtime_start = 0 # Always starts at 0
|
|
260
|
+
runtime_end = header.end_address # Table is at end of MicroPython region
|
|
261
|
+
fs_start = fs_region.start_page * header.page_size
|
|
262
|
+
fs_end = fs_start + fs_region.length_bytes
|
|
263
|
+
version = micropython_region.hash_pointer_data
|
|
264
|
+
|
|
265
|
+
return DeviceInfo(
|
|
266
|
+
flash_page_size=header.page_size,
|
|
267
|
+
flash_size=DEVICE_SPECS[DeviceVersion.V2].flash_size,
|
|
268
|
+
flash_start_address=DEVICE_SPECS[DeviceVersion.V2].flash_start_address,
|
|
269
|
+
flash_end_address=DEVICE_SPECS[DeviceVersion.V2].flash_size,
|
|
270
|
+
runtime_start_address=runtime_start,
|
|
271
|
+
runtime_end_address=runtime_end,
|
|
272
|
+
fs_start_address=fs_start,
|
|
273
|
+
fs_end_address=fs_end,
|
|
274
|
+
micropython_version=version,
|
|
275
|
+
device_version=DeviceVersion.V2,
|
|
276
|
+
)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Intel Hex utilities for reading data from hex files."""
|
|
3
|
+
|
|
4
|
+
from io import StringIO
|
|
5
|
+
|
|
6
|
+
from intelhex import IntelHex
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_hex(hex_data: str) -> IntelHex:
|
|
10
|
+
"""
|
|
11
|
+
Load Intel Hex data from a string.
|
|
12
|
+
|
|
13
|
+
:param hex_data: Intel Hex file content as a string.
|
|
14
|
+
:returns: IntelHex object for accessing the data.
|
|
15
|
+
"""
|
|
16
|
+
ih = IntelHex()
|
|
17
|
+
ih.loadhex(StringIO(hex_data))
|
|
18
|
+
return ih
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hex_to_string(ih: IntelHex) -> str:
|
|
22
|
+
"""
|
|
23
|
+
Convert an IntelHex object back to a hex string.
|
|
24
|
+
|
|
25
|
+
:param ih: IntelHex object.
|
|
26
|
+
:returns: Intel Hex file content as a string.
|
|
27
|
+
"""
|
|
28
|
+
output = StringIO()
|
|
29
|
+
ih.write_hex_file(output)
|
|
30
|
+
return output.getvalue()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def read_uint8(ih: IntelHex, address: int) -> int:
|
|
34
|
+
"""
|
|
35
|
+
Read an unsigned 8-bit integer from the hex data.
|
|
36
|
+
|
|
37
|
+
:param ih: IntelHex object.
|
|
38
|
+
:param address: Address to read from.
|
|
39
|
+
:returns: The byte value at the address.
|
|
40
|
+
"""
|
|
41
|
+
return int(ih[address])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_uint16(ih: IntelHex, address: int, little_endian: bool = True) -> int:
|
|
45
|
+
"""
|
|
46
|
+
Read an unsigned 16-bit integer from the hex data.
|
|
47
|
+
|
|
48
|
+
:param ih: IntelHex object.
|
|
49
|
+
:param address: Address to read from.
|
|
50
|
+
:param little_endian: If True, use little-endian byte order (default: True).
|
|
51
|
+
:returns: The 16-bit value at the address.
|
|
52
|
+
"""
|
|
53
|
+
if little_endian:
|
|
54
|
+
return int(ih[address] | (ih[address + 1] << 8))
|
|
55
|
+
else:
|
|
56
|
+
return int((ih[address] << 8) | ih[address + 1])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_uint32(ih: IntelHex, address: int, little_endian: bool = True) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Read an unsigned 32-bit integer from the hex data.
|
|
62
|
+
|
|
63
|
+
:param ih: IntelHex object.
|
|
64
|
+
:param address: Address to read from.
|
|
65
|
+
:param little_endian: If True, use little-endian byte order (default: True).
|
|
66
|
+
:returns: The 32-bit value at the address.
|
|
67
|
+
"""
|
|
68
|
+
if little_endian:
|
|
69
|
+
return int(
|
|
70
|
+
ih[address]
|
|
71
|
+
| (ih[address + 1] << 8)
|
|
72
|
+
| (ih[address + 2] << 16)
|
|
73
|
+
| (ih[address + 3] << 24)
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
return int(
|
|
77
|
+
(ih[address] << 24)
|
|
78
|
+
| (ih[address + 1] << 16)
|
|
79
|
+
| (ih[address + 2] << 8)
|
|
80
|
+
| ih[address + 3]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_bytes(ih: IntelHex, address: int, length: int) -> bytes:
|
|
85
|
+
"""
|
|
86
|
+
Read a sequence of bytes from the hex data.
|
|
87
|
+
|
|
88
|
+
:param ih: IntelHex object.
|
|
89
|
+
:param address: Start address to read from.
|
|
90
|
+
:param length: Number of bytes to read.
|
|
91
|
+
:returns: The bytes at the address range.
|
|
92
|
+
"""
|
|
93
|
+
return bytes(ih[address + i] for i in range(length))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def read_string(ih: IntelHex, address: int, max_length: int = 256) -> str:
|
|
97
|
+
"""
|
|
98
|
+
Read a null-terminated string from the hex data.
|
|
99
|
+
|
|
100
|
+
:param ih: IntelHex object.
|
|
101
|
+
:param address: Start address to read from.
|
|
102
|
+
:param max_length: Maximum length to read (default 256).
|
|
103
|
+
:returns: The string at the address (decoded as UTF-8).
|
|
104
|
+
"""
|
|
105
|
+
chars = []
|
|
106
|
+
for i in range(max_length):
|
|
107
|
+
byte = ih[address + i]
|
|
108
|
+
if byte == 0:
|
|
109
|
+
break
|
|
110
|
+
chars.append(byte)
|
|
111
|
+
return bytes(chars).decode("utf-8", errors="replace")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def has_data_at(ih: IntelHex, address: int, length: int = 1) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if the hex file has data at the specified address range.
|
|
117
|
+
|
|
118
|
+
The intelhex library returns 0xFF for addresses without data,
|
|
119
|
+
so we check if the address is within the actual data range.
|
|
120
|
+
|
|
121
|
+
:param ih: IntelHex object.
|
|
122
|
+
:param address: Start address to check.
|
|
123
|
+
:param length: Number of bytes to check (default 1).
|
|
124
|
+
:returns: True if there is data at all addresses in the range.
|
|
125
|
+
"""
|
|
126
|
+
if not ih.minaddr() <= address <= ih.maxaddr():
|
|
127
|
+
return False
|
|
128
|
+
if not ih.minaddr() <= address + length - 1 <= ih.maxaddr():
|
|
129
|
+
return False
|
|
130
|
+
# Check if any address in range is in the actual data
|
|
131
|
+
return any(addr in ih.addresses() for addr in range(address, address + length))
|
|
Binary file
|
|
Binary file
|