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,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.
|