mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
fix(ble): fixed edge case issues from ai reviewer
(cherry picked from commit fa8ca07e58)
Co-authored-by: Zhou Xiao <zhouxiao@espressif.com>
This commit is contained in:
@@ -34,8 +34,15 @@ 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 BLE 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"
|
export const BLE_DAEMON_URL = process.env.OPENCODE_BLE_DAEMON_URL ?? "http://127.0.0.1:8888"
|
||||||
|
|
||||||
|
const DEFAULT_DECISION_TIMEOUT_SECONDS = 60
|
||||||
|
|
||||||
|
function parseDecisionTimeoutSeconds(value: string | undefined): number {
|
||||||
|
const parsed = Number(value ?? String(DEFAULT_DECISION_TIMEOUT_SECONDS))
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DECISION_TIMEOUT_SECONDS
|
||||||
|
}
|
||||||
|
|
||||||
/** Maximum time to wait for a user decision from the BLE device. */
|
/** Maximum time to wait for a user decision from the BLE device. */
|
||||||
export const DECISION_TIMEOUT_SECONDS = Number(process.env.OPENCODE_BLE_DECISION_TIMEOUT_SECONDS ?? "60")
|
export const DECISION_TIMEOUT_SECONDS = parseDecisionTimeoutSeconds(process.env.OPENCODE_BLE_DECISION_TIMEOUT_SECONDS)
|
||||||
|
|
||||||
/** Message used when later prompts are skipped after an earlier same-session reject. */
|
/** Message used when later prompts are skipped after an earlier same-session reject. */
|
||||||
export const CONCURRENT_REJECT_MESSAGE = "Another concurrent permission request was rejected"
|
export const CONCURRENT_REJECT_MESSAGE = "Another concurrent permission request was rejected"
|
||||||
|
|||||||
@@ -71,22 +71,32 @@ async function notifyStateChange(
|
|||||||
export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directory }) => {
|
export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directory }) => {
|
||||||
const openCodeClient = client as OpenCodePermissionClient
|
const openCodeClient = client as OpenCodePermissionClient
|
||||||
let bleState: BLEPluginState = "unknown"
|
let bleState: BLEPluginState = "unknown"
|
||||||
|
let bleStateRefreshGeneration = 0
|
||||||
const connectedSessionNotifications = new Set<string>()
|
const connectedSessionNotifications = new Set<string>()
|
||||||
|
|
||||||
async function refreshBLEState(notifyConnected: boolean): Promise<BLEPluginState> {
|
async function refreshBLEState(notifyConnected: boolean): Promise<BLEPluginState> {
|
||||||
|
const generation = ++bleStateRefreshGeneration
|
||||||
try {
|
try {
|
||||||
const status = await getDaemonStatus()
|
const status = await getDaemonStatus()
|
||||||
|
if (generation !== bleStateRefreshGeneration) {
|
||||||
|
return bleState
|
||||||
|
}
|
||||||
const nextState = stateFromStatus(status)
|
const nextState = stateFromStatus(status)
|
||||||
if (nextState !== bleState && (nextState !== "connected" || notifyConnected)) {
|
const shouldNotify = nextState !== bleState && (nextState !== "connected" || notifyConnected)
|
||||||
|
bleState = nextState
|
||||||
|
if (shouldNotify) {
|
||||||
await notifyStateChange(openCodeClient, nextState, status)
|
await notifyStateChange(openCodeClient, nextState, status)
|
||||||
}
|
}
|
||||||
bleState = nextState
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (bleState !== "disabled") {
|
if (generation !== bleStateRefreshGeneration) {
|
||||||
|
return bleState
|
||||||
|
}
|
||||||
|
const shouldNotify = bleState !== "disabled"
|
||||||
|
bleState = "disabled"
|
||||||
|
if (shouldNotify) {
|
||||||
await notifyStateChange(openCodeClient, "disabled")
|
await notifyStateChange(openCodeClient, "disabled")
|
||||||
await appLogBestEffort(openCodeClient, "warn", "BLE UART daemon status check failed", { error: String(error) })
|
await appLogBestEffort(openCodeClient, "warn", "BLE UART daemon status check failed", { error: String(error) })
|
||||||
}
|
}
|
||||||
bleState = "disabled"
|
|
||||||
}
|
}
|
||||||
return bleState
|
return bleState
|
||||||
}
|
}
|
||||||
@@ -116,60 +126,67 @@ export const BLEDeviceBridgePlugin: Plugin = async ({ client, serverUrl, directo
|
|||||||
// await this async IIFE from the OpenCode event callback; otherwise a
|
// 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 BLE daemon could block OpenCode's own event loop.
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const previousState = bleState
|
try {
|
||||||
const state = await refreshBLEState(true)
|
const previousState = bleState
|
||||||
if (
|
const state = await refreshBLEState(true)
|
||||||
properties.status.type === "busy" &&
|
if (
|
||||||
state === "connected" &&
|
properties.status.type === "busy" &&
|
||||||
previousState === "connected" &&
|
state === "connected" &&
|
||||||
!connectedSessionNotifications.has(properties.sessionID)
|
previousState === "connected" &&
|
||||||
) {
|
!connectedSessionNotifications.has(properties.sessionID)
|
||||||
connectedSessionNotifications.add(properties.sessionID)
|
) {
|
||||||
await showToastBestEffort(
|
connectedSessionNotifications.add(properties.sessionID)
|
||||||
openCodeClient,
|
await showToastBestEffort(
|
||||||
"success",
|
openCodeClient,
|
||||||
"OpenCode BLE UART Bridge",
|
"success",
|
||||||
"BLE UART device is connected for this OpenCode session.",
|
"OpenCode BLE UART Bridge",
|
||||||
)
|
"BLE UART device is connected for this OpenCode session.",
|
||||||
}
|
)
|
||||||
if (state === "disabled") {
|
}
|
||||||
return
|
if (state === "disabled") {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// 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.
|
||||||
|
statusShouldCancelPendingPermission(properties.status) &&
|
||||||
|
markActiveBLEPermissionsExternallyResolved(properties.sessionID)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// OpenCode does not emit a permission-specific cancellation event
|
||||||
|
// when a user answers the same prompt in the TUI. This best-effort
|
||||||
|
// notification tells the daemon the BLE prompt is stale before the
|
||||||
|
// following idle status also clears the device UI.
|
||||||
|
await notifyBLE("permission.cancel", buildPermissionCancelPayload(properties.sessionID))
|
||||||
|
} catch (error) {
|
||||||
|
await appLogBestEffort(openCodeClient, "warn", "Failed to cancel stale BLE permission on device", {
|
||||||
|
error: String(error),
|
||||||
|
})
|
||||||
|
await refreshBLEState(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
|
||||||
// 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.
|
|
||||||
statusShouldCancelPendingPermission(properties.status) &&
|
|
||||||
markActiveBLEPermissionsExternallyResolved(properties.sessionID)
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
// OpenCode does not emit a permission-specific cancellation event
|
// Always forward the latest session status, even if there was no
|
||||||
// when a user answers the same prompt in the TUI. This best-effort
|
// stale permission prompt to cancel. This keeps the BLE device's
|
||||||
// notification tells the daemon the BLE prompt is stale before the
|
// display synchronized with OpenCode.
|
||||||
// following idle status also clears the device UI.
|
await notifyBLE("session.status", buildSessionStatusPayload(properties.sessionID, properties.status))
|
||||||
await notifyBLE("permission.cancel", buildPermissionCancelPayload(properties.sessionID))
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await appLogBestEffort(openCodeClient, "warn", "Failed to cancel stale BLE permission on device", {
|
await appLogBestEffort(openCodeClient, "warn", "Failed to forward session status to BLE device", {
|
||||||
error: String(error),
|
error: String(error),
|
||||||
})
|
})
|
||||||
await refreshBLEState(false)
|
await refreshBLEState(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Always forward the latest session status, even if there was no
|
|
||||||
// stale permission prompt to cancel. This keeps the BLE device's
|
|
||||||
// display synchronized with OpenCode.
|
|
||||||
await notifyBLE("session.status", buildSessionStatusPayload(properties.sessionID, properties.status))
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await appLogBestEffort(openCodeClient, "warn", "Failed to forward session status to BLE device", {
|
await appLogBestEffort(openCodeClient, "warn", "session.status handler failed", {
|
||||||
error: String(error),
|
error: String(error),
|
||||||
})
|
})
|
||||||
await refreshBLEState(false)
|
debugLog("session.status handler failed", { error: String(error) })
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user