@evenrealities/even_hub_sdk
v0.0.15
Published
TypeScript SDK for Even Hub developers to communicate with Even App
Readme
@evenrealities/even_hub_sdk
Even App WebView 页面使用的 TypeScript SDK。
English | 中文
⚡️ 快速开始
npm install @evenrealities/even_hub_sdkimport { waitForEvenAppBridge } from '@evenrealities/even_hub_sdk';
const bridge = await waitForEvenAppBridge();
const user = await bridge.getUserInfo();
const device = await bridge.getDeviceInfo();
await bridge.setLocalStorage('theme', 'dark');
const theme = await bridge.getLocalStorage('theme');
console.log(user.name, device?.model, theme);当 Web 页面运行在 Even App WebView 内,并需要这些能力时使用:
- App 桥接:用户、设备、本地存储。
- App 能力:定位、相册、相机。
- 眼镜 UI:启动页容器、重建、上下文菜单、文本/图片更新。
- 传感器与事件:MIC、IMU、启动来源、设备状态。
📦 安装
npm install @evenrealities/even_hub_sdk
# 或
pnpm add @evenrealities/even_hub_sdk
# 或
yarn add @evenrealities/even_hub_sdk要求:
- SDK 版本:
0.0.15 - Even App:
2.2.10或更高版本 - Node.js:
^20.0.0 || >=22.0.0 - 运行环境:提供
window.flutter_inappwebview.callHandler的 Even App WebView
🔧 常用能力
启动来源
宿主在 WebView loading 完成后推送一次启动来源。
const bridge = await waitForEvenAppBridge();
const unsubscribe = bridge.onLaunchSource((source) => {
if (source === 'glassesMenu') {
console.log('从眼镜菜单打开');
}
});
// unsubscribe();取值:
appMenuglassesMenu
设备状态
import {
DeviceConnectType,
waitForEvenAppBridge,
} from '@evenrealities/even_hub_sdk';
const bridge = await waitForEvenAppBridge();
const unsubscribe = bridge.onDeviceStatusChanged((status) => {
if (status.connectType === DeviceConnectType.Connected) {
console.log('电量:', status.batteryLevel);
}
});
// unsubscribe();App 定位
import {
AppLocationAccuracy,
waitForEvenAppBridge,
} from '@evenrealities/even_hub_sdk';
const bridge = await waitForEvenAppBridge();
const location = await bridge.getAppLocation({
accuracy: AppLocationAccuracy.High,
timeoutMs: 5000,
});
if (location) {
console.log(location.latitude, location.longitude);
}连续定位:
await bridge.startAppLocationUpdates({
accuracy: AppLocationAccuracy.Medium,
intervalMs: 1000,
distanceFilter: 5,
});
const unsubscribeLocation = bridge.onAppLocationChanged((location) => {
console.log('位置:', location.latitude, location.longitude);
});
// await bridge.stopAppLocationUpdates();
// unsubscribeLocation();相册与相机
相册只支持单选。
const albumImage = await bridge.pickImageFromAlbum();
if (albumImage) {
console.log(albumImage.name, albumImage.mimeType, albumImage.size);
// Web 侧使用 albumImage.base64
}
const cameraImage = await bridge.captureImageFromCamera();
if (cameraImage) {
console.log('拍摄成功:', cameraImage.name);
}AppImageAsset:
type AppImageAsset = {
path: string;
name: string;
mimeType: string;
size: number;
base64: string;
};MIC 来源
可选择眼镜 MIC 或手机 MIC。
import {
AudioInputSource,
AudioSpeakerRole,
waitForEvenAppBridge,
} from '@evenrealities/even_hub_sdk';
const bridge = await waitForEvenAppBridge();
await bridge.audioControl(true, AudioInputSource.Glasses);
// await bridge.audioControl(true, AudioInputSource.Phone);
const unsubscribeAudio = bridge.onEvenHubEvent((event) => {
const audio = event.audioEvent;
if (!audio) return;
console.log(audio.source); // AudioInputSource.Glasses | AudioInputSource.Phone
console.log(audio.audioPcm.length); // Uint8Array
console.log(audio.direction); // int16 有符号整数或 null
console.log(audio.speakerRole); // AudioSpeakerRole.Self | Other | Unknown
if (audio.speakerRole === AudioSpeakerRole.Self) {
console.log('App 算法将这一帧判定为本人说话');
}
});
// await bridge.audioControl(false);
// unsubscribeAudio();说明:
- 默认来源:
AudioInputSource.Glasses。 - 音频数据通过
onEvenHubEvent下发。 - 使用眼镜 MIC 前,先创建启动页。
- 眼镜
direction是与处理后 PCM 同帧的原始 int16 有符号方向标签;SDK 不转换单位或区间。 speakerRole是 App 音频算法结果,不是固件直接给出的身份结论。- 手机 MIC 和旧 Host 数据使用
direction: null与AudioSpeakerRole.Unknown。
IMU
import {
ImuReportPace,
OsEventTypeList,
waitForEvenAppBridge,
} from '@evenrealities/even_hub_sdk';
const bridge = await waitForEvenAppBridge();
await bridge.imuControl(true, ImuReportPace.P500);
const unsubscribeImu = bridge.onEvenHubEvent((event) => {
const sys = event.sysEvent;
if (sys?.eventType !== OsEventTypeList.IMU_DATA_REPORT) return;
if (!sys.imuData) return;
console.log(sys.imuData.x, sys.imuData.y, sys.imuData.z);
});
// await bridge.imuControl(false);
// unsubscribeImu();ImuReportPace 是协议档位:P100 到 P1000。
🕶️ 眼镜 UI
先调用 createStartUpPageContainer,再做其他眼镜 UI 操作。
import {
ImageContainerProperty,
ImageRawDataUpdateResult,
ListContainerProperty,
MenuContainerProperty,
StartUpPageCreateResult,
TextContainerProperty,
waitForEvenAppBridge,
} from '@evenrealities/even_hub_sdk';
const bridge = await waitForEvenAppBridge();
const listObject: ListContainerProperty[] = [{
xPosition: 100,
yPosition: 50,
width: 200,
height: 150,
containerID: 1,
containerName: 'list-1',
zOrderIndex: 1,
itemContainer: {
itemCount: 3,
itemName: ['Item 1', 'Item 2', 'Item 3'],
},
isEventCapture: 1,
}];
const textObject: TextContainerProperty[] = [{
xPosition: 100,
yPosition: 220,
width: 200,
height: 50,
containerID: 2,
containerName: 'text-1',
zOrderIndex: 2,
content: 'Hello',
textColor: 4,
isEventCapture: 0,
}];
const imageObject: ImageContainerProperty[] = [{
xPosition: 320,
yPosition: 50,
width: 100,
height: 80,
containerID: 3,
containerName: 'image-1',
zOrderIndex: 3,
}];
const menuObject: MenuContainerProperty = {
menuItems: [{
itemName: 'Pause',
itemID: 1,
}, {
itemName: 'Resume',
itemID: 2,
}],
};
const result = await bridge.createStartUpPageContainer({
containerTotalNum: 3,
listObject,
textObject,
imageObject,
menuObject,
});
if (result === StartUpPageCreateResult.success) {
await bridge.textContainerUpgrade({
containerID: 2,
containerName: 'text-1',
content: 'Updated',
textColor: 2,
});
}textColor 可选,接受 0 到 4 的整数亮度值。创建/重建时省略则使用设备默认亮度 4;在 textContainerUpgrade 中省略则保持当前亮度。SDK 会在调用宿主前拒绝超出 0..4 的值。
更新图片内容:
图片原始数据由 SDK 内部使用 LZ4 压缩,以减少传输体积,同时保持设备端快速解码。
const imageResult = await bridge.updateImageRawData({
containerID: 3,
containerName: 'image-1',
imageData: [/* 灰度图片字节 */],
});
if (imageResult !== ImageRawDataUpdateResult.success) {
console.warn('图片更新失败:', imageResult);
}重建页面:
await bridge.rebuildPageContainer({
containerTotalNum: 1,
textObject: [{
xPosition: 100,
yPosition: 80,
width: 240,
height: 60,
containerID: 4,
containerName: 'status-text',
content: 'Ready',
isEventCapture: 1,
}],
});重建时不传 menuObject 表示清除自定义菜单:
await bridge.rebuildPageContainer({
containerTotalNum: 1,
textObject: [/* ... */],
});处理上下文菜单事件:
const unsubscribeMenu = bridge.onEvenHubEvent((event) => {
if (event.menuItemClickEvent) {
console.log('动作项:', event.menuItemClickEvent.itemID);
}
});
// unsubscribeMenu();处理长按和长按释放:
const unsubscribePress = bridge.onEvenHubEvent((event) => {
const pressEvent = event.sysEvent;
if (!pressEvent) return;
if (pressEvent.eventType === OsEventTypeList.LONG_PRESS_EVENT) {
console.log('开始长按,来源:', pressEvent.eventSource);
} else if (pressEvent.eventType === OsEventTypeList.LONG_PRESS_RELEASE_EVENT) {
console.log('长按已释放,来源:', pressEvent.eventSource);
}
});
// unsubscribePress();LONG_PRESS_EVENT 的 PB 值为 9,LONG_PRESS_RELEASE_EVENT 的 PB 值为 10。当前带来源的推送通过 sysEvent 上报,其中 eventSource 保留 PB EventSourceType(右镜腿、戒指或左镜腿),不会再丢失输入来源。
监听列表、文本和系统事件:
const unsubscribeHub = bridge.onEvenHubEvent((event) => {
if (event.listEvent) {
console.log('选中:', event.listEvent.currentSelectItemName);
}
if (event.textEvent) {
console.log('文本事件:', event.textEvent.containerName);
}
if (event.sysEvent) {
console.log('系统事件:', event.sysEvent.eventType);
}
});
// unsubscribeHub();关闭眼镜页面:
await bridge.shutDownPageContainer(0);
// await bridge.shutDownPageContainer(1); // 交给前台交互层决定是否退出规则:
- 坐标原点:左上角。
containerTotalNum:1到12。textObject:最多8个。- 只能有一个容器使用
isEventCapture: 1。 - 图片容器创建后,需要再调用
updateImageRawData。 zOrderIndex只有在同一个画面的所有容器都不填写时才可以省略,用于兼容旧版本 SDK 页面。- 同一个画面里,只要任意容器填写了
zOrderIndex,所有 list/text/image 容器都必须填写。 - 同一个画面内
zOrderIndex数值必须唯一;数值越大,画面层级越靠前。 - 违反上述
zOrderIndex规则时,SDK 会在调用原生前输出EvenHubPageContainerValidationErrorCode错误日志。createStartUpPageContainer返回StartUpPageCreateResult.invalid,rebuildPageContainer返回false。 menuObject.menuItems:最多10个;每个顶层itemID必须非 0 且唯一。- 菜单
itemName:最多32个 UTF-8 字节。 - 菜单数量、ID 或名称非法时,SDK 会在调用原生前拒绝。rebuild 不传
menuObject会清除自定义菜单并恢复默认注册。 - 长按和长按释放仍通过普通 list/text/sys 事件上报,分别对应
OsEventTypeList.LONG_PRESS_EVENT与OsEventTypeList.LONG_PRESS_RELEASE_EVENT。
📚 API 速查
Bridge
| API | 返回 |
| --- | --- |
| waitForEvenAppBridge() | Promise<EvenAppBridge> |
| EvenAppBridge.getInstance() | EvenAppBridge |
| callEvenApp(method, params?) | Promise<any> |
App
| API | 返回 |
| --- | --- |
| getUserInfo() | Promise<UserInfo> |
| getDeviceInfo() | Promise<DeviceInfo \| null> |
| setLocalStorage(key, value) | Promise<boolean> |
| getLocalStorage(key) | Promise<string> |
| getAppLocation(options?) | Promise<AppLocation \| null> |
| startAppLocationUpdates(options?) | Promise<boolean> |
| stopAppLocationUpdates() | Promise<boolean> |
| pickImageFromAlbum() | Promise<AppImageAsset \| null> |
| captureImageFromCamera() | Promise<AppImageAsset \| null> |
事件
| API | 事件 |
| --- | --- |
| onLaunchSource(callback) | appMenu / glassesMenu |
| onDeviceStatusChanged(callback) | 设备状态 |
| onAppLocationChanged(callback) | App 位置 |
| onEvenHubEvent(callback) | list / text / sys / audio / 上下文菜单 |
AudioEvent 提供 audioPcm、source、direction 和 speakerRole。
EvenHub
| API | 返回 |
| --- | --- |
| createStartUpPageContainer(container) | Promise<StartUpPageCreateResult> |
| rebuildPageContainer(container) | Promise<boolean> |
| updateImageRawData(data) | Promise<ImageRawDataUpdateResult> |
| textContainerUpgrade(container) | Promise<boolean> |
| audioControl(isOpen, source?: AudioInputSource) | Promise<boolean> |
| imuControl(isOpen, reportFrq?) | Promise<boolean> |
| shutDownPageContainer(exitMode?) | Promise<boolean> |
🧯 排障
| 现象 | 处理 |
| --- | --- |
| Flutter handler not available | 需要运行在 Even App WebView 内,普通浏览器不能调用原生能力。 |
| 收不到启动来源 | 尽早注册 onLaunchSource,宿主只在加载完成后推送一次。 |
| 眼镜 MIC 返回 false | 先创建启动页,再调用 audioControl(true, AudioInputSource.Glasses)。 |
| 连续定位没有回调 | 先调用 startAppLocationUpdates,并保持 onAppLocationChanged 订阅。 |
| 眼镜图片不显示 | 先创建/重建图片容器,再调用 updateImageRawData。 |
📜 更新日志
0.0.15
- 最低支持的 Even App 版本提升至
2.2.10。 - 长按及释放的
sysEvent保留eventSource,调用方可区分右镜腿、戒指和左镜腿来源。
0.0.14
- 最低支持的 Even App 版本提升至
2.2.9。 - 新增启动页/重建页容器上的一级上下文菜单模型。
- 新增
onEvenHubEvent中的一级菜单menuItemClickEvent解析。 - 新增 SDK 侧一级菜单校验:数量、ID 唯一性和 UTF-8 名称长度。
- 新增列表、文本和系统事件中的
LONG_PRESS_EVENT(9)与LONG_PRESS_RELEASE_EVENT(10)解析。 - 新增文本容器及增量文本更新的可选
textColor字段,支持0到4档亮度。 - 新增处理后音频的
direction与AudioSpeakerRole;手机 MIC 和旧 Host 数据安全降级为null与Unknown。
0.0.13
- 补充最低支持的 Even App 版本元数据,最低支持版本为
2.2.6。
0.0.12
- 新增列表、文本、图片容器的
zOrderIndex支持,用于控制页面中多个容器的前后叠放顺序。 - 图片原始数据更新由 SDK 内部使用 LZ4 压缩,减少传输体积,同时保持快速编解码,降低图片更新延迟。
0.0.11
- 新增 App 定位 API:单次定位和连续定位。
- 新增 App 相册图片选择,只支持单选。
- 新增 App 相机拍摄 API。
- 新增 MIC 来源选择:
glasses或phone。
0.0.10
- 增强 WebView 后台保活能力。
0.0.9
- 优化
EventSourceType兼容性。 - 增加默认来源枚举兜底。
- 提升事件来源解析一致性。
0.0.8
- 新增启动来源事件:
appMenu/glassesMenu。 - 启动页容器数量从
4扩展到12。 - 新增 IMU 控制和 IMU 数据事件。
0.0.1
- 初始桥接、本地存储、设备信息、EvenHub 协议和事件 API。
