bbr-buttonboard 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.
@@ -0,0 +1,3 @@
1
+ dist/
2
+ __pycache__/
3
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Building Block Robotics
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,121 @@
1
+ Metadata-Version: 2.5
2
+ Name: bbr-buttonboard
3
+ Version: 0.1.0
4
+ Summary: Drive a BBR Button Board's LEDs, displays and bindings from the Driver Station laptop
5
+ Project-URL: Documentation, https://buttonboard.buildingblockrobotics.com/driver-station/install.html
6
+ Project-URL: Source, https://github.com/BuildingBlockRobotics/BBRButtonBoard
7
+ Project-URL: Homepage, https://bbrbuttonboard.com/
8
+ Author: Building Block Robotics
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: FIRST,FRC,FTC,button box,driver station,robotics
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Education
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: System :: Hardware
17
+ Requires-Python: >=3.9
18
+ Requires-Dist: pyserial>=3.4
19
+ Provides-Extra: networktables
20
+ Requires-Dist: pyntcore; extra == 'networktables'
21
+ Provides-Extra: pictures
22
+ Requires-Dist: pillow; extra == 'pictures'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # bbr-buttonboard
26
+
27
+ Python client for the [BBR Button Board](https://bbrbuttonboard.com/), a
28
+ modular driver station panel for FIRST robotics.
29
+
30
+ The board works as a game controller with nothing installed. This library is
31
+ for making the panel react to the robot: a short script on the Driver Station
32
+ laptop reads robot state (from NetworkTables, in FRC) and sets key colours,
33
+ display text and pictures to match.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install bbr-buttonboard
39
+ ```
40
+
41
+ Python 3.9 or newer. The only dependency is `pyserial`. Two extras:
42
+
43
+ ```sh
44
+ pip install "bbr-buttonboard[pictures]" # Pillow, to load images onto display tiles
45
+ pip install "bbr-buttonboard[networktables]" # pyntcore, for the companion app
46
+ ```
47
+
48
+ ## A first script
49
+
50
+ ```python
51
+ from buttonboard import Board
52
+
53
+ with Board.open() as board: # finds the port by itself
54
+ leds = board.led_layer()
55
+
56
+ while True:
57
+ arm_fault = read_from_networktables("arm_fault")
58
+ in_range = read_from_networktables("in_scoring_range")
59
+
60
+ leds.set(3, "red" if arm_fault else "off")
61
+ leds.set(7, "green" if in_range else "off", fx="breathe")
62
+ leds.flush() # sends only what changed
63
+ ```
64
+
65
+ Display tiles take text the same way:
66
+
67
+ ```python
68
+ board.screen(5).text(0, "ABORT", fg="black", bg="red").flush()
69
+ ```
70
+
71
+ `MockBoard` is an in-process board, so a script can be written and tested
72
+ with no hardware attached:
73
+
74
+ ```python
75
+ from buttonboard import MockBoard
76
+
77
+ board = MockBoard()
78
+ board.led(3, "red")
79
+ print(board.state.leds) # {('btn', 3): 'led 3 FF0000'}
80
+ ```
81
+
82
+ Only one program can have the board open at a time. Close the config app's
83
+ browser tab before running a script, and the other way round.
84
+
85
+ ## bbctl
86
+
87
+ Installing the package also installs `bbctl`, a console for the board's
88
+ config channel:
89
+
90
+ ```sh
91
+ bbctl # interactive
92
+ bbctl hello stats # run commands and exit
93
+ bbctl -p COM7 "led reset" # explicit port
94
+ ```
95
+
96
+ ## Documentation
97
+
98
+ * [Install and first script](https://buttonboard.buildingblockrobotics.com/driver-station/install.html)
99
+ * [API summary](https://buttonboard.buildingblockrobotics.com/driver-station/api.html)
100
+ * [Config channel commands](https://buttonboard.buildingblockrobotics.com/reference/commands.html)
101
+
102
+ ## Tests
103
+
104
+ ```sh
105
+ python tests/test_buttonboard.py
106
+ ```
107
+
108
+ No hardware and no pytest needed.
109
+
110
+ ## About this repository
111
+
112
+ This is a published copy of the library. It is developed alongside the
113
+ Button Board firmware and exported here for each release, so a pull request
114
+ is welcome but will be applied upstream and come back in the next export.
115
+
116
+ The `§` references in the source point into the Button Board protocol
117
+ specification, which is not published yet.
118
+
119
+ ## Licence
120
+
121
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,97 @@
1
+ # bbr-buttonboard
2
+
3
+ Python client for the [BBR Button Board](https://bbrbuttonboard.com/), a
4
+ modular driver station panel for FIRST robotics.
5
+
6
+ The board works as a game controller with nothing installed. This library is
7
+ for making the panel react to the robot: a short script on the Driver Station
8
+ laptop reads robot state (from NetworkTables, in FRC) and sets key colours,
9
+ display text and pictures to match.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pip install bbr-buttonboard
15
+ ```
16
+
17
+ Python 3.9 or newer. The only dependency is `pyserial`. Two extras:
18
+
19
+ ```sh
20
+ pip install "bbr-buttonboard[pictures]" # Pillow, to load images onto display tiles
21
+ pip install "bbr-buttonboard[networktables]" # pyntcore, for the companion app
22
+ ```
23
+
24
+ ## A first script
25
+
26
+ ```python
27
+ from buttonboard import Board
28
+
29
+ with Board.open() as board: # finds the port by itself
30
+ leds = board.led_layer()
31
+
32
+ while True:
33
+ arm_fault = read_from_networktables("arm_fault")
34
+ in_range = read_from_networktables("in_scoring_range")
35
+
36
+ leds.set(3, "red" if arm_fault else "off")
37
+ leds.set(7, "green" if in_range else "off", fx="breathe")
38
+ leds.flush() # sends only what changed
39
+ ```
40
+
41
+ Display tiles take text the same way:
42
+
43
+ ```python
44
+ board.screen(5).text(0, "ABORT", fg="black", bg="red").flush()
45
+ ```
46
+
47
+ `MockBoard` is an in-process board, so a script can be written and tested
48
+ with no hardware attached:
49
+
50
+ ```python
51
+ from buttonboard import MockBoard
52
+
53
+ board = MockBoard()
54
+ board.led(3, "red")
55
+ print(board.state.leds) # {('btn', 3): 'led 3 FF0000'}
56
+ ```
57
+
58
+ Only one program can have the board open at a time. Close the config app's
59
+ browser tab before running a script, and the other way round.
60
+
61
+ ## bbctl
62
+
63
+ Installing the package also installs `bbctl`, a console for the board's
64
+ config channel:
65
+
66
+ ```sh
67
+ bbctl # interactive
68
+ bbctl hello stats # run commands and exit
69
+ bbctl -p COM7 "led reset" # explicit port
70
+ ```
71
+
72
+ ## Documentation
73
+
74
+ * [Install and first script](https://buttonboard.buildingblockrobotics.com/driver-station/install.html)
75
+ * [API summary](https://buttonboard.buildingblockrobotics.com/driver-station/api.html)
76
+ * [Config channel commands](https://buttonboard.buildingblockrobotics.com/reference/commands.html)
77
+
78
+ ## Tests
79
+
80
+ ```sh
81
+ python tests/test_buttonboard.py
82
+ ```
83
+
84
+ No hardware and no pytest needed.
85
+
86
+ ## About this repository
87
+
88
+ This is a published copy of the library. It is developed alongside the
89
+ Button Board firmware and exported here for each release, so a pull request
90
+ is welcome but will be applied upstream and come back in the next export.
91
+
92
+ The `§` references in the source point into the Button Board protocol
93
+ specification, which is not published yet.
94
+
95
+ ## Licence
96
+
97
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bbr-buttonboard"
7
+ dynamic = ["version"]
8
+ description = "Drive a BBR Button Board's LEDs, displays and bindings from the Driver Station laptop"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "Building Block Robotics" }]
14
+ keywords = ["FRC", "FTC", "FIRST", "robotics", "driver station", "button box"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Education",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: System :: Hardware",
21
+ ]
22
+ dependencies = ["pyserial>=3.4"]
23
+
24
+ [project.optional-dependencies]
25
+ pictures = ["pillow"]
26
+ networktables = ["pyntcore"]
27
+
28
+ [project.scripts]
29
+ bbctl = "buttonboard.bbctl:main"
30
+
31
+ [project.urls]
32
+ Documentation = "https://buttonboard.buildingblockrobotics.com/driver-station/install.html"
33
+ Source = "https://github.com/BuildingBlockRobotics/BBRButtonBoard"
34
+ Homepage = "https://bbrbuttonboard.com/"
35
+
36
+ [tool.hatch.version]
37
+ path = "src/buttonboard/__init__.py"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/buttonboard"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ exclude = [".github"]
@@ -0,0 +1,52 @@
1
+ """Button Board host client (PROTOCOL.md §9).
2
+
3
+ from buttonboard import Board
4
+ with Board.open() as board: # auto-detects the COM port
5
+ board.hello()
6
+ board.led(3, "red", fx="blink")
7
+ board.screen(4).text(0, "AUTO 12.4s").flush()
8
+ board.screen(5).text(0, "ABORT", fg="black", bg="red").flush()
9
+
10
+ The intended writer is a Driver Station companion app (§9.8, §13.5, §14.4):
11
+ it reads NetworkTables and pushes LED colours and display text. For a loop
12
+ that recomputes everything each pass, use the diffing writers -- `LedLayer`
13
+ and `Screen` -- so only real changes reach the bus.
14
+
15
+ Colour (§14.7) is said the way it reads out loud -- `"red on white"`, or
16
+ `fg="black", bg="yellow"` -- and `buttonboard.attr()` turns that into the ATTR byte.
17
+ Only a tile advertising `CAP_DISP_COLOR` accepts it: a mono panel refuses
18
+ colour rather than drawing white, and this client refuses it first.
19
+
20
+ MockBoard speaks the same protocol in-process, which is how the display and
21
+ sound halves are tested without hardware. `disp` is live (§14); `sound` is
22
+ still reserved and the board answers `unknown` to it (§16). Its default chain
23
+ carries a mono `disp1` at address 4 and a colour `screenkey1` at address 5;
24
+ the screen-key has a picture store (§14.9), so `board.disp_store_image(5, 0,
25
+ "logo.png")` loads a full-colour legend once and `board.disp_slot_show(5, 0)`
26
+ puts it on the glass in a single frame. `board.rumble_bind(1, 5, 0)` binds a
27
+ legend code to that slot (§8.8), which is how a match-time `gamepad.rumble()`
28
+ from an OpMode reaches a board that has no config channel at all.
29
+ """
30
+ __version__ = "0.1.0"
31
+
32
+ from . import rumble
33
+ from .client import Board, FLASH_CHUNK, MAX_LINE
34
+ from .crc import crc16_ccitt_false, crc8_maxim
35
+ from .display import COLORS, Screen, attr, picture_bytes
36
+ from .errors import (BoardError, CommandError, NoBoardFound, NotConnected,
37
+ Timeout)
38
+ from .leds import FX, NAMED, LedLayer, rgb
39
+ from .mock import MockBoard
40
+ from .rumble import CODE_MAX as RUMBLE_CODE_MAX, CODE_STEP as RUMBLE_CODE_STEP
41
+ from .sound import TUNES
42
+ from .transport import LoopTransport, SerialTransport, find_port
43
+
44
+ __all__ = [
45
+ "Board", "MockBoard", "Screen", "LedLayer", "attr", "COLORS",
46
+ "picture_bytes",
47
+ "SerialTransport", "LoopTransport", "find_port",
48
+ "BoardError", "CommandError", "NoBoardFound", "NotConnected", "Timeout",
49
+ "rgb", "FX", "NAMED", "TUNES", "MAX_LINE", "FLASH_CHUNK",
50
+ "rumble", "RUMBLE_CODE_MAX", "RUMBLE_CODE_STEP",
51
+ "crc8_maxim", "crc16_ccitt_false",
52
+ ]
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env python3
2
+ r"""bbctl -- console for the Button Board config channel (PROTOCOL.md §9).
3
+
4
+ bbctl # interactive console
5
+ bbctl hello stats # one-shot, then exit
6
+ bbctl -p COM7 "test walk 250" # explicit port
7
+ bbctl --flash 3 tile-arc4.bin # tile firmware update (§9.4)
8
+ bbctl --disp 5 0 ABORT --style "black on red" # §14.7
9
+ bbctl --picture 5 0 logo.png # §14.9
10
+ bbctl --rumble-bind 3 5 0 # §8.8
11
+ bbctl --rumble-watch # §8.8
12
+
13
+ `bbctl` is the command `pip install bbr-buttonboard` puts on the PATH; in
14
+ this repository it is `python3 tools/bbctl.py`. A thin shell over the
15
+ `buttonboard` package, which is where the protocol handling
16
+ lives. It exists because a generic serial terminal gets three things wrong
17
+ here: Enter must send \n (a bare \r is ignored, §9.1); DTR must stay asserted
18
+ or test mode ends (§9.10); and a reply may be preceded by event lines (§9.3),
19
+ so a client must read on to the terminal {"ok":...} line.
20
+
21
+ Baud rate is ignored by the device; any value works.
22
+ """
23
+ import argparse
24
+ import os
25
+ import sys
26
+ import time
27
+
28
+ from . import rumble
29
+ from .client import Board
30
+ from .errors import BoardError, CommandError, Timeout
31
+
32
+
33
+ def one_shot(board, commands):
34
+ rc = 0
35
+ for cmd in commands:
36
+ print("> %s" % cmd)
37
+ try:
38
+ board.command(cmd)
39
+ except CommandError:
40
+ rc = 1 # the response itself was printed by the hook
41
+ except Timeout as e:
42
+ print(e)
43
+ rc = 1
44
+ return rc
45
+
46
+
47
+ def interactive(board):
48
+ print("connected. commands: PROTOCOL.md §9.2 (Ctrl-D or 'quit' to exit)")
49
+ print("try: hello | stats | tiles | test walk 250 | test off | led all off")
50
+ try:
51
+ while True:
52
+ try:
53
+ cmd = input().strip()
54
+ except EOFError:
55
+ break
56
+ if cmd in ("quit", "exit"):
57
+ break
58
+ if cmd:
59
+ board.write_line(cmd)
60
+ time.sleep(0.15) # let the reply land before the next prompt
61
+ except KeyboardInterrupt:
62
+ pass
63
+ # Closing drops DTR, which ends test mode by design (§9.10).
64
+ print("\nclosing -- DTR drops, so any running test pattern ends here.")
65
+ return 0
66
+
67
+
68
+ def disp(board, addr, row, text, style):
69
+ """Write one row on a display tile (§14.4). Returns a process exit code.
70
+
71
+ `style` is `display.attr`'s spec -- "red", "black on yellow", "white on
72
+ blue double" -- because the alternative on a command line is an ATTR byte
73
+ the user has to assemble out of §14.7's two shifts. The explicit `show`
74
+ commits it now rather than 20 ms later, which matters when the next thing
75
+ this process does is close the port.
76
+ """
77
+ try:
78
+ addr, row = int(addr), int(row)
79
+ except ValueError:
80
+ print("--disp takes ADDR ROW TEXT, with ADDR and ROW numbers")
81
+ return 1
82
+ try:
83
+ board.disp_text(addr, row, text, attr=style)
84
+ board.disp_show(addr)
85
+ except ValueError as e: # a style or a range this client knows is wrong
86
+ print(e)
87
+ return 1
88
+ except (CommandError, Timeout) as e:
89
+ print("disp failed: %s" % e)
90
+ return 1
91
+ return 0
92
+
93
+
94
+ def picture(board, addr, slot, path, fmt, show=True):
95
+ """Store a picture in a display tile's slot, §14.9. Returns an exit code.
96
+
97
+ The one config-channel operation measured in seconds -- a full-colour
98
+ 128x128 picture is 32,768 bytes, 512 chunks and 2-4 s of auxiliary slot --
99
+ so it gets the same progress bar `--flash` has, for the same reason. It is
100
+ spent once: showing the picture afterwards is a single frame.
101
+ """
102
+ try:
103
+ addr, slot = int(addr), int(slot)
104
+ except ValueError:
105
+ print("--picture takes ADDR SLOT FILE, with ADDR and SLOT numbers")
106
+ return 1
107
+ width = 40
108
+
109
+ def bar(done, total):
110
+ filled = width * done // max(1, total)
111
+ sys.stderr.write("\r [%s%s] %d/%d bytes" % ("#" * filled, "." * (width - filled),
112
+ done, total))
113
+ sys.stderr.flush()
114
+
115
+ print("> disp %d slot %d <- %s (%s)" % (addr, slot, path, fmt))
116
+ try:
117
+ board.disp_store_image(addr, slot, path, fmt, show=show, on_progress=bar)
118
+ except (ValueError, RuntimeError) as e: # Pillow missing, or a bad format
119
+ sys.stderr.write("\n")
120
+ print(e)
121
+ return 1
122
+ except (CommandError, Timeout) as e:
123
+ sys.stderr.write("\n")
124
+ print("picture failed: %s" % e)
125
+ # An interrupted store leaves the slot EMPTY rather than half a
126
+ # picture (§14.9), so the retry is just this command again.
127
+ print("slot %d on tile %d is empty; re-run to retry" % (slot, addr))
128
+ return 1
129
+ sys.stderr.write("\n")
130
+ print("ok -- slot %d holds %s%s" % (slot, os.path.basename(path),
131
+ ", and is on the panel" if show else ""))
132
+ return 0
133
+
134
+
135
+ def rumble_list(board):
136
+ """Print the map's rumble bindings, §8.8. Returns an exit code."""
137
+ try:
138
+ mapping = board.map_get()
139
+ bound = rumble.bindings(mapping)
140
+ actions = rumble.led_actions(mapping)
141
+ except (CommandError, Timeout) as e:
142
+ print("map get failed: %s" % e)
143
+ return 1
144
+ except ValueError as e: # the board's map is not one we like
145
+ print("the map's rumble section is not valid: %s" % e)
146
+ return 1
147
+ if not bound and not actions:
148
+ print("no rumble bindings; a code with no entry does nothing (§8.8)")
149
+ return 0
150
+ if bound:
151
+ print("code tile slot from an OpMode")
152
+ for entry in bound:
153
+ print("%4d %4d %4d showLegend(%d)"
154
+ % (entry["code"], entry["addr"], entry["slot"], entry["code"]))
155
+ if actions:
156
+ print("code key colour (live layer, rev 0.33)")
157
+ for entry in actions:
158
+ where = ("btn %d" % entry["btn"]) if "btn" in entry \
159
+ else ("tile %d LED %d" % (entry["addr"], entry["led"]))
160
+ print("%4d %-12s %s%s" % (entry["code"], where, entry["rgb"],
161
+ " " + entry["fx"] if "fx" in entry else ""))
162
+ return 0
163
+
164
+
165
+ def rumble_bind(board, code, addr, slot, save=True):
166
+ """Bind one legend code to a tile's stored picture. Returns an exit code."""
167
+ try:
168
+ entry = rumble.entry(code, addr, slot)
169
+ except ValueError as e:
170
+ print(e)
171
+ return 1
172
+ try:
173
+ board.rumble_bind(entry["code"], entry["addr"], entry["slot"])
174
+ if save:
175
+ board.save()
176
+ except (CommandError, Timeout) as e:
177
+ print("rumble bind failed: %s" % e)
178
+ return 1
179
+ print("ok -- code %d shows tile %d slot %d%s"
180
+ % (entry["code"], entry["addr"], entry["slot"],
181
+ "" if save else "; NOT saved, so it is gone at the next power-up"))
182
+ return 0
183
+
184
+
185
+ def rumble_unbind(board, code, save=True):
186
+ try:
187
+ code = rumble.check_code(code)
188
+ except ValueError as e:
189
+ print(e)
190
+ return 1
191
+ try:
192
+ board.rumble_unbind(code)
193
+ if save:
194
+ board.save()
195
+ except (CommandError, Timeout) as e:
196
+ print("rumble unbind failed: %s" % e)
197
+ return 1
198
+ print("ok -- code %d is unbound%s"
199
+ % (code, "" if save else "; NOT saved"))
200
+ return 0
201
+
202
+
203
+ def rumble_watch(board):
204
+ """Print rumble codes and player indices as they arrive, until Ctrl-C.
205
+
206
+ The bench half of §8.8: run the OpMode call and read the number the board
207
+ actually decoded, rather than trusting the arithmetic. Every packet is
208
+ reported whether or not it is bound, which is what makes this a binding
209
+ flow and not just a log.
210
+ """
211
+ bound = {}
212
+ try:
213
+ bound = {e["code"]: e for e in board.rumble_bindings()}
214
+ except (BoardError, ValueError):
215
+ pass # a board with no readable map still watches
216
+ board.on("rumble", lambda ev: print(
217
+ "rumble left=%-3d right=%-3d -> code %-2d %s"
218
+ % (ev.get("left", 0), ev.get("right", 0), ev.get("code", 0),
219
+ "(stop, not an action)" if not ev.get("code")
220
+ else ("-> tile %d slot %d" % (bound[ev["code"]]["addr"],
221
+ bound[ev["code"]]["slot"]))
222
+ if ev["code"] in bound else "(unbound)")))
223
+ board.on("player", lambda ev: print("player index %s" % ev.get("n")))
224
+ print("watching for rumble codes (§8.8). An OpMode sends one with")
225
+ print(" board.showLegend(c); // robot/ftc, i.e. gamepad.rumble(c/15.0, 0, ms)")
226
+ print("Ctrl-C to stop.")
227
+ try:
228
+ while True:
229
+ time.sleep(0.2)
230
+ except KeyboardInterrupt:
231
+ pass
232
+ return 0
233
+
234
+
235
+ def flash(board, addr, path):
236
+ """Stream a tile firmware image, §9.4. Returns a process exit code."""
237
+ size = os.path.getsize(path)
238
+ print("> flash %s %s (%d bytes)" % (addr, path, size))
239
+ width, state = 40, {"n": 0}
240
+
241
+ def bar(ev):
242
+ state["n"] = ev["done"]
243
+ filled = width * ev["done"] // max(1, ev["total"])
244
+ sys.stderr.write("\r [%s%s] %d/%d bytes" % ("#" * filled, "." * (width - filled),
245
+ ev["done"], ev["total"]))
246
+ sys.stderr.flush()
247
+
248
+ try:
249
+ result = board.flash(int(addr), path, on_progress=bar)
250
+ except (CommandError, Timeout) as e:
251
+ sys.stderr.write("\n")
252
+ print("flash failed: %s" % e)
253
+ # A tile that got as far as ENTER_BOOT is sitting in its bootloader at
254
+ # the same address, so the retry is just this command again (§9.4) --
255
+ # worth saying, because a half-written tile otherwise looks bricked.
256
+ # The refusals below come from the `flash` line itself, before the
257
+ # tile was ever told to reboot, so they would be a lie.
258
+ refused = isinstance(e, CommandError) and e.err in ("args", "no_tile", "unknown")
259
+ if not refused:
260
+ print("the tile is in its bootloader at address %s%s; re-run to retry"
261
+ % (addr, " (if it got that far)" if isinstance(e, Timeout) else ""))
262
+ return 1
263
+ sys.stderr.write("\n")
264
+ print("ok -- tile %s is running %s" % (result.get("addr", addr), result.get("fw", "?")))
265
+ return 0
266
+
267
+
268
+ def main():
269
+ ap = argparse.ArgumentParser(prog="bbctl", description="Button Board config channel client")
270
+ ap.add_argument("-p", "--port", help="serial port (default: auto-detect)")
271
+ ap.add_argument("--flash", nargs=2, metavar=("ADDR", "IMAGE"),
272
+ help="send a tile firmware image (the .bin from `make bin`), then exit")
273
+ ap.add_argument("--disp", nargs=3, metavar=("ADDR", "ROW", "TEXT"),
274
+ help="write one row on a display tile, then exit")
275
+ ap.add_argument("--picture", nargs=3, metavar=("ADDR", "SLOT", "FILE"),
276
+ help="store an image in a display tile's picture slot and "
277
+ "show it, then exit (§14.9). Needs Pillow.")
278
+ ap.add_argument("--fmt", default="rgb565", choices=("rgb565", "1bpp"),
279
+ help="picture format for --picture: full-colour RGB565 "
280
+ "(default) or 1 bpp")
281
+ ap.add_argument("--rumble-list", action="store_true",
282
+ help="list the map's rumble legend bindings (§8.8), then exit")
283
+ ap.add_argument("--rumble-bind", nargs=3, metavar=("CODE", "ADDR", "SLOT"),
284
+ help="bind legend CODE (1-15) to a stored picture (§14.9) "
285
+ "on tile ADDR, then save and exit")
286
+ ap.add_argument("--rumble-unbind", metavar="CODE",
287
+ help="drop a legend code's binding, then save and exit")
288
+ ap.add_argument("--rumble-watch", action="store_true",
289
+ help="print rumble codes and player indices as they "
290
+ "arrive, until Ctrl-C (§8.8, §8.5)")
291
+ ap.add_argument("--no-save", action="store_true",
292
+ help="for --rumble-bind/--rumble-unbind: leave the change "
293
+ "in RAM rather than committing it to flash")
294
+ ap.add_argument("--style", metavar="SPEC",
295
+ help='style for --disp: "red", "black on yellow", '
296
+ '"white on blue double", "inverse" (§14.7). Colour '
297
+ 'needs a colour tile; a mono panel refuses it.')
298
+ ap.add_argument("commands", nargs="*", help="commands to run, then exit")
299
+ args = ap.parse_args()
300
+ if args.style and not args.disp:
301
+ ap.error("--style is for --disp")
302
+ if args.fmt != "rgb565" and not args.picture:
303
+ ap.error("--fmt is for --picture")
304
+ if args.no_save and not (args.rumble_bind or args.rumble_unbind):
305
+ ap.error("--no-save is for --rumble-bind and --rumble-unbind")
306
+ rumbling = bool(args.rumble_list or args.rumble_bind or args.rumble_unbind
307
+ or args.rumble_watch)
308
+
309
+ try:
310
+ board = Board.open(port=args.port)
311
+ except BoardError as e:
312
+ sys.exit(str(e))
313
+ # The rumble modes print a line of their own for each event, so the raw
314
+ # echo would say everything twice.
315
+ if not args.flash and not args.picture and not rumbling:
316
+ board.on_line(print) # echo everything the board says
317
+ try:
318
+ if args.flash: # its own progress bar; raw echo would fight it
319
+ return flash(board, args.flash[0], args.flash[1])
320
+ if args.picture: # its own progress bar, like --flash
321
+ return picture(board, args.picture[0], args.picture[1],
322
+ args.picture[2], args.fmt)
323
+ if args.disp:
324
+ return disp(board, args.disp[0], args.disp[1], args.disp[2], args.style)
325
+ if args.rumble_bind:
326
+ return rumble_bind(board, args.rumble_bind[0], args.rumble_bind[1],
327
+ args.rumble_bind[2], save=not args.no_save)
328
+ if args.rumble_unbind:
329
+ return rumble_unbind(board, args.rumble_unbind, save=not args.no_save)
330
+ if args.rumble_list:
331
+ return rumble_list(board)
332
+ if args.rumble_watch:
333
+ return rumble_watch(board)
334
+ return one_shot(board, args.commands) if args.commands else interactive(board)
335
+ finally:
336
+ board.close()
337
+
338
+
339
+ if __name__ == "__main__":
340
+ sys.exit(main())