426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
ci / test (push) Waiting to run
ci / lint-and-format (push) Waiting to run
ci / build (push) Waiting to run
ci / dev-startup (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
gitleaks / gitleaks (push) Waiting to run
Markdown Links / Relative Markdown Links (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Quality (Extended) / Homepage Build (PR smoke) (push) Waiting to run
Quality (Extended) / Comment-only diff guard (push) Waiting to run
Quality (Extended) / Format + Type Safety Ratchet (push) Waiting to run
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Waiting to run
Quality (Extended) / Develop Gate (lint) (push) Waiting to run
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
629 lines
19 KiB
TypeScript
629 lines
19 KiB
TypeScript
// Supports the Smartglasses example described in this package README.
|
|
import type {
|
|
G1ConnectionReadyMode,
|
|
G1Event,
|
|
GlassSide,
|
|
SmartglassesAudioEncoding,
|
|
SmartglassesStatus,
|
|
} from "@elizaos/plugin-facewear";
|
|
|
|
export type HardwareWriteSide = GlassSide | "both";
|
|
|
|
export type HardwarePhysicalBlocker =
|
|
| "transport_unavailable"
|
|
| "disconnected"
|
|
| "headset_not_found"
|
|
| "partial_headset"
|
|
| "in_charging_base"
|
|
| "wearing_state_missing"
|
|
| null;
|
|
|
|
export type HardwareScanDiagnosis =
|
|
| "not_scanned"
|
|
| "no_ble_devices"
|
|
| "ble_seen_no_g1_candidates"
|
|
| "g1_candidates_seen"
|
|
| "left_lens_missing"
|
|
| "right_lens_missing"
|
|
| "whole_headset_seen";
|
|
|
|
export type HardwareEvidenceReport = {
|
|
ok: boolean;
|
|
startedAt: string;
|
|
finishedAt?: string;
|
|
scanTimeoutMs?: number;
|
|
holdMs?: number;
|
|
initMode: G1ConnectionReadyMode;
|
|
checks: Record<string, boolean>;
|
|
discoveredDevices?: Array<{
|
|
name: string | null;
|
|
address?: string;
|
|
rssi?: number | null;
|
|
serviceUuids?: string[];
|
|
manufacturerIds?: number[];
|
|
matchesG1: boolean;
|
|
matchReason?: "name" | "uart_service" | null;
|
|
}>;
|
|
pairedG1Devices?: Array<{
|
|
name: string;
|
|
side: GlassSide;
|
|
connected: boolean;
|
|
section?: string | null;
|
|
}>;
|
|
bluetoothAdapter?: {
|
|
available?: boolean;
|
|
state?: string | null;
|
|
discoverable?: string | null;
|
|
chipset?: string | null;
|
|
address?: string | null;
|
|
} | null;
|
|
scanDiagnosis?: HardwareScanDiagnosis;
|
|
evidenceOrder: number;
|
|
writes: Array<{
|
|
at: string;
|
|
order?: number;
|
|
side: HardwareWriteSide;
|
|
command: string;
|
|
bytes: number;
|
|
hex: string;
|
|
}>;
|
|
events: Array<{
|
|
at: string;
|
|
order?: number;
|
|
side: string;
|
|
type: string;
|
|
label?: string;
|
|
stateCategory?: string;
|
|
stateName?: string;
|
|
sequence?: number;
|
|
serialNumber?: string;
|
|
}>;
|
|
lenses: Record<
|
|
GlassSide,
|
|
{
|
|
connected: boolean;
|
|
name?: string;
|
|
address?: string;
|
|
}
|
|
>;
|
|
audio: Array<{
|
|
at: string;
|
|
order?: number;
|
|
side: string;
|
|
sampleRate: number;
|
|
encoding: string | null;
|
|
sequence?: number;
|
|
bytes: number;
|
|
}>;
|
|
status?: SmartglassesStatus;
|
|
headsetState: {
|
|
physical: string | null;
|
|
battery: string | null;
|
|
device: string | null;
|
|
};
|
|
setupHint?: string;
|
|
error?: string;
|
|
};
|
|
|
|
export const REQUIRED_HARDWARE_EVIDENCE = [
|
|
"connected",
|
|
"connectionReadySent",
|
|
"displayPacketsSent",
|
|
"serialRequested",
|
|
"serialObserved",
|
|
"settingsSent",
|
|
"tapObserved",
|
|
"microphoneEnabledByTap",
|
|
"microphoneEnableWriteAfterTap",
|
|
"microphoneDisabledByTap",
|
|
"microphoneDisableWriteAfterTap",
|
|
"audioObserved",
|
|
] as const;
|
|
|
|
export function createHardwareEvidenceReport(options: {
|
|
initMode: G1ConnectionReadyMode;
|
|
scanTimeoutMs?: number;
|
|
holdMs?: number;
|
|
}): HardwareEvidenceReport {
|
|
return {
|
|
ok: false,
|
|
startedAt: new Date().toISOString(),
|
|
scanTimeoutMs: options.scanTimeoutMs,
|
|
holdMs: options.holdMs,
|
|
initMode: options.initMode,
|
|
checks: {
|
|
connected: false,
|
|
connectionReadySent: false,
|
|
displayPacketsSent: false,
|
|
serialRequested: false,
|
|
serialObserved: false,
|
|
settingsSent: false,
|
|
microphoneEnabled: false,
|
|
microphoneEnabledByTap: false,
|
|
microphoneEnableWriteAfterTap: false,
|
|
tapObserved: false,
|
|
microphoneDisabledByTap: false,
|
|
microphoneDisableWriteAfterTap: false,
|
|
microphoneDisabledByCommand: false,
|
|
audioObserved: false,
|
|
},
|
|
discoveredDevices: [],
|
|
pairedG1Devices: [],
|
|
bluetoothAdapter: null,
|
|
scanDiagnosis: "not_scanned",
|
|
evidenceOrder: 0,
|
|
writes: [],
|
|
events: [],
|
|
lenses: {
|
|
left: { connected: false },
|
|
right: { connected: false },
|
|
},
|
|
audio: [],
|
|
headsetState: {
|
|
physical: null,
|
|
battery: null,
|
|
device: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function recordHardwareLens(
|
|
report: HardwareEvidenceReport,
|
|
side: GlassSide,
|
|
lens: {
|
|
connected?: boolean;
|
|
name?: string;
|
|
address?: string;
|
|
},
|
|
): void {
|
|
report.lenses[side] = {
|
|
connected: lens.connected ?? true,
|
|
name: lens.name,
|
|
address: lens.address,
|
|
};
|
|
}
|
|
|
|
export function recordHardwareWrite(
|
|
report: HardwareEvidenceReport,
|
|
side: HardwareWriteSide,
|
|
data: Uint8Array,
|
|
): void {
|
|
const command = hardwareCommandName(data);
|
|
report.writes.push({
|
|
at: new Date().toISOString(),
|
|
order: nextEvidenceOrder(report),
|
|
side,
|
|
command,
|
|
bytes: data.length,
|
|
hex: bytesToHex(data.slice(0, 24)),
|
|
});
|
|
if (command === "init" || command === "right-init")
|
|
report.checks.connectionReadySent = true;
|
|
if (command === "display-result") report.checks.displayPacketsSent = true;
|
|
if (command === "get-serial") report.checks.serialRequested = true;
|
|
if (command === "open-mic" && side === "right") {
|
|
updateTapDrivenMicWriteChecks(report);
|
|
}
|
|
if (
|
|
command === "brightness" ||
|
|
command === "dashboard" ||
|
|
command === "head-up-angle" ||
|
|
command === "wear-detection"
|
|
) {
|
|
report.checks.settingsSent = true;
|
|
}
|
|
}
|
|
|
|
export function recordHardwareEvent(
|
|
report: HardwareEvidenceReport,
|
|
event: G1Event,
|
|
): void {
|
|
if (event.stateCategory === "physical") {
|
|
report.headsetState.physical = event.stateName ?? event.label ?? null;
|
|
} else if (event.stateCategory === "battery") {
|
|
report.headsetState.battery = event.stateName ?? event.label ?? null;
|
|
} else if (event.stateCategory === "device") {
|
|
report.headsetState.device = event.stateName ?? event.label ?? null;
|
|
}
|
|
report.events.push({
|
|
at: new Date().toISOString(),
|
|
order: nextEvidenceOrder(report),
|
|
side: event.side,
|
|
type: event.type,
|
|
label: event.label,
|
|
stateCategory: event.stateCategory,
|
|
stateName: event.stateName,
|
|
sequence: event.sequence,
|
|
serialNumber: event.serialNumber,
|
|
});
|
|
if (event.label?.includes("tap") || event.label === "long_press")
|
|
report.checks.tapObserved = true;
|
|
if (event.label === "single_tap" || event.label === "long_press") {
|
|
report.checks.microphoneEnabled = true;
|
|
report.checks.microphoneEnabledByTap = true;
|
|
}
|
|
if (event.label === "double_tap" || event.label === "stop_ai_recording")
|
|
report.checks.microphoneDisabledByTap = true;
|
|
updateTapDrivenMicWriteChecks(report);
|
|
if (event.type === "serial" && event.serialNumber)
|
|
report.checks.serialObserved = true;
|
|
}
|
|
|
|
export function recordHardwareAudio(
|
|
report: HardwareEvidenceReport,
|
|
audio: Uint8Array,
|
|
sampleRate: number,
|
|
side: GlassSide,
|
|
encoding: SmartglassesAudioEncoding | undefined,
|
|
sequence?: number,
|
|
): void {
|
|
report.checks.audioObserved = true;
|
|
report.audio.push({
|
|
at: new Date().toISOString(),
|
|
order: nextEvidenceOrder(report),
|
|
side,
|
|
sampleRate,
|
|
encoding: encoding ?? null,
|
|
sequence,
|
|
bytes: audio.length,
|
|
});
|
|
}
|
|
|
|
export function markHardwareMicrophoneCommand(
|
|
report: HardwareEvidenceReport,
|
|
enabled: boolean,
|
|
): void {
|
|
report.checks[enabled ? "microphoneEnabled" : "microphoneDisabledByCommand"] =
|
|
true;
|
|
}
|
|
|
|
export function missingHardwareEvidence(
|
|
report: HardwareEvidenceReport,
|
|
): string[] {
|
|
return REQUIRED_HARDWARE_EVIDENCE.filter((check) => !report.checks[check]);
|
|
}
|
|
|
|
export type CompleteHardwareEvidenceOptions = {
|
|
requireFinishedAt?: boolean;
|
|
};
|
|
|
|
export function missingCompleteHardwareEvidence(
|
|
report: HardwareEvidenceReport,
|
|
options: CompleteHardwareEvidenceOptions = {},
|
|
): string[] {
|
|
const failures = [...missingHardwareEvidence(report)];
|
|
if (options.requireFinishedAt && !report.finishedAt)
|
|
failures.push("missingFinishedAt");
|
|
if (!report.status?.connected) failures.push("statusNotConnected");
|
|
if (!report.lenses.left.connected) failures.push("missingLeftLensConnection");
|
|
if (!report.lenses.right.connected)
|
|
failures.push("missingRightLensConnection");
|
|
if (!report.status?.connectedLenses?.left?.connected)
|
|
failures.push("missingStatusLeftLensConnection");
|
|
if (!report.status?.connectedLenses?.right?.connected)
|
|
failures.push("missingStatusRightLensConnection");
|
|
if (!report.status?.lastSerialNumber) failures.push("missingSerialNumber");
|
|
if ((report.status?.audioChunksReceived ?? 0) < 1)
|
|
failures.push("missingStatusAudioChunks");
|
|
if (report.writes.length === 0) failures.push("missingWrites");
|
|
if (report.events.length === 0) failures.push("missingEvents");
|
|
if (report.audio.length === 0) failures.push("missingAudioChunks");
|
|
if (
|
|
!report.writes.some(
|
|
(write) => write.command === "init" || write.command === "right-init",
|
|
)
|
|
)
|
|
failures.push("missingInitWrite");
|
|
if (!report.writes.some((write) => write.command === "display-result"))
|
|
failures.push("missingDisplayWrite");
|
|
if (!report.writes.some((write) => write.command === "get-serial"))
|
|
failures.push("missingSerialRequestWrite");
|
|
if (
|
|
!report.writes.some((write) =>
|
|
["brightness", "dashboard", "head-up-angle", "wear-detection"].includes(
|
|
write.command,
|
|
),
|
|
)
|
|
)
|
|
failures.push("missingSettingsWrite");
|
|
if (!report.events.some((event) => event.type === "serial"))
|
|
failures.push("missingSerialEvent");
|
|
if (
|
|
!report.events.some(
|
|
(event) => event.label === "single_tap" || event.label === "long_press",
|
|
)
|
|
)
|
|
failures.push("missingMicEnableTapEvent");
|
|
if (
|
|
!report.events.some(
|
|
(event) =>
|
|
event.label === "double_tap" || event.label === "stop_ai_recording",
|
|
)
|
|
)
|
|
failures.push("missingMicDisableTapEvent");
|
|
if (!hasRightMicWrite(report, "enable"))
|
|
failures.push("missingMicEnableWrite");
|
|
if (!hasRightMicWrite(report, "disable"))
|
|
failures.push("missingMicDisableWrite");
|
|
if (!hasTapDrivenRightMicWrite(report, "enable"))
|
|
failures.push("missingMicEnableWriteAfterTap");
|
|
if (!hasTapDrivenRightMicWrite(report, "disable"))
|
|
failures.push("missingMicDisableWriteAfterTap");
|
|
if (!report.audio.some((chunk) => chunk.bytes > 0))
|
|
failures.push("missingNonEmptyAudioChunk");
|
|
if (!report.audio.some((chunk) => chunk.side === "right" && chunk.bytes > 0))
|
|
failures.push("missingRightLensAudioChunk");
|
|
if (
|
|
report.headsetState.physical !== "wearing" &&
|
|
isCradleOrChargingState(
|
|
report.headsetState.physical,
|
|
report.headsetState.battery,
|
|
)
|
|
) {
|
|
failures.push("headsetInCradle");
|
|
}
|
|
if (report.headsetState.physical !== "wearing")
|
|
failures.push("wearingStateNotObserved");
|
|
return [...new Set(failures)];
|
|
}
|
|
|
|
export function updateHardwareEvidenceStatus(
|
|
report: HardwareEvidenceReport,
|
|
status: SmartglassesStatus,
|
|
): void {
|
|
report.status = status;
|
|
if (status.physicalState !== null) {
|
|
report.headsetState.physical = status.physicalState;
|
|
}
|
|
if (status.batteryState !== null) {
|
|
report.headsetState.battery = status.batteryState;
|
|
}
|
|
if (status.deviceState !== null) {
|
|
report.headsetState.device = status.deviceState;
|
|
}
|
|
if (status.connectedLenses?.left) {
|
|
recordHardwareLens(report, "left", status.connectedLenses.left);
|
|
}
|
|
if (status.connectedLenses?.right) {
|
|
recordHardwareLens(report, "right", status.connectedLenses.right);
|
|
}
|
|
const derivedScanDiagnosis = hardwareScanDiagnosis(report);
|
|
if (
|
|
!report.scanDiagnosis ||
|
|
report.scanDiagnosis === "not_scanned" ||
|
|
derivedScanDiagnosis === "whole_headset_seen" ||
|
|
derivedScanDiagnosis === "left_lens_missing" ||
|
|
derivedScanDiagnosis === "right_lens_missing"
|
|
) {
|
|
report.scanDiagnosis = derivedScanDiagnosis;
|
|
}
|
|
report.setupHint = setupHintForHardwareBlocker(
|
|
hardwarePhysicalBlocker(report),
|
|
report,
|
|
);
|
|
updateTapDrivenMicWriteChecks(report);
|
|
report.ok = missingCompleteHardwareEvidence(report).length === 0;
|
|
}
|
|
|
|
export function hardwarePhysicalBlocker(
|
|
report: HardwareEvidenceReport,
|
|
): HardwarePhysicalBlocker {
|
|
const wholeHeadsetConnected = Boolean(
|
|
report.status?.connected &&
|
|
report.lenses.left.connected &&
|
|
report.lenses.right.connected &&
|
|
report.status.connectedLenses?.left?.connected &&
|
|
report.status.connectedLenses?.right?.connected,
|
|
);
|
|
const anyLensConnected = Boolean(
|
|
report.lenses.left.connected ||
|
|
report.lenses.right.connected ||
|
|
report.status?.connectedLenses?.left?.connected ||
|
|
report.status?.connectedLenses?.right?.connected,
|
|
);
|
|
if (report.status && !report.status.available) return "transport_unavailable";
|
|
if (report.status?.available && !anyLensConnected) return "headset_not_found";
|
|
if (!report.status?.connected) return "disconnected";
|
|
if (!wholeHeadsetConnected) return "partial_headset";
|
|
if (
|
|
isCradleOrChargingState(
|
|
report.headsetState.physical,
|
|
report.headsetState.battery,
|
|
)
|
|
) {
|
|
return "in_charging_base";
|
|
}
|
|
return report.headsetState.physical === "wearing"
|
|
? null
|
|
: "wearing_state_missing";
|
|
}
|
|
|
|
export function hardwareScanDiagnosis(
|
|
report: HardwareEvidenceReport,
|
|
): HardwareScanDiagnosis {
|
|
if (report.lenses.left.connected && report.lenses.right.connected) {
|
|
return "whole_headset_seen";
|
|
}
|
|
if (report.lenses.left.connected) return "right_lens_missing";
|
|
if (report.lenses.right.connected) return "left_lens_missing";
|
|
const discovered = report.discoveredDevices ?? [];
|
|
if (discovered.length === 0)
|
|
return report.finishedAt ? "no_ble_devices" : "not_scanned";
|
|
if (discovered.some((device) => device.matchesG1))
|
|
return "g1_candidates_seen";
|
|
return report.finishedAt ? "ble_seen_no_g1_candidates" : "not_scanned";
|
|
}
|
|
|
|
export function setupHintForHardwareBlocker(
|
|
physicalBlocker: HardwarePhysicalBlocker,
|
|
report: Pick<HardwareEvidenceReport, "headsetState">,
|
|
): string | undefined {
|
|
if (physicalBlocker === "transport_unavailable") {
|
|
return "The hardware transport is unavailable before headset discovery. Use the Bleak/CoreBluetooth smoke on macOS, or install/rebuild the Noble native BLE binding for this runtime.";
|
|
}
|
|
if (physicalBlocker === "disconnected") {
|
|
return "Connect both lenses as one headset before running hardware validation.";
|
|
}
|
|
if (physicalBlocker === "headset_not_found") {
|
|
return "No G1 lenses were found. Remove both lenses from the charging base, keep them near this device, and rerun hardware pairing.";
|
|
}
|
|
if (physicalBlocker === "partial_headset") {
|
|
return "Reconnect the whole headset so both left and right lenses are present.";
|
|
}
|
|
if (physicalBlocker === null) return undefined;
|
|
return headsetSetupHint(report);
|
|
}
|
|
|
|
export function nextActionForHardwareBlocker(
|
|
physicalBlocker: HardwarePhysicalBlocker,
|
|
): string | null {
|
|
if (physicalBlocker === "transport_unavailable") {
|
|
return "From the repo root, run `bun run --cwd packages/examples/smartglasses hardware:prove:bleak` for macOS CoreBluetooth proof, or rebuild @abandonware/noble before using `bun run --cwd packages/examples/smartglasses hardware:prove:noble`.";
|
|
}
|
|
if (physicalBlocker === "disconnected") {
|
|
return "Connect both lenses as one headset before running hardware validation.";
|
|
}
|
|
if (physicalBlocker === "headset_not_found") {
|
|
return "Remove both lenses from the charging base, keep them near this device, then run `bun run --cwd packages/examples/smartglasses hardware:prove:bleak:watch` from the repo root.";
|
|
}
|
|
if (physicalBlocker === "partial_headset") {
|
|
return "Reconnect the whole headset so both left and right lenses are present.";
|
|
}
|
|
if (physicalBlocker === "in_charging_base") {
|
|
return "Remove both lenses from the charging base, wear the glasses, single tap, speak, then double tap.";
|
|
}
|
|
if (physicalBlocker === "wearing_state_missing") {
|
|
return "Wear the glasses until they report wearing, or run `bun run --cwd packages/examples/smartglasses hardware:prove:bleak:watch` from the repo root for a longer latest-report proof window; then single tap, speak, and double tap.";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function headsetSetupHint(
|
|
report: Pick<HardwareEvidenceReport, "headsetState">,
|
|
): string | undefined {
|
|
const { physical, battery } = report.headsetState;
|
|
if (physical === "wearing") return undefined;
|
|
const stateText =
|
|
[physical, battery].filter(Boolean).join(" / ") ||
|
|
"no wearing state observed";
|
|
if (isCradleOrChargingState(physical, battery)) {
|
|
return `Glasses are reporting ${stateText}; remove them from the charging base and wear them before tap or microphone validation.`;
|
|
}
|
|
return `Tap and microphone validation requires the glasses to report wearing; current state is ${stateText}.`;
|
|
}
|
|
|
|
export function isCradleOrChargingState(
|
|
physical: string | null,
|
|
battery: string | null,
|
|
): boolean {
|
|
return (
|
|
physical === "cradle_open" ||
|
|
physical === "cradle_closed" ||
|
|
physical === "charged_in_cradle" ||
|
|
battery === "glasses_fully_charged" ||
|
|
battery === "cradle_charging_cable_changed" ||
|
|
battery === "cradle_fully_charged"
|
|
);
|
|
}
|
|
|
|
export function hardwareCommandName(data: Uint8Array): string {
|
|
const command = data[0];
|
|
switch (command) {
|
|
case 0x4d:
|
|
return "init";
|
|
case 0xf4:
|
|
return "right-init";
|
|
case 0x4e:
|
|
return "display-result";
|
|
case 0x0e:
|
|
return "open-mic";
|
|
case 0x34:
|
|
return "get-serial";
|
|
case 0x01:
|
|
return "brightness";
|
|
case 0x22:
|
|
return "dashboard";
|
|
case 0x0b:
|
|
return "head-up-angle";
|
|
case 0x27:
|
|
return "wear-detection";
|
|
default:
|
|
return `0x${command.toString(16).padStart(2, "0")}`;
|
|
}
|
|
}
|
|
|
|
function bytesToHex(data: Uint8Array): string {
|
|
return [...data].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
function hasRightMicWrite(
|
|
report: HardwareEvidenceReport,
|
|
mode: "enable" | "disable",
|
|
): boolean {
|
|
const suffix = mode === "enable" ? "01" : "00";
|
|
return report.writes.some(
|
|
(write) =>
|
|
write.side === "right" &&
|
|
write.command === "open-mic" &&
|
|
write.hex.startsWith(`0e${suffix}`),
|
|
);
|
|
}
|
|
|
|
function hasTapDrivenRightMicWrite(
|
|
report: HardwareEvidenceReport,
|
|
mode: "enable" | "disable",
|
|
): boolean {
|
|
const labels =
|
|
mode === "enable"
|
|
? ["single_tap", "long_press"]
|
|
: ["double_tap", "stop_ai_recording"];
|
|
const micValue = mode === "enable" ? "01" : "00";
|
|
const tapEvents = report.events.filter((event) =>
|
|
labels.includes(event.label ?? ""),
|
|
);
|
|
const micWrites = report.writes.filter(
|
|
(write) =>
|
|
write.side === "right" &&
|
|
write.command === "open-mic" &&
|
|
write.hex.startsWith(`0e${micValue}`),
|
|
);
|
|
return tapEvents.some((event) =>
|
|
micWrites.some((write) => evidenceHappenedAfter(write, event)),
|
|
);
|
|
}
|
|
|
|
function updateTapDrivenMicWriteChecks(report: HardwareEvidenceReport): void {
|
|
report.checks.microphoneEnableWriteAfterTap = hasTapDrivenRightMicWrite(
|
|
report,
|
|
"enable",
|
|
);
|
|
report.checks.microphoneDisableWriteAfterTap = hasTapDrivenRightMicWrite(
|
|
report,
|
|
"disable",
|
|
);
|
|
}
|
|
|
|
function evidenceHappenedAfter(
|
|
later: { at: string; order?: number },
|
|
earlier: { at: string; order?: number },
|
|
): boolean {
|
|
if (
|
|
Number.isFinite(later.order) &&
|
|
Number.isFinite(earlier.order) &&
|
|
later.order !== undefined &&
|
|
earlier.order !== undefined
|
|
) {
|
|
return later.order > earlier.order;
|
|
}
|
|
const laterMs = Date.parse(later.at);
|
|
const earlierMs = Date.parse(earlier.at);
|
|
return Number.isFinite(laterMs) && Number.isFinite(earlierMs)
|
|
? laterMs >= earlierMs
|
|
: false;
|
|
}
|
|
|
|
function nextEvidenceOrder(report: HardwareEvidenceReport): number {
|
|
report.evidenceOrder = (report.evidenceOrder ?? 0) + 1;
|
|
return report.evidenceOrder;
|
|
}
|