Protobuf (roboparty.v1) gateway for RK3588 with authenticated WebSocket control.
Legacy UDP / Serial / Bluetooth and direct CAN gamepad implementations remain
in source but cannot be enabled until they support shared control ownership.
Frontend UI moved to Roboparty/roboparty_example
proto/ roboparty.v1 contract (source of truth) ← IDL
protocol/ generated bindings + codec + handler ← core abstraction
transport/ WebSocket / Serial / Bluetooth / UDP ← four transport channels
drivers/ motors / imu / joy / policy ← hardware drivers
App / native clients -> WS authentication -> control lease -> CommandHandler -> drivers
Every message is a roboparty.v1.Envelope. Bindings are generated from
proto/ via scripts/gen_proto.sh (requires python3-protobuf 4.21.12 on the
board, grpcio-tools on the build host). The generated bindings under
src/rp_server/protocol/gen/ are committed; CI
(.github/workflows/proto.yml) regenerates them and fails on drift.
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check + hardware status |
| GET | /identity |
Public robot identity: serial, model, machineId (SN/model from TUI), plus the Ed25519 device key's spkiSha256/devicePublicKey (cached 60s) |
| GET | /api/status |
Full hardware snapshot |
| GET | /sysinfo |
CPU / memory / load |
| WS | /ws |
Bearer authentication; protobuf binary commands and JSON lease management |
| WS | /ws-json |
App/diagnostic JSON control/telemetry; Bearer token or app-key challenge; v2 requires WSS |
- On the robot:
rp-server-pair code→ 6-digit code plus aroboparty://pair?data=...URI containingtlsSpkiSha256,deviceSpkiSha256, board serial, software SN and a device-key signature (TTL 5 min, localhost only). - App: Settings → 授权与绑定 → paste the URI (or scan its QR) → save the TLS
pin locally →
POST /pair/registerwith{code, appPublicKey, deviceName}. - v2 clients reject trust-all, skip-hostname-verification and plaintext control.
- Default
pairing.single_owner: true: a successful new pairing revokes old user app keys (replaced-by-new-pairing); admin-scoped keys are preserved. - Registered keys live in
/etc/rp-server/auth/apps.json(0600):rp-server-pair list/rp-server-pair revoke <appKeyId>. POST /pair/code,GET /pair/apps,POST /pair/revokeare localhost-only;POST /pair/registeris LAN-facing but requires the code and is rate-limited.
/ws-jsonkeeps the app's existing JSON packet shapes and translates them toEnvelopeinternally.left_stick_x/yis mirrored ontoAXIS_RX/RYwhen the right stick is centered, so the app's primary touch stick drives translation.- Auth: a valid
Authorization: Bearer(local service token or unrevoked App JWT) is accepted directly. Without a header, clients useauth_request(device_id= pairedappKeyId) →challenge→challenge_response(ECDSA/Ed25519 signature over<appKeyId>:<challenge>) →auth_resultwith a short-lived JWT (EdDSA, scopesread control). tls.enabled: trueruns uvicorn withssl_certfile/ssl_keyfile. Generate a self-signed cert withrp-server-gen-tls(installed fromscripts/rp-server-gen-tls);/identity.tlsSpkiSha256is the pin value.- Android and MCP clients support explicit lease acquisition. Old clients must migrate before sending commands. UDP is disabled, including telemetry.
- HTTP routes require
Authorization: Bearer <token>:readfor GET/HEAD andadminfor writes./healthand/identityare public; pairing endpoints retain their local-operator/one-time-code checks. Forwarded client addresses are disabled in the bundled server entrypoint. /wsrequires the same Bearer header at connection time. Use a paired App JWT or configure a randomRP_SERVICE_TOKENof at least 32 characters in the protected service environment. Native clients use the same token. Defaultauth.service_scopesare[read, control, admin]so the local MCP service can usereset; dropadminwhen it is not needed. Never put tokens in URLs. Use TLS for network deployment./ws-jsonaccepts a valid Bearer directly and keeps the signed challenge as a fallback. Tokens are session-bound; expiry, pairing revocation and reduced local permissions are rechecked during sessions.- After authentication, send text JSON
{"type":"lease.acquire"}. Both WS endpoints reply{"type":"lease","status":"acquired","ttl_ms":3000, "input_timeout_ms":500}. A competing session receivesCONTROL_BUSY. - Send
{"type":"heartbeat"}to renew ownership TTL. Control/joystick/button frames must independently stay fresh once input starts. Heartbeats alone do not keep stale input active. Use{"type":"lease.release"}to release. - Timeout, disconnect and credential loss revoke ownership, zero the virtual gamepad and stop only the policy process managed by this service. No motor error clearing occurs. Clients must explicitly acquire again; reconnect alone never resumes control. This is a software action, not proof of hardware stop.
- JSON
stop {status:"request"}and protobufStopRequestrequirecontrol, may stop another owner, and latch the software stop. While latched, new leases are rejected withCONTROL_STOPPED; clear it withstop {status:"clear"}(orStopRequest.status="clear") and acquire control again. Protobuf policy STOP is a policy-only stop and does not latch. Administrative protobufresetstill clears errors and requiresadminplus ownership; it is not an emergency-stop command. - Legacy
transports.udp/serial/bluetoothandcan_gamepad.enabledmust be false. Enabling them fails startup rather than providing an unleased control bypass.
The changes have offline mock tests; they have not been deployed to a robot.
/identity also exposes the robot's Ed25519 public key:
spkiSha256: SHA-256 of the DER SubjectPublicKeyInfo (pin this in the app)devicePublicKey: base64 DER SPKIdeviceKeySource: key path, orunavailable
The private key lives at /etc/rp-server/auth/device.key (0600), generated on
first start. Provision one key per board; never commit it. If the key cannot be
read/generated, the fields are null and clients should treat identity as
degraded.
rp_server proxies the robopi-tui public JSON-RPC protocol
(robopi-tui rpc --protocol 1). Motion and telemetry remain in rp_server;
only board snapshots/tasks are delegated. The Debian package depends on
robopi-tui (>= 1.1.2); since 1.1.2 the TUI is Architecture: all, so the
same package installs on arm64 boards and x86 hosts (only generic plugins
are listed on non-arm64).
| Method | Path | Description |
|---|---|---|
| GET | /tui/health |
Backend availability and job policy |
| GET | /tui/tasks |
tasks.list (packages-check / apt-upgrade / hpm-flash / can-check / pipeline) |
| GET | /tui/tasks/{id} |
tasks.describe |
| GET | /tui/snapshot |
snapshot.get (read-only board snapshot) |
| GET | /tui/jobs |
jobs.list |
| GET | /tui/jobs/active |
jobs.active |
| GET | /tui/jobs/{id} |
jobs.get |
| POST | /tui/jobs |
jobs.start ({"taskId","requestId"?}; needs tui.allow_jobs: true) |
| POST | /tui/jobs/{id}/acknowledge |
jobs.acknowledge (needs tui.allow_jobs: true) |
Motor errors are also delegated: Query(WHAT_ERRORS) over WebSocket and
/api/status.motor_errors call TUI's read-only motors.get (create drivers +
read feedback; never enable or transmit). Results are cached for
tui.motor_cache_s (default 2s) so polling does not spawn a TUI process each
time. If the TUI backend is disabled or fails, rp_server falls back to its
direct motor driver.
Proxied results carry a server field (tui.server, default rp_server,
injected as RP_TUI_SERVER) so clients can tell which fronting server produced
them; calling robopi-tui directly reports robopi-tui.
tui.enabled defaults to true; tui.allow_jobs defaults to false because
apt-upgrade and hpm-flash are destructive. Set RP_TUI_BIN or
tui.executable if robopi-tui is not on PATH. Reading the proxy requires
read; job submission/acknowledgement additionally requires admin and the
existing tui.allow_jobs gate.
Wire format is Protocol Buffers; see proto/roboparty/v1/*.proto. Each
transport frames one Envelope:
| Transport | Framing |
|---|---|
| WebSocket | one Envelope per binary frame |
| UDP | 0x52 + version 0x01 + Envelope |
| Serial / Bluetooth | varint length-delimited Envelope stream |
Commands (Envelope body):
| Body | Description |
|---|---|
query (WHAT_CONN/SYSINFO/POLICY/ERRORS) |
read-only queries |
button / joystick / control |
gamepad input |
policy_command |
start/stop inference |
reset |
administrative reset: zero joystick + clear errors + stop policy; admin + lease |
skill_action |
skill request (logged) |
Responses: conn, button_ack, sysinfo, policy_response,
motor_errors, reset_response, error.
Telemetry (auto-broadcast): imu (50Hz), battery (50Hz),
motors (1Hz), motor_error_push (10Hz).
scripts/build_proto_wheel.py packages
src/rp_server/protocol/gen/roboparty as
roboparty_proto-1.0.0-py3-none-any.whl. CI attaches it to the GitHub release
(alongside the .deb) so downstream clients such as the MCP server can install
the published bindings instead of vendoring a copy. The wheel version tracks
roboparty.v1, not the rp_server package version, so the
releases/latest/download/... URL stays stable.
Legacy session (UDP, currently disabled): auth_request → challenge → challenge_response →
auth_result, plus heartbeat and token-authenticated stop/stop_ack
(status="" request, "received" ack confirmation, "clear" release latch).
| Direction | Joystick input | Effect |
|---|---|---|
| Forward | Right stick ↑ (AXIS_RY = -1) |
Move forward |
| Backward | Right stick ↓ (AXIS_RY = 1) |
Move backward |
| Left turn | Right stick ← (AXIS_RX = -1) |
Turn left |
| Right turn | Right stick → (AXIS_RX = 1) |
Turn right |
X (enable) → A (reset) → B (inference) → joystick → robot moves
scripts/dev_run.sh --mock --port 9000scripts/dev_run.sh uses the system fastapi/pydantic 1.x and only borrows
google.protobuf from the user site when needed. This avoids the common local
conflict where a pip-installed pydantic 2.x shadows the Debian fastapi 0.63.
Install the deb package (see Install (deb) below). Its postinst
enables and starts rp-server.service. The legacy rp-btwifi.service is left
installed but disabled, and is gated by /etc/rp-server/enable-btwifi; it is
not part of the product provisioning flow.
systemctl status rp-server
journalctl -u rp-server -f
systemctl restart rp-server| Variable | Meaning |
|---|---|
RP_HOST / RP_PORT |
Listen address (default 0.0.0.0:9000) |
RP_LOG_LEVEL |
Logging level (CLI > env > server.yaml) |
RP_UDP_SECRET |
UDP auth shared secret (required in production) |
RP_STOP_SCRIPT |
Software-stop script path |
RP_MOCK |
Force mock mode (DO NOT set in production) |
server.mock: must befalsein productionhardware.required: default["motors", "imu"]robot.launch_cmd: inference launch command. The ROS package name isroboparty_inference(underscore, not the Debian package name);{policy}is replaced by the validatedPolicyCommand.name.
- WS receives
+CONNand@IMU/@BAT - After reboot:
systemctl is-active rp-server→active
X5 uses ROS 2 colcon workspace under ~/atom01_deploy. Point RP_ROBOT_CONFIG
(in /etc/default/rp-server) at the workspace config, then install the deb
package.
- Symlink pybind modules into
dist-packages - X5 has no BMS —
hardware.required: ["motors", "imu"] - For
python3-evdevuse the official deb — do NOT pip install - rp_server and
start_robot.shcompete for CAN bus — only one at a time
dpkg-buildpackage -us -uc -b
sudo dpkg -i ../roboparty-rp-server_*.debThe Debian package declares its Python dependencies in debian/control
(Depends/Recommends). pip and Debian names differ, so
scripts/check_python_depends.py scans the source imports and verifies each
third-party module is covered:
python3 scripts/check_python_depends.py # fail if an import is missing
python3 scripts/check_python_depends.py --list # show import -> Debian mappingIf a local ~/.local pydantic 2.x shadows the Debian fastapi 0.63, use
scripts/dev_run.sh for mock/dev runs; it selects pydantic 1.x and the system
protobuf-compatible path. Production/board installs should use the Debian
package and its declared dependencies.
Core mapping: fastapi→python3-fastapi, uvicorn→python3-uvicorn,
websockets→python3-websockets, google→python3-protobuf,
yaml→python3-yaml, psutil→python3-psutil, evdev→python3-evdev,
serial_asyncio→python3-serial-asyncio, bluetooth→python3-bluez
(optional, in Recommends). Hardware pybind modules (motors_py, imu_py) come from the
roboparty-* packages, not PyPI. The check runs
before dh_auto_configure in debian/rules.
The server writes both file logs and console output for production troubleshooting.
| File | Purpose |
|---|---|
rp_server.log |
Main log (system, errors, connection state) |
packets.log |
Packet log (raw data received over UDP/WebSocket) |
policy.log |
inference/ros2 subprocess stdout+stderr |
log-spool.sqlite3 |
Durable structured-event spool for log_server upload |
python3 -m rp_server --config config/server.yaml --mock --log-level DEBUG --log-dir logs| Argument | Description | Default |
|---|---|---|
--log-level |
Log level | INFO |
--log-dir |
Log file directory | logs |
Configure logging in config/server.yaml:
logging:
level: "INFO" # DEBUG/INFO/WARNING/ERROR/CRITICAL
dir: "logs" # log file directory
file: "rp_server.log" # main log file
packet_file: "packets.log" # packet log file
max_size: 10485760 # max file size (bytes), 10MB
backup_count: 5 # number of backups keptPriority: command-line arguments > config file > defaults.
config/server.yaml has a log_server section (enabled by default) that sends
rp_server logger records to the local roboparty-log-server at
http://127.0.0.1:8092. Events are first written to
<log-dir>/log-spool.sqlite3, so an offline or restarted log server does not
lose them. RP_LOG_SERVER_URL, RP_LOG_TOKEN, RP_LOG_DEVICE_ID,
RP_DEVICE_MODEL and RP_FIRMWARE_VERSION override the config.
Schema v1 fields: schema_version, event_type, session_id, boot_id,
software_version, firmware_version, device_model, plus the existing
event_id/device_id/service/timestamp/level/message/fields.
| Level | Use |
|---|---|
DEBUG |
Detailed debugging (protocol messages, UDP packets) |
INFO |
Normal operation (connections, service status) |
WARNING |
Warnings (hardware anomalies, failed commands) |
ERROR |
Errors (hardware init failure, degraded service) |
CRITICAL |
Severe errors (service cannot start) |
System lifecycle:
2026-08-08 10:00:00 [rp_server] INFO 日志系统初始化完成 level=INFO dir=logs file=rp_server.log
2026-08-08 10:00:01 [rp_server.transport] INFO mock: skipping hardware driver init
2026-08-08 10:00:01 [rp_server.transport] INFO rp_server ready mock=True port_cfg={'host': '0.0.0.0', 'port': 9000}
Connection management:
2026-08-08 10:01:00 [rp_server.transport] INFO WebSocket 连接建立: 192.168.1.100
2026-08-08 10:01:05 [rp_server.transport] INFO WebSocket 连接断开: 192.168.1.100 (时长: 5.0s)
2026-08-08 10:02:00 [rp_server.udp] INFO UDP listener ready on 10.43.21.32:9000
Protocol messages (DEBUG level):
2026-08-08 10:01:02 [rp_server.transport] INFO WS_RECV src=192.168.1.100 body=button size=12
2026-08-08 10:01:02 [rp_server.protocol] DEBUG unhandled command body: none
2026-08-08 10:01:03 [rp_server.udp] INFO UDP_RECV src=192.168.1.100:54321 size=40 body=control
Hardware status:
2026-08-08 10:00:01 [rp_server.drivers.motors] INFO motors initialised: 4
2026-08-08 10:00:01 [rp_server.drivers.imu] INFO IMU initialised
2026-08-08 10:05:00 [rp_server.drivers.motors] WARNING motor_mit_cmd[0] failed: timeout
Packet log (packets.log) — message body + byte size:
2026-08-08 10:01:02 [rp_server.packets] WS_RECV src=192.168.1.100 body=button size=12
2026-08-08 10:01:03 [rp_server.packets] WS_RECV src=192.168.1.100 body=joystick size=10
2026-08-08 10:02:00 [rp_server.packets] UDP_RECV src=192.168.1.100:54321 size=40 body=control
2026-08-08 10:02:01 [rp_server.packets] UDP_RECV src=192.168.1.100:54321 size=38 body=control
Live tail:
tail -f logs/rp_server.logSearching:
grep "ERROR" logs/rp_server.log
grep "192.168.1.100" logs/rp_server.log
grep "WS_RECV" logs/packets.logLog files rotate automatically once they reach max_size:
logs/
├── rp_server.log # current
├── rp_server.log.1 # previous
├── rp_server.log.2 # older
├── ...
└── rp_server.log.5 # oldest backup
Under systemd, logs are also written to the journal:
journalctl -u rp-server -f
tail -f ${ROBOOS_PREFIX:-/opt/roboparty}/share/roboparty-rp-server/logs/rp_server.logGPL-3.0 — Copyright (C) 2026 mustaf-osman (https://github.com/mustaf-osman) / wentywenty (https://github.com/wentywenty)