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.
Files changed (40) hide show
  1. klein_bt-0.8.0/LICENSE +21 -0
  2. klein_bt-0.8.0/PKG-INFO +204 -0
  3. klein_bt-0.8.0/README.md +166 -0
  4. klein_bt-0.8.0/klein/__init__.py +8 -0
  5. klein_bt-0.8.0/klein/btlog.py +186 -0
  6. klein_bt-0.8.0/klein/cli.py +113 -0
  7. klein_bt-0.8.0/klein/gateway.py +457 -0
  8. klein_bt-0.8.0/klein/groot2_protocol.py +291 -0
  9. klein_bt-0.8.0/klein/layout.py +523 -0
  10. klein_bt-0.8.0/klein/mock_robot.py +961 -0
  11. klein_bt-0.8.0/klein/recorder.py +184 -0
  12. klein_bt-0.8.0/klein/recording.py +570 -0
  13. klein_bt-0.8.0/klein/robot_link.py +124 -0
  14. klein_bt-0.8.0/klein/routes.py +208 -0
  15. klein_bt-0.8.0/klein/static/app.js +400 -0
  16. klein_bt-0.8.0/klein/static/blackboard.js +478 -0
  17. klein_bt-0.8.0/klein/static/cursor.js +108 -0
  18. klein_bt-0.8.0/klein/static/d3.v7.min.js +2 -0
  19. klein_bt-0.8.0/klein/static/drawer.js +241 -0
  20. klein_bt-0.8.0/klein/static/index.html +222 -0
  21. klein_bt-0.8.0/klein/static/log.js +360 -0
  22. klein_bt-0.8.0/klein/static/overview.js +320 -0
  23. klein_bt-0.8.0/klein/static/recording.js +807 -0
  24. klein_bt-0.8.0/klein/static/renderers.js +314 -0
  25. klein_bt-0.8.0/klein/static/styles.css +1602 -0
  26. klein_bt-0.8.0/klein/static/timeline.js +453 -0
  27. klein_bt-0.8.0/klein/static/transport.js +200 -0
  28. klein_bt-0.8.0/klein/static/tree.js +856 -0
  29. klein_bt-0.8.0/klein/static/ui.js +248 -0
  30. klein_bt-0.8.0/klein/static/view.js +253 -0
  31. klein_bt-0.8.0/klein/static/window.js +166 -0
  32. klein_bt-0.8.0/klein/streaming.py +202 -0
  33. klein_bt-0.8.0/klein_bt.egg-info/PKG-INFO +204 -0
  34. klein_bt-0.8.0/klein_bt.egg-info/SOURCES.txt +38 -0
  35. klein_bt-0.8.0/klein_bt.egg-info/dependency_links.txt +1 -0
  36. klein_bt-0.8.0/klein_bt.egg-info/entry_points.txt +3 -0
  37. klein_bt-0.8.0/klein_bt.egg-info/requires.txt +8 -0
  38. klein_bt-0.8.0/klein_bt.egg-info/top_level.txt +1 -0
  39. klein_bt-0.8.0/pyproject.toml +63 -0
  40. 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.
@@ -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
+ ![klein showing a live CrossDoor behavior tree: running nodes pulse, the blackboard updates, and the Timeline below draws each node's RUNNING spans as the robot ticks](https://raw.githubusercontent.com/johanubbink/klein-bt/main/assets/klein-demo.gif)
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
+ ![klein rewinding a live robot: hover the minimap (its scrollbar shows), drag its box back, click a burst, scrub, step, hover a key, play, back to live](https://raw.githubusercontent.com/johanubbink/klein-bt/main/assets/klein-rewind.gif)
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.
@@ -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
+ ![klein showing a live CrossDoor behavior tree: running nodes pulse, the blackboard updates, and the Timeline below draws each node's RUNNING spans as the robot ticks](https://raw.githubusercontent.com/johanubbink/klein-bt/main/assets/klein-demo.gif)
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
+ ![klein rewinding a live robot: hover the minimap (its scrollbar shows), drag its box back, click a burst, scrub, step, hover a key, play, back to live](https://raw.githubusercontent.com/johanubbink/klein-bt/main/assets/klein-rewind.gif)
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)