mcpField
D
device-features

device-features

github.com/even-realities开发79138 次安装1 周前更新
代码仓库

关于 device-features

用于实现 Even Hub G2 硬件与应用集成的参考技能,介绍音频、IMU、位置、照片、设备和用户信息及持久化存储,面向 Even Hub 应用开发者。

主要功能

  1. 01采集眼镜或手机麦克风音频
  2. 02控制 IMU 报告并读取运动数据
  3. 03读取一次性或连续手机位置
  4. 04选择相册图片或用手机相机拍摄
  5. 05获取设备与用户信息
  6. 06通过 SDK 存储应用状态

适用场景

  1. 01为 Even Hub 应用添加麦克风输入
  2. 02响应 G2 的 IMU 数据
  3. 03显示位置更新或导入手机照片
  4. 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 — AppLocationAccuracy
  • timeoutMs — 仅用于单次读取;等待定位的最长时间
  • 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/png
  • size — 数字,字节数
  • 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–100
  • isCharging? — 布尔值
  • 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 — 字符串,用户头像图片的 URL
  • country — 字符串,用户的国家代码

本地存储

将数据持久化到 Even Realities App(应用重启后仍会保留):

  • await bridge.setLocalStorage(key, value) — 存储字符串值;返回一个表示是否成功的 boolean
  • await 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

评论

支持 Markdown