Skip to content

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:

  1. 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/play could reuse the whole UI by implementing one interface.
  2. Front-ends share render.py. A tree looks the same in snapshot, watch and 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:

with Groot2MockServer(port=1667) as server:
    ...   # server.stop() on exit

Testing conventions

  • Ports come from the free_port() helper in conftest.py; nothing hard-codes one.
  • Anything timing-dependent goes through wait_for(predicate, timeout=...) rather than sleep.
  • 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.