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.
@@ -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