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.
- bbr_buttonboard-0.1.0/.gitignore +3 -0
- bbr_buttonboard-0.1.0/LICENSE +21 -0
- bbr_buttonboard-0.1.0/PKG-INFO +121 -0
- bbr_buttonboard-0.1.0/README.md +97 -0
- bbr_buttonboard-0.1.0/pyproject.toml +43 -0
- bbr_buttonboard-0.1.0/src/buttonboard/__init__.py +52 -0
- bbr_buttonboard-0.1.0/src/buttonboard/bbctl.py +340 -0
- bbr_buttonboard-0.1.0/src/buttonboard/client.py +912 -0
- bbr_buttonboard-0.1.0/src/buttonboard/companion.py +344 -0
- bbr_buttonboard-0.1.0/src/buttonboard/crc.py +31 -0
- bbr_buttonboard-0.1.0/src/buttonboard/display.py +436 -0
- bbr_buttonboard-0.1.0/src/buttonboard/errors.py +38 -0
- bbr_buttonboard-0.1.0/src/buttonboard/leds.py +145 -0
- bbr_buttonboard-0.1.0/src/buttonboard/mock.py +650 -0
- bbr_buttonboard-0.1.0/src/buttonboard/rumble.py +537 -0
- bbr_buttonboard-0.1.0/src/buttonboard/sound.py +72 -0
- bbr_buttonboard-0.1.0/src/buttonboard/transport.py +141 -0
- bbr_buttonboard-0.1.0/tests/test_buttonboard.py +866 -0
|
@@ -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())
|