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 ADDED
@@ -0,0 +1,2 @@
1
+ from ._version import __version__
2
+ from .picoboot import PicoBoot, Model
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
@@ -0,0 +1,6 @@
1
+ from picoboot import PicoBoot
2
+
3
+ pb = PicoBoot.open()
4
+ data = pb.guess_flash_size()
5
+ print(data)
6
+
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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ picoboot
2
+ picoboot/core
3
+ picoboot/tools