multiboot 0.9.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.
multiboot/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """GBA multiboot ROM uploader package."""
multiboot/cli.py ADDED
@@ -0,0 +1,290 @@
1
+ """GBA multiboot ROM uploader CLI.
2
+
3
+ Based on upload.py from https://github.com/jojolebarjos/gba-multiboot
4
+ See LICENSE for original license details.
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ import time
10
+
11
+ import serial
12
+ import serial.tools.list_ports
13
+ from rich.console import Console
14
+ from rich.progress import (
15
+ BarColumn,
16
+ Progress,
17
+ SpinnerColumn,
18
+ TaskProgressColumn,
19
+ TextColumn,
20
+ )
21
+
22
+ console = Console()
23
+
24
+
25
+ PICO_VID = 0x2E8A
26
+ PICO_PID = 0x000A
27
+
28
+
29
+ def xfer32(port, output):
30
+ """Exchange 32 bit payloads with the GBA attached to the given serial port."""
31
+ # Send bytes
32
+ output_bytes = bytes(
33
+ (
34
+ output & 0xFF,
35
+ (output >> 8) & 0xFF,
36
+ (output >> 16) & 0xFF,
37
+ (output >> 24) & 0xFF,
38
+ )
39
+ )
40
+ port.write(output_bytes)
41
+ port.flush()
42
+
43
+ # Get back the reply
44
+ input_bytes = port.read(4)
45
+ input = (
46
+ input_bytes[0]
47
+ | input_bytes[1] << 8
48
+ | input_bytes[2] << 16
49
+ | input_bytes[3] << 24
50
+ )
51
+ return input
52
+
53
+
54
+ def xfer16(port, output):
55
+ """Exchange 16 bit payloads with the GBA attached to the given serial port."""
56
+ # Upper bits sent by master are zero
57
+ input = xfer32(port, output & 0xFFFF)
58
+
59
+ # Lower bits sent by slave are the same as previously sent by master, ignored
60
+ return input >> 16
61
+
62
+
63
+ def upload(port_name, rom_path, timeout=10.0):
64
+ """Upload a GBA multiboot ROM via the given serial port."""
65
+ # Read ROM file
66
+ with open(rom_path, "rb") as file:
67
+ rom = file.read()
68
+
69
+ # Multiboot ROM must fit the 256Ko RAM
70
+ if len(rom) > 0x40000:
71
+ raise ValueError("file too large")
72
+
73
+ # Make sure size is a multiple of 16 bytes
74
+ while len(rom) % 16 != 0:
75
+ rom = rom + b"\0"
76
+
77
+ # Open serial connection
78
+ with serial.Serial(port_name) as port:
79
+ # Send connection request, wait for slave to enter Normal mode
80
+ with console.status("[cyan]Attempting to connect to GBA..."):
81
+ start_time = time.time()
82
+
83
+ while True:
84
+ r = xfer16(port, 0x6202)
85
+
86
+ # Check whether slave has entered correct mode, recognition okay
87
+ if (r & 0xFFF0) == 0x7200:
88
+ # Slave replies with its ID (bit 1, 2, or 3, is set)
89
+ # Note: in Normal mode, there can be only one slave
90
+ x = r & 0xF
91
+ assert x == 2
92
+ break
93
+
94
+ # Check for timeout
95
+ if time.time() - start_time > timeout:
96
+ console.print("[red]Error:[/red] Connection timeout.")
97
+ sys.exit(1)
98
+
99
+ # Wait a bit before trying again
100
+ time.sleep(1 / 16)
101
+
102
+ console.print("[green]✓[/green] Connected to GBA")
103
+
104
+ # Exchange master/slave info
105
+ r = xfer16(port, 0x6102)
106
+ assert r == 0x7202
107
+
108
+ # Send header
109
+ for i in range(0, 0xC0, 2):
110
+ # Send 2 bytes at a time
111
+ xxxx = rom[i] | rom[i + 1] << 8
112
+ r = xfer16(port, xxxx)
113
+
114
+ # Slave replies with index and id
115
+ assert ((r >> 8) & 0xFF) == (0xC0 - i) // 2
116
+ assert (r & 0xFF) == 2
117
+
118
+ # Transfer is complete
119
+ r = xfer16(port, 0x6200)
120
+ assert r == 2
121
+
122
+ # Exchange master/slave info (again)
123
+ r = xfer16(port, 0x6202)
124
+ assert r == 0x7202
125
+
126
+ # Choose palette
127
+ # Note: we really don't care about this, but it seems we could configure it?
128
+ pp = 0xD1
129
+
130
+ # Wait until slave is ready
131
+ while True:
132
+ r = xfer16(port, 0x6300 | pp)
133
+ if (r & 0xFF00) == 0x7300:
134
+ break
135
+
136
+ # Random client data
137
+ cc = r & 0xFF
138
+
139
+ # Compute handshake data
140
+ # Note: client data for missing clients 2 and 3 are 0xff
141
+ hh = (0x11 + cc + 0xFF + 0xFF) & 0xFF
142
+
143
+ # Send handshake
144
+ # Note: client returns a random value, which is ignored
145
+ r = xfer16(port, 0x6400 | hh)
146
+ assert (r & 0xFF00) == 0x7300
147
+
148
+ # Wait a bit
149
+ time.sleep(1 / 16)
150
+
151
+ # Send length information
152
+ llll = (len(rom) - 0xC0) // 4 - 0x34
153
+ r = xfer16(port, llll)
154
+ assert (r & 0xFF00) == 0x7300
155
+ rr = r & 0xFF
156
+
157
+ # Send encrypted payload
158
+ c = 0xC387
159
+ x = 0xC37B
160
+ k = 0x43202F2F
161
+ m = 0xFFFF0000 | cc << 8 | pp
162
+ f = 0xFFFF0000 | rr << 8 | hh
163
+
164
+ with Progress(
165
+ SpinnerColumn(),
166
+ TextColumn("[cyan]Uploading..."),
167
+ BarColumn(),
168
+ TaskProgressColumn(),
169
+ ) as progress:
170
+ task = progress.add_task("upload", total=(len(rom) - 0xC0) // 4)
171
+
172
+ for i in range(0xC0, len(rom), 4):
173
+ # Send 4 bytes at a time
174
+ data = rom[i] | rom[i + 1] << 8 | rom[i + 2] << 16 | rom[i + 3] << 24
175
+
176
+ # Update checksum
177
+ c ^= data
178
+ for _ in range(32):
179
+ carry = c & 1
180
+ c >>= 1
181
+ if carry:
182
+ c ^= x
183
+
184
+ # Encrypt and send data
185
+ m = (0x6F646573 * m + 1) & 0xFFFFFFFF
186
+ complement = (-0x2000000 - i) & 0xFFFFFFFF
187
+ yyyyyyyy = data ^ complement ^ m ^ k
188
+ r = xfer32(port, yyyyyyyy)
189
+
190
+ # Client replies with lower bits of destination address
191
+ assert r >> 16 == i & 0xFFFF
192
+
193
+ progress.update(task, advance=1)
194
+
195
+ # Final checksum update
196
+ c ^= f
197
+ for _ in range(32):
198
+ carry = c & 1
199
+ c >>= 1
200
+ if carry:
201
+ c ^= x
202
+
203
+ # Client just acknowledged the last bits
204
+ r = xfer16(port, 0x0065)
205
+ assert r == len(rom) & 0xFFFF
206
+
207
+ # Wait until all slaves are ready for CRC transfer
208
+ while True:
209
+ r = xfer16(port, 0x0065)
210
+ if r == 0x0075:
211
+ break
212
+ assert r == 0x0074
213
+
214
+ # Signal that CRC will follow
215
+ r = xfer16(port, 0x0066)
216
+ assert r == 0x0075
217
+
218
+ # Exchange CRC
219
+ r = xfer16(port, c)
220
+ assert r == c
221
+
222
+ console.print("[green]✓[/green] Upload complete!")
223
+
224
+
225
+ def detect_pico_ports():
226
+ """Detect all connected Raspberry Pi Pico devices."""
227
+ return [
228
+ port
229
+ for port in serial.tools.list_ports.comports()
230
+ if port.vid == PICO_VID and port.pid == PICO_PID
231
+ ]
232
+
233
+
234
+ def main():
235
+ """Main entry point."""
236
+ parser = argparse.ArgumentParser(
237
+ description="GBA multiboot ROM uploader", prog="multiboot"
238
+ )
239
+ parser.add_argument("path", help="Path to the GBA multiboot ROM file to upload")
240
+ parser.add_argument(
241
+ "-p",
242
+ "--port",
243
+ help="Serial device to use (auto-detected if omitted)",
244
+ )
245
+ parser.add_argument(
246
+ "-t",
247
+ "--timeout",
248
+ type=int,
249
+ default=10,
250
+ help="Connection timeout in seconds (default: 10)",
251
+ )
252
+ args = parser.parse_args()
253
+
254
+ # Determine serial port
255
+ if args.port:
256
+ port_name = args.port
257
+ else:
258
+ # Find any connected Pico devices
259
+ pico_ports = detect_pico_ports()
260
+
261
+ # No devices found
262
+ if not pico_ports:
263
+ console.print(
264
+ "[red]Error:[/red] No device found, use --port to specify port manually."
265
+ )
266
+ sys.exit(1)
267
+
268
+ # Multiple devices found
269
+ if len(pico_ports) > 1:
270
+ console.print("[red]Error:[/red] Multiple serial devices found:")
271
+ for port in pico_ports:
272
+ console.print(f" [cyan]{port.device}[/cyan]")
273
+
274
+ console.print(
275
+ f"\nSpecify one with: [cyan]--port {pico_ports[0].device}[/cyan]"
276
+ )
277
+ sys.exit(1)
278
+
279
+ # Exactly one device found, use it
280
+ port_name = pico_ports[0].device
281
+ console.print(
282
+ f"[green]✓[/green] Auto-detected device at [cyan]{port_name}[/cyan]"
283
+ )
284
+
285
+ # Perform upload
286
+ upload(port_name, args.path, args.timeout)
287
+
288
+
289
+ if __name__ == "__main__":
290
+ main()
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: multiboot
3
+ Version: 0.9.0
4
+ Summary: Upload GBA multiboot payloads via Raspberry Pi Pico
5
+ Project-URL: Documentation, https://github.com/loopj/gba-multiboot-pico#readme
6
+ Project-URL: Issues, https://github.com/loopj/gba-multiboot-pico/issues
7
+ Project-URL: Source, https://github.com/loopj/gba-multiboot-pico
8
+ Author-email: James Smith <james@loopj.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: pyserial
13
+ Requires-Dist: rich
14
+ Provides-Extra: dev
15
+ Requires-Dist: bumpver==2024.1130; extra == 'dev'
16
+ Requires-Dist: pyright==1.1.400; extra == 'dev'
17
+ Requires-Dist: ruff==0.11.8; extra == 'dev'
@@ -0,0 +1,7 @@
1
+ multiboot/__init__.py,sha256=4hCpfSNReaRlDosUeJk_MaqatBykVrMjFN4SlKynGzs,42
2
+ multiboot/cli.py,sha256=shLEIaQcDr4PVZ4EDifiOLAlE__7IvmrMLSAPKaz6fw,8064
3
+ multiboot-0.9.0.dist-info/METADATA,sha256=pn39NAzaBU8HqteFd92_a8mUTcFRImy7z3P2gzeBftc,639
4
+ multiboot-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ multiboot-0.9.0.dist-info/entry_points.txt,sha256=F8_kdw0TBkjUj9ehAG4Est4PY3VEKUZzXO7RU6oVnKk,49
6
+ multiboot-0.9.0.dist-info/licenses/LICENSE,sha256=ExnTZE2Nmea8P3D7ttrQNopa5eLG8e_Qp-HM91ocldA,1099
7
+ multiboot-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ multiboot = multiboot.cli:main
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Johan Berdat
4
+ Copyright (c) 2025 James Smith
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.