klein-bt 0.8.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.
- klein_bt-0.8.0/LICENSE +21 -0
- klein_bt-0.8.0/PKG-INFO +204 -0
- klein_bt-0.8.0/README.md +166 -0
- klein_bt-0.8.0/klein/__init__.py +8 -0
- klein_bt-0.8.0/klein/btlog.py +186 -0
- klein_bt-0.8.0/klein/cli.py +113 -0
- klein_bt-0.8.0/klein/gateway.py +457 -0
- klein_bt-0.8.0/klein/groot2_protocol.py +291 -0
- klein_bt-0.8.0/klein/layout.py +523 -0
- klein_bt-0.8.0/klein/mock_robot.py +961 -0
- klein_bt-0.8.0/klein/recorder.py +184 -0
- klein_bt-0.8.0/klein/recording.py +570 -0
- klein_bt-0.8.0/klein/robot_link.py +124 -0
- klein_bt-0.8.0/klein/routes.py +208 -0
- klein_bt-0.8.0/klein/static/app.js +400 -0
- klein_bt-0.8.0/klein/static/blackboard.js +478 -0
- klein_bt-0.8.0/klein/static/cursor.js +108 -0
- klein_bt-0.8.0/klein/static/d3.v7.min.js +2 -0
- klein_bt-0.8.0/klein/static/drawer.js +241 -0
- klein_bt-0.8.0/klein/static/index.html +222 -0
- klein_bt-0.8.0/klein/static/log.js +360 -0
- klein_bt-0.8.0/klein/static/overview.js +320 -0
- klein_bt-0.8.0/klein/static/recording.js +807 -0
- klein_bt-0.8.0/klein/static/renderers.js +314 -0
- klein_bt-0.8.0/klein/static/styles.css +1602 -0
- klein_bt-0.8.0/klein/static/timeline.js +453 -0
- klein_bt-0.8.0/klein/static/transport.js +200 -0
- klein_bt-0.8.0/klein/static/tree.js +856 -0
- klein_bt-0.8.0/klein/static/ui.js +248 -0
- klein_bt-0.8.0/klein/static/view.js +253 -0
- klein_bt-0.8.0/klein/static/window.js +166 -0
- klein_bt-0.8.0/klein/streaming.py +202 -0
- klein_bt-0.8.0/klein_bt.egg-info/PKG-INFO +204 -0
- klein_bt-0.8.0/klein_bt.egg-info/SOURCES.txt +38 -0
- klein_bt-0.8.0/klein_bt.egg-info/dependency_links.txt +1 -0
- klein_bt-0.8.0/klein_bt.egg-info/entry_points.txt +3 -0
- klein_bt-0.8.0/klein_bt.egg-info/requires.txt +8 -0
- klein_bt-0.8.0/klein_bt.egg-info/top_level.txt +1 -0
- klein_bt-0.8.0/pyproject.toml +63 -0
- klein_bt-0.8.0/setup.cfg +4 -0
klein_bt-0.8.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johan Ubbink
|
|
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.
|
klein_bt-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: klein-bt
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: Lightweight CLI telemetry dashboard for BehaviorTree.CPP v4 over ZeroMQ.
|
|
5
|
+
Author-email: Johan Ubbink <25775970+johanubbink@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/johanubbink/klein-bt
|
|
8
|
+
Project-URL: Issues, https://github.com/johanubbink/klein-bt/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/johanubbink/klein-bt/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: behaviortree,behaviortree.cpp,groot,groot2,robotics,zeromq,telemetry,dashboard,visualization
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
25
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
26
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: pyzmq>=25
|
|
31
|
+
Requires-Dist: websockets>=14
|
|
32
|
+
Requires-Dist: msgpack>=1.0
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: build; extra == "dev"
|
|
35
|
+
Requires-Dist: twine; extra == "dev"
|
|
36
|
+
Requires-Dist: playwright; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# klein
|
|
40
|
+
|
|
41
|
+
klein lets you watch a running BehaviorTree.CPP v4 tree live in your browser.
|
|
42
|
+
You see which nodes are running, succeeding or failing, plus the current
|
|
43
|
+
blackboard values, as the robot ticks.
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
Install it from [PyPI](https://pypi.org/project/klein-bt/) with
|
|
50
|
+
[uv](https://docs.astral.sh/uv/):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv tool install klein-bt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
uv fetches a newer Python by itself if yours is older than the 3.10 klein
|
|
57
|
+
needs. [pipx](https://pipx.pypa.io) works too:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pipx install klein-bt
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This gives you two commands: `klein-bt` (the dashboard) and `klein-bt-mock`
|
|
64
|
+
(a fake robot for trying it out).
|
|
65
|
+
|
|
66
|
+
## Run it
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
klein-bt # robot on 127.0.0.1:1667
|
|
70
|
+
klein-bt --robot-host 10.0.0.5 --robot-port 1667 # robot somewhere else
|
|
71
|
+
klein-bt --port 8080 --no-browser # don't open a browser
|
|
72
|
+
klein-bt --open run.btlog # a saved recording, no robot
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Flag | Default | Meaning |
|
|
76
|
+
| ----------------- | ----------- | ------------------------------------ |
|
|
77
|
+
| `--robot-host` | `127.0.0.1` | IP address of the robot |
|
|
78
|
+
| `--robot-port` | `1667` | Groot2 publisher port on the robot |
|
|
79
|
+
| `--port` | `8080` | port the dashboard is served on |
|
|
80
|
+
| `--no-browser` | off | don't open the browser automatically |
|
|
81
|
+
| `--record-buffer` | `10m` | how much recent history to record (`90s`, `30m`, `1h`); `0` turns recording off |
|
|
82
|
+
| `--open FILE` | | show a saved `.btlog` (and `FILE.bb.jsonl` beside it) instead of a robot |
|
|
83
|
+
|
|
84
|
+
klein opens `http://localhost:8080` for you. You can start it before the robot
|
|
85
|
+
is up; it keeps retrying until the robot appears.
|
|
86
|
+
|
|
87
|
+
## Using the dashboard
|
|
88
|
+
|
|
89
|
+
- Node colours show status: green is SUCCESS, pulsing amber is RUNNING, red is
|
|
90
|
+
FAILURE, grey is IDLE.
|
|
91
|
+
- Scroll to zoom and drag to pan. Click a node to fold or unfold its children.
|
|
92
|
+
- Press `R` to see the whole tree again, or `F` to jump to whatever is running.
|
|
93
|
+
- Hover a node to see its ports.
|
|
94
|
+
- **Layout** switches between a vertical and a horizontal tree.
|
|
95
|
+
- The **Blackboards** list shows every subtree's values. A value that just
|
|
96
|
+
changed gets a thin amber edge that fades; a key that changes all the time
|
|
97
|
+
(a position, a counter) gets a steady dim edge and a `~` instead. Click a
|
|
98
|
+
row to expand it, or hover a board to find its node in the tree. See
|
|
99
|
+
[docs/blackboards.md](https://github.com/johanubbink/klein-bt/blob/main/docs/blackboards.md) for how values are shown.
|
|
100
|
+
- Hover a key to see which nodes use it: writers outlined solid, readers
|
|
101
|
+
dashed. Hover a node to ring its keys. Writers get a small amber dot when
|
|
102
|
+
the key changes.
|
|
103
|
+
- Drag the side panel's right edge to make it wider or narrower, or fold it
|
|
104
|
+
away with its `◂` button (or a double-click on the edge) for more room.
|
|
105
|
+
klein remembers both.
|
|
106
|
+
|
|
107
|
+
If the robot loads a different tree, the dashboard switches to it by itself.
|
|
108
|
+
|
|
109
|
+
## Record, rewind and replay
|
|
110
|
+
|
|
111
|
+
klein records everything the robot does (BehaviorTree.CPP 4.3.3 or newer),
|
|
112
|
+
blackboard included, and keeps the last 10 minutes. Change that with
|
|
113
|
+
`--record-buffer` (`30m`, `1h`, or `0` to turn recording off). Don't log
|
|
114
|
+
from Groot2 against the same robot at the same time: each would get only
|
|
115
|
+
some of the transitions.
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
The drawer under the tree shows the recording. Its top row works in every
|
|
120
|
+
tab, even with the drawer folded:
|
|
121
|
+
|
|
122
|
+
- |◀ ▶| step to the previous or next transition, and ▶ plays at 1×.
|
|
123
|
+
While live it reads ❚❚: press it to freeze the tree, the blackboard and
|
|
124
|
+
the clock right where they are.
|
|
125
|
+
- The green **● Live** says you are live. In the past it turns into
|
|
126
|
+
**Go live**, which takes you back; the tree gets a thin amber edge,
|
|
127
|
+
and the clock beside the bar says which moment you see. Changes are marked
|
|
128
|
+
in the past too: stepping or clicking shows which values just changed.
|
|
129
|
+
- The bar is a minimap of the recording, eight of the Timeline's windows
|
|
130
|
+
long (live, now is its right edge): amber columns show how busy the robot
|
|
131
|
+
was, hatched stretches are outages, and a pink dashed line marks where
|
|
132
|
+
the robot started another tree. Click or drag it to go to that moment;
|
|
133
|
+
hover it for the time. On the Timeline tab a box on it is the Timeline's
|
|
134
|
+
window: drag it (or turn the wheel) to pan. While the pointer is over the
|
|
135
|
+
bar, a thin scrollbar for the whole recording shows underneath.
|
|
136
|
+
- The keys: ←/→ step, Space plays or pauses, Home goes to the oldest moment
|
|
137
|
+
kept, End or Esc goes live, and `\` zooms the Timeline to fit everything
|
|
138
|
+
kept (also its ⤢ button).
|
|
139
|
+
|
|
140
|
+
Below it are two tabs, a filter and the recording pill:
|
|
141
|
+
|
|
142
|
+
- **Log**: every transition. Click a row to see the tree and blackboard at
|
|
143
|
+
that moment.
|
|
144
|
+
- **Timeline**: each node's running time as bars, so every retry shows. Drag
|
|
145
|
+
the playhead to go back in time; − and + change the window.
|
|
146
|
+
- The **Filter** beside the tabs narrows both tabs, and stepping, to the
|
|
147
|
+
nodes and subtrees you name.
|
|
148
|
+
- **Save**, on the recording pill (`● Recording 10 min · 148 kB`),
|
|
149
|
+
downloads one `.zip` with a `.btlog` per tree run (Groot2 opens these)
|
|
150
|
+
and its blackboard in a `.bb.jsonl` beside it.
|
|
151
|
+
|
|
152
|
+
`klein-bt --open FILE.btlog` shows a saved recording without a robot, in the
|
|
153
|
+
same Log and Timeline; **Go to end** takes you to its end. Unzip a saved
|
|
154
|
+
`.zip` first; the `.bb.jsonl` beside the file brings its blackboard along.
|
|
155
|
+
|
|
156
|
+
## Set up your robot
|
|
157
|
+
|
|
158
|
+
klein talks to the Groot2 publisher that comes with BehaviorTree.CPP v4. If
|
|
159
|
+
Groot2 can connect to your robot, klein can too. If not, add a publisher after
|
|
160
|
+
you create the tree:
|
|
161
|
+
|
|
162
|
+
```cpp
|
|
163
|
+
#include "behaviortree_cpp/loggers/groot2_publisher.h"
|
|
164
|
+
|
|
165
|
+
auto tree = factory.createTreeFromFile("my_tree.xml");
|
|
166
|
+
BT::Groot2Publisher publisher(tree, 1667);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The publisher also uses the next port up (1668), so leave that one free.
|
|
170
|
+
|
|
171
|
+
## Try it without a robot
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
klein-bt-mock --port 1777 # in one terminal
|
|
175
|
+
klein-bt --robot-port 1777 # in another
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Add `--switch-every 200` to the mock to make it swap between two trees every
|
|
179
|
+
20 seconds or so.
|
|
180
|
+
|
|
181
|
+
## Docs
|
|
182
|
+
|
|
183
|
+
- [docs/blackboards.md](https://github.com/johanubbink/klein-bt/blob/main/docs/blackboards.md): reading blackboard values, and
|
|
184
|
+
adding a renderer for your own message types.
|
|
185
|
+
- [docs/architecture.md](https://github.com/johanubbink/klein-bt/blob/main/docs/architecture.md): how klein works inside.
|
|
186
|
+
- [docs/protocol.md](https://github.com/johanubbink/klein-bt/blob/main/docs/protocol.md): the Groot2 wire protocol klein speaks.
|
|
187
|
+
- [docs/testing.md](https://github.com/johanubbink/klein-bt/blob/main/docs/testing.md): the test tiers and harness, and
|
|
188
|
+
`scripts/make_gifs.py`, which remakes the GIFs above.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
klein is released under the MIT License — see [LICENSE](https://github.com/johanubbink/klein-bt/blob/main/LICENSE).
|
|
193
|
+
|
|
194
|
+
The dashboard bundles [D3.js](https://d3js.org) v7 (ISC License, © Mike
|
|
195
|
+
Bostock), served locally so it works on air-gapped networks.
|
|
196
|
+
|
|
197
|
+
## Disclaimer
|
|
198
|
+
|
|
199
|
+
klein is an independent tool. It is not affiliated with, endorsed by, or
|
|
200
|
+
sponsored by the BehaviorTree.CPP project or the authors of Groot / Groot2.
|
|
201
|
+
"BehaviorTree.CPP", "Groot", and "Groot2" are the property of their respective
|
|
202
|
+
owners. klein interoperates over the Groot2 publisher wire protocol that is part
|
|
203
|
+
of the open-source, MIT-licensed BehaviorTree.CPP library; it contains no code
|
|
204
|
+
copied from those projects.
|
klein_bt-0.8.0/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# klein
|
|
2
|
+
|
|
3
|
+
klein lets you watch a running BehaviorTree.CPP v4 tree live in your browser.
|
|
4
|
+
You see which nodes are running, succeeding or failing, plus the current
|
|
5
|
+
blackboard values, as the robot ticks.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Install it from [PyPI](https://pypi.org/project/klein-bt/) with
|
|
12
|
+
[uv](https://docs.astral.sh/uv/):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv tool install klein-bt
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
uv fetches a newer Python by itself if yours is older than the 3.10 klein
|
|
19
|
+
needs. [pipx](https://pipx.pypa.io) works too:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pipx install klein-bt
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This gives you two commands: `klein-bt` (the dashboard) and `klein-bt-mock`
|
|
26
|
+
(a fake robot for trying it out).
|
|
27
|
+
|
|
28
|
+
## Run it
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
klein-bt # robot on 127.0.0.1:1667
|
|
32
|
+
klein-bt --robot-host 10.0.0.5 --robot-port 1667 # robot somewhere else
|
|
33
|
+
klein-bt --port 8080 --no-browser # don't open a browser
|
|
34
|
+
klein-bt --open run.btlog # a saved recording, no robot
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
| Flag | Default | Meaning |
|
|
38
|
+
| ----------------- | ----------- | ------------------------------------ |
|
|
39
|
+
| `--robot-host` | `127.0.0.1` | IP address of the robot |
|
|
40
|
+
| `--robot-port` | `1667` | Groot2 publisher port on the robot |
|
|
41
|
+
| `--port` | `8080` | port the dashboard is served on |
|
|
42
|
+
| `--no-browser` | off | don't open the browser automatically |
|
|
43
|
+
| `--record-buffer` | `10m` | how much recent history to record (`90s`, `30m`, `1h`); `0` turns recording off |
|
|
44
|
+
| `--open FILE` | | show a saved `.btlog` (and `FILE.bb.jsonl` beside it) instead of a robot |
|
|
45
|
+
|
|
46
|
+
klein opens `http://localhost:8080` for you. You can start it before the robot
|
|
47
|
+
is up; it keeps retrying until the robot appears.
|
|
48
|
+
|
|
49
|
+
## Using the dashboard
|
|
50
|
+
|
|
51
|
+
- Node colours show status: green is SUCCESS, pulsing amber is RUNNING, red is
|
|
52
|
+
FAILURE, grey is IDLE.
|
|
53
|
+
- Scroll to zoom and drag to pan. Click a node to fold or unfold its children.
|
|
54
|
+
- Press `R` to see the whole tree again, or `F` to jump to whatever is running.
|
|
55
|
+
- Hover a node to see its ports.
|
|
56
|
+
- **Layout** switches between a vertical and a horizontal tree.
|
|
57
|
+
- The **Blackboards** list shows every subtree's values. A value that just
|
|
58
|
+
changed gets a thin amber edge that fades; a key that changes all the time
|
|
59
|
+
(a position, a counter) gets a steady dim edge and a `~` instead. Click a
|
|
60
|
+
row to expand it, or hover a board to find its node in the tree. See
|
|
61
|
+
[docs/blackboards.md](https://github.com/johanubbink/klein-bt/blob/main/docs/blackboards.md) for how values are shown.
|
|
62
|
+
- Hover a key to see which nodes use it: writers outlined solid, readers
|
|
63
|
+
dashed. Hover a node to ring its keys. Writers get a small amber dot when
|
|
64
|
+
the key changes.
|
|
65
|
+
- Drag the side panel's right edge to make it wider or narrower, or fold it
|
|
66
|
+
away with its `◂` button (or a double-click on the edge) for more room.
|
|
67
|
+
klein remembers both.
|
|
68
|
+
|
|
69
|
+
If the robot loads a different tree, the dashboard switches to it by itself.
|
|
70
|
+
|
|
71
|
+
## Record, rewind and replay
|
|
72
|
+
|
|
73
|
+
klein records everything the robot does (BehaviorTree.CPP 4.3.3 or newer),
|
|
74
|
+
blackboard included, and keeps the last 10 minutes. Change that with
|
|
75
|
+
`--record-buffer` (`30m`, `1h`, or `0` to turn recording off). Don't log
|
|
76
|
+
from Groot2 against the same robot at the same time: each would get only
|
|
77
|
+
some of the transitions.
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
The drawer under the tree shows the recording. Its top row works in every
|
|
82
|
+
tab, even with the drawer folded:
|
|
83
|
+
|
|
84
|
+
- |◀ ▶| step to the previous or next transition, and ▶ plays at 1×.
|
|
85
|
+
While live it reads ❚❚: press it to freeze the tree, the blackboard and
|
|
86
|
+
the clock right where they are.
|
|
87
|
+
- The green **● Live** says you are live. In the past it turns into
|
|
88
|
+
**Go live**, which takes you back; the tree gets a thin amber edge,
|
|
89
|
+
and the clock beside the bar says which moment you see. Changes are marked
|
|
90
|
+
in the past too: stepping or clicking shows which values just changed.
|
|
91
|
+
- The bar is a minimap of the recording, eight of the Timeline's windows
|
|
92
|
+
long (live, now is its right edge): amber columns show how busy the robot
|
|
93
|
+
was, hatched stretches are outages, and a pink dashed line marks where
|
|
94
|
+
the robot started another tree. Click or drag it to go to that moment;
|
|
95
|
+
hover it for the time. On the Timeline tab a box on it is the Timeline's
|
|
96
|
+
window: drag it (or turn the wheel) to pan. While the pointer is over the
|
|
97
|
+
bar, a thin scrollbar for the whole recording shows underneath.
|
|
98
|
+
- The keys: ←/→ step, Space plays or pauses, Home goes to the oldest moment
|
|
99
|
+
kept, End or Esc goes live, and `\` zooms the Timeline to fit everything
|
|
100
|
+
kept (also its ⤢ button).
|
|
101
|
+
|
|
102
|
+
Below it are two tabs, a filter and the recording pill:
|
|
103
|
+
|
|
104
|
+
- **Log**: every transition. Click a row to see the tree and blackboard at
|
|
105
|
+
that moment.
|
|
106
|
+
- **Timeline**: each node's running time as bars, so every retry shows. Drag
|
|
107
|
+
the playhead to go back in time; − and + change the window.
|
|
108
|
+
- The **Filter** beside the tabs narrows both tabs, and stepping, to the
|
|
109
|
+
nodes and subtrees you name.
|
|
110
|
+
- **Save**, on the recording pill (`● Recording 10 min · 148 kB`),
|
|
111
|
+
downloads one `.zip` with a `.btlog` per tree run (Groot2 opens these)
|
|
112
|
+
and its blackboard in a `.bb.jsonl` beside it.
|
|
113
|
+
|
|
114
|
+
`klein-bt --open FILE.btlog` shows a saved recording without a robot, in the
|
|
115
|
+
same Log and Timeline; **Go to end** takes you to its end. Unzip a saved
|
|
116
|
+
`.zip` first; the `.bb.jsonl` beside the file brings its blackboard along.
|
|
117
|
+
|
|
118
|
+
## Set up your robot
|
|
119
|
+
|
|
120
|
+
klein talks to the Groot2 publisher that comes with BehaviorTree.CPP v4. If
|
|
121
|
+
Groot2 can connect to your robot, klein can too. If not, add a publisher after
|
|
122
|
+
you create the tree:
|
|
123
|
+
|
|
124
|
+
```cpp
|
|
125
|
+
#include "behaviortree_cpp/loggers/groot2_publisher.h"
|
|
126
|
+
|
|
127
|
+
auto tree = factory.createTreeFromFile("my_tree.xml");
|
|
128
|
+
BT::Groot2Publisher publisher(tree, 1667);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The publisher also uses the next port up (1668), so leave that one free.
|
|
132
|
+
|
|
133
|
+
## Try it without a robot
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
klein-bt-mock --port 1777 # in one terminal
|
|
137
|
+
klein-bt --robot-port 1777 # in another
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Add `--switch-every 200` to the mock to make it swap between two trees every
|
|
141
|
+
20 seconds or so.
|
|
142
|
+
|
|
143
|
+
## Docs
|
|
144
|
+
|
|
145
|
+
- [docs/blackboards.md](https://github.com/johanubbink/klein-bt/blob/main/docs/blackboards.md): reading blackboard values, and
|
|
146
|
+
adding a renderer for your own message types.
|
|
147
|
+
- [docs/architecture.md](https://github.com/johanubbink/klein-bt/blob/main/docs/architecture.md): how klein works inside.
|
|
148
|
+
- [docs/protocol.md](https://github.com/johanubbink/klein-bt/blob/main/docs/protocol.md): the Groot2 wire protocol klein speaks.
|
|
149
|
+
- [docs/testing.md](https://github.com/johanubbink/klein-bt/blob/main/docs/testing.md): the test tiers and harness, and
|
|
150
|
+
`scripts/make_gifs.py`, which remakes the GIFs above.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
klein is released under the MIT License — see [LICENSE](https://github.com/johanubbink/klein-bt/blob/main/LICENSE).
|
|
155
|
+
|
|
156
|
+
The dashboard bundles [D3.js](https://d3js.org) v7 (ISC License, © Mike
|
|
157
|
+
Bostock), served locally so it works on air-gapped networks.
|
|
158
|
+
|
|
159
|
+
## Disclaimer
|
|
160
|
+
|
|
161
|
+
klein is an independent tool. It is not affiliated with, endorsed by, or
|
|
162
|
+
sponsored by the BehaviorTree.CPP project or the authors of Groot / Groot2.
|
|
163
|
+
"BehaviorTree.CPP", "Groot", and "Groot2" are the property of their respective
|
|
164
|
+
owners. klein interoperates over the Groot2 publisher wire protocol that is part
|
|
165
|
+
of the open-source, MIT-licensed BehaviorTree.CPP library; it contains no code
|
|
166
|
+
copied from those projects.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""klein — a lightweight CLI telemetry dashboard for BehaviorTree.CPP v4.
|
|
2
|
+
|
|
3
|
+
Connects to a running robot node over ZeroMQ (the Groot2 publisher protocol),
|
|
4
|
+
unrolls nested subtrees, and streams live status and blackboard values to an
|
|
5
|
+
interactive D3.js browser dashboard. Run ``klein-bt --help`` for options.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.8.0"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""klein.btlog — FileLogger2 ``.btlog`` files, read and written in memory.
|
|
2
|
+
|
|
3
|
+
The format BehaviorTree.CPP's ``FileLogger2`` writes and Groot2 opens
|
|
4
|
+
(``bt_file_logger_v2.cpp``; the header comment in ``bt_file_logger_v2.h``
|
|
5
|
+
leaves out the magic and the version byte — the .cpp is authoritative).
|
|
6
|
+
All little-endian::
|
|
7
|
+
|
|
8
|
+
b"BTCPP4-FileLogger2" | u8 version (1) | i32 xml_len | xml (UTF-8)
|
|
9
|
+
| i64 first_timestamp (µs since the epoch)
|
|
10
|
+
| N x 9-byte records: u48 offset_us (from first_timestamp) | u16 uid | u8 status
|
|
11
|
+
|
|
12
|
+
A record is exactly a GET_TRANSITIONS record, so the packing is shared with
|
|
13
|
+
``groot2_protocol``. Statuses are live NodeStatus ints (IDLE is a plain 0).
|
|
14
|
+
|
|
15
|
+
``export_run`` and ``export_blackboard`` save one tree run of a ``Recording``
|
|
16
|
+
(docs/protocol.md, "Saving a recording"); ``load_btlog`` turns a file and its
|
|
17
|
+
sidecar back into one (``klein-bt --open``, docs/protocol.md, "Opening a file").
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import collections
|
|
21
|
+
import itertools
|
|
22
|
+
import json
|
|
23
|
+
import struct
|
|
24
|
+
|
|
25
|
+
from .groot2_protocol import TRANSITION_RECORD_SIZE, decode_transitions, encode_transition
|
|
26
|
+
from .recording import Recording, apply_transition, diff_to_transitions
|
|
27
|
+
|
|
28
|
+
MAGIC = b"BTCPP4-FileLogger2"
|
|
29
|
+
VERSION = 1
|
|
30
|
+
_XML_OFFSET = len(MAGIC) + 1 + 4 # the XML starts after magic, version, xml_len
|
|
31
|
+
_TIMESTAMP_SIZE = 8
|
|
32
|
+
|
|
33
|
+
BtlogFile = collections.namedtuple("BtlogFile", "xml first_timestamp records trailing")
|
|
34
|
+
BtlogFile.__doc__ = ("A parsed ``.btlog``. ``records`` are ``(offset_us, uid, status)``; "
|
|
35
|
+
"``trailing`` counts the bytes of an incomplete last record.")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BtlogError(ValueError):
|
|
39
|
+
"""The data is not a FileLogger2 ``.btlog`` klein can read."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def read_btlog(data):
|
|
43
|
+
"""Parse ``.btlog`` bytes into a ``BtlogFile``.
|
|
44
|
+
|
|
45
|
+
A trailing partial record — a robot stopped mid-write — is left out of
|
|
46
|
+
``records`` and counted in ``trailing``, not raised.
|
|
47
|
+
"""
|
|
48
|
+
if not data.startswith(MAGIC):
|
|
49
|
+
raise BtlogError("not a FileLogger2 .btlog (no BTCPP4-FileLogger2 magic)")
|
|
50
|
+
version = data[len(MAGIC):len(MAGIC) + 1]
|
|
51
|
+
if version != bytes([VERSION]):
|
|
52
|
+
shown = version[0] if version else "missing"
|
|
53
|
+
raise BtlogError(f"unknown .btlog format version {shown}")
|
|
54
|
+
xml_len = int.from_bytes(data[len(MAGIC) + 1:_XML_OFFSET], "little", signed=True)
|
|
55
|
+
if not 0 <= xml_len <= len(data) - _XML_OFFSET - _TIMESTAMP_SIZE:
|
|
56
|
+
raise BtlogError(f"XML length {xml_len} does not fit the file")
|
|
57
|
+
body = _XML_OFFSET + xml_len + _TIMESTAMP_SIZE # where the records start
|
|
58
|
+
xml = data[_XML_OFFSET:_XML_OFFSET + xml_len].decode("utf-8")
|
|
59
|
+
(first_timestamp,) = struct.unpack_from("<q", data, body - _TIMESTAMP_SIZE)
|
|
60
|
+
records = data[body:]
|
|
61
|
+
return BtlogFile(xml, first_timestamp, decode_transitions(records),
|
|
62
|
+
len(records) % TRANSITION_RECORD_SIZE)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _header(xml, first_timestamp):
|
|
66
|
+
body = xml.encode("utf-8")
|
|
67
|
+
return b"".join([MAGIC, bytes([VERSION]), struct.pack("<i", len(body)), body,
|
|
68
|
+
struct.pack("<q", first_timestamp)])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_btlog(xml, first_timestamp, records):
|
|
72
|
+
"""Serialize a ``.btlog``; ``records`` are ``(offset_us, uid, status)``."""
|
|
73
|
+
return b"".join([_header(xml, first_timestamp)]
|
|
74
|
+
+ [encode_transition(*record) for record in records])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def export_run(run, xml):
|
|
78
|
+
"""One tree run (``Recording.runs()``) as ``.btlog`` bytes.
|
|
79
|
+
|
|
80
|
+
``first_timestamp`` is the run's retained start. Replayed from all-IDLE, as
|
|
81
|
+
Groot2 and klein start a file, the file gives the recorded state at every
|
|
82
|
+
retained time: each segment opens with the transitions from the state
|
|
83
|
+
before it (all-IDLE for the first) to its retained start — the prefix at
|
|
84
|
+
offset 0, and at each restart or re-arm inside the run.
|
|
85
|
+
|
|
86
|
+
A record ``apply_transition`` ignores (an IDLE on a node already idle: an
|
|
87
|
+
arm-overlap record the baseline already held) is left out, so a reader
|
|
88
|
+
with the plain publisher rule never sees "was IDLE".
|
|
89
|
+
"""
|
|
90
|
+
first = run[0].t_start
|
|
91
|
+
state = bytearray(len(run[0].baseline)) # the file's state, as written so far
|
|
92
|
+
out = bytearray(_header(xml, first))
|
|
93
|
+
for segment in run:
|
|
94
|
+
start = segment.state_at_seq(segment.start_seq)
|
|
95
|
+
opening = [(segment.t_start, uid, status)
|
|
96
|
+
for uid, status in diff_to_transitions(state, start)]
|
|
97
|
+
recorded = ((ts, uid, status) for _seq, ts, uid, status
|
|
98
|
+
in segment.iter_records(segment.start_seq, segment.head_seq))
|
|
99
|
+
for ts, uid, status in itertools.chain(opening, recorded):
|
|
100
|
+
if apply_transition(state, uid, status):
|
|
101
|
+
out += encode_transition(ts - first, uid, status)
|
|
102
|
+
return bytes(out)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def export_blackboard(run, tree_id):
|
|
106
|
+
"""One tree run's blackboard history as ``.bb.jsonl`` bytes: a header line,
|
|
107
|
+
then one line per change at its offset from the run's retained start.
|
|
108
|
+
|
|
109
|
+
Each segment opens with the lines that turn the previous values (none, for
|
|
110
|
+
the first) into its values at its retained start, so the file stands alone
|
|
111
|
+
after eviction and across restarts.
|
|
112
|
+
"""
|
|
113
|
+
first = run[0].t_start
|
|
114
|
+
lines = [json.dumps({"klein_blackboard": 1, "first_timestamp": first, "tree_id": tree_id})]
|
|
115
|
+
values = {} # (board, key) -> JSON text, as of the last line
|
|
116
|
+
|
|
117
|
+
def line(t, board, key, text):
|
|
118
|
+
entry = json.dumps({"t": t - first, "board": board, "key": key})[:-1]
|
|
119
|
+
if text is None:
|
|
120
|
+
del values[board, key]
|
|
121
|
+
lines.append(f'{entry}, "removed": true}}')
|
|
122
|
+
else:
|
|
123
|
+
values[board, key] = text # the stored JSON text goes in as it is
|
|
124
|
+
lines.append(f'{entry}, "value": {text}}}')
|
|
125
|
+
|
|
126
|
+
for segment in run:
|
|
127
|
+
track = segment.blackboard
|
|
128
|
+
if track.t_start is None:
|
|
129
|
+
continue
|
|
130
|
+
base = max(track.t_start, first)
|
|
131
|
+
at_base, later = {}, []
|
|
132
|
+
for t, board, key, text in track.changes():
|
|
133
|
+
if t <= base:
|
|
134
|
+
at_base[board, key] = text
|
|
135
|
+
else:
|
|
136
|
+
later.append((t, board, key, text))
|
|
137
|
+
for name in [name for name in values if at_base.get(name) is None]:
|
|
138
|
+
line(base, *name, None)
|
|
139
|
+
for name, text in at_base.items():
|
|
140
|
+
if text is not None and values.get(name) != text:
|
|
141
|
+
line(base, *name, text)
|
|
142
|
+
for change in later:
|
|
143
|
+
line(*change)
|
|
144
|
+
return "".join(entry + "\n" for entry in lines).encode("utf-8")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_btlog(log, layout, sidecar=None):
|
|
148
|
+
"""A ``Recording`` of one parsed file (``read_btlog``), as the gateway
|
|
149
|
+
would have recorded it: one ended segment from ``first_timestamp``,
|
|
150
|
+
replayed from all-IDLE as Groot2 starts a file, with its head at the last
|
|
151
|
+
record.
|
|
152
|
+
|
|
153
|
+
``layout`` is the file's ``TreeLayout`` (``parse_layout`` of its XML),
|
|
154
|
+
the segment's layout. ``sidecar`` is the ``.bb.jsonl`` text
|
|
155
|
+
(``export_blackboard``), or None. Its lines become blackboard samples
|
|
156
|
+
holding every board in the layout's ``blackboard_names``, so a board with
|
|
157
|
+
no lines (no keys of its own) still shows, empty, as it does live. The
|
|
158
|
+
first sample is at the first line, or at the sidecar's start when it has
|
|
159
|
+
none.
|
|
160
|
+
"""
|
|
161
|
+
recording = Recording()
|
|
162
|
+
segment = recording.begin_segment(layout, log.first_timestamp, bytes(layout.size))
|
|
163
|
+
recording.append([(log.first_timestamp + offset, uid, status)
|
|
164
|
+
for offset, uid, status in log.records])
|
|
165
|
+
if sidecar is not None:
|
|
166
|
+
_load_blackboard(recording, sidecar, layout.blackboard_names)
|
|
167
|
+
recording.end_segment(segment.last_ts)
|
|
168
|
+
recording.advance_head(segment.last_ts)
|
|
169
|
+
return recording
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _load_blackboard(recording, text, boards):
|
|
173
|
+
header, *lines = [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
174
|
+
first = header["first_timestamp"]
|
|
175
|
+
values = {board: {} for board in boards}
|
|
176
|
+
by_time = collections.defaultdict(list)
|
|
177
|
+
for line in lines:
|
|
178
|
+
by_time[line["t"]].append(line)
|
|
179
|
+
for t in sorted(by_time) or [0]:
|
|
180
|
+
for line in by_time[t]:
|
|
181
|
+
entries = values.setdefault(line["board"], {})
|
|
182
|
+
if line.get("removed"):
|
|
183
|
+
entries.pop(line["key"], None)
|
|
184
|
+
else:
|
|
185
|
+
entries[line["key"]] = line["value"]
|
|
186
|
+
recording.add_blackboard(first + t, values)
|