pypicoboot 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.
- picoboot/__init__.py +2 -0
- picoboot/_version.py +21 -0
- picoboot/core/__init__.py +1 -0
- picoboot/core/enums.py +20 -0
- picoboot/picoboot.py +479 -0
- picoboot/tools/picotool.py +6 -0
- picoboot/utils.py +25 -0
- pypicoboot-1.0.dist-info/METADATA +57 -0
- pypicoboot-1.0.dist-info/RECORD +11 -0
- pypicoboot-1.0.dist-info/WHEEL +5 -0
- pypicoboot-1.0.dist-info/top_level.txt +3 -0
picoboot/__init__.py
ADDED
picoboot/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
/*
|
|
4
|
+
* This file is part of the pypicoboot distribution (https://github.com/polhenarejos/pypicoboot).
|
|
5
|
+
* Copyright (c) 2025 Pol Henarejos.
|
|
6
|
+
*
|
|
7
|
+
* This program is free software: you can redistribute it and/or modify
|
|
8
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
9
|
+
* the Free Software Foundation, version 3.
|
|
10
|
+
*
|
|
11
|
+
* This program is distributed in the hope that it will be useful, but
|
|
12
|
+
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
14
|
+
* Affero General Public License for more details.
|
|
15
|
+
*
|
|
16
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
17
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
18
|
+
*/
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .enums import NamedIntEnum
|
picoboot/core/enums.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
class NamedIntEnum(enum.IntEnum):
|
|
5
|
+
def __str__(self):
|
|
6
|
+
return self.name
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
def from_string(cls, value: Union[str, int]) -> "NamedIntEnum":
|
|
10
|
+
if not value:
|
|
11
|
+
return cls.UNKNOWN
|
|
12
|
+
|
|
13
|
+
value = value.strip().lower()
|
|
14
|
+
|
|
15
|
+
for member in cls:
|
|
16
|
+
if member.value == value or member.name.lower() == value:
|
|
17
|
+
return member
|
|
18
|
+
|
|
19
|
+
return cls.UNKNOWN
|
|
20
|
+
|
picoboot/picoboot.py
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# picoboot.py
|
|
2
|
+
from binascii import hexlify
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import usb.core
|
|
5
|
+
import usb.util
|
|
6
|
+
import struct
|
|
7
|
+
import itertools
|
|
8
|
+
from .utils import uint_to_int
|
|
9
|
+
from .core.enums import NamedIntEnum
|
|
10
|
+
|
|
11
|
+
# Valors per defecte segons el datasheet (es poden canviar via OTP) :contentReference[oaicite:4]{index=4}
|
|
12
|
+
DEFAULT_VID = 0x2E8A
|
|
13
|
+
DEFAULT_PID_RP2040 = 0x0003
|
|
14
|
+
DEFAULT_PID_RP2350 = 0x000F
|
|
15
|
+
|
|
16
|
+
PICOBOOT_MAGIC = 0x431FD10B
|
|
17
|
+
|
|
18
|
+
# Bit 7 = direcció de transferència de dades (IN si està posat) :contentReference[oaicite:5]{index=5}
|
|
19
|
+
CMD_DIR_IN = 0x80
|
|
20
|
+
|
|
21
|
+
# IDs de comanda (secció 5.6.4) :contentReference[oaicite:6]{index=6}
|
|
22
|
+
class CommandID(NamedIntEnum):
|
|
23
|
+
EXCLUSIVE_ACCESS = 0x01
|
|
24
|
+
REBOOT = 0x02
|
|
25
|
+
FLASH_ERASE = 0x03
|
|
26
|
+
READ = 0x84
|
|
27
|
+
WRITE = 0x05
|
|
28
|
+
EXIT_XIP = 0x06
|
|
29
|
+
ENTER_XIP = 0x07
|
|
30
|
+
REBOOT2 = 0x0A
|
|
31
|
+
GET_INFO = 0x8B
|
|
32
|
+
OTP_READ = 0x8C
|
|
33
|
+
OTP_WRITE = 0x0D
|
|
34
|
+
|
|
35
|
+
# Control requests (secció 5.6.5) :contentReference[oaicite:7]{index=7}
|
|
36
|
+
class ControlRequest(NamedIntEnum):
|
|
37
|
+
REQ_INTERFACE_RESET = 0x41
|
|
38
|
+
REQ_GET_COMMAND_STATUS = 0x42
|
|
39
|
+
BMREQ_RESET = 0x41 # Host->Device, Class, Interface
|
|
40
|
+
BMREQ_GET_STATUS = 0xC1 # Device->Host, Class, Interface
|
|
41
|
+
|
|
42
|
+
class InfoType(NamedIntEnum):
|
|
43
|
+
SYS = 0x01
|
|
44
|
+
PARTITION = 0x02
|
|
45
|
+
UF2_TARGET_PARTITION = 0x03
|
|
46
|
+
UF2_STATUS = 0x04
|
|
47
|
+
|
|
48
|
+
class SysInfoFlags(NamedIntEnum):
|
|
49
|
+
CHIP_INFO = 0x01
|
|
50
|
+
CRITICAL = 0x02
|
|
51
|
+
CPU = 0x04
|
|
52
|
+
FLASH = 0x08
|
|
53
|
+
BOOT_RANDOM = 0x10
|
|
54
|
+
NONCE = 0x20
|
|
55
|
+
BOOT_INFO = 0x40
|
|
56
|
+
|
|
57
|
+
class CriticalRegister(NamedIntEnum):
|
|
58
|
+
SECURE_BOOT = 0x01
|
|
59
|
+
SECURE_DEBUG_DISABLE = 0x02
|
|
60
|
+
DEBUG_DISABLE = 0x04
|
|
61
|
+
DEFAULT_ARCHSEL = 0x08
|
|
62
|
+
GLITCH_DETECTOR_ENABLE = 0x10
|
|
63
|
+
GLITCH_DETECTOR_SENS = 0x60
|
|
64
|
+
ARM_DISABLE = 0x10000
|
|
65
|
+
RISCV_DISABLE = 0x20000
|
|
66
|
+
|
|
67
|
+
class DiagnosticPartition(NamedIntEnum):
|
|
68
|
+
REGION_SEARCHED = 0x01
|
|
69
|
+
INVALID_BLOCK_LOOPS = 0x02
|
|
70
|
+
VALID_BLOCK_LOOPS = 0x04
|
|
71
|
+
VALID_IMAGE_DEFAULTS = 0x08
|
|
72
|
+
HAS_PARTITION_TABLE = 0x10
|
|
73
|
+
CONSIDERED = 0x20
|
|
74
|
+
CHOSEN = 0x40
|
|
75
|
+
PARTITION_TABLE_MATCHING_KEY_FOR_VERIFY = 0x80
|
|
76
|
+
PARTITION_TABLE_HASH_FOR_VERIFY = 0x100
|
|
77
|
+
PARTITION_TABLE_VERIFIED_OK = 0x200
|
|
78
|
+
IMAGE_DEF_MATCHING_KEY_FOR_VERIFY = 0x400
|
|
79
|
+
IMAGE_DEF_HASH_FOR_VERIFY = 0x800
|
|
80
|
+
IMAGE_DEF_VERIFIED_OK = 0x1000
|
|
81
|
+
LOAD_MAP_ENTRIES_LOADED = 0x2000
|
|
82
|
+
IMAGE_LAUNCHED = 0x4000
|
|
83
|
+
IMAGE_CONDITION_FAILURES = 0x8000
|
|
84
|
+
|
|
85
|
+
class PartitionInfoType(NamedIntEnum):
|
|
86
|
+
PARTITION_0 = 0
|
|
87
|
+
PARTITION_1 = 1
|
|
88
|
+
PARTITION_2 = 2
|
|
89
|
+
PARTITION_3 = 3
|
|
90
|
+
PARTITION_4 = 4
|
|
91
|
+
PARTITION_5 = 5
|
|
92
|
+
PARTITION_6 = 6
|
|
93
|
+
PARTITION_7 = 7
|
|
94
|
+
PARTITION_8 = 8
|
|
95
|
+
PARTITION_9 = 9
|
|
96
|
+
PARTITION_10 = 10
|
|
97
|
+
PARTITION_11 = 11
|
|
98
|
+
PARTITION_12 = 12
|
|
99
|
+
PARTITION_13 = 13
|
|
100
|
+
PARTITION_14 = 14
|
|
101
|
+
PARTITION_15 = 15
|
|
102
|
+
NONE = -1
|
|
103
|
+
SLOT_0 = -2
|
|
104
|
+
SLOT_1 = -3
|
|
105
|
+
IMAGE = -4
|
|
106
|
+
|
|
107
|
+
class Model(NamedIntEnum):
|
|
108
|
+
RP2040 = 0x01754d
|
|
109
|
+
RP2350 = 0x02754d
|
|
110
|
+
UNKNOWN = 0x000000
|
|
111
|
+
|
|
112
|
+
class Addresses(NamedIntEnum):
|
|
113
|
+
BOOTROM_MAGIC = 0x00000010
|
|
114
|
+
|
|
115
|
+
class PicoBootError(Exception):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
class PicoBoot:
|
|
119
|
+
|
|
120
|
+
def __init__(self, dev: usb.core.Device, intf, ep_out, ep_in) -> None:
|
|
121
|
+
self.dev = dev
|
|
122
|
+
self.intf = intf
|
|
123
|
+
self.ep_out = ep_out
|
|
124
|
+
self.ep_in = ep_in
|
|
125
|
+
self._token_counter = itertools.count(1)
|
|
126
|
+
self.interface_reset()
|
|
127
|
+
self._memory = self._guess_flash_size()
|
|
128
|
+
self._model = self._determine_model()
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def open(cls, vid: int = DEFAULT_VID, pid: list[int] = [DEFAULT_PID_RP2040, DEFAULT_PID_RP2350], serial: Optional[str] = None) -> "PicoBoot":
|
|
132
|
+
class find_pids(object):
|
|
133
|
+
def __init__(self, pids):
|
|
134
|
+
self._pids = pids
|
|
135
|
+
def __call__(self, device):
|
|
136
|
+
if device.idProduct in self._pids:
|
|
137
|
+
return True
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
devices = usb.core.find(find_all=True, custom_match=find_pids(pid))
|
|
141
|
+
devices = list(devices) if devices is not None else []
|
|
142
|
+
if not devices:
|
|
143
|
+
raise PicoBootError("No device found in PICOBOOT mode")
|
|
144
|
+
|
|
145
|
+
dev = None
|
|
146
|
+
if serial is None:
|
|
147
|
+
dev = devices[0]
|
|
148
|
+
else:
|
|
149
|
+
for d in devices:
|
|
150
|
+
try:
|
|
151
|
+
s = usb.util.get_string(d, d.iSerialNumber)
|
|
152
|
+
except usb.core.USBError:
|
|
153
|
+
continue
|
|
154
|
+
if s == serial:
|
|
155
|
+
dev = d
|
|
156
|
+
break
|
|
157
|
+
if dev is None:
|
|
158
|
+
raise PicoBootError("No device found with this serial number")
|
|
159
|
+
|
|
160
|
+
# Ensure active configuration
|
|
161
|
+
# macOS does not allow detach_kernel_driver, and often returns Access Denied
|
|
162
|
+
try:
|
|
163
|
+
if dev.is_kernel_driver_active(0):
|
|
164
|
+
dev.detach_kernel_driver(0)
|
|
165
|
+
except usb.core.USBError:
|
|
166
|
+
# If it fails, we continue anyway. It's normal on macOS.
|
|
167
|
+
pass
|
|
168
|
+
except NotImplementedError:
|
|
169
|
+
# Also fine on backends that don't implement the function
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
dev.set_configuration()
|
|
173
|
+
cfg = dev.get_active_configuration()
|
|
174
|
+
|
|
175
|
+
intf = None
|
|
176
|
+
for i in cfg:
|
|
177
|
+
if i.bInterfaceClass == 0xFF and i.bInterfaceSubClass == 0 and i.bInterfaceProtocol == 0:
|
|
178
|
+
intf = i
|
|
179
|
+
break
|
|
180
|
+
if intf is None:
|
|
181
|
+
raise PicoBootError("No interface found with PICOBOOT at the device")
|
|
182
|
+
|
|
183
|
+
#usb.util.claim_interface(dev, intf.bInterfaceNumber)
|
|
184
|
+
|
|
185
|
+
ep_in = ep_out = None
|
|
186
|
+
for ep in intf.endpoints():
|
|
187
|
+
if usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN:
|
|
188
|
+
ep_in = ep
|
|
189
|
+
else:
|
|
190
|
+
ep_out = ep
|
|
191
|
+
|
|
192
|
+
if ep_in is None or ep_out is None:
|
|
193
|
+
raise PicoBootError("No PICOBOOT BULK_IN/BULK_OUT endpoints found")
|
|
194
|
+
|
|
195
|
+
return cls(dev, intf, ep_out, ep_in)
|
|
196
|
+
|
|
197
|
+
def interface_reset(self) -> None:
|
|
198
|
+
self.dev.ctrl_transfer(
|
|
199
|
+
ControlRequest.BMREQ_RESET,
|
|
200
|
+
ControlRequest.REQ_INTERFACE_RESET,
|
|
201
|
+
0,
|
|
202
|
+
self.intf.bInterfaceNumber,
|
|
203
|
+
None
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def get_command_status(self) -> dict:
|
|
207
|
+
data = self.dev.ctrl_transfer(
|
|
208
|
+
ControlRequest.BMREQ_GET_STATUS,
|
|
209
|
+
ControlRequest.REQ_GET_COMMAND_STATUS,
|
|
210
|
+
0,
|
|
211
|
+
self.intf.bInterfaceNumber,
|
|
212
|
+
16,
|
|
213
|
+
)
|
|
214
|
+
b = bytes(data)
|
|
215
|
+
dToken, dStatusCode = struct.unpack_from("<II", b, 0)
|
|
216
|
+
bCmdId = b[8]
|
|
217
|
+
bInProgress = b[9]
|
|
218
|
+
return {
|
|
219
|
+
"token": dToken,
|
|
220
|
+
"status": dStatusCode,
|
|
221
|
+
"cmd_id": bCmdId,
|
|
222
|
+
"in_progress": bool(bInProgress),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def _next_token(self) -> int:
|
|
226
|
+
return next(self._token_counter) & 0xFFFFFFFF
|
|
227
|
+
|
|
228
|
+
def _build_command(self, cmd_id: CommandID, args: bytes = b"", transfer_length: int = 0, token: Optional[int] = None) -> tuple[int, bytes]:
|
|
229
|
+
if token is None:
|
|
230
|
+
token = self._next_token()
|
|
231
|
+
if len(args) > 16:
|
|
232
|
+
raise ValueError("Too many args: maximum 16 bytes")
|
|
233
|
+
bCmdSize = len(args)
|
|
234
|
+
args = args.ljust(16, b"\x00")
|
|
235
|
+
header = struct.pack(
|
|
236
|
+
"<I I B B H I 16s",
|
|
237
|
+
PICOBOOT_MAGIC,
|
|
238
|
+
token,
|
|
239
|
+
cmd_id & 0xFF,
|
|
240
|
+
bCmdSize & 0xFF,
|
|
241
|
+
0, # reserved
|
|
242
|
+
transfer_length & 0xFFFFFFFF,
|
|
243
|
+
args,
|
|
244
|
+
)
|
|
245
|
+
return token, header
|
|
246
|
+
|
|
247
|
+
def _send_command(
|
|
248
|
+
self,
|
|
249
|
+
cmd_id: CommandID,
|
|
250
|
+
args: bytes = b"",
|
|
251
|
+
data_out: bytes | None = None,
|
|
252
|
+
transfer_length: int | None = None,
|
|
253
|
+
timeout: int = 3000,
|
|
254
|
+
) -> bytes:
|
|
255
|
+
is_in = bool(cmd_id & CMD_DIR_IN)
|
|
256
|
+
|
|
257
|
+
if transfer_length is None:
|
|
258
|
+
transfer_length = 0 if data_out is None else len(data_out)
|
|
259
|
+
|
|
260
|
+
token, header = self._build_command(cmd_id, args=args, transfer_length=transfer_length)
|
|
261
|
+
print(f"Sending command {cmd_id} (0x{cmd_id:02X}) with token {token} (0x{token:08X}) and transfer_length {transfer_length}")
|
|
262
|
+
|
|
263
|
+
self.ep_out.write(header, timeout=timeout)
|
|
264
|
+
|
|
265
|
+
data_in = b""
|
|
266
|
+
|
|
267
|
+
if transfer_length:
|
|
268
|
+
if is_in:
|
|
269
|
+
remaining = transfer_length
|
|
270
|
+
chunks = []
|
|
271
|
+
maxpkt = self.ep_in.wMaxPacketSize
|
|
272
|
+
while remaining > 0:
|
|
273
|
+
chunk = bytes(self.ep_in.read(min(maxpkt, remaining), timeout=timeout))
|
|
274
|
+
if not chunk:
|
|
275
|
+
break
|
|
276
|
+
chunks.append(chunk)
|
|
277
|
+
remaining -= len(chunk)
|
|
278
|
+
data_in = b"".join(chunks)
|
|
279
|
+
if len(data_in) != transfer_length:
|
|
280
|
+
raise PicoBootError(f"Expected {transfer_length} bytes, got {len(data_in)}")
|
|
281
|
+
else:
|
|
282
|
+
if data_out is None or len(data_out) < transfer_length:
|
|
283
|
+
raise ValueError("data_out missing or too short for OUT command")
|
|
284
|
+
self.ep_out.write(data_out[:transfer_length], timeout=timeout)
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
if is_in:
|
|
288
|
+
self.ep_out.write(b"", timeout=timeout)
|
|
289
|
+
else:
|
|
290
|
+
ack = self.ep_in.read(1, timeout=timeout)
|
|
291
|
+
except usb.core.USBError:
|
|
292
|
+
raise PicoBootError("No ACK received after command")
|
|
293
|
+
|
|
294
|
+
return data_in
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def flash_erase(self, addr: int, size: int) -> None:
|
|
298
|
+
if addr % 4096 != 0 or size % 4096 != 0:
|
|
299
|
+
raise ValueError("addr i size must be aligned to 4kB")
|
|
300
|
+
args = struct.pack("<II", addr, size)
|
|
301
|
+
self._send_command(CommandID.FLASH_ERASE, args=args, transfer_length=0)
|
|
302
|
+
|
|
303
|
+
def flash_read(self, addr: int, size: int) -> bytes:
|
|
304
|
+
args = struct.pack("<II", addr, size)
|
|
305
|
+
data = self._send_command(CommandID.READ, args=args, transfer_length=size)
|
|
306
|
+
if len(data) != size:
|
|
307
|
+
raise PicoBootError(f"READ returned {len(data)} bytes, expected {size}")
|
|
308
|
+
return data
|
|
309
|
+
|
|
310
|
+
def flash_write(self, addr: int, data: bytes) -> None:
|
|
311
|
+
if addr % 256 != 0 or len(data) % 256 != 0:
|
|
312
|
+
raise ValueError("addr i len(data) must be aligned/multiple of 256 bytes")
|
|
313
|
+
args = struct.pack("<II", addr, len(data))
|
|
314
|
+
self._send_command(CommandID.WRITE, args=args, data_out=data, transfer_length=len(data))
|
|
315
|
+
|
|
316
|
+
def reboot1(self, pc: int = 0, sp: int = 0, delay_ms: int = 0) -> None:
|
|
317
|
+
args = struct.pack("<III", pc, sp, delay_ms)
|
|
318
|
+
self._send_command(CommandID.REBOOT, args=args, transfer_length=0)
|
|
319
|
+
|
|
320
|
+
def reboot2(self, flags: int = 0, delay_ms: int = 0, p0: int = 0, p1: int = 0) -> None:
|
|
321
|
+
args = struct.pack("<IIII", flags, delay_ms, p0, p1)
|
|
322
|
+
self._send_command(CommandID.REBOOT2, args=args, transfer_length=0)
|
|
323
|
+
|
|
324
|
+
def reboot(self, delay_ms: int = 100) -> None:
|
|
325
|
+
if (self.model == Model.RP2040):
|
|
326
|
+
self.reboot1(delay_ms=delay_ms)
|
|
327
|
+
elif (self.model == Model.RP2350):
|
|
328
|
+
self.reboot2(delay_ms=delay_ms)
|
|
329
|
+
|
|
330
|
+
def exit_xip(self) -> None:
|
|
331
|
+
self._send_command(CommandID.EXIT_XIP, transfer_length=0)
|
|
332
|
+
|
|
333
|
+
def exclusive_access(self) -> None:
|
|
334
|
+
self._send_command(CommandID.EXCLUSIVE_ACCESS, args=struct.pack("<B", 1), transfer_length=0)
|
|
335
|
+
|
|
336
|
+
def _determine_model(self) -> str:
|
|
337
|
+
if (hasattr(self, "_model")) and (self._model is not None):
|
|
338
|
+
return self._model
|
|
339
|
+
data = self.flash_read(Addresses.BOOTROM_MAGIC, 4)
|
|
340
|
+
(magic,) = struct.unpack("<I", data)
|
|
341
|
+
return Model(magic & 0xf0ffffff)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def model(self) -> str:
|
|
345
|
+
return self._model
|
|
346
|
+
|
|
347
|
+
def _guess_flash_size(self) -> int:
|
|
348
|
+
if (hasattr(self, "_memory")) and (self._memory is not None):
|
|
349
|
+
return self._memory
|
|
350
|
+
FLASH_BASE = 0x10000000
|
|
351
|
+
PAGE_SIZE = 256
|
|
352
|
+
|
|
353
|
+
self.exclusive_access()
|
|
354
|
+
self.exit_xip()
|
|
355
|
+
|
|
356
|
+
pages = self.flash_read(FLASH_BASE, 2 * PAGE_SIZE)
|
|
357
|
+
|
|
358
|
+
if pages[:PAGE_SIZE] == pages[PAGE_SIZE:]:
|
|
359
|
+
if (pages[:PAGE_SIZE] == b'\xFF' * PAGE_SIZE):
|
|
360
|
+
self.flash_write(FLASH_BASE, b'\x50\x49\x43\x4F' + b'\xFF' * (PAGE_SIZE - 4))
|
|
361
|
+
return self._guess_flash_size()
|
|
362
|
+
|
|
363
|
+
candidates = [
|
|
364
|
+
8*1024*1024,
|
|
365
|
+
4*1024*1024,
|
|
366
|
+
2*1024*1024,
|
|
367
|
+
1*1024*1024,
|
|
368
|
+
512*1024,
|
|
369
|
+
256*1024,
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
for size in candidates:
|
|
373
|
+
new_pages = self.flash_read(FLASH_BASE + size, 2 * PAGE_SIZE)
|
|
374
|
+
if new_pages == pages:
|
|
375
|
+
continue
|
|
376
|
+
else:
|
|
377
|
+
return size * 2
|
|
378
|
+
|
|
379
|
+
return candidates[-1]
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def memory(self) -> int:
|
|
383
|
+
return self._memory
|
|
384
|
+
|
|
385
|
+
def get_info(self, info_type: InfoType, param0: int = 0, max_len: int = 32) -> bytes:
|
|
386
|
+
args = struct.pack("<IIII", info_type, param0, 0, 0)
|
|
387
|
+
data = self._send_command(CommandID.GET_INFO, args=args, transfer_length=max_len)
|
|
388
|
+
return data
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def build_diagnostic_partition_info(value: int) -> dict:
|
|
392
|
+
return {
|
|
393
|
+
'value': value,
|
|
394
|
+
'region_searched': bool(value & DiagnosticPartition.REGION_SEARCHED),
|
|
395
|
+
'invalid_block_loops': bool(value & DiagnosticPartition.INVALID_BLOCK_LOOPS),
|
|
396
|
+
'valid_block_loops': bool(value & DiagnosticPartition.VALID_BLOCK_LOOPS),
|
|
397
|
+
'valid_image_defaults': bool(value & DiagnosticPartition.VALID_IMAGE_DEFAULTS),
|
|
398
|
+
'has_partition_table': bool(value & DiagnosticPartition.HAS_PARTITION_TABLE),
|
|
399
|
+
'considered': bool(value & DiagnosticPartition.CONSIDERED),
|
|
400
|
+
'chosen': bool(value & DiagnosticPartition.CHOSEN),
|
|
401
|
+
'partition_table_matching_key_for_verify': bool(value & DiagnosticPartition.PARTITION_TABLE_MATCHING_KEY_FOR_VERIFY),
|
|
402
|
+
'partition_table_hash_for_verify': bool(value & DiagnosticPartition.PARTITION_TABLE_HASH_FOR_VERIFY),
|
|
403
|
+
'partition_table_verified_ok': bool(value & DiagnosticPartition.PARTITION_TABLE_VERIFIED_OK),
|
|
404
|
+
'image_def_matching_key_for_verify': bool(value & DiagnosticPartition.IMAGE_DEF_MATCHING_KEY_FOR_VERIFY),
|
|
405
|
+
'image_def_hash_for_verify': bool(value & DiagnosticPartition.IMAGE_DEF_HASH_FOR_VERIFY),
|
|
406
|
+
'image_def_verified_ok': bool(value & DiagnosticPartition.IMAGE_DEF_VERIFIED_OK),
|
|
407
|
+
'load_map_entries_loaded': bool(value & DiagnosticPartition.LOAD_MAP_ENTRIES_LOADED),
|
|
408
|
+
'image_launched': bool(value & DiagnosticPartition.IMAGE_LAUNCHED),
|
|
409
|
+
'image_condition_failures': bool(value & DiagnosticPartition.IMAGE_CONDITION_FAILURES),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
def get_info_sys(self, flags: SysInfoFlags = SysInfoFlags.CHIP_INFO | SysInfoFlags.CRITICAL | SysInfoFlags.CPU | SysInfoFlags.FLASH | SysInfoFlags.BOOT_RANDOM | SysInfoFlags.BOOT_INFO) -> dict:
|
|
413
|
+
data = self.get_info(InfoType.SYS, param0=flags, max_len=256)
|
|
414
|
+
if len(data) < 24:
|
|
415
|
+
raise PicoBootError("INFO_SYS response too short")
|
|
416
|
+
|
|
417
|
+
offset = 0
|
|
418
|
+
(count,rflags,) = struct.unpack_from("<II", data, offset)
|
|
419
|
+
offset += 8
|
|
420
|
+
ret = {}
|
|
421
|
+
if (rflags & SysInfoFlags.CHIP_INFO):
|
|
422
|
+
(chip_info, dev_id_low, dev_id_high) = struct.unpack_from("<III", data, offset)
|
|
423
|
+
offset += 12
|
|
424
|
+
ret['chip_info'] = {
|
|
425
|
+
'package_sel': chip_info,
|
|
426
|
+
'device_id_low': dev_id_low,
|
|
427
|
+
'device_id_high': dev_id_high,
|
|
428
|
+
}
|
|
429
|
+
if (rflags & SysInfoFlags.CRITICAL):
|
|
430
|
+
(critical_flags,) = struct.unpack_from("<I", data, offset)
|
|
431
|
+
offset += 4
|
|
432
|
+
ret['critical_flags'] = {
|
|
433
|
+
'value': critical_flags,
|
|
434
|
+
'secure_boot': bool(critical_flags & CriticalRegister.SECURE_BOOT),
|
|
435
|
+
'secure_debug_disable': bool(critical_flags & CriticalRegister.SECURE_DEBUG_DISABLE),
|
|
436
|
+
'debug_disable': bool(critical_flags & CriticalRegister.DEBUG_DISABLE),
|
|
437
|
+
'default_archsel': bool(critical_flags & CriticalRegister.DEFAULT_ARCHSEL),
|
|
438
|
+
'glitch_detector_enable': bool(critical_flags & CriticalRegister.GLITCH_DETECTOR_ENABLE),
|
|
439
|
+
'glitch_detector_sensitivity': (critical_flags & CriticalRegister.GLITCH_DETECTOR_SENS) >> 5,
|
|
440
|
+
'arm_disable': bool(critical_flags & CriticalRegister.ARM_DISABLE),
|
|
441
|
+
'riscv_disable': bool(critical_flags & CriticalRegister.RISCV_DISABLE),
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (rflags & SysInfoFlags.CPU):
|
|
445
|
+
(architecture,) = struct.unpack_from("<I", data, offset)
|
|
446
|
+
offset += 4
|
|
447
|
+
ret['architecture'] = architecture
|
|
448
|
+
if (rflags & SysInfoFlags.FLASH):
|
|
449
|
+
(flash_size, ) = struct.unpack_from("<I", data, offset)
|
|
450
|
+
offset += 4
|
|
451
|
+
bits1 = (flash_size & 0xF000) >> 12
|
|
452
|
+
bits0 = (flash_size & 0x0F00) >> 8
|
|
453
|
+
print(bits0, bits1)
|
|
454
|
+
ret['flash_size'] = {
|
|
455
|
+
'slot0': 4096 << bits0 if bits0 != 0 else 0,
|
|
456
|
+
'slot1': 4096 << bits1 if bits1 != 0 else 0,
|
|
457
|
+
'raw': flash_size,
|
|
458
|
+
}
|
|
459
|
+
if (rflags & SysInfoFlags.BOOT_RANDOM):
|
|
460
|
+
(boot_random0, boot_random1, boot_random2, boot_random3) = struct.unpack_from("<IIII", data, offset)
|
|
461
|
+
offset += 16
|
|
462
|
+
ret['boot_random'] = (boot_random0, boot_random1, boot_random2, boot_random3)
|
|
463
|
+
if (rflags & SysInfoFlags.BOOT_INFO):
|
|
464
|
+
(w0, w1, w2, w3) = struct.unpack_from("<IIII", data, offset)
|
|
465
|
+
offset += 16
|
|
466
|
+
d1 = (w1 & 0xFFFF0000) >> 16
|
|
467
|
+
d0 = (w1 & 0x0000FFFF)
|
|
468
|
+
ret['boot_info'] = {
|
|
469
|
+
'tbyb': uint_to_int((w0 & 0xFF000000) >> 24),
|
|
470
|
+
'recent_boot_partition': PartitionInfoType(uint_to_int((w0 & 0x00FF0000) >> 16)),
|
|
471
|
+
'boot_type_recent_boot': uint_to_int((w0 & 0x0000FF00) >> 8),
|
|
472
|
+
'recent_boot_diagnostic_partition': PartitionInfoType(uint_to_int((w0 & 0x000000FF))),
|
|
473
|
+
'recent_boot_diagnostic': uint_to_int((w1 & 0xFFFFFFFF)),
|
|
474
|
+
'last_reboot_param0': uint_to_int(w2),
|
|
475
|
+
'last_reboot_param1': uint_to_int(w3),
|
|
476
|
+
'diagnostic_slot1': PicoBoot.build_diagnostic_partition_info(d1),
|
|
477
|
+
'diagnostic_slot0': PicoBoot.build_diagnostic_partition_info(d0),
|
|
478
|
+
}
|
|
479
|
+
return ret
|
picoboot/utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
/*
|
|
3
|
+
* This file is part of the pypicoboot distribution (https://github.com/polhenarejos/pypicoboot).
|
|
4
|
+
* Copyright (c) 2025 Pol Henarejos.
|
|
5
|
+
*
|
|
6
|
+
* This program is free software: you can redistribute it and/or modify
|
|
7
|
+
* it under the terms of the GNU Affero General Public License as published by
|
|
8
|
+
* the Free Software Foundation, version 3.
|
|
9
|
+
*
|
|
10
|
+
* This program is distributed in the hope that it will be useful, but
|
|
11
|
+
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
13
|
+
* Affero General Public License for more details.
|
|
14
|
+
*
|
|
15
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def uint_to_int(value: int, bits: int = 8) -> int:
|
|
21
|
+
"""Interpret the unsigned integer `value` as a signed integer with `bits` bits."""
|
|
22
|
+
mask = (1 << bits) - 1
|
|
23
|
+
value &= mask # ensure value fits in `bits`
|
|
24
|
+
sign_bit = 1 << (bits - 1)
|
|
25
|
+
return value - (1 << bits) if (value & sign_bit) else value
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pypicoboot
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: Pico Boot for Python
|
|
5
|
+
Home-page: https://github.com/polhenarejos/pypicoboot
|
|
6
|
+
Author: Pol Henarejos
|
|
7
|
+
Author-email: pol.henarejos@cttc.es
|
|
8
|
+
License: AGPL
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Plugins
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 2
|
|
16
|
+
Classifier: Programming Language :: Python :: 2.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Topic :: System :: Systems Administration
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
Requires-Dist: setuptools
|
|
30
|
+
Requires-Dist: pyusb
|
|
31
|
+
Dynamic: author
|
|
32
|
+
Dynamic: author-email
|
|
33
|
+
Dynamic: classifier
|
|
34
|
+
Dynamic: description
|
|
35
|
+
Dynamic: description-content-type
|
|
36
|
+
Dynamic: home-page
|
|
37
|
+
Dynamic: license
|
|
38
|
+
Dynamic: requires-dist
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
Dynamic: summary
|
|
41
|
+
|
|
42
|
+
# pypicoboot
|
|
43
|
+
Pico Boot tools for Python
|
|
44
|
+
|
|
45
|
+
## Introduction
|
|
46
|
+
|
|
47
|
+
Pico Boot is a tool in Python to communicate with RP2040, RP2350 and RP2354 boards.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
pip install pypicoboot
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
pypicoboot can be used as a Python module (picoboot.py) or through command line (picotool.py).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
picoboot/__init__.py,sha256=cQPxH_qPx7ph9SQ-Z4Jc2YCU3SKYnDta6wzU-Q65suc,72
|
|
2
|
+
picoboot/_version.py,sha256=nmRHw4pFLiAYN7s_mmXwW9N1ukqIRnIMbHr1_sGD7Ow,800
|
|
3
|
+
picoboot/picoboot.py,sha256=UpIDkhhZ_0LOgjcbxiil-82zBsUAv6c3FFoERtotWnA,18778
|
|
4
|
+
picoboot/utils.py,sha256=SjpAoPZrgyD9Ws7NIFVBHt0zPOb8Lpu2_lsmfeT2rXE,1083
|
|
5
|
+
picoboot/core/__init__.py,sha256=fmoYRI4KbzhLjXblEs_H9zgdCs7oDuSKeNEG_-kMgYo,32
|
|
6
|
+
picoboot/core/enums.py,sha256=DFM5GVnHbzfr4eNd60ieRlRE9Ooy3NZXXjT06AnqvIE,455
|
|
7
|
+
picoboot/tools/picotool.py,sha256=e2xjIxP5UZn9yfjxUWkJXq6KQHyw4YsGbuQbTauo0-Q,94
|
|
8
|
+
pypicoboot-1.0.dist-info/METADATA,sha256=te2KhoiGZxJO8TTK1WR_XKIyb-KIZUR0dll1_LcXFNI,1745
|
|
9
|
+
pypicoboot-1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
pypicoboot-1.0.dist-info/top_level.txt,sha256=fmizLN8AlCiqKBjhQzVg-AdZ2p23V5I6X9wKEugRsII,38
|
|
11
|
+
pypicoboot-1.0.dist-info/RECORD,,
|