关于 device-features
用于实现 Even Hub G2 硬件与应用集成的参考技能,介绍音频、IMU、位置、照片、设备和用户信息及持久化存储,面向 Even Hub 应用开发者。
主要功能
- 01采集眼镜或手机麦克风音频
- 02控制 IMU 报告并读取运动数据
- 03读取一次性或连续手机位置
- 04选择相册图片或用手机相机拍摄
- 05获取设备与用户信息
- 06通过 SDK 存储应用状态
适用场景
- 01为 Even Hub 应用添加麦克风输入
- 02响应 G2 的 IMU 数据
- 03显示位置更新或导入手机照片
- 04跨应用重启保存偏好和内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ even-realities/ everything-evenhub --skill device-features安装后在 Claude Code 中输入/device-features确认已加载。
详细介绍
你正在为 Even Hub 应用实现 G2 硬件功能集成。请根据以下参考,实现 $ARGUMENTS 所描述的内容。
前置条件
调用 imuControl 或 audioControl(true, AudioInputSource.Glasses) 之前,必须先成功调用 createStartUpPageContainer——这两个功能都依赖先建立眼镜端启动页面。手机麦克风音频、位置 API、pickImageFromAlbum 和 captureImageFromCamera 不需要启动页面——它们通过手机而非眼镜工作。
音频采集
使用 await bridge.audioControl(true, source) 启动麦克风,并使用 await bridge.audioControl(false) 停止。source 参数用于选择要采集声音的麦克风:
AudioInputSource.Glasses— G2 四麦克风阵列(默认;需要先调用createStartUpPageContainer)AudioInputSource.Phone— 手机麦克风(不需要启动页面)
省略第二个参数时,SDK 默认使用 AudioInputSource.Glasses(与 0.0.11 之前的代码保持向后兼容)。
音频数据通过 onEvenHubEvent 监听器传入。通过 event.audioEvent 访问,其中包含 source(缓冲区数据来源的麦克风)和 audioPcm(Uint8Array)。
两种来源的格式相同: PCM、16 kHz 采样率、有符号 16 位小端序、单声道。
import { AudioInputSource } from '@evenrealities/even_hub_sdk'
// Glasses MIC — requires createStartUpPageContainer to have succeeded
await bridge.audioControl(true, AudioInputSource.Glasses)
// Phone MIC — no startup-page requirement
// await bridge.audioControl(true, AudioInputSource.Phone)
const unsubscribe = bridge.onEvenHubEvent(event => {
const audio = event.audioEvent
if (!audio) return
// audio.source: AudioInputSource.Glasses | AudioInputSource.Phone
// audio.audioPcm: Uint8Array
// Process PCM (Web Audio API, speech recognition, etc.)
})
// Stop and clean up
await bridge.audioControl(false)
unsubscribe()
权限: 在 app.json 中,为 AudioInputSource.Glasses 声明 g2-microphone,为 AudioInputSource.Phone 声明 phone-microphone。只声明应用实际使用的来源对应的权限。
IMU 控制
使用 await bridge.imuControl(true, ImuReportPace.P500) 启动 IMU 报告,并使用 await bridge.imuControl(false) 停止。
ImuReportPace 枚举值: P100、P200、P300、P400、P500、P600、P700、P800、P900、P1000。这些是协议节奏代码,并非实际的 Hz 值。
IMU 数据通过 onEvenHubEvent 传入。通过 event.sysEvent.imuData 访问,数据形状为 { x: float, y: float, z: float }。通过检查 sys.eventType === OsEventTypeList.IMU_DATA_REPORT 来筛选事件。
import { ImuReportPace, OsEventTypeList } from '@evenrealities/even_hub_sdk'
await bridge.imuControl(true, ImuReportPace.P500)
const unsubscribe = bridge.onEvenHubEvent(event => {
const sys = event.sysEvent
if (!sys?.imuData) return
if (sys.eventType !== OsEventTypeList.IMU_DATA_REPORT) return
const { x, y, z } = sys.imuData
console.log('IMU:', x, y, z)
})
await bridge.imuControl(false)
unsubscribe()
位置
有两种模式——单次读取和连续更新。两种模式都读取手机的位置服务;调用前需在 app.json 权限中声明 location。无需调用 createStartUpPageContainer。
单次读取 — getAppLocation(options?)
import { AppLocationAccuracy } from '@evenrealities/even_hub_sdk'
const fix = await bridge.getAppLocation({
accuracy: AppLocationAccuracy.High,
timeoutMs: 5000,
})
if (fix) {
console.log(fix.latitude, fix.longitude)
} else {
// No fix in time, permission denied, or invalid coords. Handle gracefully.
}
返回 AppLocation | null。务必检查是否为 null——如果主机未能及时获取定位、用户拒绝了权限或坐标无效,就会返回 null。
连续更新 — startAppLocationUpdates(options?) + onAppLocationChanged(cb)
await bridge.startAppLocationUpdates({
accuracy: AppLocationAccuracy.Medium,
intervalMs: 1000,
distanceFilter: 5, // meters — host skips pushes smaller than this
})
const unsubscribeLocation = bridge.onAppLocationChanged(loc => {
console.log(loc.latitude, loc.longitude, loc.speed)
})
// Stop and clean up
await bridge.stopAppLocationUpdates()
unsubscribeLocation()
AppLocationAccuracy 的值
AppLocationAccuracy.Low— 城市级精度;成本最低、最省电AppLocationAccuracy.Medium— 街区级精度;平衡的默认值AppLocationAccuracy.High— 可用的最佳定位;耗电最多
AppLocation 数据形状
latitude— 数字,单位为度longitude— 数字,单位为度accuracy?— 数字,水平精度,单位为米altitude?— 数字,海拔高度,单位为米speed?— 数字,速度,单位为米每秒heading?— 数字,相对于真北的角度timestamp?— 数字,Unix 毫秒时间戳
AppLocationOptions 字段(均为可选)
accuracy—AppLocationAccuracytimeoutMs— 仅用于单次读取;等待定位的最长时间intervalMs— 仅用于连续更新;请求的推送间隔distanceFilter— 仅用于连续更新;距离,单位为米
后台行为
在 Android 上,WebView 挂起时会停止 startAppLocationUpdates(参见 background-state)。应用回到前台时需重新启用;等待期间可以使用 bridge.setLocalStorage 中缓存的定位结果进行渲染。
照片
有两种方式可以将图像从手机导入应用——从相册中选择,或使用手机摄像头拍摄。两种方式都只执行一次,返回一个 AppImageAsset,并使用手机硬件。G2 没有眼镜端摄像头。
const fromAlbum = await bridge.pickImageFromAlbum() // requires `album` permission
const fromCamera = await bridge.captureImageFromCamera() // requires `camera` permission
if (fromAlbum) {
// fromAlbum.base64 is ready to drop into an <img src="data:...">
}
用户取消选择器或相机操作,或者拒绝权限时,两种方式都会返回 null。相册选择器仅支持单选——不支持多张导入。
AppImageAsset 结构
path— 字符串,主机端路径(对 WebView 不透明)name— 字符串,原始文件名mimeType— 字符串,例如image/jpeg、image/pngsize— 数字,字节数base64— 字符串,可直接用于<img>显示或进一步处理的内嵌数据
将图像发送到眼镜
base64 可立即在 WebView 中显示,但即使 SDK 使用了内部 LZ4 压缩(0.0.12+),通过 updateImageRawData 将 1200 万像素的 JPEG 传入眼镜容器仍然开销很大——压缩能缩小传输数据,但不会调整像素大小。**将图像缩小到容器尺寸,并在发送像素到眼镜之前转换为 4 位灰度图像。**请参阅 glasses-ui 中的图像容器模式。
设备信息
await bridge.getDeviceInfo() 返回一个 DeviceInfo 对象或 null。
DeviceInfo 字段:
model—DeviceModel枚举(DeviceModel.G1 = "g1"、DeviceModel.G2 = "g2"、DeviceModel.Ring1 = "ring1")sn— 序列号字符串status—DeviceStatus对象
DeviceStatus 接口字段:
sn— 序列号connectType—DeviceConnectType枚举值isWearing?— 布尔值,表示用户是否佩戴设备batteryLevel?— 整数,范围为 0–100isCharging?— 布尔值isInCase?— 布尔值
DeviceConnectType 枚举值: None、Connecting、Connected、Disconnected、ConnectionFailed
DeviceStatus 辅助方法: isNone()、isConnected()、isConnecting()、isDisconnected()、isConnectionFailed()
如需实时状态更新,请通过 bridge.onDeviceStatusChanged 订阅:
const unsubscribe = bridge.onDeviceStatusChanged(status => {
console.log('Battery:', status.batteryLevel)
console.log('Connected:', status.isConnected())
})
调用 unsubscribe() 停止监听。
用户信息
await bridge.getUserInfo() 返回一个 UserInfo 对象。
UserInfo 字段:
uid— 数字,用户唯一标识符name— 字符串,显示名称avatar— 字符串,用户头像图片的 URLcountry— 字符串,用户的国家代码
本地存储
将数据持久化到 Even Realities App(应用重启后仍会保留):
await bridge.setLocalStorage(key, value)— 存储字符串值;返回一个表示是否成功的booleanawait bridge.getLocalStorage(key)— 获取已存储的字符串;如果键不存在,则返回空字符串
SDK localStorage 是唯一可靠的持久化方式
Even App WebView 是 Flutter WebView。在此环境中,浏览器 IndexedDB 和浏览器 localStorage 无法可靠地跨应用重启持久化——用户关闭并重新打开应用时,保存在其中的数据可能会丢失。
所有用户状态都应使用 bridge.setLocalStorage / bridge.getLocalStorage:设置、进度、书签、偏好设置、缓存内容。对于大型内容(例如电子书文本),请将内容分块存储到多个键中:
const CHUNK_SIZE = 50_000 // chars per key
const PREFIX = 'myapp.content_'
async function saveContent(bridge: EvenAppBridge, id: string, text: string) {
const chunks = Math.ceil(text.length / CHUNK_SIZE)
await bridge.setLocalStorage(`${PREFIX}${id}_n`, String(chunks))
for (let i = 0; i < chunks; i++) {
await bridge.setLocalStorage(
`${PREFIX}${id}_${i}`,
text.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE),
)
}
}
关于防抖和串行化桥接写入,请参阅 glasses-ui → 最佳实践。
退出时清理
应用退出或销毁时,务必停止硬件功能并取消订阅事件监听器。否则,眼镜硬件上的麦克风或 IMU 可能会继续运行。
// Store all unsubscribe functions and stop hardware on exit
window.addEventListener('beforeunload', () => {
bridge.audioControl(false)
bridge.imuControl(false)
bridge.stopAppLocationUpdates()
unsubscribe() // from onEvenHubEvent
unsubscribeLocation() // from onAppLocationChanged
})
需要显式停止/启动控制的任何硬件功能(audioControl、imuControl、startAppLocationUpdates)都应采用相同模式。
SDK 未提供的功能
以下功能无法通过 Even Hub SDK 使用:
- 无法直接访问 Bluetooth
- 无法任意绘制像素
- 无音频输出(设备没有扬声器)
- 无法控制文本对齐方式
- 无法控制字体
- 无法设置背景颜色
- 无法设置列表中各项的样式
- 无法以编程方式设置滚动位置
- 无动画
- 无眼镜侧摄像头(G2 没有摄像头——可通过手机摄像头
captureImageFromCamera进行访问,请参阅上文的“照片”) - 图像仅支持灰度
任务
$ARGUMENTS