Operate
Drive and monitor a robot from a phone, tablet, laptop, or desktop webview.
Run a friendly control room for ROS 2, then extend it with panels that remain independent of the core application.
Robo-Boy combines camera streaming, configurable control pads, 3D visualization, behavior-tree editing, TF inspection, and custom themes in one touch-friendly workspace. The web, desktop, and mobile apps share the same React codebase; the native packages are thin shells and do not bundle ROS.
Drive and monitor a robot from a phone, tablet, laptop, or desktop webview.
Inspect cameras, TF, laser scans, point clouds, poses, odometry, and URDF models.
Build reusable control pads and behavior trees without making a one-off dashboard.
Ship trusted, standalone ESM panels against a small, versioned, framework-neutral SDK.
A custom gamepad is an in-app JSON layout made from the built-in joystick, button, D-pad, toggle, slider, camera, plot, and heartbeat components. An external panel is independently built JavaScript, loaded through the Panel SDK. New gamepad component types are internal core development; they are not external SDK panels.
The native app is the shorter route. It needs only the ROS services, so there is no certificate to create and no proxy to run. Choose the browser stack when Robo-Boy should open directly from a browser.
These stable links advance with each Robo-Boy application release. The macOS image is the Electron build for Apple silicon, the Linux packages are x86-64, and the iOS package is unsigned and must be signed before installation.
git clone git@github.com:tessel-la/robo-boy.git
cd robo-boy
docker compose up -d --build ros-stack
This one service supplies rosbridge, rosapi, and web_video_server. The web frontend, proxy, and certificate are not started, because the native app talks to these ports directly.
If the robot publishes message, service, or action types of its own, mount that workspace as well, or those topics arrive without a definition to read them with. See robot-specific interfaces.
Use Quick Connect when ROS runs on the same computer. When it runs elsewhere, open the advanced options, choose Host or IP, and enter that computer's hostname, LAN address, or VPN name.
docker compose logs -f ros-stack
docker compose downOpen Manage installations… to install WebRTC / RTSP Camera. Robo-Boy 0.7.0-alpha and later supply dedicated WHEP, discovery, and HLS gateway endpoints. Browsers negotiate WHEP; a Linux desktop webview without RTCPeerConnection automatically uses the gateway's low-latency HLS stream and reports Live · HLS. The robot or simulator deployment must run MediaMTX with WHEP on :8889, discovery on :9997, and HLS on :8888.
Robo-Boy never starts, stops, or bundles ROS or the media gateway. Run those services on the robot or simulator computer instead of locally and only step 2 changes.
Serving Robo-Boy to a browser adds two requirements the native apps do not have: a browser needs HTTPS before it will grant a page a camera and other secure-context features, and the app and the robot services have to appear under one origin. Caddy supplies both, and a locally trusted certificate is what makes the first one work without warnings. You need Docker with Compose and mkcert.
git clone git@github.com:tessel-la/robo-boy.git
cd robo-boy
mkcert -install
mkdir -p infra/caddy/certs
mkcert \
-key-file infra/caddy/certs/local-key.pem \
-cert-file infra/caddy/certs/local-cert.pem \
localhost 127.0.0.1 ::1 YOUR_HOST_IP
Include the computer's LAN or VPN address as YOUR_HOST_IP when another device will open Robo-Boy.
docker compose up -d --build
Open https://localhost, or https://YOUR_HOST_IP from a device on the same network. Every ROS and DDS setting has a default, so no environment file is needed for this configuration; add one only to select a robot workspace overlay.
docker compose logs -f app
docker compose logs -f caddy
docker compose logs -f ros-stack
docker compose down
Node.js 20+ and npm are required. ROS, video, and mesh routes need separate services or mocks.
npm ci
npm run dev
# http://localhost:5173Install Tauri v2 prerequisites and run the ROS stack separately. The desktop app connects directly to its host.
docker compose up -d --build ros-stack
npm ci
npm run desktop:devThe ROS stack supplies rosapi, rosbridge, and web_video_server. Robo-Boy discovers topics and interfaces after the shared ROSLIB WebSocket connects.
localhost.
Robo-Boy and ROS services share a machine.
ws://HOST:9090http://HOST:8080http://HOST:8000http://HOST:11434On the web, Quick Connect normally resolves these through same-origin /websocket, /video_stream, /mesh_resources, and /ollama routes. Advanced Ports override the first three service ports. Tauri connects directly.
The ros-stack service uses host networking for DDS discovery. Every container that must discover the same graph needs compatible ROS_DOMAIN_ID, ROS_LOCALHOST_ONLY, and RMW_IMPLEMENTATION settings.
Robo-Boy reads a topic through the interface definition that describes it, and it only has the definitions present inside ros-stack. A robot with message, service, or action types of its own therefore needs its built workspace mounted there, or those topics appear in the graph and cannot be decoded. Mount the workspace's install directory under a unique path in a Compose override:
services:
ros-stack:
volumes:
- /path/to/robot_ws/install:/overlay_ws/robot:roCOMPOSE_FILE=docker-compose.yml:infra/compose/robot.yml docker compose up -d --buildOverrides for the bundled Aerostack and manipulator workspaces already exist under infra/compose/. A workspace built with colcon --symlink-install also needs its build and source directories mounted, since its installed files point back at them.
ROS_DOMAIN_ID=0
ROS_AUTOMATIC_DISCOVERY_RANGE=SUBNET
ROS_STATIC_PEERS=
RMW_IMPLEMENTATION=rmw_fastrtps_cpp
FASTDDS_BUILTIN_TRANSPORTS=UDPv4A robot workspace overlay is selected by pointing Compose at one of the bundled override files, either in .env or on the command line. After changing overlays, recreate the service:
COMPOSE_FILE=docker-compose.yml:infra/compose/aerostack.yml
# or infra/compose/manipulator.ymldocker compose up -d --build --force-recreate ros-stackRobo-Boy has no application server or database. The browser or Tauri webview owns the interface, ROSLIB owns the active bridge connection, and browser storage holds serializable local state.
App owns connection-screen transitions and global theme state. MainControlView coordinates the workspace and the single shared ROS connection.
Behavior trees, custom gamepads, themes, TF, and 3D visualization keep their logic, persistence, and lifecycle code in focused modules.
Layouts, themes, trees, visualization settings, panel sizing, and external-panel state use versioned browser storage. Live ROS clients and rendering objects are never serialized.
Caddy gives the web app and robot services one origin. Tauri packages only the frontend and connects directly to services on the selected host.
Built-in and compatible external panels are merged into one catalog. The workspace stores panel instances and layout locally; an external tile retains its reverse-domain panel ID so Robo-Boy can show an actionable unavailable state if the deployment later removes that panel.
Discovers ROS image topics and renders the selected stream through web_video_server.
Visualizes TF, point clouds, laser scans, pose/odometry data, camera info, and URDF models.
ros · network · storageBuilds and runs sequence, selector, parallel, action, service, topic, and subscriber nodes.
ros · network · storageSubscribes to live /tf and /tf_static data to inspect the transform hierarchy.
Loads a configurable control surface with publish, subscribe, camera, plotting, and heartbeat components.
ros · storageLoads a reviewed same-origin module lazily and gives each tile its own activation context and state namespace.
declared per manifestOpen +, clone a template or start empty, place controls, assign ROS topics and mappings, then save or export the versioned JSON.
Add visualizations, choose a fixed frame and topics, adjust render options, and rely on local storage for the serialized configuration.
Discover ROS interfaces, configure nodes, connect the graph, then run in browser-owned mode or opt into a ROS-owned persistent execution.
Select a built-in palette or create a local custom theme. External panels can consume Robo-Boy CSS variables without importing theme internals.
Panel SDK 2.2.0 is a type-only, additive update to the Panel API 2.0 contract. A panel exports one framework-neutral definition, runs in its own opaque-origin iframe, owns everything below the supplied DOM element, and receives only declared host services over a private channel. The release adds the webxr capability, which delegates XR spatial tracking and fullscreen for an immersive session, on top of the dedicated WHEP, discovery, and HLS host endpoints and binary arrayBuffer() responses introduced in 2.1.0; Robo-Boy does not scan node_modules or import panel source into its application build.
Owns metadata and integrity checks, workspace layout, iframe lifecycle, permission brokers, persistence, and tile-local errors.
Owns dependencies, tests, manifest, immutable artifacts, and distribution policy.
panels/installed.json lists the verified releases installed into that Robo-Boy deployment from its configured catalogs.
Each panel runs without allow-same-origin, direct HTTP is blocked by sandbox CSP, and host services enforce manifest permissions. A panel can still consume CPU or misuse data explicitly granted to it, so install only reviewed artifacts.
The host owns tile creation, sizing, ordering, saved workspace layouts, and replacement. The panel owns only DOM below its mount element; v2 exposes no parent-page or layout-mutation API. Store panel configuration through context.storage, and keep framework/runtime dependencies inside the panel's own bundle.
rosSupplies brokered discovery, topic selection, subscribe, publish, and service calls constrained by permissions.ros.storageSupplies a 64 KiB, instance-owned JSON store.networkSupplies credential-free brokered fetch and explicitly approved host endpoints constrained by permissions.network.web-bluetoothDeclares Web Bluetooth use.web-usbDeclares WebUSB use.web-serialDeclares Web Serial use.cameraDeclares camera/media capture.microphoneDeclares microphone/media capture.webxrDelegates XR spatial tracking and fullscreen for an immersive WebXR session.{
"capabilities": ["ros", "network"],
"permissions": {
"ros": {
"selectTopic": true,
"publish": ["/cmd_vel"],
"services": ["/robot/reset"]
},
"network": {
"origins": ["https://telemetry.example.com"],
"hostEndpoints": ["webrtcWhep", "webrtcDiscovery", "webrtcHls"]
}
}
}ROS and network capabilities require matching permission blocks. Static topic/service patterns are checked for every operation. selectTopic opens a trusted Robo-Boy picker and grants only the chosen topic to that tile; panels never receive the unfiltered graph. Brokered network requests omit ambient browser credentials, revalidate redirects, and can return text, JSON, or binary bytes. videoStream remains available for older panels; new stream panels should request the dedicated gateway endpoints.
Fetch and validate registry JSON. No panel code runs.
Verify SHA-256, create an opaque iframe, and transfer source over a private channel.
Import the ESM in the sandbox and create one instance with brokered services.
Render below the supplied HTML element.
React to effective visibility through setActive.
Release every owned resource, deterministically.
unmount() must releasesetActive(isActive) is optional; mount and unmount are mandatory. Effective active state combines workspace activity, tile intersection, and document visibility. Removing the iframe terminates the panel realm even if cleanup fails.
interface RoboBoyPanelDefinition {
apiVersion: string;
id: string;
activate(
context: RoboBoyPanelContext
): RoboBoyPanelInstance | Promise<RoboBoyPanelInstance>;
}
interface RoboBoyPanelInstance {
mount(container: HTMLElement): void | Promise<void>;
setActive?(isActive: boolean): void | Promise<void>;
unmount(): void | Promise<void>;
}
interface RoboBoyPanelContext {
readonly panelId: string;
readonly instanceId: string;
readonly capabilities: readonly RoboBoyPanelCapability[];
readonly ros: RoboBoyPanelRos | null;
readonly storage: RoboBoyPanelStorage | null;
readonly network: RoboBoyPanelNetwork | null;
readonly runtime: RoboBoyPanelRuntime;
readonly connection: RoboBoyPanelConnection;
readonly viewport: RoboBoyPanelViewport;
readonly theme: RoboBoyPanelTheme;
readonly logger: RoboBoyPanelLogger;
}panelIdstringStable manifest/definition ID.instanceIdstringUnique workspace tile identity.capabilitiesreadonly capability arrayValidated declarations from the manifest.rosbroker or nullPermission-checked JSON ROS operations when ros is declared.storagestore or nullPer-instance JSON state when storage is declared.networkbroker or nullAllowlisted, credential-free fetch and approved named host endpoints.runtimeweb | desktopCurrent host target; the complete endpoint map is intentionally private.connectionsnapshot + subscribeStatus and monotonically increasing ROS generation.viewportsnapshot + subscribe + fullscreenTile size, visibility, effective activity, fullscreen request.themesnapshot + subscribeLight/dark state and the reviewed public CSS token set.loggerdebug/info/warn/errorPanel- and instance-prefixed host logging.interface RoboBoyPanelRuntime {
readonly target: 'web' | 'desktop';
}
interface RoboBoyPanelConnectionSnapshot {
readonly status: 'disconnected' | 'connecting' | 'connected';
readonly generation: number;
}
interface RoboBoyPanelConnection {
getSnapshot(): RoboBoyPanelConnectionSnapshot;
subscribe(
listener: (snapshot: RoboBoyPanelConnectionSnapshot) => void
): () => void;
}
interface RoboBoyPanelViewportSnapshot {
readonly width: number;
readonly height: number;
readonly isIntersecting: boolean;
readonly isDocumentVisible: boolean;
readonly isActive: boolean;
}
interface RoboBoyPanelViewport {
getSnapshot(): RoboBoyPanelViewportSnapshot;
subscribe(
listener: (snapshot: RoboBoyPanelViewportSnapshot) => void
): () => void;
requestFullscreen(): Promise<void>;
}
interface RoboBoyPanelLogger {
debug(message: string, ...details: unknown[]): void;
info(message: string, ...details: unknown[]): void;
warn(message: string, ...details: unknown[]): void;
error(message: string, ...details: unknown[]): void;
}interface RoboBoyPanelRos {
getTopics(): Promise<RoboBoyRosTopic[]>;
selectTopic(options?: { currentTopic?: string }): Promise<RoboBoyRosTopic>;
subscribe(
options: RoboBoyRosSubscriptionOptions,
listener: (message: RoboBoyJsonObject) => void
): Promise<RoboBoyRosSubscription>;
publish(options: RoboBoyRosPublishOptions): Promise<void>;
callService(options: RoboBoyRosServiceOptions): Promise<RoboBoyJsonObject>;
}
type RoboBoyHostEndpoint =
| 'videoStream'
| 'webrtcWhep'
| 'webrtcDiscovery'
| 'webrtcHls';
interface RoboBoyPanelNetworkResponse {
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly headers: { get(name: string): string | null };
text(): Promise<string>;
json<T extends RoboBoyJsonValue = RoboBoyJsonValue>(): Promise<T>;
arrayBuffer(): Promise<ArrayBuffer>;
}
interface RoboBoyPanelNetwork {
readonly endpoints: Partial<Record<RoboBoyHostEndpoint, string>>;
fetch(
url: string,
request?: RoboBoyPanelNetworkRequest
): Promise<RoboBoyPanelNetworkResponse>;
}ROS permissions can allow filtered discovery, a trusted host topic picker, subscription patterns, publish patterns, and services. Network permissions allow exact HTTPS origins, self, https:, or named host endpoints. The broker rechecks every operation and redirect; arrayBuffer() carries media segments and other non-text responses without bypassing the panel sandbox.
interface RoboBoyPanelThemeSnapshot {
readonly colorScheme: 'light' | 'dark';
readonly tokens: Readonly<
Partial<Record<RoboBoyPanelThemeToken, string>>
>;
}
interface RoboBoyPanelTheme {
getSnapshot(): RoboBoyPanelThemeSnapshot;
subscribe(
listener: (snapshot: RoboBoyPanelThemeSnapshot) => void
): () => void;
}Robo-Boy also writes the public tokens into the sandbox as CSS custom properties. Prefer variables such as --background-color, --card-bg, --text-color, --border-color, --primary-color, and --font-family-ui.
interface RoboBoyPanelStorage {
readonly schemaVersion: 1;
readonly quotaBytes: number;
get<T extends RoboBoyJsonValue>(key: string, fallback: T): T;
set(key: string, value: RoboBoyJsonValue): void;
remove(key: string): void;
sizeBytes(): number;
}Keys are 1–128 characters using letters, numbers, dots, underscores, or dashes. Values must be finite, acyclic, plain JSON no deeper than 20 levels. Each tile has a 64 KiB serialized quota.
This minimal standalone panel uses the v2 sandbox lifecycle and brokered storage API. It has no runtime framework dependency; TypeScript emits a browser-ready ESM file.
mkdir my-roboboy-panel
cd my-roboboy-panel
mkdir -p src scriptsFor local staging, keep it next to robo-boy/. A local desired-state source points at the repository explicitly; it does not scan sibling projects.
package.json{
"name": "my-roboboy-panel",
"version": "0.1.0",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"integrity": "node scripts/print-integrity.mjs"
},
"devDependencies": {
"@tessel-la/roboboy-panel-sdk": "https://github.com/tessel-la/robo-boy/releases/download/panel-sdk-v2.2.0/tessel-la-roboboy-panel-sdk-2.2.0.tgz",
"typescript": "^5.2.2"
}
}npm installThe SDK is distributed as a release tarball, not through the npm registry. Pin it and commit the lockfile.
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM"],
"strict": true,
"declaration": false,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src"]
}src/index.tsimport type {
RoboBoyPanelContext,
RoboBoyPanelDefinition,
} from '@tessel-la/roboboy-panel-sdk';
const activate = (context: RoboBoyPanelContext) => {
let root: HTMLElement | null = null;
let button: HTMLButtonElement | null = null;
let count = context.storage?.get('greetings', 0) ?? 0;
const renderCount = () => {
if (button) button.textContent = `Send greeting (${count})`;
};
const sendGreeting = () => {
count += 1;
context.storage?.set('greetings', count);
context.logger.info(`Greeting ${count} sent.`);
renderCount();
};
return {
mount(container: HTMLElement) {
root = document.createElement('section');
root.style.cssText = `
height: 100%; display: grid; place-items: center;
padding: 24px; box-sizing: border-box;
color: var(--text-color, #f5f7fb);
background: var(--background-secondary, #252b35);
`;
const content = document.createElement('div');
content.style.textAlign = 'center';
const heading = document.createElement('h2');
heading.textContent = 'Hello from outside Robo-Boy';
button = document.createElement('button');
button.type = 'button';
button.addEventListener('click', sendGreeting);
renderCount();
content.append(heading, button);
root.append(content);
container.replaceChildren(root);
},
setActive(isActive: boolean) {
root?.toggleAttribute('data-inactive', !isActive);
},
unmount() {
button?.removeEventListener('click', sendGreeting);
root?.remove();
button = null;
root = null;
},
};
};
const definition: RoboBoyPanelDefinition = {
apiVersion: '2.0.0',
id: 'com.example.greeting',
activate,
};
export default definition;roboboy.panel.json{
"schemaVersion": 1,
"id": "com.example.greeting",
"name": "Greeting",
"description": "A minimal persistent greeting panel.",
"version": "0.1.0",
"entryPoint": "./dist/index.js",
"integrity": "sha256-REPLACE_WITH_THE_GENERATED_BASE64_DIGEST=",
"compatibility": {
"panelApi": "^2.0.0",
"roboboy": ">=0.3.0-0 <1.0.0"
},
"capabilities": ["storage"],
"author": { "name": "Your team" },
"repository": "https://github.com/YOUR_ORG/my-roboboy-panel",
"tags": ["example"]
}Use a stable lowercase reverse-domain ID you control. The definition, manifest, desired-state selection, and published catalog entry must agree on identity and compatibility.
Create scripts/print-integrity.mjs:
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
const bundle = await readFile(new URL('../dist/index.js', import.meta.url));
console.log(`sha256-${createHash('sha256')
.update(bundle)
.digest('base64')}`);npm run typecheck
npm run build
npm run integrityReplace the placeholder in the manifest with the complete sha256-... output after every bundle change.
Create an ignored config/panel-sources.json inside robo-boy/. Schema v2 uses typed sources and explicit selection; local development does not require an inventory entry.
{
"schemaVersion": 2,
"sources": [
{
"type": "local",
"name": "development",
"root": "../..",
"rootEnv": "ROBOBOY_PANEL_WORKSPACE",
"repositories": ["my-roboboy-panel"]
}
],
"selection": {
"mode": "include",
"panelIds": ["com.example.greeting"]
}
}cd ../robo-boy
npm run panels:stage-local -- \
--config config/panel-sources.json
npm run dev:panels -- \
--config config/panel-sources.jsonOpen the workspace, choose Add panel, and select Greeting. Click the button, remove and re-add the tile, and confirm the instance-owned count returns. The normal npm run dev command intentionally keeps the tracked empty panel registry.
Panel API v2 brokers ROS as JSON operations. Panels do not receive ROSLIB, the rosbridge URL, or the complete graph; every request is checked against the manifest permissions.
import type { RoboBoyRosSubscription } from
'@tessel-la/roboboy-panel-sdk';
let subscription: RoboBoyRosSubscription | null = null;
if (context.ros) {
const selected = await context.ros.selectTopic({
currentTopic: '/battery_state',
});
subscription = await context.ros.subscribe({
topic: selected.name,
messageType: selected.messageType,
queueLength: 1,
throttleMs: 100,
compression: 'none',
}, (message) => {
renderBattery(message);
});
}
// In async unmount():
await subscription?.unsubscribe();
subscription = null;Declare capabilities: ["ros"] and permissions.ros.selectTopic: true. The complete graph stays in trusted Robo-Boy UI; the panel receives and is granted only the selected topic and message type.
await context.ros?.publish({
topic: '/cmd_vel',
messageType: 'geometry_msgs/msg/Twist',
message: {
linear: { x: 0.2, y: 0, z: 0 },
angular: { x: 0, y: 0, z: 0 },
},
});Declare the exact publish path in permissions.ros.publish and match the robot's interface definition. For continuous controls, throttle publishing, cancel pending work, and deliberately publish a safe released/stopped state when the panel owns that responsibility.
import type { RoboBoyJsonObject } from
'@tessel-la/roboboy-panel-sdk';
type Settings = { topic: string; compact: boolean };
const fallback: Settings = {
topic: '/battery_state',
compact: false,
};
const stored = context.storage?.get(
'settings',
fallback as unknown as RoboBoyJsonObject,
);
let settings = (stored as unknown as Settings) ?? fallback;
function save(next: Settings) {
settings = next;
try {
context.storage?.set(
'settings',
next as unknown as RoboBoyJsonObject,
);
} catch (error) {
context.logger.warn('Unable to persist settings.', error);
}
}v2 has no host-rendered settings pane. Render controls inside your tile, sanitize loaded data, keep secrets out of storage, and handle quota errors.
let frame: number | null = null;
let active = context.viewport.getSnapshot().isActive;
const draw = () => {
if (!active) return;
renderLatestSample();
frame = requestAnimationFrame(draw);
};
const stop = context.viewport.subscribe((snapshot) => {
active = snapshot.isActive;
if (active && frame === null) frame = requestAnimationFrame(draw);
if (!active && frame !== null) cancelAnimationFrame(frame);
if (!active) frame = null;
});
// In unmount():
stop();
if (frame !== null) cancelAnimationFrame(frame);Use viewport width and height for compact layouts. Use setActive for coarse lifecycle changes and the viewport subscription when size or detailed visibility matters.
For core ROS image topics, prefer the built-in Camera panel or a custom gamepad camera component. A transport-specific external panel can declare network with permissions.network.hostEndpoints: ["webrtcWhep", "webrtcDiscovery", "webrtcHls"], use the matching context.network?.endpoints, and own its media lifecycle. The official WebRTC panel 3.0.1:
/webrtc/_discovery/paths resource.RTCPeerConnection; binary segments are read with arrayBuffer().let device: BluetoothDevice | null = null;
async function connectFromUserGesture() {
device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['YOUR_STANDARD_OR_VENDOR_SERVICE_UUID'] }],
});
const server = await device.gatt?.connect();
// Resolve your documented GATT service and characteristics.
}
// In unmount():
device?.gatt?.disconnect();
device = null;Declare web-bluetooth. The browser API normally requires HTTPS, compatible hardware/browser support, and a user gesture. Use the exact service and characteristic UUIDs from the device specification; the SDK does not broker or wrap Bluetooth. Apply the same ownership rule to WebUSB and Web Serial.
Unit-test message normalization, field selection, sizing, state migration, and protocol helpers without a browser.
Exercise activation, mount, active-state changes, repeated cleanup, missing services, quota errors, and failures.
Import dist/index.js; verify its default ID/API, required methods, no unresolved bare imports, and SHA-256.
Stage through schema-v2 desired state and test add, sandbox startup, permission denials, trusted topic selection, reload, reconnect, hide/show, resize, remove, and retry.
workspace/
robo-boy/ # host application
my-roboboy-panel/ # independent source + dist + manifestThe local source lists repository directories explicitly, then verifies bundle, manifest, permissions, compatibility, and integrity before generating an ignored .panel-stage/public/ tree. A separate inventory entry is needed only when publishing.
npm run lint
npm run test:run
npm run build
npm run e2eFor Docker development with the known examples, add infra/compose/panels.yml. For a private sibling panel, mount its repository through an uncommitted deployment-local Compose override and select it in the schema-v2 source configuration.
roboboy.panel.jsondist/index.jsinstalled.json. The browser transfers verified same-origin bytes into isolated panel sandboxes.# Official remote source overlay
export ROBOBOY_PANEL_MANAGER_TOKEN='use-a-long-random-deployment-secret'
docker compose \
-f docker-compose.yml \
-f infra/compose/panels.remote.yml \
build app panel-manager
docker compose \
-f docker-compose.yml \
-f infra/compose/panels.remote.yml \
up -dThe Compose manager exposes authenticated preview/apply installation controls through Manage installations…. Private sources can use HTTPS release storage and authorization supplied through an approved named environment variable. Credentials stay on the manager side, redirects are revalidated, and duplicate IDs across configured sources are rejected.
2.2.0's panel-sdk/index.d.ts and API version 2.0.0.MainControlView, ExternalPanelHost, loader/registry internals, or localStorage keys.Create clients inside the instance that cleans them up. Make unmount safe to call after partial startup and more than once.
Version and sanitize your settings. Store only plain JSON. Keep live objects, handles, credentials, and large sample buffers in memory.
Stop decoding, polling, plotting, and device traffic while inactive. Design from a small tile upward using viewport snapshots.
Watch connection generation. Discard stale async results and re-establish brokered subscriptions after reconnects.
Bundle only what the panel needs. Shared host dependencies are deliberately not part of v2, and relative dynamic chunks are not supported.
Use the supplied logger, provide meaningful empty/error states, and allow exceptions to remain attributable to your versioned bundle.
Check docker compose logs ros-stack, confirm rosbridge listens on the selected/default port, and verify the web proxy or direct host is reachable. Every DDS participant must use compatible domain, discovery, and middleware settings.
Regenerate the mkcert certificate with the machine's current LAN/VPN IP and trust the mkcert CA on the client device. Camera, Bluetooth, and similar browser APIs generally require a secure context.
Mount a built install workspace under /overlay_ws/<name>, recreate ros-stack, and inspect its logs for the activation line. Generated hooks may also require their original build/source paths.
Normal dev and build use an empty tracked registry. Run a panel-aware command. Check ID uniqueness, manifest shape, compatibility ranges, same-origin versioned entry point, registry size, and the browser console for registry issues.
Confirm the schema-v2 configuration names the local repository explicitly and its root resolves correctly on the host or through the approved root environment variable. Rebuild, regenerate the digest, and verify the manifest ID, API compatibility, capabilities, permissions, and integrity.
Inspect the panel-prefixed console error. Common causes are an invalid default export, mismatched ID/API version, rejected integrity, unresolved runtime imports, sandbox startup failure, failure in activate/mount, or a denied capability/permission. Fix the artifact and publish/stage a new immutable version.
Declare storage; validate keys and plain JSON values; avoid cycles, non-finite numbers, deep nesting, and the 64 KiB per-instance limit. Browser storage is isolated by origin and can be unavailable or full.
React to effective activity, unsubscribe high-rate topics, cancel animation frames, stop media tracks/timers, close peer/device connections, and dispose rendering resources in unmount.
These are current platform boundaries found in the SDK, host loader, and project documentation. They are not hidden APIs to work around.
Compose provides authenticated preview/apply installation management, but there is no rollback UI, scheduled update policy, publisher revocation service, or garbage collector. CLI staging remains available for builds and Tauri.
The opaque-origin iframe prevents parent-page access and brokers host services, but a panel can still consume main-thread resources or misuse data explicitly granted to it. Install only reviewed artifacts.
Panels render their own settings inside the tile. There is no standardized settings schema, secret store, notifications API, or application-level command surface.
The installer rejects extra manifest assets until inventory metadata defines immutable URLs for every asset.
Panel authors bundle any framework and runtime libraries themselves. This preserves independence but can duplicate code across panel sandboxes.
SHA-256 detects drift but does not authenticate a publisher. Signing, inventory review policy, transactional rollback metadata, and revocation remain future hardening work.
Lifecycle failures are tile-local and removing the iframe terminates its realm, but CPU-heavy synchronous code can still affect the main thread. Per-panel CPU and bandwidth limits remain future work.
API v2 supports discovery, trusted topic selection, subscriptions, publishing, and service calls. Binary payloads, actions, and streaming service responses are not part of the contract.