Audience: Web team building the hosted management UI (e.g.
lab.akiraos.io).
Status: Draft v1.0 — covers USB Web Serial transport + existing WiFi REST API.
Overview
AkiraConsole exposes a management web interface through two transports:
| Transport | Technology | Use case |
|---|---|---|
| WiFi (existing) | HTTP REST + WebSocket on port 8080 | Device on local network |
| USB (new) | CDC ACM serial + JSON-RPC 2.0 via Web Serial API | Direct cable connection, no WiFi required |
The hosted web app (lab.akiraos.dev or self-hosted) detects which transport is available and uses USB-over-serial when WiFi is not configured, exactly as Flipper Zero’s lab.flipper.net handles its device.
The device-side USB transport is implemented in src/connectivity/usb/usb_cdc_serial.c and enabled by CONFIG_AKIRA_USB_CDC_SERIAL.
Transport 1 — WiFi REST API (existing)
Base URL: http://<device-ip>:8080/api
When the device has joined a WiFi network the same REST endpoints are available as documented below. The web app discovers the device via mDNS (_akira._tcp.local) as described in HUB_INTEGRATION.md.
Endpoints are shared with the USB JSON-RPC transport — see the Endpoint Reference section.
Transport 2 — USB Web Serial (new)
Browser requirements
- Chrome 89+ or Edge 89+ on Windows, macOS, Linux, ChromeOS.
- Firefox and Safari currently do not support the Web Serial API.
- User must click a button to grant port access (Web Serial permission model).
Connecting
// Request a port (shows browser permission dialog)
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
const encoder = new TextEncoderStream();
const decoder = new TextDecoderStream();
encoder.readable.pipeTo(port.writable);
port.readable.pipeTo(decoder.writable);
const writer = encoder.writable.getWriter();
const reader = decoder.readable.getReader();
// Detect the device by sending a status request
await writer.write(JSON.stringify({
jsonrpc: "2.0", id: 1, method: "GET", path: "/api/status"
}) + "\n");
Protocol — JSON-RPC 2.0 over serial
Every message is a single-line JSON object terminated by \n (LF). Carriage returns (\r) are ignored so CRLF line endings work too.
Request format (browser → device)
{ "jsonrpc": "2.0", "id": <integer>, "method": "<VERB>", "path": "<path>", "params": {} }
| Field | Type | Required | Description |
|---|---|---|---|
jsonrpc | "2.0" | ✅ | Protocol version |
id | integer | ✅ | Request ID — echoed in response |
method | string | ✅ | HTTP verb: GET, POST |
path | string | ✅ | API path (see Endpoint Reference) |
params | object | ✗ | Query parameters as a JSON object |
Response format (device → browser)
{ "jsonrpc": "2.0", "id": <integer>, "status": <http-code>, "body": <json> }
Unsolicited event format (device → browser)
{ "jsonrpc": "2.0", "id": null, "event": "<type>", "data": <json-or-string> }
| Event type | Trigger | data content |
|---|---|---|
log | System log line | string |
status | Device state change | device status object |
Chunked binary upload (app install / firmware OTA)
Binary data (.akpkg or .akfw files) is sent as a sequence of base64-encoded chunks. The chunk and total fields control reassembly on the device.
{
"jsonrpc": "2.0",
"id": 10,
"method": "POST",
"path": "/api/apps/install",
"chunk": 1,
"total": 5,
"data": "<base64-encoded-bytes>"
}
chunk: 1-based chunk indextotal: total number of chunksdata: base64-encoded payload (≤ 3 072 raw bytes per chunk → ≤ 4 096 base64 chars)
The device responds 206 Partial Content for each non-final chunk and 200 OK (or an error) when the last chunk has been installed.
Suggested chunk size: 2 048 bytes of raw data per chunk to avoid exceeding the line buffer (LINE_BUF_SIZE = 1024 chars after base64 expansion is well within 4 096 limit).
async function uploadFile(writer, reader, filePath, arrayBuffer) {
const CHUNK_BYTES = 2048;
const bytes = new Uint8Array(arrayBuffer);
const total = Math.ceil(bytes.length / CHUNK_BYTES);
for (let i = 0; i < total; i++) {
const slice = bytes.slice(i * CHUNK_BYTES, (i + 1) * CHUNK_BYTES);
const b64 = btoa(String.fromCharCode(...slice));
await writer.write(JSON.stringify({
jsonrpc: "2.0",
id: 100 + i,
method: "POST",
path: "/api/apps/install",
chunk: i + 1,
total,
data: b64,
}) + "\n");
// Wait for 206/200 before sending the next chunk
const { value } = await reader.read();
const resp = JSON.parse(value);
if (resp.status !== 206 && resp.status !== 200) {
throw new Error(`Upload error at chunk ${i + 1}: ${JSON.stringify(resp)}`);
}
}
}
Endpoint Reference
All endpoints are available over both WiFi HTTP and USB JSON-RPC.
Device
| Method | Path | Description |
|---|---|---|
GET | /api/status | Device status (IP, uptime, free heap, firmware version) |
GET | /api/system | Extended system info (board, PSRAM, CPU load) |
POST | /api/reboot | Reboot device |
GET | /api/logs | Recent log buffer (over WiFi); subscribe to log events over USB |
GET /api/status response
{
"fw_version": "1.4.8",
"codename": "GL1TCH",
"uptime_ms": 120000,
"free_heap_bytes": 131072,
"wifi_ip": "192.168.1.42",
"wifi_rssi": -55,
"bt_mode": "companion"
}
Apps
| Method | Path | Params | Description |
|---|---|---|---|
GET | /api/apps/list | — | List installed WASM apps |
POST | /api/apps/start | name=<app> | Start an app |
POST | /api/apps/stop | name=<app> | Stop a running app |
POST | /api/apps/uninstall | name=<app> | Remove an app |
POST | /api/apps/install | chunked binary | Install .akpkg from browser |
GET /api/apps/list response
[
{ "name": "hello_world", "version": "1.0.0", "state": "stopped" },
{ "name": "cube3d", "version": "2.1.0", "state": "running" }
]
OTA Firmware
| Method | Path | Description |
|---|---|---|
GET | /api/ota/status | OTA progress / last result |
POST | /api/ota/confirm | Confirm pending image (MCUboot swap) |
Shell
| Method | Path | Params | Description |
|---|---|---|---|
GET | /api/cmd | c=<command> | Execute a Zephyr shell command, get stdout in response |
// Request
{ "jsonrpc": "2.0", "id": 5, "method": "GET", "path": "/api/cmd", "params": { "c": "akira status" } }
// Response
{ "jsonrpc": "2.0", "id": 5, "status": 200, "body": { "output": "AkiraOS 1.4.8 running\n" } }
WebSocket (WiFi transport only)
When connecting over WiFi the web app may upgrade the HTTP connection to a WebSocket on port 8081 for real-time log streaming:
ws://<device-ip>:8081/ws
The device pushes newline-terminated log lines as text frames.
On the USB transport, subscribe to log unsolicited events instead.
Device Discovery
USB
After navigator.serial.requestPort() the site should send GET /api/status to verify the device is an AkiraConsole. Check fw_version in the response.
WiFi / mDNS
Service: _akira._tcp.local
Instance: AkiraConsole-<last4ofMAC>._akira._tcp.local
Port: 8080
TXT: fw=1.4.8 api=v1
Security Considerations
-
USB: The Web Serial API requires explicit user permission; access is not granted silently. The JSON-RPC handler rejects lines longer than 1 024 bytes. Shell command execution via
/api/cmdis gated byCONFIG_AKIRA_SHELL_WEB_CMD_ENABLE(defaulty). -
WiFi: No authentication is required on the local REST API (same as Flipper’s local web UI). For production deployments on shared networks, enable
CONFIG_AKIRA_HTTP_AUTHto add a Bearer token. -
OTA and app uploads: App packages are verified against the Ed25519 signature before installation even when transferred via USB or WiFi — the signature is required regardless of transport.
Minimum Viable Web-App Checklist
For the initial lab.akiraos.io USB feature the web team must implement:
- Web Serial port picker +
GET /api/statusdevice detection - Display device status panel (firmware, heap, running apps)
- App list view: list, start, stop, uninstall
- App install: file picker → chunked base64 upload → progress bar
- Shell terminal: type command →
GET /api/cmd→ display output - OTA firmware upload (same chunked protocol as app install but targeting
/api/ota/upload) - Graceful fallback to WiFi HTTP when USB not available
- Unsolicited
logevent handler → append to terminal pane