warpedpinball 0.1.0__tar.gz
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.
- warpedpinball-0.1.0/.github/workflows/ci.yml +51 -0
- warpedpinball-0.1.0/.github/workflows/publish.yml +37 -0
- warpedpinball-0.1.0/.gitignore +11 -0
- warpedpinball-0.1.0/LICENSE +21 -0
- warpedpinball-0.1.0/PKG-INFO +324 -0
- warpedpinball-0.1.0/README.md +293 -0
- warpedpinball-0.1.0/docs/README.md +30 -0
- warpedpinball-0.1.0/docs/address-maps.md +135 -0
- warpedpinball-0.1.0/docs/http-api.md +132 -0
- warpedpinball-0.1.0/docs/memory.md +163 -0
- warpedpinball-0.1.0/notes/DECISIONS.md +64 -0
- warpedpinball-0.1.0/notes/TODO.md +87 -0
- warpedpinball-0.1.0/pyproject.toml +61 -0
- warpedpinball-0.1.0/tests/conftest.py +165 -0
- warpedpinball-0.1.0/tests/test_addresses.py +149 -0
- warpedpinball-0.1.0/tests/test_auth.py +61 -0
- warpedpinball-0.1.0/tests/test_cli.py +85 -0
- warpedpinball-0.1.0/tests/test_connect.py +71 -0
- warpedpinball-0.1.0/tests/test_discovery.py +68 -0
- warpedpinball-0.1.0/tests/test_http.py +171 -0
- warpedpinball-0.1.0/tests/test_machine.py +336 -0
- warpedpinball-0.1.0/tests/test_usb.py +125 -0
- warpedpinball-0.1.0/warpedpinball/__init__.py +178 -0
- warpedpinball-0.1.0/warpedpinball/addresses.py +216 -0
- warpedpinball-0.1.0/warpedpinball/auth.py +60 -0
- warpedpinball-0.1.0/warpedpinball/cli.py +215 -0
- warpedpinball-0.1.0/warpedpinball/discovery.py +126 -0
- warpedpinball-0.1.0/warpedpinball/exceptions.py +104 -0
- warpedpinball-0.1.0/warpedpinball/machine.py +547 -0
- warpedpinball-0.1.0/warpedpinball/models.py +99 -0
- warpedpinball-0.1.0/warpedpinball/transports/__init__.py +136 -0
- warpedpinball-0.1.0/warpedpinball/transports/http.py +157 -0
- warpedpinball-0.1.0/warpedpinball/transports/usb.py +188 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Install ruff
|
|
17
|
+
run: pip install ruff
|
|
18
|
+
- name: Lint
|
|
19
|
+
run: ruff check .
|
|
20
|
+
|
|
21
|
+
test:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-python@v5
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
- name: Install
|
|
33
|
+
run: pip install -e ".[dev,usb]"
|
|
34
|
+
- name: Test
|
|
35
|
+
run: python -m pytest
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
- name: Install build
|
|
45
|
+
run: pip install build
|
|
46
|
+
- name: Build distributions
|
|
47
|
+
run: python -m build
|
|
48
|
+
- uses: actions/upload-artifact@v4
|
|
49
|
+
with:
|
|
50
|
+
name: dist
|
|
51
|
+
path: dist/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Install build
|
|
17
|
+
run: pip install build
|
|
18
|
+
- name: Build distributions
|
|
19
|
+
run: python -m build
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/download-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
- name: Publish to PyPI (Trusted Publishing)
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Warped Pinball
|
|
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,324 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: warpedpinball
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for Warped Pinball Vector boards (HTTP, USB serial, and UDP discovery)
|
|
5
|
+
Project-URL: Homepage, https://github.com/warped-pinball/python-library
|
|
6
|
+
Project-URL: Issues, https://github.com/warped-pinball/python-library/issues
|
|
7
|
+
Author: Warped Pinball
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: pinball,raspberry-pi-pico,vector,warped-pinball
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Games/Entertainment :: Arcade
|
|
20
|
+
Classifier: Topic :: System :: Hardware
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: requests>=2.25
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build; extra == 'dev'
|
|
25
|
+
Requires-Dist: pyserial>=3.5; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
28
|
+
Provides-Extra: usb
|
|
29
|
+
Requires-Dist: pyserial>=3.5; extra == 'usb'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# warpedpinball
|
|
33
|
+
|
|
34
|
+
[](https://github.com/warped-pinball/python-library/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/warpedpinball/)
|
|
36
|
+
|
|
37
|
+
A Python client for **Warped Pinball "Vector"** boards — the WiFi board
|
|
38
|
+
(Raspberry Pi Pico 2W) that lives inside a pinball machine and exposes:
|
|
39
|
+
|
|
40
|
+
- an **HTTP API** on port 80 (scores, players, live game status, raw SRAM
|
|
41
|
+
access, firmware updates, and more),
|
|
42
|
+
- **UDP discovery** on port 37020, so boards on your LAN can be found by name,
|
|
43
|
+
- a **USB serial tunnel** of the same HTTP routes, for when you're plugged in
|
|
44
|
+
directly, and
|
|
45
|
+
- **HMAC-SHA256 challenge/response authentication** for mutating routes.
|
|
46
|
+
|
|
47
|
+
This library wraps all of that behind one `Machine` object, plus a `vector`
|
|
48
|
+
command-line tool.
|
|
49
|
+
|
|
50
|
+
Detailed guides live in [`docs/`](docs/README.md) — including
|
|
51
|
+
[reading & writing memory](docs/memory.md), [address maps](docs/address-maps.md),
|
|
52
|
+
and the [HTTP API reference](docs/http-api.md) for the raw
|
|
53
|
+
`/api/address/read` / `/api/address/write` routes.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
Requires Python 3.9+.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install warpedpinball
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For USB serial support (adds [pyserial](https://pypi.org/project/pyserial/)):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "warpedpinball[usb]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quickstart
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import warpedpinball
|
|
73
|
+
|
|
74
|
+
# Find every Vector board on the LAN (UDP broadcast, port 37020)
|
|
75
|
+
for m in warpedpinball.discover(timeout=5):
|
|
76
|
+
print(m.name, m.ip)
|
|
77
|
+
|
|
78
|
+
# Connect by machine name (case-insensitive; unique prefix/substring works too)
|
|
79
|
+
machine = warpedpinball.connect("elvira", password="hunter2")
|
|
80
|
+
|
|
81
|
+
# ...or by IP address (skips discovery)
|
|
82
|
+
machine = warpedpinball.connect("192.168.1.42")
|
|
83
|
+
|
|
84
|
+
# ...or over USB (auto-picks the port when exactly one board is attached)
|
|
85
|
+
machine = warpedpinball.connect_usb()
|
|
86
|
+
|
|
87
|
+
# Machines are context managers
|
|
88
|
+
with warpedpinball.connect("elvira") as m:
|
|
89
|
+
print(m.version())
|
|
90
|
+
print(m.game_status())
|
|
91
|
+
print(m.leaderboard())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`connect()` raises `MachineNotFoundError` (listing the names it *did* see) or
|
|
95
|
+
`AmbiguousMachineError` (listing the candidates) when a name doesn't resolve to
|
|
96
|
+
exactly one board.
|
|
97
|
+
|
|
98
|
+
## Authentication
|
|
99
|
+
|
|
100
|
+
Read-only routes need no credentials. Mutating routes (reboot, writes, updates,
|
|
101
|
+
score resets, ...) are protected by an HMAC-SHA256 challenge/response scheme.
|
|
102
|
+
You just provide the password — the library fetches a single-use challenge and
|
|
103
|
+
signs each request automatically and invisibly. Give it the password any of
|
|
104
|
+
three ways:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# 1. At connect time
|
|
108
|
+
m = warpedpinball.connect("elvira", password="hunter2")
|
|
109
|
+
|
|
110
|
+
# 2. On the machine object
|
|
111
|
+
m.password = "hunter2"
|
|
112
|
+
|
|
113
|
+
# 3. Via the environment
|
|
114
|
+
# export VECTOR_PASSWORD=hunter2
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Over **USB no password is needed at all** — the firmware trusts physical
|
|
118
|
+
access and skips HMAC for requests arriving on the serial port.
|
|
119
|
+
|
|
120
|
+
To validate credentials up front instead of failing on the first write:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
if not m.verify_password():
|
|
124
|
+
raise SystemExit("wrong password")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
If an authenticated call is attempted with no password configured, you get an
|
|
128
|
+
`AuthenticationRequiredError` before any network traffic; a rejected signature
|
|
129
|
+
raises `AuthenticationError`.
|
|
130
|
+
|
|
131
|
+
## API tour
|
|
132
|
+
|
|
133
|
+
### Common wrappers
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
m.version() # firmware version
|
|
137
|
+
m.game_status() # live game status (scores, ball in play, ...)
|
|
138
|
+
m.leaderboard() # high scores
|
|
139
|
+
m.players() # player roster
|
|
140
|
+
m.update_player(id=3, initials="MAX", full_name="Max M.") # authenticated
|
|
141
|
+
|
|
142
|
+
# Firmware updates, with streamed progress
|
|
143
|
+
info = m.check_for_updates() # note: 10 s server-side cooldown
|
|
144
|
+
m.apply_update(progress=lambda rec: print(rec.get("percent"), rec.get("log")))
|
|
145
|
+
m.wait_until_reachable() # poll /api/version until the board is back
|
|
146
|
+
|
|
147
|
+
# Logs (authenticated; 10 s server-side cooldown) — streamed as bytes
|
|
148
|
+
for chunk in m.logs():
|
|
149
|
+
print(chunk.decode(errors="replace"), end="")
|
|
150
|
+
|
|
151
|
+
# Power
|
|
152
|
+
m.reboot() # reboot the Vector board
|
|
153
|
+
m.reboot_game() # power-cycle the pinball machine itself
|
|
154
|
+
m.wait_until_reachable(timeout=120)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Other wrappers include `machine_id()`, `game_name()`, `active_config()`,
|
|
158
|
+
`wifi_status()`, `faults()`, `peers()`, `tournament()`,
|
|
159
|
+
`claimable_scores()` / `claim_score()`, `export_scores()` / `import_scores()`,
|
|
160
|
+
`reset_leaderboard()` / `reset_tournament()`, `date()` / `set_date()`, and the
|
|
161
|
+
adjustments family (`adjustments()`, `capture_adjustments()`,
|
|
162
|
+
`restore_adjustments()`, `name_adjustment()`).
|
|
163
|
+
|
|
164
|
+
### The raw escape hatch
|
|
165
|
+
|
|
166
|
+
Every firmware route is reachable even without a wrapper:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
m.call("/api/game/name")
|
|
170
|
+
m.call("/api/player/update",
|
|
171
|
+
body={"id": 1, "initials": "ABC"},
|
|
172
|
+
authenticated=True)
|
|
173
|
+
|
|
174
|
+
# Streaming variant: an iterator of raw byte chunks
|
|
175
|
+
for chunk in m.call_stream("/api/memory-snapshot"):
|
|
176
|
+
...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Bodies are serialized to JSON exactly once; the same string is signed and
|
|
180
|
+
transmitted, so authenticated `call()`s work for any route.
|
|
181
|
+
|
|
182
|
+
### Watching a game live
|
|
183
|
+
|
|
184
|
+
`watch_game()` polls `/api/game/status` and yields `GameEvent`s
|
|
185
|
+
(`game_started`, `game_ended`, `ball_changed`, `score_changed`,
|
|
186
|
+
`status_changed`) forever:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
for event in m.watch_game(interval=1.0): # interval is clamped to >= 0.5 s
|
|
190
|
+
if event.type == "score_changed":
|
|
191
|
+
print(f"player {event.player}: {event.old} -> {event.new}")
|
|
192
|
+
elif event.type == "game_ended":
|
|
193
|
+
print("game over", event.status)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Named memory addresses
|
|
197
|
+
|
|
198
|
+
The firmware exposes raw SRAM reads/writes, but it can't store names. An
|
|
199
|
+
`AddressMap` maps friendly names to `(offset, length, encoding)` so mod makers
|
|
200
|
+
can ship an address map for a specific game ROM alongside their code:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from warpedpinball import AddressMap
|
|
204
|
+
|
|
205
|
+
amap = AddressMap(active_config="elvira_l4")
|
|
206
|
+
amap.define("credits", 0x2134) # raw single byte
|
|
207
|
+
amap.define("player1_score", 0x0200, length=4, encoding="bcd")
|
|
208
|
+
amap.define("counter", 0x0300, length=2, encoding="le_uint")
|
|
209
|
+
|
|
210
|
+
m.addresses = amap
|
|
211
|
+
score = m.read("player1_score") # decoded int
|
|
212
|
+
m.write("credits", 5)
|
|
213
|
+
|
|
214
|
+
# Raw offsets work too
|
|
215
|
+
m.read(0x2134) # single byte -> int
|
|
216
|
+
m.read(0x0200, count=4) # -> bytes
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Built-in encodings are `"bcd"` (packed BCD, most-significant byte first — the
|
|
220
|
+
classic Williams/Bally score format), `"le_uint"`, and `"be_uint"`; you can
|
|
221
|
+
also pass a custom `(decode, encode)` callable pair. Reads and writes are
|
|
222
|
+
automatically chunked at 256 bytes per request (the firmware's limit).
|
|
223
|
+
|
|
224
|
+
### Sharing maps as JSON
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
amap.save("elvira_l4.json")
|
|
228
|
+
amap = AddressMap.load("elvira_l4.json") # warns if the machine's
|
|
229
|
+
# active_config doesn't match
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
There's also a registry convention: drop a map at
|
|
233
|
+
`~/.warpedpinball/addressmaps/<active_config>.json` and `connect()` /
|
|
234
|
+
`connect_usb()` will auto-load it based on the machine's
|
|
235
|
+
`/api/game/active_config` value (only when that directory exists and you didn't
|
|
236
|
+
pass `addresses=` yourself). `AddressMap.load_registry("elvira_l4")` does the
|
|
237
|
+
lookup explicitly.
|
|
238
|
+
|
|
239
|
+
### Finding addresses
|
|
240
|
+
|
|
241
|
+
Use full-memory snapshots and diff them to hunt for the byte you care about:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
before = m.memory_snapshot() # full SRAM dump (streamed)
|
|
245
|
+
# ... add a credit on the machine ...
|
|
246
|
+
after = m.memory_snapshot()
|
|
247
|
+
|
|
248
|
+
for offset, old, new in m.diff_snapshots(before, after):
|
|
249
|
+
print(f"{offset:#06x}: {old} -> {new}")
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Being kind to the device
|
|
253
|
+
|
|
254
|
+
The board is a single-threaded microcontroller. The library helps you not
|
|
255
|
+
overwhelm it:
|
|
256
|
+
|
|
257
|
+
- **Requests are serialized per machine** — each `Machine` holds a lock, so
|
|
258
|
+
even multi-threaded programs send one request at a time (auth challenges are
|
|
259
|
+
single-use, so this also keeps signing correct).
|
|
260
|
+
- **`watch_game()` enforces a minimum 0.5 s poll interval.**
|
|
261
|
+
- **Some routes have server-side cooldowns**: `/api/logs` 10 s,
|
|
262
|
+
`/api/update/check` 10 s, `/api/adjustments/restore` 5 s. Hitting one raises
|
|
263
|
+
`CooldownError` with a best-effort `retry_after` hint (seconds).
|
|
264
|
+
- **`RateLimitedError`** means the device had too many outstanding auth
|
|
265
|
+
challenges (it holds ~10; expired ones are purged on each challenge request).
|
|
266
|
+
The HTTP transport already retries this a few times internally; if it still
|
|
267
|
+
surfaces, sleep briefly and retry.
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
import time
|
|
271
|
+
from warpedpinball import CooldownError
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
m.check_for_updates()
|
|
275
|
+
except CooldownError as exc:
|
|
276
|
+
time.sleep(exc.retry_after or 10)
|
|
277
|
+
m.check_for_updates()
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Command line
|
|
281
|
+
|
|
282
|
+
Installing the package adds a `vector` command:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
vector discover # find boards on the LAN
|
|
286
|
+
vector status elvira # show live game status
|
|
287
|
+
vector version elvira # show firmware version
|
|
288
|
+
vector leaders elvira # show the leaderboard
|
|
289
|
+
vector read elvira 0x2134 --count 4 # read SRAM bytes
|
|
290
|
+
vector write elvira 0x2134 5 --password hunter2
|
|
291
|
+
vector snapshot elvira -o dump.bin # dump full SRAM
|
|
292
|
+
vector update elvira --password hunter2 # check for + apply a firmware update
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Each machine-targeting subcommand accepts a machine name or IP address, plus
|
|
296
|
+
`--password/-p` (or `$VECTOR_PASSWORD`), `--timeout` for discovery, and
|
|
297
|
+
`--usb [PORT]` to go over USB serial instead of the network.
|
|
298
|
+
|
|
299
|
+
## Errors
|
|
300
|
+
|
|
301
|
+
All exceptions derive from `warpedpinball.VectorError`:
|
|
302
|
+
|
|
303
|
+
| Exception | Meaning |
|
|
304
|
+
| --- | --- |
|
|
305
|
+
| `TransportError` | Connection, timeout, or protocol-level failure |
|
|
306
|
+
| `MachineNotFoundError` | Discovery found no matching machine (`.seen_names` lists what it did see) |
|
|
307
|
+
| `AmbiguousMachineError` | A name matched more than one machine (`.candidates`) |
|
|
308
|
+
| `AuthenticationRequiredError` | Authenticated route called with no password set (raised before any traffic) |
|
|
309
|
+
| `AuthenticationError` | The device rejected the credentials (HTTP 401; `.reason`) |
|
|
310
|
+
| `RateLimitedError` | Too many outstanding auth challenges (HTTP 429 on the challenge route); retry after a short sleep |
|
|
311
|
+
| `CooldownError` | Route locked or in cooldown (HTTP 409/429; `.retry_after` hint in seconds) |
|
|
312
|
+
| `VectorServerError` | The device handler raised an error (HTTP 5xx; `.status`) |
|
|
313
|
+
| `UnsupportedFirmwareError` | The route doesn't exist on this firmware (HTTP 404); an update may be required |
|
|
314
|
+
|
|
315
|
+
## Development
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
git clone https://github.com/warped-pinball/python-library
|
|
319
|
+
cd python-library
|
|
320
|
+
pip install -e ".[dev,usb]"
|
|
321
|
+
|
|
322
|
+
pytest # run the tests
|
|
323
|
+
ruff check . # lint
|
|
324
|
+
```
|