rpprog 1.0.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.
rpprog/rpprog.py ADDED
@@ -0,0 +1,769 @@
1
+ #!/usr/bin/env python3
2
+ # ===================================================================================
3
+ # Project: rpprog - USB Programming Tool for Raspberry Pi RP2040 and RP2350
4
+ # Version: v1.0
5
+ # Year: 2026
6
+ # Author: Stefan Wagner
7
+ # Github: https://github.com/wagiminator
8
+ # License: MIT License
9
+ # ===================================================================================
10
+ #
11
+ # Description:
12
+ # ------------
13
+ # Simple Python tool for flashing Raspberry Pi RP2040 and RP2350 microcontrollers
14
+ # (e.g. Raspberry Pi Pico, Pico W, Pico 2, Pico 2 W) via USB using the PICOBOOT
15
+ # interface of the factory built-in USB bootloader (BOOTSEL mode). No drive has to
16
+ # be mounted. Supported file formats: BIN, UF2, HEX (Intel HEX) and ELF.
17
+ #
18
+ # The tool can also restart a running firmware into BOOTSEL mode, either via the
19
+ # reset interface of the Raspberry Pi Pico SDK (stdio_usb) or via a "1200 baud
20
+ # touch" of a USB serial port (e.g. MCU-Templates usb_cdc library, Arduino cores).
21
+ #
22
+ # References:
23
+ # -----------
24
+ # - Raspberry Pi picotool: https://github.com/raspberrypi/picotool
25
+ # - RP2040 and RP2350 datasheets (chapter "USB Bootloader", "PICOBOOT Interface")
26
+ # - Microsoft UF2: https://github.com/microsoft/uf2
27
+ #
28
+ # Dependencies:
29
+ # -------------
30
+ # - PyUSB
31
+ # - PySerial (only for the 1200 baud touch)
32
+ # - libusb-package (Windows only, provides the libusb DLL)
33
+ #
34
+ # Operating Instructions:
35
+ # -----------------------
36
+ # You need to install PyUSB and PySerial to use rpprog. Install them via
37
+ # "pip install pyusb pyserial". On Windows, also install the libusb backend via
38
+ # "pip install libusb-package".
39
+ #
40
+ # Linux users need permission to access the device. Run:
41
+ # echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="2e8a", ATTR{idProduct}=="0003", MODE="666"' | sudo tee /etc/udev/rules.d/99-rp2-boot.rules
42
+ # echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="2e8a", ATTR{idProduct}=="000f", MODE="666"' | sudo tee -a /etc/udev/rules.d/99-rp2-boot.rules
43
+ # sudo udevadm control --reload-rules
44
+ #
45
+ # On Windows, the PICOBOOT interface of the RP2040 needs the WinUSB driver. Install it
46
+ # with Zadig (https://zadig.akeo.ie/): click "Options" and "List All Devices", select
47
+ # "RP2 Boot (Interface 1)" and install the WinUSB driver. The RP2350 provides the
48
+ # Microsoft OS descriptors, so Windows usually installs the driver automatically.
49
+ #
50
+ # Hold the BOOTSEL button while connecting the board via USB (BOOTSEL mode). Run:
51
+ # - rpprog.py [-h] [-b] [-p PORT] [-e] [-f FLASH] [-a ADDR] [-r READ] [-s SIZE]
52
+ # [-x] [-n]
53
+ # -h, --help show help message and exit
54
+ # -b, --bootsel restart running firmware into BOOTSEL mode first
55
+ # -p PORT, --port PORT serial port for the 1200 baud touch (with -b)
56
+ # -e, --erase perform a whole chip erase
57
+ # -f FLASH, --flash FLASH write BIN/UF2/HEX/ELF file to flash and verify
58
+ # -a ADDR, --address ADDR flash address for BIN files (default: 0x10000000)
59
+ # -r READ, --read READ read flash and save it as BIN file
60
+ # -s SIZE, --size SIZE size for -e and -r (default: detected flash size)
61
+ # -x, --reboot reboot and start firmware (implied with -e and -f)
62
+ # -n, --noreboot stay in BOOTSEL mode after -e or -f
63
+ # Without action arguments, the chip and its bootloader are shown.
64
+ #
65
+ # - Example:
66
+ # python3 rpprog.py -f firmware.uf2
67
+ # python3 rpprog.py -b -f firmware.bin
68
+
69
+
70
+ import sys
71
+ import time
72
+ import struct
73
+ import argparse
74
+ import usb.core
75
+ import usb.util
76
+
77
+ # ===================================================================================
78
+ # Main Function
79
+ # ===================================================================================
80
+
81
+ def _main():
82
+ # Parse command line arguments
83
+ parser = argparse.ArgumentParser(description='Minimal command line interface for RP2040/RP2350 PICOBOOT')
84
+ parser.add_argument('-b', '--bootsel', action='store_true', help='restart running firmware into BOOTSEL mode first')
85
+ parser.add_argument('-p', '--port', help='serial port for the 1200 baud touch (with -b)')
86
+ parser.add_argument('-e', '--erase', action='store_true', help='perform a whole chip erase')
87
+ parser.add_argument('-f', '--flash', help='write BIN/UF2/HEX/ELF file to flash and verify')
88
+ parser.add_argument('-a', '--address', type=lambda x: int(x, 0), default=RP_FLASH_START, metavar='ADDR',
89
+ help='flash address for BIN files (default: 0x10000000)')
90
+ parser.add_argument('-r', '--read', help='read flash and save it as BIN file')
91
+ parser.add_argument('-s', '--size', type=lambda x: int(x, 0), help='size for -e and -r (default: detected flash size)')
92
+ parser.add_argument('-x', '--reboot', action='store_true', help='reboot and start firmware (implied with -e and -f)')
93
+ parser.add_argument('-n', '--noreboot', action='store_true', help='stay in BOOTSEL mode after -e or -f')
94
+ args = parser.parse_args(sys.argv[1:])
95
+
96
+ # Check arguments
97
+ if args.reboot and args.noreboot:
98
+ sys.stderr.write('ERROR: Options -x and -n exclude each other!\n')
99
+ sys.exit(1)
100
+ if args.size is not None and args.size <= 0:
101
+ sys.stderr.write('ERROR: Invalid size!\n')
102
+ sys.exit(1)
103
+
104
+ # Read firmware file
105
+ image = None
106
+ if args.flash is not None:
107
+ try:
108
+ image = FirmwareImage(args.flash, args.address)
109
+ except Exception as ex:
110
+ sys.stderr.write('ERROR: ' + str(ex) + '!\n')
111
+ sys.exit(1)
112
+
113
+ # Restart running firmware into BOOTSEL mode
114
+ if args.bootsel:
115
+ try:
116
+ if find_bootsel_device() is None:
117
+ print('Restarting firmware into BOOTSEL mode ...')
118
+ method = enter_bootsel(args.port)
119
+ print('SUCCESS: Restart requested via %s.' % method)
120
+ print('Waiting for USB bootloader ...')
121
+ if not wait_bootsel_device(RP_BOOTSEL_TIMEOUT):
122
+ raise Exception('USB bootloader did not appear')
123
+ else:
124
+ print('INFO: Device is already in BOOTSEL mode.')
125
+ except Exception as ex:
126
+ sys.stderr.write('ERROR: ' + str(ex) + '!\n')
127
+ sys.exit(1)
128
+
129
+ # Establish connection to USB bootloader
130
+ try:
131
+ print('Connecting to USB bootloader ...')
132
+ isp = Programmer()
133
+ print('SUCCESS: Found %s with bootrom v%d.' % (isp.chipname, isp.romversion))
134
+ except Exception as ex:
135
+ sys.stderr.write('ERROR: ' + str(ex) + '!\n')
136
+ sys.exit(1)
137
+
138
+ # Performing actions
139
+ try:
140
+ # Show chip info only (no action arguments)
141
+ if not any( (args.erase, args.flash, args.read, args.reboot) ):
142
+ for name, value in isp.info():
143
+ print('%-12s %s' % (name + ':', value))
144
+ isp.close()
145
+ print('DONE.')
146
+ sys.exit(0)
147
+
148
+ # Check image
149
+ if image is not None:
150
+ isp.checkimage(image)
151
+ if image.family is not None and image.family not in isp.families:
152
+ raise Exception('UF2 family %s does not match %s' % (uf2_family_name(image.family), isp.chipname))
153
+ for msg in image.warnings(isp.chip):
154
+ print('WARNING: ' + msg)
155
+
156
+ # Determine flash size (if needed)
157
+ size = args.size
158
+ if size is None and (args.erase or args.read):
159
+ print('Detecting flash size ...')
160
+ size = isp.flashsize()
161
+ if size:
162
+ print('SUCCESS: Flash size is %s.' % format_size(size))
163
+ else:
164
+ size = isp.defaultsize
165
+ print('INFO: Flash is empty, size cannot be detected, using %s (set size with -s).' % format_size(size))
166
+
167
+ # Perform chip erase
168
+ if args.erase:
169
+ print('Erasing %s of flash ...' % format_size(size))
170
+ with Progress(size, 'Erasing') as progress:
171
+ isp.erase(RP_FLASH_START, size, progress.update)
172
+ print('SUCCESS: Flash is erased.')
173
+
174
+ # Flash firmware image
175
+ if image is not None:
176
+ print('Flashing %s to %s (%d bytes in %d sectors) ...' % \
177
+ (args.flash, isp.chipname, image.datasize, len(image.sectors)))
178
+ total = len(image.sectors) * RP_SECTOR_SIZE
179
+ with Progress(total, 'Writing') as progress:
180
+ isp.writesectors(image.sectors, not args.erase, progress.update)
181
+ with Progress(total, 'Verifying') as progress:
182
+ isp.verifysectors(image.sectors, progress.update)
183
+ print('SUCCESS: %d bytes written and verified.' % image.datasize)
184
+
185
+ # Read flash
186
+ if args.read is not None:
187
+ print('Reading %s of flash ...' % format_size(size))
188
+ with Progress(size, 'Reading') as progress:
189
+ data = isp.read(RP_FLASH_START, size, progress.update)
190
+ with open(args.read, 'wb') as f: f.write(data)
191
+ print('SUCCESS: %d bytes written to %s.' % (len(data), args.read))
192
+
193
+ # Reboot and start firmware or stay in bootloader
194
+ if (args.reboot or args.erase or image is not None) and not args.noreboot:
195
+ print('Rebooting and starting firmware ...')
196
+ isp.reboot()
197
+ else:
198
+ isp.close()
199
+
200
+ except Exception as ex:
201
+ sys.stderr.write('ERROR: ' + str(ex) + '!\n')
202
+ isp.close()
203
+ sys.exit(1)
204
+
205
+ print('DONE.')
206
+ sys.exit(0)
207
+
208
+ # ===================================================================================
209
+ # Programmer Class
210
+ # ===================================================================================
211
+
212
+ class Programmer:
213
+ # Find device and connect to PICOBOOT interface
214
+ def __init__(self):
215
+ self.dev = find_bootsel_device()
216
+ if self.dev is None:
217
+ raise Exception('No RP2040/RP2350 in BOOTSEL mode found')
218
+
219
+ # Chip parameters
220
+ if self.dev.idProduct == RP2040_PRODUCT_ID:
221
+ self.chip = 'RP2040'
222
+ self.families = (UF2_FAMILY_RP2040,)
223
+ self.defaultsize = RP2040_DEFAULT_SIZE
224
+ else:
225
+ self.chip = 'RP2350'
226
+ self.families = (UF2_FAMILY_RP2350_ARM_S, UF2_FAMILY_RP2350_ARM_NS, UF2_FAMILY_RP2350_RISCV,
227
+ UF2_FAMILY_ABSOLUTE, UF2_FAMILY_DATA)
228
+ self.defaultsize = RP2350_DEFAULT_SIZE
229
+ self.chipname = self.chip
230
+
231
+ # Find PICOBOOT interface (vendor class, two bulk endpoints)
232
+ try:
233
+ cfg = self.dev.get_active_configuration()
234
+ except usb.core.USBError:
235
+ cfg = None
236
+ try:
237
+ if cfg is None:
238
+ self.dev.set_configuration()
239
+ cfg = self.dev.get_active_configuration()
240
+ except Exception:
241
+ raise Exception('Failed to access USB device. Check permissions')
242
+ self.intf = None
243
+ for intf in cfg:
244
+ if intf.bInterfaceClass == 0xff and intf.bNumEndpoints == 2:
245
+ self.intf = intf
246
+ if self.intf is None:
247
+ raise Exception('PICOBOOT interface not found (disabled by the firmware?)')
248
+ self.ifnum = self.intf.bInterfaceNumber
249
+ self.ep_out = usb.util.find_descriptor(self.intf, custom_match = lambda e: \
250
+ usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT)
251
+ self.ep_in = usb.util.find_descriptor(self.intf, custom_match = lambda e: \
252
+ usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN)
253
+
254
+ # Claim interface
255
+ try:
256
+ if self.dev.is_kernel_driver_active(self.ifnum):
257
+ self.dev.detach_kernel_driver(self.ifnum)
258
+ except Exception:
259
+ pass
260
+ try:
261
+ usb.util.claim_interface(self.dev, self.ifnum)
262
+ except Exception:
263
+ raise Exception('Failed to claim PICOBOOT interface. Check permissions/driver')
264
+
265
+ # Reset interface and get bootrom version
266
+ self.token = 1
267
+ self.xip = None # XIP state (None: unknown)
268
+ self.eject = False # drive ejected (exclusive access)
269
+ self.reset()
270
+ rom = self.read(RP_ROM_START + 0x10, 4)
271
+ if rom[0:2] != b'Mu':
272
+ raise Exception('Invalid bootrom signature')
273
+ self.romversion = rom[3]
274
+
275
+ # RP2350: get chip info (package, chip ID)
276
+ self.chipid = None
277
+ if self.chip == 'RP2350':
278
+ info = self.getsysinfo(SYS_INFO_CHIP_INFO)
279
+ if info[0] & SYS_INFO_CHIP_INFO:
280
+ self.chipname = 'RP2350' + ('A' if info[1] & 1 else 'B')
281
+ self.chipid = (info[3] << 32) | info[2]
282
+
283
+ # -------------------------------------------------------------------------------
284
+ # PICOBOOT Protocol
285
+ # -------------------------------------------------------------------------------
286
+
287
+ # Reset PICOBOOT interface (clear halt condition of the endpoints)
288
+ def reset(self):
289
+ for ep in (self.ep_in, self.ep_out):
290
+ try: self.dev.clear_halt(ep)
291
+ except Exception: pass
292
+ self.dev.ctrl_transfer(0x41, PICOBOOT_IF_RESET, 0, self.ifnum, None, RP_USB_TIMEOUT)
293
+
294
+ # Get status of the last command (returns status code)
295
+ def status(self):
296
+ try:
297
+ s = self.dev.ctrl_transfer(0xc1, PICOBOOT_IF_CMD_STATUS, 0, self.ifnum, 16, RP_USB_TIMEOUT)
298
+ return struct.unpack('<IIBB', bytes(s[:10]))[1]
299
+ except Exception:
300
+ return None
301
+
302
+ # Send command, transfer data and wait for acknowledge
303
+ def command(self, cmdid, args=b'', data=None, length=0, timeout=None):
304
+ if timeout is None: timeout = RP_USB_TIMEOUT
305
+ if data is not None: length = len(data)
306
+ cmd = struct.pack('<IIBBHI', PICOBOOT_MAGIC, self.token, cmdid, len(args), 0, length)
307
+ cmd += args + bytes(16 - len(args))
308
+ self.token += 1
309
+ reply = None
310
+ try:
311
+ self.ep_out.write(cmd, RP_USB_TIMEOUT)
312
+ if length:
313
+ if cmdid & 0x80:
314
+ reply = bytes(self.ep_in.read(length, timeout))
315
+ if len(reply) != length:
316
+ raise Exception('Data transfer incomplete')
317
+ else:
318
+ self.ep_out.write(data, timeout)
319
+ # Acknowledge in opposite direction
320
+ if cmdid & 0x80:
321
+ self.ep_out.write(b'\x00', timeout)
322
+ else:
323
+ self.ep_in.read(64, timeout)
324
+ except Exception as ex:
325
+ code = self.status()
326
+ try: self.reset()
327
+ except Exception: pass
328
+ reason = PICOBOOT_STATUS.get(code, str(ex))
329
+ raise Exception('PICOBOOT command 0x%02x failed (%s)' % (cmdid, reason))
330
+ if cmdid not in (PC_READ, PC_WRITE, PC_EXIT_XIP):
331
+ self.xip = None # other commands may change XIP state
332
+ return reply
333
+
334
+ # Exclusive access (0: not exclusive, 1: exclusive, 2: exclusive and eject drive)
335
+ def exclusive(self, mode):
336
+ if mode == EXCLUSIVE_AND_EJECT and self.eject:
337
+ return
338
+ self.command(PC_EXCLUSIVE_ACCESS, bytes((mode,)))
339
+ self.eject = (mode == EXCLUSIVE_AND_EJECT)
340
+
341
+ # Leave XIP mode (necessary before flash operations; the mass storage interface
342
+ # may change the XIP state as long as the drive is not ejected)
343
+ def exitxip(self):
344
+ if self.xip is not False or not self.eject:
345
+ self.command(PC_EXIT_XIP)
346
+ self.xip = False
347
+
348
+ # Get system info (RP2350 only), returns list of 32-bit words
349
+ def getsysinfo(self, flags):
350
+ reply = self.command(PC_GET_INFO, struct.pack('<BBHIII', PICOBOOT_GET_INFO_SYS, 0, 0, flags, 0, 0),
351
+ length=256)
352
+ return list(struct.unpack('<64I', reply))
353
+
354
+ # -------------------------------------------------------------------------------
355
+ # Memory Operations
356
+ # -------------------------------------------------------------------------------
357
+
358
+ # Read memory (ROM, RAM or flash)
359
+ def read(self, addr, size, progress=None):
360
+ if is_flash(addr):
361
+ self.exitxip()
362
+ data = bytearray()
363
+ while len(data) < size:
364
+ chunk = min(RP_SECTOR_SIZE, size - len(data))
365
+ data += self.command(PC_READ, struct.pack('<II', addr + len(data), chunk), length=chunk)
366
+ if progress: progress(chunk)
367
+ return bytes(data)
368
+
369
+ # Erase flash (address and size are aligned to sectors)
370
+ def erase(self, addr, size, progress=None):
371
+ start = addr & ~(RP_SECTOR_SIZE - 1)
372
+ end = (addr + size + RP_SECTOR_SIZE - 1) & ~(RP_SECTOR_SIZE - 1)
373
+ self.exclusive(EXCLUSIVE_AND_EJECT)
374
+ self.exitxip()
375
+ while start < end:
376
+ if (start % RP_BLOCK_SIZE == 0) and (end - start >= RP_BLOCK_SIZE):
377
+ chunk = RP_BLOCK_SIZE
378
+ else:
379
+ chunk = RP_SECTOR_SIZE
380
+ self.command(PC_FLASH_ERASE, struct.pack('<II', start, chunk), timeout=RP_USB_TIMEOUT_ERASE)
381
+ start += chunk
382
+ if progress: progress(chunk)
383
+
384
+ # Erase (optional) and write flash sectors (dict: address -> 4096 bytes)
385
+ def writesectors(self, sectors, erase=True, progress=None):
386
+ self.exclusive(EXCLUSIVE_AND_EJECT)
387
+ self.exitxip()
388
+ for addr in sorted(sectors):
389
+ if erase:
390
+ self.command(PC_FLASH_ERASE, struct.pack('<II', addr, RP_SECTOR_SIZE), timeout=RP_USB_TIMEOUT_ERASE)
391
+ self.command(PC_WRITE, struct.pack('<II', addr, RP_SECTOR_SIZE), data=sectors[addr],
392
+ timeout=RP_USB_TIMEOUT_ERASE)
393
+ if progress: progress(RP_SECTOR_SIZE)
394
+
395
+ # Verify flash sectors
396
+ def verifysectors(self, sectors, progress=None):
397
+ for addr in sorted(sectors):
398
+ if self.read(addr, RP_SECTOR_SIZE) != sectors[addr]:
399
+ raise Exception('Verification failed at 0x%08x' % addr)
400
+ if progress: progress(RP_SECTOR_SIZE)
401
+
402
+ # Detect flash size by searching for mirrored data (returns 0 if flash is empty)
403
+ def flashsize(self):
404
+ first = self.read(RP_FLASH_START, 2 * RP_PAGE_SIZE)
405
+ if first[:RP_PAGE_SIZE] == first[RP_PAGE_SIZE:]:
406
+ return 0
407
+ size = RP_FLASH_MAX_SIZE // 2
408
+ while size >= RP_SECTOR_SIZE:
409
+ if self.read(RP_FLASH_START + size, 2 * RP_PAGE_SIZE) != first:
410
+ break
411
+ size //= 2
412
+ return size * 2
413
+
414
+ # Check if firmware image fits into the XIP address range
415
+ def checkimage(self, image):
416
+ for addr in image.sectors:
417
+ if not is_flash(addr) or not is_flash(addr + RP_SECTOR_SIZE - 1):
418
+ raise Exception('Address 0x%08x is outside of the flash address range' % addr)
419
+
420
+ # -------------------------------------------------------------------------------
421
+ # Info, Reboot, Close
422
+ # -------------------------------------------------------------------------------
423
+
424
+ # Get chip info as list of (name, value)
425
+ def info(self):
426
+ result = [('Chip', self.chipname), ('Bootrom', 'v%d' % self.romversion)]
427
+ if self.chipid is not None:
428
+ result.append(('Chip ID', '%016X' % self.chipid))
429
+ try:
430
+ serial = usb.util.get_string(self.dev, self.dev.iSerialNumber)
431
+ result.append(('USB serial', serial))
432
+ except Exception:
433
+ pass
434
+ size = self.flashsize()
435
+ result.append(('Flash', format_size(size) + ' (detected)' if size else 'empty (size cannot be detected)'))
436
+ if size:
437
+ result.append(('Firmware', describe_firmware(self.read(RP_FLASH_START, RP_SECTOR_SIZE), self.chip)))
438
+ return result
439
+
440
+ # Reboot and start firmware from flash
441
+ def reboot(self):
442
+ try:
443
+ if self.chip == 'RP2040':
444
+ self.command(PC_REBOOT, struct.pack('<III', 0, RP2040_SRAM_END, RP_REBOOT_DELAY))
445
+ else:
446
+ self.command(PC_REBOOT2, struct.pack('<IIII', REBOOT2_FLAG_REBOOT_TYPE_FLASH_UPDATE,
447
+ RP_REBOOT_DELAY, RP_FLASH_START, 0))
448
+ except Exception:
449
+ pass
450
+ self.close()
451
+
452
+ # Release USB device (and the drive, if exclusive access was requested)
453
+ def close(self):
454
+ try: self.exclusive(NOT_EXCLUSIVE)
455
+ except Exception: pass
456
+ try: usb.util.release_interface(self.dev, self.ifnum)
457
+ except Exception: pass
458
+ try: usb.util.dispose_resources(self.dev)
459
+ except Exception: pass
460
+
461
+ # ===================================================================================
462
+ # Firmware Image Class (BIN, UF2, HEX, ELF)
463
+ # ===================================================================================
464
+
465
+ class FirmwareImage:
466
+ def __init__(self, filename, binaddr=None):
467
+ if binaddr is None: binaddr = RP_FLASH_START
468
+ with open(filename, 'rb') as f: raw = f.read()
469
+ if len(raw) == 0:
470
+ raise Exception('File is empty')
471
+ self.family = None
472
+ self.chunks = [] # list of (address, data)
473
+ name = filename.lower()
474
+ if raw[:4] == UF2_MAGIC_START0.to_bytes(4, 'little') or name.endswith('.uf2'):
475
+ self.parseuf2(raw)
476
+ elif raw[:4] == b'\x7fELF':
477
+ self.parseelf(raw)
478
+ elif name.endswith('.hex') or name.endswith('.ihex'):
479
+ self.parsehex(raw)
480
+ else:
481
+ self.chunks.append((binaddr, raw))
482
+ if not self.chunks:
483
+ raise Exception('File contains no data for the flash')
484
+
485
+ # Build sectors (4096 bytes, unused bytes are 0xff)
486
+ self.sectors = {}
487
+ self.datasize = 0
488
+ for addr, data in self.chunks:
489
+ self.datasize += len(data)
490
+ for i in range(len(data)):
491
+ a = addr + i
492
+ s = a & ~(RP_SECTOR_SIZE - 1)
493
+ if s not in self.sectors:
494
+ self.sectors[s] = bytearray(b'\xff' * RP_SECTOR_SIZE)
495
+ self.sectors[s][a - s] = data[i]
496
+ for s in self.sectors:
497
+ self.sectors[s] = bytes(self.sectors[s])
498
+
499
+ # UF2 file: 512-byte blocks with 256 bytes payload each
500
+ def parseuf2(self, raw):
501
+ if len(raw) % 512:
502
+ raise Exception('Invalid UF2 file size')
503
+ for i in range(0, len(raw), 512):
504
+ block = raw[i:i+512]
505
+ m0, m1, flags, addr, size, blockno, numblocks, family = struct.unpack('<8I', block[:32])
506
+ mend = struct.unpack('<I', block[508:512])[0]
507
+ if m0 != UF2_MAGIC_START0 or m1 != UF2_MAGIC_START1 or mend != UF2_MAGIC_END:
508
+ raise Exception('Invalid UF2 block %d' % (i // 512))
509
+ if flags & UF2_FLAG_NOT_MAIN_FLASH:
510
+ continue
511
+ if flags & UF2_FLAG_FAMILY_ID_PRESENT:
512
+ if family == UF2_FAMILY_ABSOLUTE and addr == UF2_ABS_BLOCK_ADDR:
513
+ continue # RP2350-E10 workaround block
514
+ if self.family is None and family not in (UF2_FAMILY_ABSOLUTE, UF2_FAMILY_DATA):
515
+ self.family = family
516
+ if size > 476:
517
+ raise Exception('Invalid payload size in UF2 block %d' % (i // 512))
518
+ self.chunks.append((addr, block[32:32+size]))
519
+
520
+ # Intel HEX file (record types 00, 01, 02, 04; 03 and 05 are ignored)
521
+ def parsehex(self, raw):
522
+ base = 0
523
+ for line in raw.decode('ascii', errors='replace').splitlines():
524
+ line = line.strip()
525
+ if not line: continue
526
+ if line[0] != ':':
527
+ raise Exception('Invalid HEX file')
528
+ rec = bytes.fromhex(line[1:])
529
+ if len(rec) < 5 or len(rec) != rec[0] + 5 or (sum(rec) & 0xff):
530
+ raise Exception('Invalid HEX record: %s' % line)
531
+ addr, rtype, data = (rec[1] << 8) | rec[2], rec[3], rec[4:-1]
532
+ if rtype == 0x00: self.chunks.append((base + addr, data))
533
+ elif rtype == 0x01: break
534
+ elif rtype == 0x02: base = ((data[0] << 8) | data[1]) << 4
535
+ elif rtype == 0x04: base = ((data[0] << 8) | data[1]) << 16
536
+
537
+ # ELF file (32-bit little endian, loadable segments at their physical address)
538
+ def parseelf(self, raw):
539
+ if raw[4] != 1 or raw[5] != 1:
540
+ raise Exception('Only 32-bit little endian ELF files are supported')
541
+ phoff, = struct.unpack('<I', raw[28:32])
542
+ phentsize, phnum = struct.unpack('<HH', raw[42:46])
543
+ for i in range(phnum):
544
+ p = phoff + i * phentsize
545
+ ptype, offset, vaddr, paddr, filesz = struct.unpack('<5I', raw[p:p+20])
546
+ if ptype == 1 and filesz > 0: # PT_LOAD
547
+ self.chunks.append((paddr, raw[offset:offset+filesz]))
548
+
549
+ # Check for typical problems of the image
550
+ def warnings(self, chip):
551
+ result = []
552
+ first = self.sectors.get(RP_FLASH_START)
553
+ if first is None:
554
+ result.append('Image does not start at 0x10000000, the chip may not boot')
555
+ elif chip == 'RP2040' and not rp2040_boot2_valid(first):
556
+ result.append('No valid second stage bootloader (boot2) found, the RP2040 will not boot')
557
+ elif chip == 'RP2350' and RP2350_BLOCK_MARKER not in first:
558
+ result.append('No IMAGE_DEF block found in the first 4KB, the RP2350 will not boot')
559
+ return result
560
+
561
+ # ===================================================================================
562
+ # Helper Functions
563
+ # ===================================================================================
564
+
565
+ # Get libusb backend (libusb-package provides the libusb DLL on Windows)
566
+ def _backend():
567
+ try:
568
+ import libusb_package
569
+ return libusb_package.get_libusb1_backend()
570
+ except ImportError:
571
+ return None
572
+
573
+ # Find device in BOOTSEL mode
574
+ def find_bootsel_device():
575
+ for pid in (RP2040_PRODUCT_ID, RP2350_PRODUCT_ID):
576
+ dev = usb.core.find(idVendor = RP_VENDOR_ID, idProduct = pid, backend = _backend())
577
+ if dev is not None:
578
+ return dev
579
+ return None
580
+
581
+ # Wait for device in BOOTSEL mode
582
+ def wait_bootsel_device(timeout):
583
+ end = time.time() + timeout
584
+ while time.time() < end:
585
+ if find_bootsel_device() is not None:
586
+ time.sleep(0.5) # let the OS finish enumeration
587
+ return True
588
+ time.sleep(0.2)
589
+ return False
590
+
591
+ # Restart running firmware into BOOTSEL mode, returns used method
592
+ def enter_bootsel(port=None):
593
+ # Pico SDK reset interface (vendor class, subclass 0x00, protocol 0x01)
594
+ if port is None:
595
+ for dev in usb.core.find(find_all = True, backend = _backend()):
596
+ try:
597
+ for cfg in dev:
598
+ for intf in cfg:
599
+ if intf.bInterfaceClass == 0xff and intf.bInterfaceSubClass == RESET_INTERFACE_SUBCLASS \
600
+ and intf.bInterfaceProtocol == RESET_INTERFACE_PROTOCOL:
601
+ try:
602
+ dev.ctrl_transfer(0x21, RESET_REQUEST_BOOTSEL, 0, intf.bInterfaceNumber, None, 2000)
603
+ except Exception:
604
+ pass # device may reset before answering
605
+ return 'reset interface of %04x:%04x' % (dev.idVendor, dev.idProduct)
606
+ except Exception:
607
+ continue
608
+
609
+ # 1200 baud touch on a USB serial port
610
+ try:
611
+ import serial
612
+ from serial.tools.list_ports import comports
613
+ except ImportError:
614
+ raise Exception('No reset interface found and PySerial is not installed')
615
+ if port is None:
616
+ candidates = [p.device for p in comports() if (p.vid, p.pid) in RP_SERIAL_IDS or p.vid == RP_VENDOR_ID]
617
+ if len(candidates) == 0:
618
+ raise Exception('No running firmware with reset interface or USB serial port found (set port with -p)')
619
+ if len(candidates) > 1:
620
+ raise Exception('Several serial ports found (%s), select one with -p' % ', '.join(candidates))
621
+ port = candidates[0]
622
+ try:
623
+ ser = serial.Serial(port, 1200)
624
+ ser.dtr = False
625
+ time.sleep(0.1)
626
+ ser.close()
627
+ except Exception as ex:
628
+ raise Exception('Failed to open serial port %s (%s)' % (port, str(ex)))
629
+ return '1200 baud touch on ' + port
630
+
631
+ # Check if address is in the XIP flash range
632
+ def is_flash(addr):
633
+ return RP_FLASH_START <= addr < RP_FLASH_START + RP_FLASH_MAX_SIZE
634
+
635
+ # CRC-32/MPEG-2 (used by the RP2040 boot2 checksum)
636
+ def crc32_mpeg2(data):
637
+ crc = 0xffffffff
638
+ for b in data:
639
+ crc ^= b << 24
640
+ for _ in range(8):
641
+ crc = ((crc << 1) ^ 0x04c11db7) & 0xffffffff if crc & 0x80000000 else (crc << 1) & 0xffffffff
642
+ return crc
643
+
644
+ # Check boot2 checksum of the RP2040 (first 252 bytes, CRC in bytes 252..255)
645
+ def rp2040_boot2_valid(sector):
646
+ return crc32_mpeg2(sector[:252]) == struct.unpack('<I', sector[252:256])[0]
647
+
648
+ # Short description of the firmware in the first flash sector
649
+ def describe_firmware(sector, chip):
650
+ if sector == b'\xff' * len(sector):
651
+ return 'none (first sector is empty)'
652
+ if chip == 'RP2040':
653
+ return 'valid boot2 found' if rp2040_boot2_valid(sector) else 'no valid boot2 (will not boot)'
654
+ return 'IMAGE_DEF block found' if RP2350_BLOCK_MARKER in sector else 'no IMAGE_DEF block in the first 4KB'
655
+
656
+ # Format size in bytes as string
657
+ def format_size(size):
658
+ if size >= 1024 * 1024 and size % (1024 * 1024) == 0: return '%d MB' % (size // (1024 * 1024))
659
+ if size >= 1024 and size % 1024 == 0: return '%d KB' % (size // 1024)
660
+ return '%d bytes' % size
661
+
662
+ # Name of UF2 family ID
663
+ def uf2_family_name(family):
664
+ return UF2_FAMILY_NAMES.get(family, '0x%08x' % family)
665
+
666
+ # Simple progress bar
667
+ class Progress:
668
+ def __init__(self, total, desc):
669
+ self.total = max(total, 1)
670
+ self.done = 0
671
+ self.desc = desc
672
+ self.show()
673
+ def update(self, n):
674
+ self.done = min(self.done + n, self.total)
675
+ self.show()
676
+ def show(self):
677
+ pct = self.done * 100 // self.total
678
+ bar = '#' * (pct // 4)
679
+ sys.stdout.write('\r%-10s [%-25s] %3d%%' % (self.desc, bar, pct))
680
+ sys.stdout.flush()
681
+ def __enter__(self):
682
+ return self
683
+ def __exit__(self, exc_type, exc, tb):
684
+ sys.stdout.write('\n')
685
+ sys.stdout.flush()
686
+
687
+ # ===================================================================================
688
+ # Protocol Constants
689
+ # ===================================================================================
690
+
691
+ RP_VENDOR_ID = 0x2e8a # Raspberry Pi
692
+ RP2040_PRODUCT_ID = 0x0003 # RP2040 USB bootloader
693
+ RP2350_PRODUCT_ID = 0x000f # RP2350 USB bootloader
694
+ RP_SERIAL_IDS = ((0x16c0, 0x27dd),) # USB CDC of the MCU-Templates (1200 baud touch)
695
+
696
+ RP_USB_TIMEOUT = 3000 # timeout for USB operations (ms)
697
+ RP_USB_TIMEOUT_ERASE = 10000 # timeout for flash erase and write (ms)
698
+ RP_BOOTSEL_TIMEOUT = 10 # time to wait for the USB bootloader after restart (s)
699
+ RP_REBOOT_DELAY = 500 # delay before reboot (ms)
700
+
701
+ RP_ROM_START = 0x00000000 # boot ROM
702
+ RP_FLASH_START = 0x10000000 # flash (XIP)
703
+ RP_FLASH_MAX_SIZE = 0x01000000 # max addressable flash size (16 MB)
704
+ RP2040_SRAM_END = 0x20042000 # end of SRAM (RP2040)
705
+ RP_PAGE_SIZE = 256 # flash page size (programming)
706
+ RP_SECTOR_SIZE = 4096 # flash sector size (erasing)
707
+ RP_BLOCK_SIZE = 65536 # flash block size (fast erase)
708
+ RP2040_DEFAULT_SIZE = 2 * 1024 * 1024 # default flash size (Raspberry Pi Pico)
709
+ RP2350_DEFAULT_SIZE = 4 * 1024 * 1024 # default flash size (Raspberry Pi Pico 2)
710
+ RP2350_BLOCK_MARKER = b'\xd3\xde\xff\xff' # PICOBIN_BLOCK_MARKER_START (0xffffded3)
711
+
712
+ PICOBOOT_MAGIC = 0x431fd10b
713
+ PICOBOOT_IF_RESET = 0x41
714
+ PICOBOOT_IF_CMD_STATUS = 0x42
715
+
716
+ PC_EXCLUSIVE_ACCESS = 0x01
717
+ PC_REBOOT = 0x02 # RP2040 only
718
+ PC_FLASH_ERASE = 0x03
719
+ PC_READ = 0x84
720
+ PC_WRITE = 0x05
721
+ PC_EXIT_XIP = 0x06
722
+ PC_ENTER_CMD_XIP = 0x07
723
+ PC_REBOOT2 = 0x0a # RP2350 only
724
+ PC_GET_INFO = 0x8b # RP2350 only
725
+
726
+ NOT_EXCLUSIVE = 0
727
+ EXCLUSIVE = 1
728
+ EXCLUSIVE_AND_EJECT = 2
729
+
730
+ PICOBOOT_GET_INFO_SYS = 1
731
+ SYS_INFO_CHIP_INFO = 0x0001
732
+ REBOOT2_FLAG_REBOOT_TYPE_FLASH_UPDATE = 0x4
733
+
734
+ RESET_INTERFACE_SUBCLASS = 0x00
735
+ RESET_INTERFACE_PROTOCOL = 0x01
736
+ RESET_REQUEST_BOOTSEL = 0x01
737
+
738
+ PICOBOOT_STATUS = {
739
+ 0: 'ok', 1: 'unknown command', 2: 'invalid command length', 3: 'invalid transfer length',
740
+ 4: 'invalid address', 5: 'bad alignment', 6: 'interleaved write', 7: 'rebooting',
741
+ 8: 'unknown error', 9: 'invalid state', 10: 'not permitted', 11: 'invalid argument',
742
+ 12: 'buffer too small', 13: 'precondition not met', 14: 'modified data', 15: 'invalid data',
743
+ 16: 'not found', 17: 'unsupported modification'
744
+ }
745
+
746
+ UF2_MAGIC_START0 = 0x0a324655
747
+ UF2_MAGIC_START1 = 0x9e5d5157
748
+ UF2_MAGIC_END = 0x0ab16f30
749
+ UF2_FLAG_NOT_MAIN_FLASH = 0x00000001
750
+ UF2_FLAG_FAMILY_ID_PRESENT = 0x00002000
751
+ UF2_ABS_BLOCK_ADDR = 0x10ffff00 # address of the RP2350-E10 workaround block
752
+
753
+ UF2_FAMILY_RP2040 = 0xe48bff56
754
+ UF2_FAMILY_ABSOLUTE = 0xe48bff57
755
+ UF2_FAMILY_DATA = 0xe48bff58
756
+ UF2_FAMILY_RP2350_ARM_S = 0xe48bff59
757
+ UF2_FAMILY_RP2350_RISCV = 0xe48bff5a
758
+ UF2_FAMILY_RP2350_ARM_NS = 0xe48bff5b
759
+
760
+ UF2_FAMILY_NAMES = {
761
+ UF2_FAMILY_RP2040: 'rp2040', UF2_FAMILY_ABSOLUTE: 'absolute', UF2_FAMILY_DATA: 'data',
762
+ UF2_FAMILY_RP2350_ARM_S: 'rp2350-arm-s', UF2_FAMILY_RP2350_RISCV: 'rp2350-riscv',
763
+ UF2_FAMILY_RP2350_ARM_NS: 'rp2350-arm-ns'
764
+ }
765
+
766
+ # ===================================================================================
767
+
768
+ if __name__ == "__main__":
769
+ _main()
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: rpprog
3
+ Version: 1.0.0
4
+ Summary: USB Programming Tool for Raspberry Pi RP2040 and RP2350
5
+ Author-email: Stefan Wagner <wagiminator@web.de>
6
+ Maintainer-email: Stefan Wagner <wagiminator@web.de>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 Stefan Wagner
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Project-URL: Homepage, https://wagiminator.github.io/
29
+ Project-URL: Repository, https://github.com/wagiminator/MCU-Flash-Tools
30
+ Project-URL: Issues, https://github.com/wagiminator/MCU-Flash-Tools/issues
31
+ Keywords: ISP,PICOBOOT,RP2040,RP2350,Raspberry Pi Pico,Pico 2,UF2,microcontroller
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Intended Audience :: Developers
37
+ Classifier: Topic :: Software Development :: Embedded Systems
38
+ Requires-Python: >=3.6
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: pyusb
42
+ Requires-Dist: pyserial
43
+ Requires-Dist: libusb-package; platform_system == "Windows"
44
+ Dynamic: license-file
45
+
46
+ # USB Programming Tool for Raspberry Pi RP2040 and RP2350
47
+ ## Description
48
+ With this tool, Raspberry Pi RP2040 and RP2350 microcontrollers (e.g. on the Raspberry Pi Pico, Pico W, Pico 2 and Pico 2 W) can be flashed via USB using the PICOBOOT interface of the factory built-in USB bootloader (BOOTSEL mode). In contrast to drag-and-drop, the USB drive does not need to be mounted. Firmware can be written from BIN, UF2, Intel HEX and ELF files and is verified afterwards. In addition, the flash can be erased and read, and a running firmware can be restarted into BOOTSEL mode.
49
+
50
+ Supported microcontrollers: RP2040, RP2350A, RP2350B (Arm and RISC-V firmware), RP2354A/B.
51
+
52
+ ## Preparations
53
+ On Linux you do not need to install a driver. However, by default Linux will not expose enough permission to upload your code. In order to fix this, open a terminal and run the following commands:
54
+
55
+ ```
56
+ echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="2e8a", ATTR{idProduct}=="0003", MODE="666"' | sudo tee /etc/udev/rules.d/99-rp2-boot.rules
57
+ echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="2e8a", ATTR{idProduct}=="000f", MODE="666"' | sudo tee -a /etc/udev/rules.d/99-rp2-boot.rules
58
+ sudo udevadm control --reload-rules
59
+ ```
60
+
61
+ On Windows, the PICOBOOT interface of the RP2040 needs the WinUSB driver. Install it with the [Zadig](https://zadig.akeo.ie/) tool: click "Options" -> "List All Devices", select "RP2 Boot (Interface 1)" and install the WinUSB driver. The board must be connected in BOOTSEL mode. The RP2350 provides the necessary Microsoft OS descriptors, so Windows usually installs the driver automatically.
62
+
63
+ ## Installation
64
+ Ensure that the [prerequisites](https://packaging.python.org/en/latest/tutorials/installing-packages/) for installing Python packages are met. Then execute the following command in the command line:
65
+
66
+ ```
67
+ pip install rpprog
68
+ ```
69
+
70
+ ## Usage
71
+ To enter the USB bootloader (BOOTSEL mode), hold the BOOTSEL button while connecting the board to the USB port of your PC, then release the button. Alternatively, `-b` restarts a running firmware into BOOTSEL mode, either via the reset interface of the Raspberry Pi Pico SDK (stdio_usb) or via a "1200 baud touch" of a USB serial port (e.g. MCU-Templates usb_cdc library, Arduino cores). If several serial ports are present, select the port with `-p`.
72
+
73
+ The flash size is detected automatically by searching for mirrored data, which requires a non-empty flash. If the flash is empty, the default size of the Raspberry Pi Pico (2MB) or Pico 2 (4MB) is used for `-e` and `-r`; use `-s` to set another size. Before writing, rpprog checks whether the image contains a valid second stage bootloader (RP2040) or an IMAGE_DEF block (RP2350) and shows a warning if not. UF2 files of the wrong chip family are rejected.
74
+
75
+ ```
76
+ Usage: rpprog [-h] [-b] [-p PORT] [-e] [-f FLASH] [-a ADDR] [-r READ]
77
+ [-s SIZE] [-x] [-n]
78
+
79
+ Optional arguments:
80
+ -h, --help show this help message and exit
81
+ -b, --bootsel restart running firmware into BOOTSEL mode first
82
+ -p PORT, --port PORT serial port for the 1200 baud touch (with -b)
83
+ -e, --erase perform a whole chip erase
84
+ -f FLASH, --flash FLASH write BIN/UF2/HEX/ELF file to flash and verify
85
+ -a ADDR, --address ADDR flash address for BIN files (default: 0x10000000)
86
+ -r READ, --read READ read flash and save it as BIN file
87
+ -s SIZE, --size SIZE size for -e and -r (default: detected flash size)
88
+ -x, --reboot reboot and start firmware (implied with -e and -f)
89
+ -n, --noreboot stay in BOOTSEL mode after -e or -f
90
+
91
+ Without action arguments, rpprog shows the chip, its bootrom version,
92
+ the flash size and whether a bootable firmware is present.
93
+
94
+ Examples:
95
+ rpprog -f firmware.uf2
96
+ rpprog -b -f firmware.bin
97
+ rpprog -r backup.bin
98
+ ```
99
+
100
+ ## Links
101
+ - [MCU Flash Tools](https://github.com/wagiminator/MCU-Flash-Tools)
102
+ - [MCU Templates](https://github.com/wagiminator/MCU-Templates)
103
+ - [MCU Development Boards](https://github.com/wagiminator/Development-Boards)
104
+ - [Raspberry Pi picotool](https://github.com/raspberrypi/picotool)
@@ -0,0 +1,7 @@
1
+ rpprog/rpprog.py,sha256=d22W48MABB-dTAAVIFtCuBIyF7z0UBV746q4TqYnhTk,33811
2
+ rpprog-1.0.0.dist-info/licenses/LICENSE,sha256=1IE6unKx3TaOsuZhH_JE4fFNwtN-oYS0zdhtPL2b4y0,1089
3
+ rpprog-1.0.0.dist-info/METADATA,sha256=hIWpYB-yctEWuswJfcyOKErD1L29Ea2B6m7vh3wi5jE,6334
4
+ rpprog-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
5
+ rpprog-1.0.0.dist-info/entry_points.txt,sha256=tg9dSmGbe9aOvkEb5ASzf9boiFHis3Kpl923BLkEX5Y,47
6
+ rpprog-1.0.0.dist-info/top_level.txt,sha256=_LyNmTZkaIc-gNhfamk8cTNdCxur1_hGLpouciWkDS8,7
7
+ rpprog-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ rpprog = rpprog.rpprog:_main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stefan Wagner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rpprog