Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
d951cfa
dotbot/console: add the unified web console app
geonnave Jul 9, 2026
fa1ccc7
dotbot/console: add dev harness (fake swarmit server + demo fleet)
geonnave Jul 9, 2026
4ebfdfe
dotbot/console: rename to console-web (a console could also be a TUI)
geonnave Jul 9, 2026
e8420e1
dotbot/console-web: add list/grid views, minimap pan, toasts
geonnave Jul 9, 2026
ea1cb54
dotbot/console-web: fidelity pass to match design v1 semantics and gl…
geonnave Jul 9, 2026
90ac4b1
dotbot/console-web: drop wheel zoom (not in the design; zoom buttons …
geonnave Jul 9, 2026
663151d
dotbot/console-web: add testbed rail with missions panel
geonnave Jul 9, 2026
2ce66e8
dotbot/console-web: persistent planned missions, square minimap, tall…
geonnave Jul 9, 2026
56ff026
dotbot/console-web: upgrade fake swarmit to a stateful write-capable …
geonnave Jul 9, 2026
8eddf0a
dotbot/console-web: orchestration write path (flash dialog, SSE progr…
geonnave Jul 9, 2026
9db9543
dotbot/console-web: track canvas size live so the arena keeps its mar…
geonnave Jul 9, 2026
ffd1391
dotbot/console-web: theme dev param + puppeteer screenshot helper
geonnave Jul 9, 2026
5e0be46
dotbot/console-web: manual test guide
geonnave Jul 9, 2026
9d8174f
dotbot/console-web: file-manager selection in list/grid (shift range,…
geonnave Jul 9, 2026
6b1a9c9
dotbot/console-web: fix heading pointer and pad steering sense vs con…
geonnave Jul 9, 2026
bff28e4
dotbot/console-web: background click deselects; click-again on sole i…
geonnave Jul 9, 2026
d6e933d
dotbot/console-web: screen-relative pad steering (drag where the bot …
geonnave Jul 9, 2026
276f62a
dotbot/config: add [run.controller] swarmit_url and --swarmit-url
geonnave Jul 10, 2026
6167d36
dotbot/server: serve the console at /console and proxy /swarmit/*
geonnave Jul 10, 2026
18990ef
dotbot/console-web: relative asset base and live host label
geonnave Jul 10, 2026
2b20bcf
dotbot/console-web: add vitest suite, eslint, and typecheck script
geonnave Jul 10, 2026
9c940a2
.github/workflows: check console-web (lint, typecheck, tests, build)
geonnave Jul 10, 2026
21ff5c4
tox: lint console-web in the web env
geonnave Jul 10, 2026
ef437e7
pyproject: ship the console-web app in sdist and wheel
geonnave Jul 10, 2026
bfa62d4
dotbot/server: black reformat
geonnave Jul 10, 2026
38e4f25
dotbot/console-web: black reformat
geonnave Jul 10, 2026
5948273
fix(console-web): interpolate bot positions with adaptive duration
RasdaCorentin Aug 4, 2026
2d282fd
utils/check_wheel_contents: require the built console app
geonnave Aug 19, 2026
5938350
dotbot: render device addresses uppercase everywhere
geonnave Aug 19, 2026
10185ff
dotbot/console-web: show the running image and last reset cause
geonnave Aug 19, 2026
6553780
dotbot/console-web: drive the pad body-relative and open loop
geonnave Aug 19, 2026
85f2e33
dotbot/console-web: open the rail and real-scale bots by default
geonnave Aug 19, 2026
3ff97e8
dotbot/console-web: flash a real image chosen from disk
geonnave Aug 19, 2026
64e8c40
dotbot/console-web: add an inspector panel and a crashed-bots filter
geonnave Aug 19, 2026
ba153cf
dotbot/console-web: remember recently flashed image names
geonnave Aug 19, 2026
cee9904
dotbot/console-web: reflash a recent image from the flash dialog
geonnave Aug 19, 2026
b6cd8f8
dotbot/console-web: only the drag surfaces block text selection
geonnave Aug 19, 2026
b8c6b15
dotbot/server: report how the controller reaches the swarm
geonnave Aug 19, 2026
e797498
dotbot/console-web: show the connection and swarm id in the title bar
geonnave Aug 19, 2026
c89783c
dotbot/console-web: drop the dead close button from the title bar
geonnave Aug 19, 2026
f3d1706
dotbot/console-web: render swarmit's display strings instead of copyi…
geonnave Aug 19, 2026
07c53e3
dotbot/console-web: read the battery on the CLI's scale
geonnave Aug 19, 2026
0816789
dotbot/console-web: render swarmit's battery reading per robot
geonnave Aug 19, 2026
bbcbabe
dotbot/console-web: split the sandbox and control-plane states
geonnave Aug 19, 2026
82b2bda
dotbot/console-web: widen the inspector so a version fits one line
geonnave Aug 19, 2026
6d30ae6
dotbot/console-web: hide the fault registers a watchdog never wrote
geonnave Aug 19, 2026
4d27689
dotbot/console-web: label the title-bar chip "swarm id"
geonnave Aug 19, 2026
0df488a
dotbot/console-web: pick firmware from the testbed rail
geonnave Aug 19, 2026
bc19f23
dotbot/console-web: one firmware picker, pinning instead of a second …
geonnave Aug 19, 2026
48baae2
dotbot/console-web: drop the Reset button
geonnave Aug 19, 2026
e6a34d9
dotbot/console-web: badge bots whose last reset was not routine
geonnave Aug 19, 2026
1fadf83
dotbot/console-web: mark an abnormal reset with a warning triangle
geonnave Aug 19, 2026
d34ead6
dotbot/tests: apply black
geonnave Aug 19, 2026
583dbd6
codecov: gate on the Python coverage, report the web apps
geonnave Aug 19, 2026
76a587f
dotbot/console-web: serve realistic device info from the fake swarmit
geonnave Aug 19, 2026
fade376
dotbot/dotbot_simulator: match addresses on the canonical rendering
geonnave Aug 25, 2026
f9623be
dotbot/console-web: stop reporting failures as successes
geonnave Aug 25, 2026
7ae0941
dotbot/joystick: accept either case for the address flag
geonnave Aug 25, 2026
4c2382d
doc: uppercase the device addresses in the examples
geonnave Aug 25, 2026
38566c9
dotbot/server: open the console by default
geonnave Aug 25, 2026
59e6481
dotbot/tests: stop the suite opening real browser windows
geonnave Aug 25, 2026
55715c0
dotbot/controller: bind the API to loopback by default
geonnave Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
matrix.node-version == 20
uses: codecov/codecov-action@v4
with:
flags: python
verbose: true

control_loop:
Expand Down Expand Up @@ -111,15 +112,46 @@ jobs:
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
flags: frontend
verbose: true
- name: Upload frontend build
uses: actions/upload-artifact@v4
with:
name: frontend
path: ./dotbot/frontend/build

console:
name: check console
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up nodejs
uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install
working-directory: ./dotbot/console-web
- run: npm run lint
working-directory: ./dotbot/console-web
- run: npm run typecheck
working-directory: ./dotbot/console-web
- run: npm run test
working-directory: ./dotbot/console-web
- run: npm run build
working-directory: ./dotbot/console-web
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
flags: console
verbose: true
- name: Upload console build
uses: actions/upload-artifact@v4
with:
name: console
path: ./dotbot/console-web/dist

package:
needs: [test, doc, frontend, control_loop]
needs: [test, doc, frontend, console, control_loop]
name: build source package
runs-on: ${{ matrix.os }}
strategy:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*.log
*.egg-info/
.coverage
coverage/
.tox/
dist/
node_modules/
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,28 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Changed

- **Breaking - the controller binds loopback by default.** `dotbot run
controller` served the REST/WebSocket API on `0.0.0.0`, putting an
unauthenticated API on every interface; the new `/swarmit/*` proxy would
republish the swarmit server the same way. It now binds `127.0.0.1`. To reach
it from another machine pass `--controller-http-host 0.0.0.0`, set
`[run.controller] http_host`, or `DOTBOT_RUN_CONTROLLER_HTTP_HOST`; binding
beyond loopback logs a warning.
- **`dotbot run controller` now opens the unified web console** at `/console`
instead of the classic dashboard at `/PyDotBot`. The classic UI is still
served and still carries the qrkey demo, the REST demo and the SailBot
views. If only one of the two is built, that one is opened; if neither is,
the controller serves the API and says so rather than opening a dead tab.
- **Device addresses are rendered uppercase everywhere**, through a single
`dotbot.addr_to_hex()` helper, and are matched case-sensitively. The address
is the join key between the control plane and swarmit, which already
uppercased it, so the two now agree; `DOTBOT_ADDRESS_DEFAULT` and
`GATEWAY_ADDRESS_DEFAULT` were already written this way. Consequences:
a lowercase address in a REST path or MQTT topic now reaches no DotBot,
and a `--csv-data-output` file spanning the upgrade holds both cases for
the same robot (re-normalise with `df.address.str.upper()` before grouping).
`-d/--dotbot-address` on `dotbot run joystick` / `keyboard` accepts either
case and normalises.
- **Breaking — CLI reorganized into four object-namespaces.** The top
level is now exactly `fw` (firmware artifacts), `device` (one cabled
device), `swarm` (the fleet), and `run` (host-side processes). The flat
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,19 @@ Every command and flag is documented in the [CLI reference][cli-doc].

See the whole thing run with nothing but Python!

The command below will run a simulated swarm, which you can observe in a web UI at http://localhost:8000/PyDotBot/ :
The command below will run a simulated swarm, which you can observe in the web console at http://localhost:8000/console/ :

```bash
dotbot run simulator
```

The web UI opens automatically; pass `--headless` to suppress it (it's still
served). Drive the simulated DotBots from the UI, or run a bundled demo in a
The console opens automatically; pass `--headless` to suppress it (it's still
served). It is one map-first UI for both driving the fleet and orchestrating the
testbed - firmware flashing, start/stop and live events - when a swarmit server
is reachable. The classic UI remains at `/PyDotBot`; it is where the qrkey demo,
the REST demo and the SailBot views live.

Drive the simulated DotBots from the console, or run a bundled demo in a
second terminal:

```bash
Expand Down Expand Up @@ -140,7 +145,7 @@ dotbot swarm flash rc-car -ys # this firmware lets DotBots be remote-controlled
Observe and control your swarm from a web interface:

```bash
dotbot run controller # opens a webpage at http://localhost:8000/PyDotBot/
dotbot run controller # opens the console at http://localhost:8000/console/
```

Full walkthrough of fleet operations - status, OTA flash, start/stop, monitor -
Expand Down
44 changes: 38 additions & 6 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
# Codecov config — split project-level (long-term) from patch-level
# (per-PR) policy. Project-level keeps the long-term floor honest;
# patch-level is informational because vendoring / refactor PRs can
# legitimately ship diffs with low instantaneous coverage even when
# the project total stays healthy.
# Codecov config — the gate belongs on the Python control plane, not on the
# two web apps.
#
# Coverage arrives from three uploads (pytest, the classic frontend's vitest,
# the console's vitest). Untagged they merge into one project total, so adding
# a few thousand lines of thinly-covered TS drags the number down and fails a
# PR that did nothing to the Python. Flags keep the three apart.
#
# The web apps are reported but never block: their vitest suites cover the pure
# logic (state merge, mission grouping, SSE parsing, the drive mixing) and
# deliberately not the rendering, so their absolute number is low by design and
# says little about whether a change is safe.

flags:
python:
paths:
- dotbot/
- utils/
carryforward: true
frontend:
paths:
- dotbot/frontend/
carryforward: true
console:
paths:
- dotbot/console-web/
carryforward: true

coverage:
status:
project:
default:
# No unflagged aggregate status: that is the one that mixed the three.
default: false
python:
flags:
- python
# Compare against main's current coverage. Allow tiny dips
# (rounding / one-off branches) without flapping CI.
target: auto
threshold: 1%
web:
flags:
- frontend
- console
# Reported in the PR comment, never a CI gate.
informational: true
patch:
default:
# Report patch coverage but don't fail CI on it. Reviewers can
Expand Down
16 changes: 11 additions & 5 deletions doc/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ dotbot run --help # the full list
| `keyboard` | Drive a DotBot from the keyboard. |
| `joystick` | Drive a DotBot from a joystick. |

## `controller` - the control plane + web UI
## `controller` - the control plane + web console

Connect to a swarm and serve the dashboard at `http://localhost:8000/PyDotBot/`.
Connect to a swarm and serve the console at `http://localhost:8000/console/`.
The console is one map-first UI for driving the fleet and, when a swarmit server
is reachable, orchestrating the testbed. The classic dashboard stays served at
`/PyDotBot`, which is where the qrkey demo, the REST demo and the SailBot views
live.
`--conn` is one discriminated string: `mqtts://host:port`, a serial path, or
`simulator`.

Expand All @@ -34,8 +38,10 @@ dotbot run controller --conn /dev/ttyACM0
|---|---|
| `-n/--conn` | `mqtts://host:port`, serial path, or `simulator` |
| `-s/--swarm-id` | hex swarm id - **required for MQTT**, ignored for serial/simulator |
| `--headless` | don't open the dashboard in a browser (it's still served) |
| `--controller-http-host` | interface the API binds to (default `127.0.0.1`, loopback). Pass `0.0.0.0` to reach it from another machine - the API is unauthenticated and `/swarmit/*` reaches the swarmit server through it, so only on a network you trust. |
| `--headless` | don't open the console in a browser (it's still served) |
| `--csv-data-output` | record DotBot data to a CSV file |
| `--swarmit-url` | swarmit server behind the console's orchestration panel (default `http://localhost:8001`, matching `swarmit serve`). Also `[run.controller] swarmit_url` in dotbot.toml, or `DOTBOT_SWARMIT_URL`. |

Full options and the dashboard tour live in
[the controller guide](../guides/controller.md). See `dotbot run controller --help`.
Expand All @@ -57,7 +63,7 @@ dotbot run gateway # autodetect port, print-only (no broker)
## `simulator` - standalone simulator

No hardware, no gateway. Exactly equivalent to `run controller --conn simulator`,
so it shares the controller's flags and serves the same dashboard.
so it shares the controller's flags and serves the same console.

```bash
dotbot run simulator
Expand Down Expand Up @@ -94,7 +100,7 @@ specific DotBot by hex address.

```bash
dotbot run keyboard
dotbot run joystick -j 0 -d 1234567890abcdef
dotbot run joystick -j 0 -d 1234567890ABCDEF
```

See `dotbot run keyboard --help` / `dotbot run joystick --help` for the host,
Expand Down
2 changes: 2 additions & 0 deletions doc/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,15 @@ The four tables mirror the four CLI namespaces (`fw` / `device` / `swarm` /
| `conn` | Connection string for `dotbot run`. |
| `swarm_id` | Swarm id (topic namespace). |
| `[run.controller] http_port` | REST/WebSocket port (default 8000). |
| `[run.controller] http_host` | Interface the REST/WebSocket API binds to (default `127.0.0.1`). `0.0.0.0` exposes it to the network; the API is unauthenticated. |
| `[run.controller] map_size` | Controller map size. |
| `[run.controller] background_map` | Background map image. |
| `[run.controller] log_output` | Log output path. |
| `[run.controller] csv_data_output` | CSV data output path. |
| `[run.controller] headless` | Stay headless - don't open the web UI in a browser on start (default false; it's still served). |
| `[run.controller] gw_address` | Gateway address. |
| `[run.controller] simulator_init_state` | Initial simulator state. |
| `[run.controller] swarmit_url` | swarmit server the console's orchestration panel talks to, proxied at `/swarmit/*` (default `http://localhost:8001`, which matches `swarmit serve`). |
| `[run.gateway] serial_port` | Gateway serial port. |
| `[run.gateway] mqtt` | Gateway MQTT connection string. |

Expand Down
7 changes: 4 additions & 3 deletions doc/reference/mqtt.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ base64 string derived from the current PIN code (see [Secured brokers](#secured-
Command-topic fields:

- `<swarm-id>` - 4-hex swarm identifier (DotBots behind one gateway), e.g. `0000`.
- `<address>` - 16-hex DotBot address, e.g. `9903ef26257feb31`.
- `<address>` - 16-hex DotBot address, e.g. `9903EF26257FEB31`. Uppercase, and
matched case-sensitively: a lowercase address reaches no DotBot.
- `<app>` - application type: `0` = DotBot, `1` = SailBot.
- `<cmd>` - the command name (last segment).

Expand All @@ -51,12 +52,12 @@ Payloads are JSON. Drive a DotBot forward and turn its LED red:
```bash
# move_raw - left_y / right_y drive the wheels, values in [-100, 100]
mosquitto_pub -h <broker> \
-t '/pydotbot/<secret-topic>/command/0000/9903ef26257feb31/0/move_raw' \
-t '/pydotbot/<secret-topic>/command/0000/9903EF26257FEB31/0/move_raw' \
-m '{"left_x": 0, "left_y": 80, "right_x": 0, "right_y": 80}'

# rgb_led - 0..255 per channel
mosquitto_pub -h <broker> \
-t '/pydotbot/<secret-topic>/command/0000/9903ef26257feb31/0/rgb_led' \
-t '/pydotbot/<secret-topic>/command/0000/9903EF26257FEB31/0/rgb_led' \
-m '{"red": 255, "green": 0, "blue": 0}'
```

Expand Down
4 changes: 2 additions & 2 deletions doc/reference/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ print(requests.get("http://localhost:8000/controller/dotbots").json())

```py
import requests
addr = "9903ef26257feb31" # from the list above
addr = "9903EF26257FEB31" # uppercase; the address is matched case-sensitively
requests.put(
f"http://localhost:8000/controller/dotbots/{addr}/0/rgb_led",
json={"red": 255, "green": 0, "blue": 0},
Expand All @@ -79,7 +79,7 @@ requests.put(

```py
import requests
addr = "9903ef26257feb31"
addr = "9903EF26257FEB31"
requests.put(
f"http://localhost:8000/controller/dotbots/{addr}/0/move_raw",
json={"left_x": 0, "left_y": 60, "right_x": 0, "right_y": 60},
Expand Down
17 changes: 17 additions & 0 deletions dotbot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Pydotbot module."""

from binascii import hexlify
from importlib.metadata import PackageNotFoundError, version

from dotbot_utils.serial_interface import get_default_port
Expand All @@ -12,11 +13,27 @@
CONTROLLER_HTTP_PROTOCOL_DEFAULT = "http"
CONTROLLER_HTTP_HOSTNAME_DEFAULT = "localhost"
CONTROLLER_HTTP_PORT_DEFAULT = 8000
# Loopback by default: the REST/WS API is unauthenticated and, since the
# controller proxies /swarmit/*, binding wider also republishes the swarmit
# server at the controller's reachability.
CONTROLLER_HTTP_HOST_DEFAULT = "127.0.0.1"
CONTROLLER_ADAPTER_DEFAULT = "serial"
MQTT_HOST_DEFAULT = "localhost"
MQTT_PORT_DEFAULT = 1883
MAP_SIZE_DEFAULT = "2000x2000" # in mm unit
SIMULATOR_INIT_STATE_DEFAULT = "simulator_init_state.toml"
SWARMIT_URL_DEFAULT = "http://localhost:8001" # swarmit server default port


def addr_to_hex(addr: int) -> str:
"""Render a 64-bit device address as canonical hex.

Uppercase is the canonical form across the DotBot stack: the swarm side
(swarmit) renders addresses this way, and `DOTBOT_ADDRESS_DEFAULT` /
`GATEWAY_ADDRESS_DEFAULT` are written this way. `binascii.hexlify` returns
lowercase, so every address that becomes a string goes through here.
"""
return hexlify(addr.to_bytes(8, "big")).decode().upper()


def pydotbot_version() -> str:
Expand Down
8 changes: 4 additions & 4 deletions dotbot/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ async def start(self, on_frame_received: callable):

def _on_mari_event(event: EdgeEvent, event_data: MariNode | MariFrame):
if event == EdgeEvent.NODE_JOINED:
LOGGER.debug(f"Node joined: {event_data.address:016x}")
LOGGER.debug(f"Node joined: {event_data.address:016X}")
elif event == EdgeEvent.NODE_LEFT:
LOGGER.debug(f"Node left: {event_data.address:016x}")
LOGGER.debug(f"Node left: {event_data.address:016X}")
elif event == EdgeEvent.NODE_DATA:
if event_data.header.next_proto != NextProto.DOTBOT_APP:
return
Expand Down Expand Up @@ -183,9 +183,9 @@ async def start(self, on_frame_received: callable):

def _on_mari_event(event: EdgeEvent, event_data: MariNode | MariFrame):
if event == EdgeEvent.NODE_JOINED:
LOGGER.debug(f"Node joined: {event_data.address:016x}")
LOGGER.debug(f"Node joined: {event_data.address:016X}")
elif event == EdgeEvent.NODE_LEFT:
LOGGER.debug(f"Node left: {event_data.address:016x}")
LOGGER.debug(f"Node left: {event_data.address:016X}")
elif event == EdgeEvent.NODE_DATA:
if event_data.header.next_proto != NextProto.DOTBOT_APP:
return
Expand Down
3 changes: 2 additions & 1 deletion dotbot/cli/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import click

from dotbot import addr_to_hex
from dotbot.cli._cfg import from_config
from dotbot.cli._conn import parse_connection

Expand All @@ -46,7 +47,7 @@ def _run_gateway(port, mqtt_url, do_print): # pragma: no cover - needs a gatewa
def on_event(event, event_data):
if do_print and event == EdgeEvent.NODE_DATA:
click.echo(
f"<- {event_data.header.source:016x}: {event_data.payload.hex()}"
f"<- {addr_to_hex(event_data.header.source)}: {event_data.payload.hex()}"
)

mqtt_interface = None
Expand Down
Loading
Loading