docs(ble): add ESP-BLE-UART companion guide

Move the OpenCode companion guide into the ble_uart_service example.

Add English and Chinese Markdown guides with image assets.

Keep ESP-BLE-UART naming consistent across the example and bridge tooling.
This commit is contained in:
Zhou Xiao
2026-06-05 19:01:00 +08:00
parent bd4ab32f73
commit 926111e721
30 changed files with 1072 additions and 112 deletions
@@ -7,7 +7,7 @@ import { isPermissionDecision } from "./opencode-permission-reply"
import type { BridgeResponse, DaemonResponse, DaemonStatus } from "./types"
/**
* Check whether the local BLE daemon is reachable.
* Check whether the local ESP-BLE-UART Daemon is reachable.
*
* This probe is intentionally silent: OpenCode loads plugins during startup, so
* a missing optional daemon must not print fetch errors into the OpenCode UI.
@@ -25,13 +25,13 @@ export async function isDaemonAvailable(): Promise<boolean> {
export async function getDaemonStatus(): Promise<DaemonStatus> {
const response = await fetch(`${BLE_DAEMON_URL}/status`)
if (!response.ok) {
throw new Error(`BLE daemon status failed: HTTP ${response.status}`)
throw new Error(`ESP-BLE-UART Daemon status check failed: HTTP ${response.status}`)
}
return (await response.json()) as DaemonStatus
}
/**
* Normalize the BLE daemon's response envelope into a permission response.
* Normalize the ESP-BLE-UART Daemon's response envelope into a permission response.
*
* The daemon supports both nested `data`/`response` envelopes and a direct
* top-level decision. Keeping that tolerance here prevents transport quirks
@@ -61,11 +61,11 @@ function parseBridgeResponse(body: DaemonResponse): BridgeResponse {
return body as BridgeResponse
}
throw new Error(`BLE daemon returned an invalid response payload: ${JSON.stringify(body)}`)
throw new Error(`ESP-BLE-UART Daemon returned an invalid response payload: ${JSON.stringify(body)}`)
}
/**
* Send a one-way notification to the BLE daemon.
* Send a one-way notification to the ESP-BLE-UART Daemon.
*
* Use this for events such as session status updates or permission cancellation,
* where the BLE device should update its UI but OpenCode is not waiting for a
@@ -79,12 +79,12 @@ export async function notifyBLE(op: string, data: unknown): Promise<void> {
})
if (!response.ok) {
throw new Error(`BLE daemon notify failed: HTTP ${response.status}`)
throw new Error(`ESP-BLE-UART Daemon notify failed: HTTP ${response.status}`)
}
}
/**
* Send a request to the BLE daemon and wait for a structured response.
* Send a request to the ESP-BLE-UART Daemon and wait for a structured response.
*
* Permission prompts use this path because OpenCode cannot continue until the
* BLE device returns a decision or the request times out.
@@ -101,7 +101,7 @@ export async function sendRequestToBLE(op: string, data: unknown, timeoutSeconds
})
if (!response.ok) {
throw new Error(`BLE daemon request failed: HTTP ${response.status}`)
throw new Error(`ESP-BLE-UART Daemon request failed: HTTP ${response.status}`)
}
return parseBridgeResponse((await response.json()) as DaemonResponse)
@@ -31,7 +31,7 @@ export const METADATA_DISPLAY_KEYS = ["command", "path", "url"] as const
/** Enables verbose local console logging when OPENCODE_BLE_DEBUG=1. */
export const DEBUG = process.env.OPENCODE_BLE_DEBUG === "1"
/** HTTP base URL of the local BLE daemon that bridges OpenCode to the BLE device. */
/** HTTP base URL of the local ESP-BLE-UART Daemon that bridges OpenCode to the BLE device. */
export const BLE_DAEMON_URL = process.env.OPENCODE_BLE_DAEMON_URL ?? "http://127.0.0.1:8888"
const DEFAULT_DECISION_TIMEOUT_SECONDS = 60
@@ -39,12 +39,12 @@ function stateMessage(state: BLEPluginState, status?: DaemonStatus): string {
status?.reconnect_failures !== undefined && status.max_reconnect_failures !== undefined
? ` (${status.reconnect_failures}/${status.max_reconnect_failures} reconnect failures)`
: ""
return `BLE UART daemon is reachable, but the device is disconnected${attempts}. The next BLE send will try to reconnect.`
return `ESP-BLE-UART Daemon is reachable, but the device is disconnected${attempts}. The next BLE send will try to reconnect.`
}
if (status?.daemon_state === "exiting") {
return "BLE UART daemon is exiting after repeated reconnect failures. BLE forwarding is disabled."
return "ESP-BLE-UART Daemon is exiting after repeated reconnect failures. BLE forwarding is disabled."
}
return "BLE UART daemon is unreachable. BLE forwarding is disabled until the daemon is available."
return "ESP-BLE-UART Daemon is unreachable. BLE forwarding is disabled until the daemon is available."
}
async function notifyStateChange(
@@ -54,7 +54,7 @@ async function notifyStateChange(
): Promise<void> {
const variant = state === "connected" ? "success" : state === "degraded" ? "warning" : "error"
const message = stateMessage(state, status)
await showToastBestEffort(client, variant, "OpenCode BLE UART Bridge", message)
await showToastBestEffort(client, variant, "OpenCode ESP-BLE-UART Bridge", message)
await appLogBestEffort(client, variant === "error" ? "error" : variant === "warning" ? "warn" : "info", message, {
state,
status,
@@ -95,7 +95,7 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
bleState = "disabled"
if (shouldNotify) {
await notifyStateChange(openCodeClient, "disabled")
await appLogBestEffort(openCodeClient, "warn", "BLE UART daemon status check failed", { error: String(error) })
await appLogBestEffort(openCodeClient, "warn", "ESP-BLE-UART Daemon status check failed", { error: String(error) })
}
}
return bleState
@@ -124,7 +124,7 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
// Forwarding session status is best-effort background work. Do not
// await this async IIFE from the OpenCode event callback; otherwise a
// slow or unavailable BLE daemon could block OpenCode's own event loop.
// slow or unavailable ESP-BLE-UART Daemon could block OpenCode's own event loop.
void (async () => {
try {
const previousState = bleState
@@ -139,7 +139,7 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
await showToastBestEffort(
openCodeClient,
"success",
"OpenCode BLE UART Bridge",
"OpenCode ESP-BLE-UART Bridge",
"BLE UART device is connected for this OpenCode session.",
)
}
@@ -149,8 +149,8 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
if (
// If the session became idle while a BLE permission prompt is
// active, mark that prompt as externally resolved and ask the BLE
// daemon to dismiss it. This prevents an old prompt from being
// active, mark that prompt as externally resolved and ask the
// ESP-BLE-UART Daemon to dismiss it. This prevents an old prompt from being
// answered after OpenCode no longer needs the decision. Only idle
// triggers this cancellation: busy/retry are normal activity
// transitions and should not dismiss an actively displayed prompt.
@@ -174,7 +174,7 @@ export async function replyToOpenCodePermission(
throw new Error("OpenCode client does not expose a permission reply API")
}
/** Runtime guard for permission decisions parsed from BLE daemon JSON. */
/** Runtime guard for permission decisions parsed from ESP-BLE-UART Daemon JSON. */
export function isPermissionDecision(value: unknown): value is PermissionDecision {
return value === "once" || value === "reject"
}
@@ -9,7 +9,7 @@ import {
} from "./config"
import type { PermissionEventProperties } from "./types"
/** Create a unique event ID for messages sent to the BLE daemon. */
/** Create a unique event ID for messages sent to the ESP-BLE-UART Daemon. */
function eventID() {
return crypto.randomUUID()
}
@@ -116,7 +116,7 @@ export function permissionRequestID(permission: PermissionEventProperties): stri
}
/**
* Build the protocol message sent to the BLE daemon whenever OpenCode's session
* Build the protocol message sent to the ESP-BLE-UART Daemon whenever OpenCode's session
* state changes. This is a one-way notification, so the BLE device can update
* its UI but is not expected to send a reply.
*/
@@ -135,7 +135,7 @@ export function buildSessionStatusPayload(
}
/**
* Tell the BLE daemon to dismiss any permission prompt for this session.
* Tell the ESP-BLE-UART Daemon to dismiss any permission prompt for this session.
*
* This is used when OpenCode has already moved on, for example after the
* session becomes idle before the BLE device returns a decision.
@@ -156,7 +156,7 @@ export function buildPermissionCancelPayload(sessionID: string) {
/**
* Build the BLE permission prompt payload from an OpenCode permission event.
*
* This is the main protocol boundary between OpenCode and the BLE daemon. The
* This is the main protocol boundary between OpenCode and the ESP-BLE-UART Daemon. The
* outer fields describe routing and reply behavior; the nested `payload` fields
* are intentionally small and display-oriented for the device UI.
*/
@@ -174,7 +174,7 @@ async function handlePermissionQueueItem(item: PermissionQueueItem): Promise<Per
await showToastBestEffort(
item.client,
"error",
"OpenCode BLE UART Bridge",
"OpenCode ESP-BLE-UART Bridge",
"Failed to get a BLE permission decision. The request was rejected.",
)
} finally {
@@ -15,7 +15,7 @@ export type PermissionDecision = "once" | "reject"
*
* Several ID field names are optional because OpenCode permission events have
* changed across API versions. The bridge normalizes them before talking to the
* BLE daemon or replying back to OpenCode.
* ESP-BLE-UART Daemon or replying back to OpenCode.
*/
export type PermissionEventProperties = {
id?: string
@@ -32,7 +32,7 @@ export type PermissionEventProperties = {
}
/**
* Normalized response expected from the BLE daemon after a permission request.
* Normalized response expected from the ESP-BLE-UART Daemon after a permission request.
*
* `decision` is optional at the type level because the daemon response is parsed
* from JSON. Runtime code validates it before using it as a permission reply.
@@ -44,7 +44,7 @@ export type BridgeResponse = {
}
/**
* Envelope formats the BLE daemon may return.
* Envelope formats the ESP-BLE-UART Daemon may return.
*
* The daemon can wrap the actual payload in either `data` or `response`, and it
* may encode that payload as an object or a JSON string. Keeping this type loose
@@ -55,7 +55,7 @@ export type DaemonResponse = {
response?: unknown
}
/** Status payload returned by the BLE UART daemon `/status` endpoint. */
/** Status payload returned by the ESP-BLE-UART Daemon `/status` endpoint. */
export type DaemonStatus = {
device_id?: string
connection_state?: "DISCONNECTED" | "CONNECTING" | "CONNECTED" | string