Development¶
git clone https://github.com/guilyx/btview
cd btview
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
That is the whole setup. No robot, no ROS and no network are needed — the test suite spins up the mock publishers on loopback and talks to them over real ZMQ sockets.
Checks¶
pytest # ~110 tests, a few seconds
ruff check src tests # lint
ruff format src tests # format
mypy # types
CI runs exactly these on Python 3.9 through 3.13, plus an end-to-end smoke job that starts
btview mock and drives snapshot, info, echo, export, record and replay against
it. pre-commit install runs the lint and format hooks locally if you want them.
Docs¶
The site at https://guilyx.github.io/btview/ is this docs/ directory, built by
MkDocs Material.
pip install -e ".[docs]"
mkdocs serve # http://127.0.0.1:8000, live reload
mkdocs build --strict # what CI runs
--strict fails on a dead internal link, a missing anchor, or a page that is not in the
nav — so adding a page means adding it to mkdocs.yml too. Links that point outside
docs/ (at source files, say) have to be absolute GitHub URLs; a relative one works on
GitHub and 404s on the site.
Images live in media/ at the repository root because the README needs them there;
tools/mkdocs_hooks.py mirrors them into docs/media/ at build time, so a page refers to
media/demo.gif and it resolves in both places. Every push to main redeploys.
One-time Pages setup¶
Pages has to be switched on by hand, once, by someone with admin on the repository: Settings → Pages → Build and deployment → Source: GitHub Actions.
The workflow cannot do it for you. actions/configure-pages advertises an enablement
input that creates the site, but creating one is a repository-administration call and
GITHUB_TOKEN is not allowed to make it — the job fails with Create Pages site failed.
Error: Resource not accessible by integration. On a private repository Pages also
needs a paid plan; on a public one the setting is all that is missing.
Until that is done the build job still runs on every push and pull request, so the site
is verified even when it cannot be published; only deploy fails.
Layout¶
src/btview/
├── model.py statuses, nodes, trees — no I/O, no rendering
├── protocol/ pure encode/decode
│ ├── groot2.py v4 framing, status buffers, blackboard
│ ├── btcpp_v3.py v3 flush messages and transitions
│ └── bt_logger_fb.py FlatBuffers accessors for BT_logger.fbs
├── parser.py XML / FlatBuffers → TreeModel
├── client.py ZMQ clients; one MonitorSource interface for both protocols
├── session.py polling, transition history, per-node stats
├── render.py Rich renderers shared by every front-end
├── theme.py glyphs and colours (unicode and ASCII sets)
├── tui.py the Textual app
├── cli.py argparse commands
├── export.py XML, Graphviz, Mermaid, JSON
├── recording.py record and replay
└── mock/ synthetic publishers + a small BT interpreter
Two rules keep this navigable:
- Protocol code never touches a socket, and socket code never renders. That is why the
formats can be tested exhaustively without a server, and why
record/playcould reuse the whole UI by implementing one interface. - Front-ends share
render.py. A tree looks the same insnapshot,watchand the TUI because they call the same function.
Adding a protocol¶
Implement MonitorSource from client.py:
class MySource(MonitorSource):
label = "my-robot"
protocol = "custom"
reports_transitions = False # True if you push transitions yourself
def fetch_tree(self) -> TreeModel: ...
def poll(self) -> Optional[PollResult]: ...
Everything else — the TUI, snapshot, echo, record — works against it unchanged.
ReplaySource in recording.py is a 90-line example.
Using btview as a library¶
from btview.client import Groot2Client
from btview.session import MonitorSession
from btview.model import NodeStatus
session = MonitorSession(Groot2Client(host="192.168.1.42", port=1667))
session.refresh_tree()
while True:
session.poll()
for transition in session.recent(5):
print(transition.format_time(), transition.name, transition.status.name)
if any(session.status_of(uid) == NodeStatus.FAILURE for uid in session.statuses):
...
Other useful entry points:
from btview.parser import parse_bt_xml # XML → TreeModel
from btview.export import to_mermaid, to_json # TreeModel → text
from btview.render import render_tree # TreeModel → Rich Text
from btview.mock import Groot2MockServer # a publisher for your own tests
The mock is handy in downstream test suites:
Testing conventions¶
- Ports come from the
free_port()helper inconftest.py; nothing hard-codes one. - Anything timing-dependent goes through
wait_for(predicate, timeout=...)rather thansleep. - The tick engine is seeded, so mock behaviour is reproducible run to run.
- New protocol handling needs a byte-level test in
test_protocol.py. Those tests are the specification; if BehaviorTree.CPP changes a layout, they are what should fail.
Releasing¶
Releases are tag-driven. .github/workflows/release.yml builds the distribution, runs the
suite against it, publishes to PyPI, and cuts a GitHub Release with the changelog section
and the artifacts attached.
# 1. bump both version strings — CI refuses a tag that disagrees with the package
vim pyproject.toml # [project] version
vim src/btview/__init__.py # __version__
# 2. write the changelog section; the release notes are lifted from it verbatim
vim CHANGELOG.md # ## [X.Y.Z] — ...
# 3. tag and push
git tag vX.Y.Z && git push origin vX.Y.Z
Rehearse first if you want: Actions → Release → Run workflow → testpypi runs the same pipeline against TestPyPI without spending a version number on the real index.
PyPI setup — already done¶
Publishing uses Trusted Publishing, so there is no API token in the repository's secrets. The publisher is registered:
| Field | Value |
|---|---|
| PyPI project name | btview |
| Owner | guilyx |
| Repository name | btview |
| Workflow name | release.yml |
| Environment name | pypi |
If you ever need the TestPyPI rehearsal, register the same at
https://test.pypi.org/manage/account/publishing/ with environment testpypi.
Should the OIDC exchange fail, the build, test and GitHub Release steps still run and the release notes fall back to a git-URL install line — a tag is never wasted on a publish problem.