idftool 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.
- idftool/__init__.py +0 -0
- idftool/__main__.py +920 -0
- idftool-0.1.0.dist-info/METADATA +293 -0
- idftool-0.1.0.dist-info/RECORD +8 -0
- idftool-0.1.0.dist-info/WHEEL +5 -0
- idftool-0.1.0.dist-info/entry_points.txt +2 -0
- idftool-0.1.0.dist-info/licenses/LICENSE.md +201 -0
- idftool-0.1.0.dist-info/top_level.txt +1 -0
idftool/__main__.py
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os.path
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional, Literal
|
|
7
|
+
from zipfile import ZipFile
|
|
8
|
+
|
|
9
|
+
from esptool import ESPLoader, CHIP_DEFS, flash_size_bytes
|
|
10
|
+
from serial import SerialException
|
|
11
|
+
from serial.tools import list_ports
|
|
12
|
+
from serial.tools.list_ports_common import ListPortInfo
|
|
13
|
+
|
|
14
|
+
from esp_idf_defs import ImageMetadata, ChipId
|
|
15
|
+
from esp_idf_defs.otadata import OtaDataParameters, OtaDataSelectEntry
|
|
16
|
+
from esp_idf_defs.partitions import PARTITION_TABLE_SIZE, PARTITION_TABLE_OFFSET, PartitionTable, print_partition_table, \
|
|
17
|
+
PartitionDefinition, BOOTLOADER_TYPE, SUBTYPES, PARTITION_TABLE_TYPE, APP_TYPE, NUM_PARTITION_SUBTYPE_APP_OTA, \
|
|
18
|
+
DATA_TYPE
|
|
19
|
+
from esptool.cmds import detect_chip, read_flash, write_flash, erase_region, merge_bin, erase_flash, detect_flash_size
|
|
20
|
+
|
|
21
|
+
PARTITION_SLICE_REGEX = re.compile(r"^(?P<partition>.+?)(?:\[(?:(?P<start>[-+]?(?:0x)?[0-9A-Fa-f]+)?(?::(?P<stop>[-+]?(?:0x)?[0-9A-Fa-f]+)?)?)?])?$")
|
|
22
|
+
PARTITION_OFFSET_REGEX = re.compile(r"^(?P<partition>.+?)(?:\[(?:(?P<start>[-+]?(?:0x)?[0-9A-Fa-f]+)?)?])?$")
|
|
23
|
+
|
|
24
|
+
# TODO: Use esptool version when merged
|
|
25
|
+
def get_port_list() -> list[str]:
|
|
26
|
+
"""Get the list of serial ports names with optional filters.
|
|
27
|
+
|
|
28
|
+
For backwards compatibility, this function returns a list of port names.
|
|
29
|
+
"""
|
|
30
|
+
return [port.device for port in _get_port_list()]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_port_list() -> list[ListPortInfo]:
|
|
34
|
+
ports = []
|
|
35
|
+
for port in list_ports.comports():
|
|
36
|
+
if sys.platform == "darwin" and port.device.endswith(
|
|
37
|
+
("Bluetooth-Incoming-Port", "wlan-debug", "cu.debug-console")
|
|
38
|
+
):
|
|
39
|
+
continue
|
|
40
|
+
ports.append(port)
|
|
41
|
+
|
|
42
|
+
# Constants for sorting optimization
|
|
43
|
+
ESPRESSIF_VID = 0x303A
|
|
44
|
+
LINUX_DEVICE_PATTERNS = ("ttyUSB", "ttyACM")
|
|
45
|
+
MACOS_DEVICE_PATTERNS = ("usbserial", "usbmodem")
|
|
46
|
+
|
|
47
|
+
def _port_sort_key_linux(port_info: ListPortInfo) -> tuple[int, str]:
|
|
48
|
+
if port_info.vid == ESPRESSIF_VID:
|
|
49
|
+
return (3, port_info.device)
|
|
50
|
+
|
|
51
|
+
if any(pattern in port_info.device for pattern in LINUX_DEVICE_PATTERNS):
|
|
52
|
+
return (2, port_info.device)
|
|
53
|
+
|
|
54
|
+
return (1, port_info.device)
|
|
55
|
+
|
|
56
|
+
def _port_sort_key_macos(port_info: ListPortInfo) -> tuple[int, str]:
|
|
57
|
+
if port_info.vid == ESPRESSIF_VID:
|
|
58
|
+
return (3, port_info.device)
|
|
59
|
+
|
|
60
|
+
if any(pattern in port_info.device for pattern in MACOS_DEVICE_PATTERNS):
|
|
61
|
+
return (2, port_info.device)
|
|
62
|
+
|
|
63
|
+
return (1, port_info.device)
|
|
64
|
+
|
|
65
|
+
def _port_sort_key_windows(port_info: ListPortInfo) -> tuple[int, str]:
|
|
66
|
+
if port_info.vid == ESPRESSIF_VID:
|
|
67
|
+
return (2, port_info.device)
|
|
68
|
+
|
|
69
|
+
return (1, port_info.device)
|
|
70
|
+
|
|
71
|
+
if sys.platform == "win32":
|
|
72
|
+
key_func = _port_sort_key_windows
|
|
73
|
+
elif sys.platform == "darwin":
|
|
74
|
+
key_func = _port_sort_key_macos
|
|
75
|
+
else:
|
|
76
|
+
key_func = _port_sort_key_linux
|
|
77
|
+
|
|
78
|
+
sorted_port_info = sorted(ports, key=key_func)
|
|
79
|
+
return sorted_port_info
|
|
80
|
+
|
|
81
|
+
def parse_bootloader_offset(x):
|
|
82
|
+
try:
|
|
83
|
+
return int(x, 0)
|
|
84
|
+
except ValueError:
|
|
85
|
+
if CHIP_DEFS[x] is None:
|
|
86
|
+
raise argparse.ArgumentTypeError(f"Invalid bootloader offset or chip name: {x}")
|
|
87
|
+
return CHIP_DEFS[x].BOOTLOADER_FLASH_OFFSET
|
|
88
|
+
|
|
89
|
+
def auto_int(x):
|
|
90
|
+
return int(x, 0)
|
|
91
|
+
|
|
92
|
+
def get_esp(port: str | None, baud: int) -> ESPLoader:
|
|
93
|
+
esp: ESPLoader | None = None
|
|
94
|
+
if port:
|
|
95
|
+
esp = detect_chip(port, baud=baud)
|
|
96
|
+
else:
|
|
97
|
+
ports = get_port_list()
|
|
98
|
+
for port in ports:
|
|
99
|
+
try:
|
|
100
|
+
print(f"Serial port {port} (baud={baud})")
|
|
101
|
+
esp = detect_chip(port, baud=baud)
|
|
102
|
+
break
|
|
103
|
+
except RuntimeError:
|
|
104
|
+
pass
|
|
105
|
+
if not esp:
|
|
106
|
+
raise RuntimeError("No ESP found")
|
|
107
|
+
return esp.run_stub()
|
|
108
|
+
|
|
109
|
+
def get_partition(
|
|
110
|
+
partition_table: PartitionTable,
|
|
111
|
+
partition_table_entry: PartitionDefinition,
|
|
112
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
113
|
+
label: str
|
|
114
|
+
) -> PartitionDefinition:
|
|
115
|
+
try:
|
|
116
|
+
try:
|
|
117
|
+
address = int(label, 0)
|
|
118
|
+
return next(
|
|
119
|
+
(p for p in partition_table if p.offset == address),
|
|
120
|
+
)
|
|
121
|
+
except ValueError:
|
|
122
|
+
pass
|
|
123
|
+
return partition_table[label]
|
|
124
|
+
except ValueError:
|
|
125
|
+
if label == partition_table_entry.name:
|
|
126
|
+
return partition_table_entry
|
|
127
|
+
elif bootloader_entry and label == bootloader_entry.name:
|
|
128
|
+
return bootloader_entry
|
|
129
|
+
else:
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
def get_partition_slice(
|
|
133
|
+
partition_table: PartitionTable,
|
|
134
|
+
partition_table_entry: PartitionDefinition,
|
|
135
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
136
|
+
s: str
|
|
137
|
+
) -> tuple[PartitionDefinition, int, int]:
|
|
138
|
+
match = PARTITION_SLICE_REGEX.fullmatch(s)
|
|
139
|
+
if not match:
|
|
140
|
+
raise ValueError(f"Invalid partition slice format: {s}")
|
|
141
|
+
|
|
142
|
+
partition_name = match.group("partition")
|
|
143
|
+
start_str = match.group("start")
|
|
144
|
+
stop_str = match.group("stop")
|
|
145
|
+
|
|
146
|
+
partition = get_partition(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
147
|
+
|
|
148
|
+
start = int(start_str, 0) if start_str else 0
|
|
149
|
+
if start < 0:
|
|
150
|
+
start += partition.size
|
|
151
|
+
|
|
152
|
+
if stop_str is None:
|
|
153
|
+
stop = partition.size
|
|
154
|
+
else:
|
|
155
|
+
stop = int(stop_str, 0)
|
|
156
|
+
if stop_str[0] == '+':
|
|
157
|
+
stop = start + stop
|
|
158
|
+
if stop < 0:
|
|
159
|
+
stop += partition.size
|
|
160
|
+
|
|
161
|
+
if start < 0 or stop < 0 or start > partition.size or stop > partition.size or start > stop:
|
|
162
|
+
raise ValueError(f"Invalid slice range [{start:#x}:{stop:#x}] for partition {partition_name} of size {partition.size:#x}")
|
|
163
|
+
|
|
164
|
+
return partition, partition.offset + start, stop - start
|
|
165
|
+
|
|
166
|
+
def get_partition_address(
|
|
167
|
+
partition_table: PartitionTable,
|
|
168
|
+
partition_table_entry: PartitionDefinition,
|
|
169
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
170
|
+
s: str
|
|
171
|
+
) -> tuple[PartitionDefinition, int]:
|
|
172
|
+
match = PARTITION_OFFSET_REGEX.fullmatch(s)
|
|
173
|
+
if not match:
|
|
174
|
+
raise ValueError(f"Invalid partition offset format: {s}")
|
|
175
|
+
|
|
176
|
+
partition_name = match.group("partition")
|
|
177
|
+
start_str = match.group("start")
|
|
178
|
+
|
|
179
|
+
partition = get_partition(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
180
|
+
|
|
181
|
+
start = int(start_str, 0) if start_str else 0
|
|
182
|
+
if start < 0:
|
|
183
|
+
start += partition.size
|
|
184
|
+
|
|
185
|
+
if start < 0 or start > partition.size:
|
|
186
|
+
raise ValueError(f"Invalid offset [{start:#x}] for partition {partition_name} of size {partition.size:#x}")
|
|
187
|
+
|
|
188
|
+
return partition, partition.offset + start
|
|
189
|
+
|
|
190
|
+
def hexdump(data: bytes, start = 0, width = 16):
|
|
191
|
+
def to_printable_ascii(byte):
|
|
192
|
+
return chr(byte) if 32 <= byte <= 126 else "."
|
|
193
|
+
|
|
194
|
+
# If start is not aligned, print an empty row with padding
|
|
195
|
+
misalignment = start % width
|
|
196
|
+
start -= misalignment
|
|
197
|
+
|
|
198
|
+
offset = 0
|
|
199
|
+
while offset < len(data):
|
|
200
|
+
chunk = data[offset : offset + width - misalignment]
|
|
201
|
+
hex_values = "-- " * misalignment + " ".join(f"{byte:02x}" for byte in chunk) + " " + "-- " * (width - len(chunk) - misalignment)
|
|
202
|
+
ascii_values = "-" * misalignment + "".join(to_printable_ascii(byte) for byte in chunk) + "-" * (width - len(chunk) - misalignment)
|
|
203
|
+
print(f"0x{start + offset:08x} {hex_values} |{ascii_values}|")
|
|
204
|
+
offset += width - misalignment
|
|
205
|
+
misalignment = 0
|
|
206
|
+
|
|
207
|
+
def read_otadata(esp: ESPLoader, partition_table: PartitionTable) -> tuple[PartitionDefinition, OtaDataParameters]:
|
|
208
|
+
ota_app_count = len(list(
|
|
209
|
+
part for part in partition_table if
|
|
210
|
+
part.type == APP_TYPE and SUBTYPES[APP_TYPE]['ota_0'] <= part.subtype <=
|
|
211
|
+
SUBTYPES[APP_TYPE]['ota_15'] + NUM_PARTITION_SUBTYPE_APP_OTA)
|
|
212
|
+
)
|
|
213
|
+
if ota_app_count == 0:
|
|
214
|
+
raise ValueError("No OTA partitions found") # TODO Better error type
|
|
215
|
+
|
|
216
|
+
otadata_partition = next(
|
|
217
|
+
(p for p in partition_table if p.type == DATA_TYPE and p.subtype == SUBTYPES[DATA_TYPE]['ota']),
|
|
218
|
+
None
|
|
219
|
+
)
|
|
220
|
+
if not otadata_partition:
|
|
221
|
+
raise ValueError("No otadata partition found") # TODO Better error type
|
|
222
|
+
|
|
223
|
+
otadata_a_bytes = esp.read_flash(otadata_partition.offset, OtaDataSelectEntry.SIZE)
|
|
224
|
+
otadata_b_bytes = esp.read_flash(otadata_partition.offset + esp.FLASH_SECTOR_SIZE, OtaDataSelectEntry.SIZE)
|
|
225
|
+
otadata_a = OtaDataSelectEntry.from_bytes(otadata_a_bytes)
|
|
226
|
+
otadata_b = OtaDataSelectEntry.from_bytes(otadata_b_bytes)
|
|
227
|
+
|
|
228
|
+
otadata, a_or_b = OtaDataSelectEntry.select(otadata_a, otadata_b)
|
|
229
|
+
|
|
230
|
+
return otadata_partition, OtaDataParameters(otadata, a_or_b, ota_app_count)
|
|
231
|
+
|
|
232
|
+
def write_otadata(esp: ESPLoader, partition: PartitionDefinition, otadata: OtaDataParameters):
|
|
233
|
+
esp.flash_begin(
|
|
234
|
+
size=OtaDataSelectEntry.SIZE,
|
|
235
|
+
offset=partition.offset + (0 if otadata.a_or_b == 'a' else esp.FLASH_SECTOR_SIZE)
|
|
236
|
+
)
|
|
237
|
+
esp.flash_block(data=otadata.otadata.to_bytes(), seq=0)
|
|
238
|
+
esp.flash_finish()
|
|
239
|
+
|
|
240
|
+
def validate_app_binary(esp: ESPLoader, app_binary: bytes) -> tuple[bytes, ImageMetadata]:
|
|
241
|
+
try:
|
|
242
|
+
image_metadata = ImageMetadata.from_bytes(app_binary, app_required=True)
|
|
243
|
+
except ValueError as e:
|
|
244
|
+
raise RuntimeError(f"Invalid application binary: {e}")
|
|
245
|
+
|
|
246
|
+
if image_metadata.header.chip_id.value != esp.IMAGE_CHIP_ID:
|
|
247
|
+
raise RuntimeError(
|
|
248
|
+
f"Chip ID mismatch: "
|
|
249
|
+
f"attempting to flash {image_metadata.header.chip_id.name} image "
|
|
250
|
+
f"to {ChipId(esp.IMAGE_CHIP_ID).name} device"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return app_binary, image_metadata
|
|
254
|
+
|
|
255
|
+
def load_app_binary(esp: ESPLoader, app_binary_file: str, partition: PartitionDefinition) -> tuple[bytes, ImageMetadata]:
|
|
256
|
+
app_binary_size = os.path.getsize(app_binary_file)
|
|
257
|
+
if app_binary_size > partition.size:
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
f"Application binary size '{app_binary_size}' is greater than partition size {partition.size}")
|
|
260
|
+
|
|
261
|
+
app_binary = open(app_binary_file, 'rb').read()
|
|
262
|
+
return validate_app_binary(esp, app_binary)
|
|
263
|
+
|
|
264
|
+
def command_read(
|
|
265
|
+
esp: ESPLoader,
|
|
266
|
+
partition_table: PartitionTable,
|
|
267
|
+
partition_table_entry: PartitionDefinition,
|
|
268
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
269
|
+
label: str,
|
|
270
|
+
output_file: str
|
|
271
|
+
):
|
|
272
|
+
partition, address, length = get_partition_slice(partition_table, partition_table_entry, bootloader_entry, label)
|
|
273
|
+
print(f"Reading partition {partition.name} (offset={address:#x}, size={length:#x})")
|
|
274
|
+
read_flash(esp, address=address, size=length, output=output_file)
|
|
275
|
+
|
|
276
|
+
def command_write(
|
|
277
|
+
esp: ESPLoader,
|
|
278
|
+
partition_table: PartitionTable,
|
|
279
|
+
partition_table_entry: PartitionDefinition,
|
|
280
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
281
|
+
files: list[str]
|
|
282
|
+
):
|
|
283
|
+
if len(files) % 2 != 0:
|
|
284
|
+
raise ValueError("Files list must contain pairs of partition name and input file")
|
|
285
|
+
|
|
286
|
+
addr_data = []
|
|
287
|
+
for partition_name, input_file in list(zip(files[::2], files[1::2])):
|
|
288
|
+
partition, address = get_partition_address(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
289
|
+
input_file_size = os.path.getsize(input_file)
|
|
290
|
+
if input_file_size > partition.size:
|
|
291
|
+
raise ValueError(f"Input file {input_file} size {input_file_size:#x} exceeds partition {partition.name} size {partition.size:#x}")
|
|
292
|
+
addr_data.append((address, input_file))
|
|
293
|
+
print(f"Writing file {input_file} (size={input_file_size:#x}) to partition {partition.name} (offset={address:#x}, size={partition.size:#x})")
|
|
294
|
+
|
|
295
|
+
write_flash(
|
|
296
|
+
esp=esp,
|
|
297
|
+
addr_data=addr_data,
|
|
298
|
+
flash_size='detect'
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def command_erase(
|
|
302
|
+
esp: ESPLoader,
|
|
303
|
+
partition_table: PartitionTable,
|
|
304
|
+
partition_table_entry: PartitionDefinition,
|
|
305
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
306
|
+
label: str
|
|
307
|
+
):
|
|
308
|
+
partition, address, length = get_partition_slice(partition_table, partition_table_entry, bootloader_entry, label)
|
|
309
|
+
|
|
310
|
+
print(f"Erasing partition {partition.name} (offset={address:#x}, size={length:#x})")
|
|
311
|
+
erase_region(esp, address=address, size=length)
|
|
312
|
+
|
|
313
|
+
def command_view(
|
|
314
|
+
esp: ESPLoader,
|
|
315
|
+
partition_table: PartitionTable,
|
|
316
|
+
partition_table_entry: PartitionDefinition,
|
|
317
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
318
|
+
label: str,
|
|
319
|
+
output: Literal['str', 'hex'],
|
|
320
|
+
width: int
|
|
321
|
+
):
|
|
322
|
+
partition, address, length = get_partition_slice(partition_table, partition_table_entry, bootloader_entry, label)
|
|
323
|
+
|
|
324
|
+
print(f"Viewing partition {partition.name} (offset={address:#x}, size={length:#x})")
|
|
325
|
+
buffer = esp.read_flash(offset=address, length=length)
|
|
326
|
+
if output == 'str':
|
|
327
|
+
print(buffer.decode('utf-8', errors='replace'), flush=True)
|
|
328
|
+
elif output == 'hex':
|
|
329
|
+
hexdump(buffer, address - partition.offset, width)
|
|
330
|
+
else:
|
|
331
|
+
raise ValueError(f"Invalid output type {output}")
|
|
332
|
+
|
|
333
|
+
def command_create_image(
|
|
334
|
+
partition_table: PartitionTable,
|
|
335
|
+
partition_table_entry: PartitionDefinition,
|
|
336
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
337
|
+
files: list[str],
|
|
338
|
+
flash_partition_table: bool,
|
|
339
|
+
output_format: Literal['raw', 'uf2', 'hex'],
|
|
340
|
+
output_file: str
|
|
341
|
+
):
|
|
342
|
+
if len(files) % 2 != 0:
|
|
343
|
+
raise ValueError("Files list must contain pairs of partition name and input file")
|
|
344
|
+
|
|
345
|
+
addr_data = []
|
|
346
|
+
for partition_name, input_file in list(zip(files[::2], files[1::2])):
|
|
347
|
+
partition, address = get_partition_address(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
348
|
+
input_file_size = os.path.getsize(input_file)
|
|
349
|
+
if input_file_size > partition.size:
|
|
350
|
+
raise ValueError(
|
|
351
|
+
f"Input file {input_file} size {input_file_size:#x} exceeds partition {partition.name} size {partition.size:#x}")
|
|
352
|
+
addr_data.append((address, input_file))
|
|
353
|
+
print(f"Merging file {input_file} (size={input_file_size:#x}) to partition {partition.name} (offset={address:#x}, size={partition.size:#x})")
|
|
354
|
+
|
|
355
|
+
# Add partition table if requested
|
|
356
|
+
if flash_partition_table:
|
|
357
|
+
partition_table_binary = partition_table.to_binary()
|
|
358
|
+
addr_data.append((partition_table_entry.offset, partition_table_binary))
|
|
359
|
+
|
|
360
|
+
merge_bin(
|
|
361
|
+
addr_data=addr_data,
|
|
362
|
+
flash_freq="keep",
|
|
363
|
+
flash_mode="keep",
|
|
364
|
+
flash_size="keep",
|
|
365
|
+
chip='esp32', # Does not matter since we are keeping all flash parameters
|
|
366
|
+
output=output_file,
|
|
367
|
+
output_format=output_format,
|
|
368
|
+
partition_table=partition_table
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def command_create_bundle(
|
|
372
|
+
partition_table: PartitionTable,
|
|
373
|
+
partition_table_entry: PartitionDefinition,
|
|
374
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
375
|
+
files: list[str],
|
|
376
|
+
flash_partition_table: bool,
|
|
377
|
+
output_file: str
|
|
378
|
+
):
|
|
379
|
+
if len(files) % 2 != 0:
|
|
380
|
+
raise ValueError("Files list must contain pairs of partition name and input file")
|
|
381
|
+
|
|
382
|
+
with ZipFile(output_file, 'w') as zf:
|
|
383
|
+
# Add specified partition files
|
|
384
|
+
for partition_name, input_file in list(zip(files[::2], files[1::2])):
|
|
385
|
+
partition = get_partition(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
386
|
+
input_file_size = os.path.getsize(input_file)
|
|
387
|
+
if input_file_size > partition.size:
|
|
388
|
+
raise ValueError(
|
|
389
|
+
f"Input file {input_file} size {input_file_size:#x} exceeds partition {partition.name} size {partition.size:#x}")
|
|
390
|
+
zf.write(input_file, arcname=f"{partition.name}.bin")
|
|
391
|
+
print(f"Adding file {input_file} (size={input_file_size:#x}) to bundle as partition {partition.name} (offset={partition.offset:#x}, size={partition.size:#x})")
|
|
392
|
+
|
|
393
|
+
# Add empty files for erased partitions
|
|
394
|
+
# for partition_name in args.erase:
|
|
395
|
+
# partition = get_partition(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
396
|
+
# zf.writestr(f"{partition.name}.bin", bytes(partition.size))
|
|
397
|
+
# print(f"Adding empty file to bundle for erased partition {partition.name} (offset={partition.offset:#x}, size={partition.size:#x})")
|
|
398
|
+
|
|
399
|
+
# Add partition table if requested
|
|
400
|
+
if flash_partition_table:
|
|
401
|
+
zf.writestr("partition_table.csv", partition_table.to_csv())
|
|
402
|
+
print(f"Adding partition table CSV to bundle")
|
|
403
|
+
|
|
404
|
+
def check_write_bundle_has_partition_table(file: str) -> bool:
|
|
405
|
+
with ZipFile(file, 'r') as zf:
|
|
406
|
+
return any(m == 'partition_table.csv' for m in zf.namelist())
|
|
407
|
+
|
|
408
|
+
def command_write_bundle(
|
|
409
|
+
esp: ESPLoader,
|
|
410
|
+
partition_table: PartitionTable,
|
|
411
|
+
partition_table_entry: PartitionDefinition,
|
|
412
|
+
bootloader_entry: Optional[PartitionDefinition],
|
|
413
|
+
input_file: str
|
|
414
|
+
):
|
|
415
|
+
with ZipFile(input_file, 'r') as zf:
|
|
416
|
+
# Flash files
|
|
417
|
+
addr_data = []
|
|
418
|
+
for member in filter(lambda m: m.endswith('.bin'), zf.namelist()):
|
|
419
|
+
partition_name = member[:-4] # Remove .bin extension
|
|
420
|
+
partition = get_partition(partition_table, partition_table_entry, bootloader_entry, partition_name)
|
|
421
|
+
data = zf.read(member)
|
|
422
|
+
if len(data) > partition.size:
|
|
423
|
+
raise ValueError(f"Input file {member.name} size {len(data):#x} exceeds partition {partition.name} size {partition.size:#x}")
|
|
424
|
+
|
|
425
|
+
if partition.type == APP_TYPE:
|
|
426
|
+
validate_app_binary(esp, data)
|
|
427
|
+
|
|
428
|
+
print(f"Writing partition {partition.name} (offset={partition.offset:#x}, size={partition.size:#x}) from bundle")
|
|
429
|
+
addr_data.append((partition.offset, data))
|
|
430
|
+
|
|
431
|
+
# Flash partition table if present
|
|
432
|
+
if any(m == 'partition_table.csv' for m in zf.namelist()):
|
|
433
|
+
partition = next(
|
|
434
|
+
(e for e in partition_table if e.type == PARTITION_TABLE_TYPE and e.subtype == SUBTYPES[e.type]['primary'])
|
|
435
|
+
)
|
|
436
|
+
print(f"Writing partition table (offset={partition.offset:#x}, size={partition.size:#x}) from bundle")
|
|
437
|
+
addr_data.append((partition.offset, partition_table.to_binary()))
|
|
438
|
+
|
|
439
|
+
# Perform write
|
|
440
|
+
write_flash(
|
|
441
|
+
esp=esp,
|
|
442
|
+
addr_data=addr_data,
|
|
443
|
+
flash_size='detect',
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
def command_get_boot(esp: ESPLoader, partition_table: PartitionTable):
|
|
447
|
+
_, otadata = read_otadata(esp, partition_table)
|
|
448
|
+
|
|
449
|
+
if otadata.slot is None:
|
|
450
|
+
print("OTA slot not set")
|
|
451
|
+
else:
|
|
452
|
+
print(f"OTA slot 'ota_{otadata.slot}' (seq={otadata.otadata.seq}, state={otadata.otadata.ota_state})")
|
|
453
|
+
|
|
454
|
+
def command_set_boot(esp: ESPLoader, partition_table: PartitionTable, label: str):
|
|
455
|
+
otadata_partition, otadata = read_otadata(esp, partition_table)
|
|
456
|
+
|
|
457
|
+
partition = partition_table[label]
|
|
458
|
+
if partition.type != APP_TYPE:
|
|
459
|
+
raise ValueError(f"Partition {label} is not an app partition")
|
|
460
|
+
if partition.subtype < SUBTYPES[APP_TYPE]['ota_0'] or partition.subtype > SUBTYPES[APP_TYPE]['ota_15']:
|
|
461
|
+
raise ValueError(f"Partition {label} is not an OTA partition")
|
|
462
|
+
ota_slot = partition.subtype & 0x0F
|
|
463
|
+
|
|
464
|
+
print(f"Setting boot partition to '{partition.name}'...")
|
|
465
|
+
otadata = otadata.incremented_and_swapped(ota_slot)
|
|
466
|
+
write_otadata(esp, otadata_partition, otadata)
|
|
467
|
+
|
|
468
|
+
def command_clear_boot(esp: ESPLoader, partition_table: PartitionTable):
|
|
469
|
+
otadata_partition = next(
|
|
470
|
+
(p for p in partition_table if p.type == DATA_TYPE and p.subtype == SUBTYPES[DATA_TYPE]['ota']),
|
|
471
|
+
None
|
|
472
|
+
)
|
|
473
|
+
if not otadata_partition:
|
|
474
|
+
raise ValueError("No otadata partition found") # TODO Better error type
|
|
475
|
+
|
|
476
|
+
print("Clearing boot partition...")
|
|
477
|
+
esp.erase_region(offset=otadata_partition.offset, size=otadata_partition.size)
|
|
478
|
+
|
|
479
|
+
def command_ota(esp: ESPLoader, partition_table: PartitionTable, app_binary_file: str):
|
|
480
|
+
otadata_partition, otadata = read_otadata(esp, partition_table)
|
|
481
|
+
|
|
482
|
+
partition: PartitionDefinition
|
|
483
|
+
next_slot = otadata.next_slot
|
|
484
|
+
partition = next(
|
|
485
|
+
(p for p in partition_table if p.type == APP_TYPE and p.subtype == SUBTYPES[APP_TYPE]['ota_0'] + next_slot),
|
|
486
|
+
None
|
|
487
|
+
)
|
|
488
|
+
if not partition:
|
|
489
|
+
raise ValueError(f"Partition ota_{next_slot} not found")
|
|
490
|
+
|
|
491
|
+
app_binary, image_metadata = load_app_binary(esp, app_binary_file, partition)
|
|
492
|
+
|
|
493
|
+
print(f"Writing '{image_metadata.app_description.title}' to partition '{partition.name}'...")
|
|
494
|
+
write_flash(esp=esp, addr_data=[(partition.offset, app_binary_file)])
|
|
495
|
+
|
|
496
|
+
print(f"Setting boot partition to 'ota_{next_slot}'...")
|
|
497
|
+
otadata = otadata.incremented_and_swapped(next_slot)
|
|
498
|
+
write_otadata(esp, otadata_partition, otadata)
|
|
499
|
+
|
|
500
|
+
def command_factory(esp: ESPLoader, partition_table: PartitionTable, app_binary_file: str):
|
|
501
|
+
partition = next(
|
|
502
|
+
(part for part in partition_table if part.type == APP_TYPE and part.subtype == SUBTYPES[APP_TYPE]['factory']),
|
|
503
|
+
None
|
|
504
|
+
)
|
|
505
|
+
if not partition:
|
|
506
|
+
partition = next(
|
|
507
|
+
(part for part in partition_table if part.type == APP_TYPE and part.subtype == SUBTYPES[APP_TYPE]['ota_0']),
|
|
508
|
+
None
|
|
509
|
+
)
|
|
510
|
+
if not partition:
|
|
511
|
+
raise ValueError("No factory or OTA partition found")
|
|
512
|
+
|
|
513
|
+
app_binary, image_metadata = load_app_binary(esp, app_binary_file, partition)
|
|
514
|
+
|
|
515
|
+
print(f"Writing '{image_metadata.app_description.title}' to partition '{partition.name}'...")
|
|
516
|
+
write_flash(esp=esp, addr_data=[(partition.offset, app_binary)])
|
|
517
|
+
|
|
518
|
+
otadata_partition = next(
|
|
519
|
+
(p for p in partition_table if p.type == DATA_TYPE and p.subtype == SUBTYPES[DATA_TYPE]['ota']),
|
|
520
|
+
None
|
|
521
|
+
)
|
|
522
|
+
if otadata_partition:
|
|
523
|
+
print("Erasing 'otadata' partition...")
|
|
524
|
+
esp.erase_region(offset=otadata_partition.offset, size=otadata_partition.size)
|
|
525
|
+
|
|
526
|
+
def command_app_info(app_binary_file: str):
|
|
527
|
+
app_binary = open(app_binary_file, 'rb').read()
|
|
528
|
+
try:
|
|
529
|
+
image_metadata = ImageMetadata.from_bytes(app_binary, app_required=True)
|
|
530
|
+
except ValueError as e:
|
|
531
|
+
raise RuntimeError(f"Invalid application binary: {e}")
|
|
532
|
+
|
|
533
|
+
desc = image_metadata.app_description
|
|
534
|
+
header = image_metadata.header
|
|
535
|
+
|
|
536
|
+
print(f"Project name: {desc.project_name}")
|
|
537
|
+
print(f"Version: {desc.version}")
|
|
538
|
+
print(f"IDF version: {desc.idf_version}")
|
|
539
|
+
print(f"Secure version: {desc.secure_version}")
|
|
540
|
+
if desc.compiled:
|
|
541
|
+
print(f"Compiled: {desc.compiled}")
|
|
542
|
+
print(f"ELF SHA256: {desc.elf_sha256.hex()}")
|
|
543
|
+
max_rev = header.max_chip_rev_full if header.max_chip_rev_full != 0xFFFF else None
|
|
544
|
+
rev_range = f"rev {header.min_chip_rev_full} to {max_rev}" if max_rev else f"rev {header.min_chip_rev_full}+"
|
|
545
|
+
print(f"Chip: {header.chip_id.name} ({rev_range})")
|
|
546
|
+
|
|
547
|
+
def command_write_image(esp: ESPLoader, bootloader_entry: PartitionDefinition, image_file_path: str):
|
|
548
|
+
image_file = open(image_file_path, 'rb')
|
|
549
|
+
image_file.seek(bootloader_entry.offset)
|
|
550
|
+
bootloader_binary = image_file.read(bootloader_entry.size)
|
|
551
|
+
try:
|
|
552
|
+
bootloader_image_metadata = ImageMetadata.from_bytes(bootloader_binary)
|
|
553
|
+
except (RuntimeError, ValueError) as e:
|
|
554
|
+
raise RuntimeError("Invalid bootloader in image, check the input file (chip type may be incorrect)")\
|
|
555
|
+
from e
|
|
556
|
+
|
|
557
|
+
if bootloader_image_metadata.header.chip_id.value != esp.IMAGE_CHIP_ID:
|
|
558
|
+
raise RuntimeError(
|
|
559
|
+
f"Chip ID mismatch: "
|
|
560
|
+
f"attempting to flash {bootloader_image_metadata.header.chip_id.name} image "
|
|
561
|
+
f"to {ChipId(esp.IMAGE_CHIP_ID).name} device"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
image_data: bytes
|
|
565
|
+
with open(image_file_path, 'rb') as image_file:
|
|
566
|
+
image_file.seek(esp.BOOTLOADER_FLASH_OFFSET)
|
|
567
|
+
image_data = image_file.read()
|
|
568
|
+
|
|
569
|
+
print(f"Writing image '{image_file_path}'...")
|
|
570
|
+
erase_flash(esp)
|
|
571
|
+
write_flash(
|
|
572
|
+
esp=esp,
|
|
573
|
+
addr_data=[(esp.BOOTLOADER_FLASH_OFFSET, image_data)],
|
|
574
|
+
flash_size='detect',
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
def command_dump_image(esp: ESPLoader, output_file: Optional[str]):
|
|
578
|
+
flash_size = flash_size_bytes(detect_flash_size(esp))
|
|
579
|
+
|
|
580
|
+
if not output_file:
|
|
581
|
+
chip = esp.CHIP_NAME.lower().replace(' ', '-')
|
|
582
|
+
try:
|
|
583
|
+
mac = esp.read_mac("BASE_MAC")
|
|
584
|
+
serial = ''.join(f"{b:02x}" for b in mac)
|
|
585
|
+
except Exception:
|
|
586
|
+
serial = "unknown"
|
|
587
|
+
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
588
|
+
output_file = f"{chip}-{serial}-{timestamp}.img"
|
|
589
|
+
|
|
590
|
+
print(f"Dumping {flash_size:#x} bytes of flash to {output_file}...")
|
|
591
|
+
read_flash(esp, address=0, size=flash_size, output=output_file)
|
|
592
|
+
|
|
593
|
+
def command_dump_bundle(esp: ESPLoader, partition_table: PartitionTable, output_file: Optional[str]):
|
|
594
|
+
if not output_file:
|
|
595
|
+
chip = esp.CHIP_NAME.lower().replace(' ', '-')
|
|
596
|
+
try:
|
|
597
|
+
mac = esp.read_mac("BASE_MAC")
|
|
598
|
+
serial = ''.join(f"{b:02x}" for b in mac)
|
|
599
|
+
except Exception:
|
|
600
|
+
serial = "unknown"
|
|
601
|
+
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
|
602
|
+
output_file = f"{chip}-{serial}-{timestamp}.zip"
|
|
603
|
+
|
|
604
|
+
with ZipFile(output_file, 'w') as zf:
|
|
605
|
+
for partition in partition_table:
|
|
606
|
+
print(f"Reading partition {partition.name} (offset={partition.offset:#x}, size={partition.size:#x})")
|
|
607
|
+
data = esp.read_flash(partition.offset, partition.size)
|
|
608
|
+
zf.writestr(f"{partition.name}.bin", data)
|
|
609
|
+
zf.writestr("partition_table.csv", partition_table.to_csv())
|
|
610
|
+
print(f"Adding partition table CSV to bundle")
|
|
611
|
+
|
|
612
|
+
print(f"Bundle written to {output_file}")
|
|
613
|
+
|
|
614
|
+
def command_enter_bootloader(port: str, baud: int, poll_interval: float = 0.05):
|
|
615
|
+
"""Fast-poll a serial port path and enter the ROM bootloader as soon as it appears.
|
|
616
|
+
|
|
617
|
+
Skips run_stub and does not reset the chip on exit, so the device is left
|
|
618
|
+
parked in the ROM bootloader (download mode) for the next tool to grab.
|
|
619
|
+
"""
|
|
620
|
+
print(f"Waiting for {port}...", file=sys.stderr)
|
|
621
|
+
while True:
|
|
622
|
+
while not os.path.exists(port):
|
|
623
|
+
time.sleep(poll_interval)
|
|
624
|
+
try:
|
|
625
|
+
esp = detect_chip(port, baud=baud)
|
|
626
|
+
break
|
|
627
|
+
except Exception as e:
|
|
628
|
+
print(f"Bootloader entry failed: {type(e).__name__}: {e}. Retrying...", file=sys.stderr)
|
|
629
|
+
time.sleep(poll_interval)
|
|
630
|
+
print(f"In download mode: {esp.CHIP_NAME} ({port})")
|
|
631
|
+
|
|
632
|
+
def main(args):
|
|
633
|
+
if args.command == 'devices':
|
|
634
|
+
devices = _get_port_list()
|
|
635
|
+
for d in devices:
|
|
636
|
+
print(f"{d.device} || {d.description} || {d.hwid}")
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
if args.command == 'enter-bootloader':
|
|
640
|
+
command_enter_bootloader(port=args.port, baud=args.baud)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
# Connect to ESP device if required
|
|
644
|
+
requires_esp = args.command not in ['list', 'create-image', 'create-bundle'] or not args.partition_table_file
|
|
645
|
+
esp = get_esp(port=args.port, baud=args.baud) if requires_esp else None
|
|
646
|
+
if not args.primary_bootloader_offset and esp:
|
|
647
|
+
args.primary_bootloader_offset = esp.BOOTLOADER_FLASH_OFFSET
|
|
648
|
+
|
|
649
|
+
if args.command == 'dump-image':
|
|
650
|
+
command_dump_image(esp=esp, output_file=args.output_file)
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
# Load partition table
|
|
654
|
+
if args.command == 'write-image':
|
|
655
|
+
with open(args.image_file, 'rb') as f:
|
|
656
|
+
f.seek(args.partition_table_offset)
|
|
657
|
+
partition_table_binary = f.read(args.partition_table_size)
|
|
658
|
+
partition_table = PartitionTable.from_binary(partition_table_binary)
|
|
659
|
+
elif args.partition_table_file:
|
|
660
|
+
with open(args.partition_table_file, 'rb') as f:
|
|
661
|
+
partition_table = PartitionTable.from_file(
|
|
662
|
+
f,
|
|
663
|
+
partition_table_offset=args.partition_table_offset,
|
|
664
|
+
primary_bootloader_offset=args.primary_bootloader_offset,
|
|
665
|
+
recovery_bootloader_offset=args.recovery_bootloader_offset
|
|
666
|
+
)[0]
|
|
667
|
+
elif args.command == 'write-bundle' and check_write_bundle_has_partition_table(args.input_file):
|
|
668
|
+
with ZipFile(args.input_file, 'r') as tar:
|
|
669
|
+
partition_table_file = tar.read('partition_table.csv')
|
|
670
|
+
if not partition_table_file:
|
|
671
|
+
raise RuntimeError("Partition table could not be loaded from bundle")
|
|
672
|
+
partition_table = PartitionTable.from_csv(
|
|
673
|
+
partition_table_file.decode('utf-8'),
|
|
674
|
+
partition_table_offset=args.partition_table_offset,
|
|
675
|
+
primary_bootloader_offset=args.primary_bootloader_offset,
|
|
676
|
+
recovery_bootloader_offset=args.recovery_bootloader_offset
|
|
677
|
+
)
|
|
678
|
+
else:
|
|
679
|
+
try:
|
|
680
|
+
partition_table_binary = esp.read_flash(offset=args.partition_table_offset, length=args.partition_table_size)
|
|
681
|
+
partition_table = PartitionTable.from_binary(partition_table_binary)
|
|
682
|
+
except RuntimeError as e:
|
|
683
|
+
raise RuntimeError("Partition table could not be loaded") from e
|
|
684
|
+
|
|
685
|
+
# Read otadata so the printed table can mark the active app partition
|
|
686
|
+
otadata_params: OtaDataParameters | None = None
|
|
687
|
+
if esp:
|
|
688
|
+
try:
|
|
689
|
+
_, otadata_params = read_otadata(esp, partition_table)
|
|
690
|
+
except ValueError:
|
|
691
|
+
pass
|
|
692
|
+
|
|
693
|
+
# Print partition table
|
|
694
|
+
print_partition_table(partition_table, esp, otadata=otadata_params)
|
|
695
|
+
|
|
696
|
+
if esp:
|
|
697
|
+
flash_size_str = detect_flash_size(esp)
|
|
698
|
+
flash_size = flash_size_bytes(flash_size_str)
|
|
699
|
+
partition_table.verify_size_fits(flash_size)
|
|
700
|
+
|
|
701
|
+
# Add virtual partition table entry if not present
|
|
702
|
+
partition_table_entry = next(
|
|
703
|
+
(e for e in partition_table if e.type == PARTITION_TABLE_TYPE and e.subtype == SUBTYPES[e.type]['primary']),
|
|
704
|
+
PartitionDefinition.default_partition_table(
|
|
705
|
+
offset=args.partition_table_offset,
|
|
706
|
+
size=args.partition_table_size
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
# Add virtual bootloader partition if not present and bootloader offsets are provided
|
|
711
|
+
bootloader_entry = next(
|
|
712
|
+
(e for e in partition_table if e.type == BOOTLOADER_TYPE and e.subtype == SUBTYPES[e.type]['primary']),
|
|
713
|
+
PartitionDefinition.default_bootloader(
|
|
714
|
+
offset=args.primary_bootloader_offset,
|
|
715
|
+
size=args.partition_table_offset - args.primary_bootloader_offset
|
|
716
|
+
) if args.primary_bootloader_offset is not None else None
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
if args.command == 'list':
|
|
720
|
+
pass
|
|
721
|
+
elif args.command == 'read':
|
|
722
|
+
command_read(
|
|
723
|
+
esp=esp,
|
|
724
|
+
partition_table=partition_table,
|
|
725
|
+
partition_table_entry=partition_table_entry,
|
|
726
|
+
bootloader_entry=bootloader_entry,
|
|
727
|
+
label=args.partition,
|
|
728
|
+
output_file=args.output_file
|
|
729
|
+
)
|
|
730
|
+
elif args.command == 'write':
|
|
731
|
+
command_write(
|
|
732
|
+
esp=esp,
|
|
733
|
+
partition_table=partition_table,
|
|
734
|
+
partition_table_entry=partition_table_entry,
|
|
735
|
+
bootloader_entry=bootloader_entry,
|
|
736
|
+
files=args.files
|
|
737
|
+
)
|
|
738
|
+
elif args.command == 'erase':
|
|
739
|
+
command_erase(
|
|
740
|
+
esp=esp,
|
|
741
|
+
partition_table=partition_table,
|
|
742
|
+
partition_table_entry=partition_table_entry,
|
|
743
|
+
bootloader_entry=bootloader_entry,
|
|
744
|
+
label=args.partition
|
|
745
|
+
)
|
|
746
|
+
elif args.command == 'view':
|
|
747
|
+
command_view(
|
|
748
|
+
esp=esp,
|
|
749
|
+
partition_table=partition_table,
|
|
750
|
+
partition_table_entry=partition_table_entry,
|
|
751
|
+
bootloader_entry=bootloader_entry,
|
|
752
|
+
label=args.partition,
|
|
753
|
+
output=args.output,
|
|
754
|
+
width=args.width
|
|
755
|
+
)
|
|
756
|
+
elif args.command == 'create-image':
|
|
757
|
+
command_create_image(
|
|
758
|
+
partition_table=partition_table,
|
|
759
|
+
partition_table_entry=partition_table_entry,
|
|
760
|
+
bootloader_entry=bootloader_entry,
|
|
761
|
+
files=args.files,
|
|
762
|
+
flash_partition_table=args.flash_partition_table,
|
|
763
|
+
output_format=args.format,
|
|
764
|
+
output_file=args.output_file
|
|
765
|
+
)
|
|
766
|
+
elif args.command == 'create-bundle':
|
|
767
|
+
command_create_bundle(
|
|
768
|
+
partition_table=partition_table,
|
|
769
|
+
partition_table_entry=partition_table_entry,
|
|
770
|
+
bootloader_entry=bootloader_entry,
|
|
771
|
+
files=args.files,
|
|
772
|
+
flash_partition_table=args.flash_partition_table,
|
|
773
|
+
output_file=args.output_file
|
|
774
|
+
)
|
|
775
|
+
elif args.command == 'dump-bundle':
|
|
776
|
+
command_dump_bundle(esp=esp, partition_table=partition_table, output_file=args.output_file)
|
|
777
|
+
elif args.command == 'write-bundle':
|
|
778
|
+
command_write_bundle(
|
|
779
|
+
esp=esp,
|
|
780
|
+
partition_table=partition_table,
|
|
781
|
+
partition_table_entry=partition_table_entry,
|
|
782
|
+
bootloader_entry=bootloader_entry,
|
|
783
|
+
input_file=args.input_file
|
|
784
|
+
)
|
|
785
|
+
elif args.command == 'get-boot':
|
|
786
|
+
command_get_boot(esp=esp, partition_table=partition_table)
|
|
787
|
+
elif args.command == 'set-boot':
|
|
788
|
+
command_set_boot(esp=esp, partition_table=partition_table, label=args.partition)
|
|
789
|
+
elif args.command == 'clear-boot':
|
|
790
|
+
command_clear_boot(esp=esp, partition_table=partition_table)
|
|
791
|
+
elif args.command == 'ota':
|
|
792
|
+
command_ota(esp=esp, partition_table=partition_table, app_binary_file=args.app_binary_file)
|
|
793
|
+
elif args.command == 'factory':
|
|
794
|
+
command_factory(esp=esp, partition_table=partition_table, app_binary_file=args.app_binary_file)
|
|
795
|
+
elif args.command == 'write-image':
|
|
796
|
+
command_write_image(esp=esp, bootloader_entry=bootloader_entry, image_file_path=args.image_file)
|
|
797
|
+
else:
|
|
798
|
+
print(f"Unknown command: {args.command}", file=sys.stderr)
|
|
799
|
+
|
|
800
|
+
if esp and not args.no_reset: esp.hard_reset()
|
|
801
|
+
|
|
802
|
+
def _main():
|
|
803
|
+
parser = argparse.ArgumentParser()
|
|
804
|
+
|
|
805
|
+
parser.add_argument('-p', '--port', help='Serial port device')
|
|
806
|
+
parser.add_argument('-b', '--baud', type=int, help='Serial port baud rate', default=ESPLoader.ESP_ROM_BAUD)
|
|
807
|
+
parser.add_argument('--no-reset', action='store_true', help='Do not reset the chip after operations')
|
|
808
|
+
|
|
809
|
+
parser.add_argument('--partition-table-file', help='Path to partition table CSV or binary file to use instead of reading from device')
|
|
810
|
+
parser.add_argument('--partition-table-offset', type=auto_int, help='Partition table offset (where to read partition table from flash, or where to place when loading partition table from CSV)', default=PARTITION_TABLE_OFFSET)
|
|
811
|
+
parser.add_argument('--partition-table-size', type=auto_int, help='Partition table size', default=PARTITION_TABLE_SIZE)
|
|
812
|
+
parser.add_argument('--primary-bootloader-offset', type=parse_bootloader_offset, help='Primary bootloader offset or chip type e.g. esp32s3 (used when loading partition table from CSV)')
|
|
813
|
+
parser.add_argument('--recovery-bootloader-offset', type=auto_int, help='Recovery bootloader offset (used when loading partition table from CSV)')
|
|
814
|
+
|
|
815
|
+
# Create subparsers
|
|
816
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands', required=True)
|
|
817
|
+
|
|
818
|
+
# Devices subcommand
|
|
819
|
+
devices_parser = subparsers.add_parser('devices', help='Device list')
|
|
820
|
+
|
|
821
|
+
# List subcommand
|
|
822
|
+
list_parser = subparsers.add_parser('list', help='List partitions')
|
|
823
|
+
list_parser.add_argument('--partition-type', help='Filter by partition type')
|
|
824
|
+
list_parser.add_argument('--partition-subtype', help='Filter by partition subtype')
|
|
825
|
+
list_parser.add_argument('--partition-name', help='Filter by partition name')
|
|
826
|
+
|
|
827
|
+
# Read subcommand
|
|
828
|
+
read_parser = subparsers.add_parser('read', help='Read partition')
|
|
829
|
+
read_parser.add_argument('partition', help='Name of the partition to read')
|
|
830
|
+
read_parser.add_argument('output_file', help='Output file to save the partition data')
|
|
831
|
+
|
|
832
|
+
# Write subcommand
|
|
833
|
+
write_parser = subparsers.add_parser('write', help='Write partitions')
|
|
834
|
+
write_parser.add_argument(
|
|
835
|
+
'files',
|
|
836
|
+
nargs='+',
|
|
837
|
+
metavar=('PARTITION', 'FILENAME'),
|
|
838
|
+
help='Partition and filename pairs'
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
# Erase subcommand
|
|
842
|
+
erase_parser = subparsers.add_parser('erase', help='Erase partition')
|
|
843
|
+
erase_parser.add_argument('partition', help='Name of the partition to erase')
|
|
844
|
+
|
|
845
|
+
# View subcommand
|
|
846
|
+
view_parser = subparsers.add_parser('view', help='View partition')
|
|
847
|
+
view_parser.add_argument('partition', help='Name of the partition to view')
|
|
848
|
+
view_parser.add_argument('-w', '--width', type=int, help='Width of the hexdump', default=16)
|
|
849
|
+
view_parser.add_argument('--hex', action='store_const', const='hex', dest='output', help='Output as hex dump', default='hex')
|
|
850
|
+
view_parser.add_argument('-s', '--string', action='store_const', const='str', dest='output', help='Output as string')
|
|
851
|
+
|
|
852
|
+
# Create Image subcommand
|
|
853
|
+
create_image_parser = subparsers.add_parser('create-image', help='Merge partition binaries into a single flash image')
|
|
854
|
+
create_image_parser.add_argument('-o', '--output', help='Output filename', dest='output_file', required=True)
|
|
855
|
+
create_image_parser.add_argument('-f', '--format', help='Output format', choices=['raw', 'uf2', 'hex'], default='raw')
|
|
856
|
+
create_image_parser.add_argument('--flash-partition-table', action='store_true', help='Include partition table in the image')
|
|
857
|
+
create_image_parser.add_argument(
|
|
858
|
+
'files',
|
|
859
|
+
nargs='+',
|
|
860
|
+
metavar='PARTITION FILENAME',
|
|
861
|
+
help='Partition and filename pairs'
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
# Dump Image subcommand
|
|
865
|
+
dump_image_parser = subparsers.add_parser('dump-image', help='Dump the entire flash to an image file')
|
|
866
|
+
dump_image_parser.add_argument('output_file', nargs='?', help='Output filename (default: {chip}-{mac}-{timestamp}.img)')
|
|
867
|
+
|
|
868
|
+
# Write Image subcommand (alias: reflash)
|
|
869
|
+
write_image_parser = subparsers.add_parser('write-image', aliases=['reflash'], help='Write a full flash image to the device')
|
|
870
|
+
write_image_parser.add_argument('image_file', help='Input file containing flash image')
|
|
871
|
+
|
|
872
|
+
# Create bundle subcommand
|
|
873
|
+
create_bundle_parser = subparsers.add_parser('create-bundle', help='Create a ZIP bundle of partition binaries')
|
|
874
|
+
create_bundle_parser.add_argument('-o', '--output', help='Output ZIP filename', dest='output_file', required=True)
|
|
875
|
+
create_bundle_parser.add_argument('--flash-partition-table', action='store_true', help='Include partition table in the bundle')
|
|
876
|
+
create_bundle_parser.add_argument(
|
|
877
|
+
'files',
|
|
878
|
+
nargs='+',
|
|
879
|
+
metavar='PARTITION FILENAME',
|
|
880
|
+
help='Partition and filename pairs'
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# Dump Bundle subcommand
|
|
884
|
+
dump_bundle_parser = subparsers.add_parser('dump-bundle', help='Dump all partitions to a ZIP bundle')
|
|
885
|
+
dump_bundle_parser.add_argument('output_file', nargs='?', help='Output ZIP filename (default: {chip}-{mac}-{timestamp}.zip)')
|
|
886
|
+
|
|
887
|
+
# Write bundle subcommand
|
|
888
|
+
write_bundle_parser = subparsers.add_parser('write-bundle', help='Write a ZIP bundle of partition binaries')
|
|
889
|
+
write_bundle_parser.add_argument('input_file', help='Input ZIP filename')
|
|
890
|
+
|
|
891
|
+
# Factory subcommand
|
|
892
|
+
factory_parser = subparsers.add_parser('factory', help='Perform factory flash and clear boot partition')
|
|
893
|
+
factory_parser.add_argument('app_binary_file', help='Input file containing app binary')
|
|
894
|
+
|
|
895
|
+
# Ota subcommand
|
|
896
|
+
ota_parser = subparsers.add_parser('ota', help='Perform OTA')
|
|
897
|
+
ota_parser.add_argument('app_binary_file', help='Input file containing app binary')
|
|
898
|
+
|
|
899
|
+
# Get Boot subcommand
|
|
900
|
+
get_boot_parser = subparsers.add_parser('get-boot', help='Get boot partition')
|
|
901
|
+
|
|
902
|
+
# Set Boot subcommand
|
|
903
|
+
set_boot_parser = subparsers.add_parser('set-boot', help='Set boot partition')
|
|
904
|
+
set_boot_parser.add_argument('partition', help='Name of the partition to set as boot')
|
|
905
|
+
|
|
906
|
+
# Clear Boot subcommand
|
|
907
|
+
clear_boot_parser = subparsers.add_parser('clear-boot', help='Clear boot partition')
|
|
908
|
+
|
|
909
|
+
# Enter Bootloader subcommand
|
|
910
|
+
enter_bootloader_parser = subparsers.add_parser('enter-bootloader', help='Fast-poll the serial port and enter the ROM bootloader as soon as the device appears, then exit without resetting')
|
|
911
|
+
|
|
912
|
+
args = parser.parse_args()
|
|
913
|
+
if args.command == 'reflash':
|
|
914
|
+
args.command = 'write-image'
|
|
915
|
+
if args.command == 'enter-bootloader' and not args.port:
|
|
916
|
+
parser.error("enter-bootloader requires -p/--port")
|
|
917
|
+
main(args)
|
|
918
|
+
|
|
919
|
+
if __name__ == "__main__":
|
|
920
|
+
_main()
|