Pure MoonBit HID descriptor generation and parsing, report encoding/decoding, and field access.
node scripts/test-compatibility.mjs| 能力 | 内容与入口 |
|---|---|
| 描述符生成 | 链式构建器 DescriptorBuilder;鼠标、键盘(含 LED)、手柄、媒体键、厂商数据五种模板;原始 item 序列化 serialize_items;CLI generate |
| 结构化声明(0.6.0 新增) | 独立 JSON 声明 v1,支持命名 flags、嵌套集合、显式 item 宽度和字节无损往返;descriptor_from_declaration / declaration_from_descriptor,CLI build / declare;见 格式说明 |
| 描述符解析 | 十六进制输入、短项/长项分词、布局编译 compile_descriptor、Collection 树、Physical/Unit 元数据、版本化 JSON v2;CLI inspect |
| 报告编码 | encode_report:Input / Output / Feature、多 Report ID、整数与超宽原始值、数量/范围/Null 校验、自动写 ID 与零填充;CLI encode |
| 报告解码 | decode_report:Variable / Array、Null State、有符号与无符号、跨字节与超宽字段;CLI decode |
| 批量重放(0.6.0 新增) | CLI replay 按行解码、保留错误行号;--expect 比较 Usage 数值、Raw、物理量和活跃集合;42 帧与固件示例 |
| 帧转换(0.6.0 新增) | transport 纯 MoonBit 包在核心 wire、HIDAPI 缓冲区和 WebHID (reportId, payload) 之间转换并验证 ID/长度;可行性与范围 |
| 字段读写 | find_fields 按 Usage 定位字段,read_field / write_field 读写单个值并保留相邻位 |
| Usage 级访问 | 参照 Windows hidpi.h:new_report 初始化报告,set_usages / clear_usages 按下、释放按键与按钮,get/set_usage_value、get/set_physical_value 读写数值与物理量;CLI encode 接受按 Usage 描述的值文件 |
| 描述符检查 | lint_descriptor 十项规则,CLI lint,网页中文提示 |
| 能力 | Windows HID 解析库(C,hidpi.h) | hid-tools(Python) | TinyUSB(C) | usbd-hid(Rust) | HIDAPI / node-hid | MoonHID(MoonBit) |
|---|---|---|---|---|---|---|
| 主要用途 | Windows 驱动与应用解析报告 | Linux HID 分析、录制与重放 | 嵌入式 USB 设备栈 | 嵌入式 USB HID 设备 | 跨平台设备通信 | 通用报告模型、编解码与生成 |
| 描述符解析、位布局 | 预解析数据、按钮/数值能力表、集合节点 | 支持 | — | — | 可读取描述符,不解析 | compile_descriptor,Collection 树与 Unit/Physical 元数据 |
| 描述符生成 | — | 人类可读声明转字节 | TUD_HID_REPORT_DESC_* 宏与 10 种模板 | gen_hid_descriptor 宏与预定义报告 | — | 构建器、五种模板(含媒体键、厂商数据)、原始 item 序列化 |
| 报告初始化 | HidP_InitializeReportForID:Null 值或零 | — | 固定报告结构体 | 固定报告结构体 | — | new_report:Null 值、Array 空值或零 |
| 按 Usage 置位按钮与按键数组 | HidP_SetUsages / UnsetUsages / GetUsagesEx | 按 Usage 名称填入数组 | 调用方填写键码,如 tud_hid_keyboard_report | 调用方填写 KeyboardReport 等字段 | — | set_usages / clear_usages / active_usages |
| 按 Usage 读写数值 | HidP_GetUsageValue / SetUsageValue | 按字段 get_values / fill_values | 结构体字段 | 结构体字段 | — | get_usage_value / set_usage_value;按字段 read_field / write_field |
| 物理量换算 | HidP_GetScaledUsageValue / SetScaledUsageValue | — | — | — | — | physical_value / logical_from_physical 等,含 Unit Exponent |
| 报告级编解码 | HidP_GetData / SetData | create_report、字段提取 | 发送固定结构体 | AsInputReport 序列化 | 原始字节 I/O | encode_report / decode_report,三方向、多 ID、超宽原始值、Buffered Bytes |
| 实际设备传输 | 配合 HidD_* 与文件 I/O | Linux hidraw / UHID | 设备端 USB 栈 | 配合 usb-device | 枚举、Input / Output / Feature | 可选 WebHID Input;核心离线处理 |
| 运行形态 | Windows 系统库 | Python / Linux 工具 | MCU 固件 | 嵌入式 Rust | 原生库 / Node.js 绑定 | wasm / wasm-gc / JS / native |
moon add We1chan/moonhid@0.6.1import {
"We1chan/moonhid" @moonhid,
}///|
test "README: generate encode and edit keyboard LEDs" {
let descriptor = match
@moonhid.DescriptorBuilder::new()
.usage_page(1)
.usage(6)
.collection(1)
.usage_page(8)
.usage_range(1, 5)
.logical_range(0L, 1L)
.report_size(1)
.report_count(5)
.output(2)
.report_size(3)
.report_count(1)
.output(1)
.end_collection()
.build() {
Ok(bytes) => bytes
Err(_) => fail("generated descriptor rejected")
}
let layout = match @moonhid.compile_descriptor(descriptor) {
Ok(layout) => layout
Err(_) => fail("layout rejected")
}
let wire = match
@moonhid.encode_report(
layout,
@moonhid.Output,
0,
[1L, 1L, 0L, 0L, 0L].map(value => @moonhid.Integer(value)),
) {
Ok(bytes) => bytes
Err(_) => fail("encoding rejected")
}
assert_eq(wire, b"\x03")
let caps_lock = match
@moonhid.find_fields(layout, @moonhid.Output, 0, { page: 8, id: 2, }) {
Ok([reference]) => reference
_ => fail("LED Usage not found")
}
assert_eq(
@moonhid.read_field(
layout,
@moonhid.Output,
wire,
caps_lock.field_index,
caps_lock.element_index,
),
Ok(@moonhid.Integer(1L)),
)
assert_eq(
@moonhid.write_field(
layout,
@moonhid.Output,
wire,
caps_lock.field_index,
caps_lock.element_index,
@moonhid.Integer(0L),
),
Ok(b"\x01"),
)
}///|
test "README: press keys and set a hat angle by Usage" {
let keyboard = match
@moonhid.DescriptorBuilder::new()
.usage_page(1)
.usage(6)
.collection(1)
.usage_page(7)
.usage_range(0xe0, 0xe7)
.logical_range(0L, 1L)
.report_size(1)
.report_count(8)
.input(2)
.usage_range(0, 0x65)
.logical_range(0L, 0x65L)
.report_size(8)
.report_count(6)
.input(0)
.end_collection()
.build() {
Ok(bytes) => @moonhid.compile_descriptor(bytes)
Err(error) => Err(error)
}
let keyboard = match keyboard {
Ok(layout) => layout
Err(_) => fail("keyboard rejected")
}
let idle = match @moonhid.new_report(keyboard, @moonhid.Input, 0) {
Ok(bytes) => bytes
Err(_) => fail("no Input report")
}
let shift_a = match
@moonhid.set_usages(keyboard, @moonhid.Input, idle, [
{ page: 7, id: 0xe1, },
{ page: 7, id: 4, },
]) {
Ok(bytes) => bytes
Err(_) => fail("keys rejected")
}
assert_eq(shift_a, b"\x02\x04\x00\x00\x00\x00\x00")
assert_eq(
@moonhid.active_usages(keyboard, @moonhid.Input, shift_a),
Ok([{ page: 7, id: 0xe1, }, { page: 7, id: 4, }]),
)
assert_eq(
@moonhid.clear_usages(keyboard, @moonhid.Input, shift_a, [
{ page: 7, id: 4, },
]),
Ok(b"\x02\x00\x00\x00\x00\x00\x00"),
)
// Hat switch: 0..7 maps to 0..315 degrees, with a Null State for "centred".
let hat = match
@moonhid.DescriptorBuilder::new()
.usage_page(1)
.usage(5)
.collection(1)
.usage(0x39)
.logical_range(0L, 7L)
.physical_range(0L, 315L)
.unit(0x14)
.report_size(4)
.report_count(1)
.input(0x42)
.input(1)
.end_collection()
.build() {
Ok(bytes) => @moonhid.compile_descriptor(bytes)
Err(error) => Err(error)
}
let hat = match hat {
Ok(layout) => layout
Err(_) => fail("hat rejected")
}
let centred = match @moonhid.new_report(hat, @moonhid.Input, 0) {
Ok(bytes) => bytes
Err(_) => fail("no Input report")
}
assert_eq(centred, b"\x0f")
let east = match
@moonhid.set_physical_value(
hat,
@moonhid.Input,
centred,
{ page: 1, id: 0x39, },
90.0,
) {
Ok(bytes) => bytes
Err(_) => fail("angle rejected")
}
assert_eq(east, b"\x02")
assert_eq(
@moonhid.get_physical_value(hat, @moonhid.Input, east, {
page: 1,
id: 0x39,
}),
Ok(90.0),
)
}///|
test "README: decode one relative axis" {
let descriptor = b"\x05\x01\x09\x30\x15\x81\x25\x7f\x75\x08\x95\x01\x81\x06"
let layout = match @moonhid.compile_descriptor(descriptor) {
Ok(layout) => layout
Err(_) => fail("descriptor rejected")
}
let report = match @moonhid.decode_report(layout, @moonhid.Input, b"\xff") {
Ok(report) => report
Err(_) => fail("report rejected")
}
assert_eq(report.values[0].value, -1L)
assert_eq(report.values[0].usage, Some({ page: 1, id: 0x30, }))
}///|
test "README: Buffered Bytes remain one application stream" {
let descriptor = match
@moonhid.DescriptorBuilder::new()
.report_id(5)
.report_size(8)
.report_count(4)
.input(0x102)
.build() {
Ok(bytes) => bytes
Err(_) => fail("descriptor rejected")
}
let layout = match @moonhid.compile_descriptor(descriptor) {
Ok(layout) => layout
Err(_) => fail("layout rejected")
}
assert_eq(
@moonhid.encode_report(layout, @moonhid.Input, 5, [@moonhid.Raw(b"MOON")]),
Ok(b"\x05MOON"),
)
let report = match
@moonhid.decode_report(layout, @moonhid.Input, b"\x05MOON") {
Ok(report) => report
Err(_) => fail("report rejected")
}
assert_eq(report.values, [])
assert_eq(report.buffered_values[0].bytes, b"MOON")
}| 类别 | 接口 | 用途 |
|---|---|---|
| 描述符生成 | DescriptorBuilder | 生成描述符;Usage、范围、集合、ID、三方向、Physical/Unit、Push/Pop;build() 校验 |
| serialize_items | 原始短项/长项序列化,保留宽度与数据,可与分词无损往返 | |
| 描述符解析 | parse_hex | 严格解析连续或 ASCII 空白分隔的十六进制字节对 |
| parse_items | 保留 item 类型、tag、原始数据和字节位置 | |
| unsigned_value / signed_value | 小端短项数值,完整保留 32 位范围 | |
| compile_descriptor | 计算 Main 字段、集合树、元数据、报告 ID 与位偏移 | |
| field_usage | 按序查询 Usage;Variable 越界重复最后一项,Array 越界返回 None | |
| 报告编码 | list_reports | 列出每个报告的方向、ID、字节长度与编码所需值数量 |
| new_report | 生成空闲报告:写入 ID,Null State 字段取 Null 值,Array 取空值,其余为零 | |
| encode_report | 根据布局编码 Integer / Raw 值,校验数量、类型、范围,自动写 ID 与零填充 | |
| 报告解码 | report_length | 查询指定方向和 ID 的完整报告字节长度 |
| decode_report | 校验长度并返回变量值、数组 Usage、Null State、超宽值与 Buffered Bytes | |
| 字段读写 | find_fields / array_selector | 查询 Variable Usage 的全部字段引用,或把 Array Usage 转为逻辑选择器 |
| read_field / write_field | 按字段和元素索引读写单个值,包含报告长度与归属校验 | |
| Usage 级访问 | set_usages / clear_usages / active_usages | 按 Usage 按下、释放、列出按钮与按键;自动处理按键数组的空位 |
| get_usage_value / set_usage_value | 按 Usage 读写单个数值控件,例如摇杆轴 | |
| get_physical_value / set_physical_value | 按 Usage 读写物理量,例如 Hat 角度 | |
| 物理量与单位 | physical_value / logical_from_physical | Logical 值与物理量互换,应用 Physical 范围和 Unit Exponent |
| decode_unit / effective_physical_range | 展开单位维度,并应用 HID 物理范围缺省规则 | |
| 描述符检查 | lint_descriptor | 检查可编译描述符的映射与结构风险,规则见 lint 说明 |
| JSON 导出 | descriptor_to_json / layout_to_json / report_to_json | 导出版本化描述符与布局、解码组件 |
| lint_to_json | 导出含级别、item 偏移与可选字段索引的 lint 组件 | |
| 位操作 | extract_bits | 提取跨字节、带符号或无符号的位字段 |
| extract_bytes | 按 LSB 位序提取原始字节,支持非字节对齐 | |
| insert_bits / insert_bytes | 在副本中写入整数或原始位字段,保留其他位 |
git clone https://github.com/We1chan/moonhid.git
cd moonhid
moon run src/cmd/main
moon run src/cmd/scenarios
moon run src/cmd/moonhid --target native -- generate keyboard
moon run src/cmd/moonhid --target js -- build examples/declarations/keyboard.json
moon run src/cmd/moonhid --target js -- declare examples/codec/keyboard.hex
moon run src/cmd/moonhid --target native -- encode --kind input --id 0 examples/codec/keyboard.hex examples/codec/keyboard-usages.jsonmoon run src/cmd/moonhid --target native -- generate keyboard
moon run src/cmd/moonhid --target native -- inspect descriptor.hex
moon run src/cmd/moonhid --target js -- inspect descriptor.hex
moon run src/cmd/moonhid --target native -- lint descriptor.hex
moon run src/cmd/moonhid --target native -- encode --kind output --id 0 examples/codec/keyboard.hex examples/codec/keyboard-led-values.json
moon run src/cmd/moonhid --target native -- encode --kind input --id 0 examples/codec/keyboard.hex examples/codec/keyboard-usages.json
moon run src/cmd/moonhid --target js -- encode --kind feature --id 7 examples/codec/vendor-feature.hex examples/codec/vendor-feature-values.json
moon run src/cmd/moonhid --target native -- decode --kind input descriptor.hex report.hex
moon run src/cmd/moonhid --target js -- replay --kind input examples/firmware-ci/descriptor.hex examples/firmware-ci/frames.txt --expect examples/firmware-ci/expected.jsonl| 子命令 | 作用与输出 |
|---|---|
| generate | 从 MoonBit 构建器生成 mouse / keyboard / gamepad / consumer / vendor 描述符,stdout 为十六进制 |
| build / declare(0.6.0 新增) | 声明 JSON v1 → 描述符十六进制,或从描述符导出带原始宽度的声明;不与展示 JSON v2 混用 |
| inspect | 输出完整描述符 JSON v2,其中 lints 数组列出检查结果 |
| lint | 每条检查结果输出一行,例如 warning usage_count @18: ...;没有提示时 stdout 为空,规则见 lint 说明 |
| encode | 按值文件编码 Input / Output / Feature 报告,stdout 为十六进制 |
| decode | 输出带 schema_version: 2 的 wire_hex / decoded 组件;报告文件应包含所需的 Report ID 前缀 |
| replay(0.6.0 新增) | 每行一帧 hex,允许空行与 # 注释;stdout 为带原文件行号的 JSONL,--expect 按 Usage 逐帧断言,见 重放说明 |
moon build src/cmd/moonhid --target native --release
./_build/native/release/build/cmd/moonhid/moonhid.exe lint descriptor.hexnode scripts/build-web.mjs
python3 scripts/serve-web.pymoonhid/
├── moon.mod # 模块配置,source = "src"
├── src/ # MoonBit 源码根目录
│ ├── *.mbt、*_test.mbt # 核心库及同包测试
│ ├── moon.pkg # 核心包依赖
│ ├── pkg.generated.mbti # 核心包公开接口
│ ├── README.mbt.md # 本文,API 示例参与自动测试
│ ├── browser/ # 浏览器 JS 桥接
│ ├── cmd/ # 文件 CLI 与演示入口
│ └── examples/ # MoonBit 合成设备样例包
├── examples/ # 设备回归数据与固件 CI 示例
├── web/ # 检查器页面
├── scripts/ # 构建、验收与打包脚本
├── docs/ # 协议说明与验收记录
└── README.md # 从可执行 README 生成,链接按仓库根目录改写moon check --target all --deny-warn
moon build
moon test
moon test --target native
moon test --target js # 需要 Node.js
node scripts/build-web.mjs
node scripts/test-web.mjs
node scripts/test-usability.mjs
node scripts/test-compatibility.mjs
node scripts/firmware-ci.mjs
moon info
moon fmt --check
node scripts/sync-readme.mjs --check| 标准 | 本项目的做法与证据 |
|---|---|
| MoonBit 为主 | 核心解析、编解码、lint、构建器、CLI、场景逻辑和浏览器桥接使用 MoonBit;JS 处理界面、设备适配和验收编排。设计说明、源码行数统计命令 |
| 仓库公开 | 公开保留开发与交付记录。提交历史、Issues 与里程碑、PR、Release |
| 能够运行 | 一键复现四后端、CLI、网页、离线包和场景演示;可选检查缺依赖明确跳过。验收说明、CI |
| 工作有效 | 仓库创建于 2026-10-01,已有 0.2.0 至 0.6.1 的版本记录,新增工作通过独立 Issue/PR 跟踪。版本记录、四场景示例 |
| 开源合规 | Apache-2.0;核心按公开协议独立实现,记录模板参考和真实描述符来源。LICENSE、真实设备来源 |
| AI 可解释 | 公开参赛者与 AI 工具的分工、质量控制和纠错记录,核心设计有独立说明。AI 协作说明、设计说明 |
node scripts/test-compatibility.mjs| 能力 | 内容与入口 |
|---|---|
| 描述符生成 | 链式构建器 DescriptorBuilder;鼠标、键盘(含 LED)、手柄、媒体键、厂商数据五种模板;原始 item 序列化 serialize_items;CLI generate |
| 结构化声明(0.6.0 新增) | 独立 JSON 声明 v1,支持命名 flags、嵌套集合、显式 item 宽度和字节无损往返;descriptor_from_declaration / declaration_from_descriptor,CLI build / declare;见 格式说明 |
| 描述符解析 | 十六进制输入、短项/长项分词、布局编译 compile_descriptor、Collection 树、Physical/Unit 元数据、版本化 JSON v2;CLI inspect |
| 报告编码 | encode_report:Input / Output / Feature、多 Report ID、整数与超宽原始值、数量/范围/Null 校验、自动写 ID 与零填充;CLI encode |
| 报告解码 | decode_report:Variable / Array、Null State、有符号与无符号、跨字节与超宽字段;CLI decode |
| 批量重放(0.6.0 新增) | CLI replay 按行解码、保留错误行号;--expect 比较 Usage 数值、Raw、物理量和活跃集合;42 帧与固件示例 |
| 帧转换(0.6.0 新增) | transport 纯 MoonBit 包在核心 wire、HIDAPI 缓冲区和 WebHID (reportId, payload) 之间转换并验证 ID/长度;可行性与范围 |
| 字段读写 | find_fields 按 Usage 定位字段,read_field / write_field 读写单个值并保留相邻位 |
| Usage 级访问 | 参照 Windows hidpi.h:new_report 初始化报告,set_usages / clear_usages 按下、释放按键与按钮,get/set_usage_value、get/set_physical_value 读写数值与物理量;CLI encode 接受按 Usage 描述的值文件 |
| 描述符检查 | lint_descriptor 十项规则,CLI lint,网页中文提示 |
| 能力 | Windows HID 解析库(C,hidpi.h) | hid-tools(Python) | TinyUSB(C) | usbd-hid(Rust) | HIDAPI / node-hid | MoonHID(MoonBit) |
|---|---|---|---|---|---|---|
| 主要用途 | Windows 驱动与应用解析报告 | Linux HID 分析、录制与重放 | 嵌入式 USB 设备栈 | 嵌入式 USB HID 设备 | 跨平台设备通信 | 通用报告模型、编解码与生成 |
| 描述符解析、位布局 | 预解析数据、按钮/数值能力表、集合节点 | 支持 | — | — | 可读取描述符,不解析 | compile_descriptor,Collection 树与 Unit/Physical 元数据 |
| 描述符生成 | — | 人类可读声明转字节 | TUD_HID_REPORT_DESC_* 宏与 10 种模板 | gen_hid_descriptor 宏与预定义报告 | — | 构建器、五种模板(含媒体键、厂商数据)、原始 item 序列化 |
| 报告初始化 | HidP_InitializeReportForID:Null 值或零 | — | 固定报告结构体 | 固定报告结构体 | — | new_report:Null 值、Array 空值或零 |
| 按 Usage 置位按钮与按键数组 | HidP_SetUsages / UnsetUsages / GetUsagesEx | 按 Usage 名称填入数组 | 调用方填写键码,如 tud_hid_keyboard_report | 调用方填写 KeyboardReport 等字段 | — | set_usages / clear_usages / active_usages |
| 按 Usage 读写数值 | HidP_GetUsageValue / SetUsageValue | 按字段 get_values / fill_values | 结构体字段 | 结构体字段 | — | get_usage_value / set_usage_value;按字段 read_field / write_field |
| 物理量换算 | HidP_GetScaledUsageValue / SetScaledUsageValue | — | — | — | — | physical_value / logical_from_physical 等,含 Unit Exponent |
| 报告级编解码 | HidP_GetData / SetData | create_report、字段提取 | 发送固定结构体 | AsInputReport 序列化 | 原始字节 I/O | encode_report / decode_report,三方向、多 ID、超宽原始值、Buffered Bytes |
| 实际设备传输 | 配合 HidD_* 与文件 I/O | Linux hidraw / UHID | 设备端 USB 栈 | 配合 usb-device | 枚举、Input / Output / Feature | 可选 WebHID Input;核心离线处理 |
| 运行形态 | Windows 系统库 | Python / Linux 工具 | MCU 固件 | 嵌入式 Rust | 原生库 / Node.js 绑定 | wasm / wasm-gc / JS / native |
moon add We1chan/moonhid@0.6.1import {
"We1chan/moonhid" @moonhid,
}///|
test "README: generate encode and edit keyboard LEDs" {
let descriptor = match
@moonhid.DescriptorBuilder::new()
.usage_page(1)
.usage(6)
.collection(1)
.usage_page(8)
.usage_range(1, 5)
.logical_range(0L, 1L)
.report_size(1)
.report_count(5)
.output(2)
.report_size(3)
.report_count(1)
.output(1)
.end_collection()
.build() {
Ok(bytes) => bytes
Err(_) => fail("generated descriptor rejected")
}
let layout = match @moonhid.compile_descriptor(descriptor) {
Ok(layout) => layout
Err(_) => fail("layout rejected")
}
let wire = match
@moonhid.encode_report(
layout,
@moonhid.Output,
0,
[1L, 1L, 0L, 0L, 0L].map(value => @moonhid.Integer(value)),
) {
Ok(bytes) => bytes
Err(_) => fail("encoding rejected")
}
assert_eq(wire, b"\x03")
let caps_lock = match
@moonhid.find_fields(layout, @moonhid.Output, 0, { page: 8, id: 2, }) {
Ok([reference]) => reference
_ => fail("LED Usage not found")
}
assert_eq(
@moonhid.read_field(
layout,
@moonhid.Output,
wire,
caps_lock.field_index,
caps_lock.element_index,
),
Ok(@moonhid.Integer(1L)),
)
assert_eq(
@moonhid.write_field(
layout,
@moonhid.Output,
wire,
caps_lock.field_index,
caps_lock.element_index,
@moonhid.Integer(0L),
),
Ok(b"\x01"),
)
}///|
test "README: press keys and set a hat angle by Usage" {
let keyboard = match
@moonhid.DescriptorBuilder::new()
.usage_page(1)
.usage(6)
.collection(1)
.usage_page(7)
.usage_range(0xe0, 0xe7)
.logical_range(0L, 1L)
.report_size(1)
.report_count(8)
.input(2)
.usage_range(0, 0x65)
.logical_range(0L, 0x65L)
.report_size(8)
.report_count(6)
.input(0)
.end_collection()
.build() {
Ok(bytes) => @moonhid.compile_descriptor(bytes)
Err(error) => Err(error)
}
let keyboard = match keyboard {
Ok(layout) => layout
Err(_) => fail("keyboard rejected")
}
let idle = match @moonhid.new_report(keyboard, @moonhid.Input, 0) {
Ok(bytes) => bytes
Err(_) => fail("no Input report")
}
let shift_a = match
@moonhid.set_usages(keyboard, @moonhid.Input, idle, [
{ page: 7, id: 0xe1, },
{ page: 7, id: 4, },
]) {
Ok(bytes) => bytes
Err(_) => fail("keys rejected")
}
assert_eq(shift_a, b"\x02\x04\x00\x00\x00\x00\x00")
assert_eq(
@moonhid.active_usages(keyboard, @moonhid.Input, shift_a),
Ok([{ page: 7, id: 0xe1, }, { page: 7, id: 4, }]),
)
assert_eq(
@moonhid.clear_usages(keyboard, @moonhid.Input, shift_a, [
{ page: 7, id: 4, },
]),
Ok(b"\x02\x00\x00\x00\x00\x00\x00"),
)
// Hat switch: 0..7 maps to 0..315 degrees, with a Null State for "centred".
let hat = match
@moonhid.DescriptorBuilder::new()
.usage_page(1)
.usage(5)
.collection(1)
.usage(0x39)
.logical_range(0L, 7L)
.physical_range(0L, 315L)
.unit(0x14)
.report_size(4)
.report_count(1)
.input(0x42)
.input(1)
.end_collection()
.build() {
Ok(bytes) => @moonhid.compile_descriptor(bytes)
Err(error) => Err(error)
}
let hat = match hat {
Ok(layout) => layout
Err(_) => fail("hat rejected")
}
let centred = match @moonhid.new_report(hat, @moonhid.Input, 0) {
Ok(bytes) => bytes
Err(_) => fail("no Input report")
}
assert_eq(centred, b"\x0f")
let east = match
@moonhid.set_physical_value(
hat,
@moonhid.Input,
centred,
{ page: 1, id: 0x39, },
90.0,
) {
Ok(bytes) => bytes
Err(_) => fail("angle rejected")
}
assert_eq(east, b"\x02")
assert_eq(
@moonhid.get_physical_value(hat, @moonhid.Input, east, {
page: 1,
id: 0x39,
}),
Ok(90.0),
)
}///|
test "README: decode one relative axis" {
let descriptor = b"\x05\x01\x09\x30\x15\x81\x25\x7f\x75\x08\x95\x01\x81\x06"
let layout = match @moonhid.compile_descriptor(descriptor) {
Ok(layout) => layout
Err(_) => fail("descriptor rejected")
}
let report = match @moonhid.decode_report(layout, @moonhid.Input, b"\xff") {
Ok(report) => report
Err(_) => fail("report rejected")
}
assert_eq(report.values[0].value, -1L)
assert_eq(report.values[0].usage, Some({ page: 1, id: 0x30, }))
}///|
test "README: Buffered Bytes remain one application stream" {
let descriptor = match
@moonhid.DescriptorBuilder::new()
.report_id(5)
.report_size(8)
.report_count(4)
.input(0x102)
.build() {
Ok(bytes) => bytes
Err(_) => fail("descriptor rejected")
}
let layout = match @moonhid.compile_descriptor(descriptor) {
Ok(layout) => layout
Err(_) => fail("layout rejected")
}
assert_eq(
@moonhid.encode_report(layout, @moonhid.Input, 5, [@moonhid.Raw(b"MOON")]),
Ok(b"\x05MOON"),
)
let report = match
@moonhid.decode_report(layout, @moonhid.Input, b"\x05MOON") {
Ok(report) => report
Err(_) => fail("report rejected")
}
assert_eq(report.values, [])
assert_eq(report.buffered_values[0].bytes, b"MOON")
}| 类别 | 接口 | 用途 |
|---|---|---|
| 描述符生成 | DescriptorBuilder | 生成描述符;Usage、范围、集合、ID、三方向、Physical/Unit、Push/Pop;build() 校验 |
| serialize_items | 原始短项/长项序列化,保留宽度与数据,可与分词无损往返 | |
| 描述符解析 | parse_hex | 严格解析连续或 ASCII 空白分隔的十六进制字节对 |
| parse_items | 保留 item 类型、tag、原始数据和字节位置 | |
| unsigned_value / signed_value | 小端短项数值,完整保留 32 位范围 | |
| compile_descriptor | 计算 Main 字段、集合树、元数据、报告 ID 与位偏移 | |
| field_usage | 按序查询 Usage;Variable 越界重复最后一项,Array 越界返回 None | |
| 报告编码 | list_reports | 列出每个报告的方向、ID、字节长度与编码所需值数量 |
| new_report | 生成空闲报告:写入 ID,Null State 字段取 Null 值,Array 取空值,其余为零 | |
| encode_report | 根据布局编码 Integer / Raw 值,校验数量、类型、范围,自动写 ID 与零填充 | |
| 报告解码 | report_length | 查询指定方向和 ID 的完整报告字节长度 |
| decode_report | 校验长度并返回变量值、数组 Usage、Null State、超宽值与 Buffered Bytes | |
| 字段读写 | find_fields / array_selector | 查询 Variable Usage 的全部字段引用,或把 Array Usage 转为逻辑选择器 |
| read_field / write_field | 按字段和元素索引读写单个值,包含报告长度与归属校验 | |
| Usage 级访问 | set_usages / clear_usages / active_usages | 按 Usage 按下、释放、列出按钮与按键;自动处理按键数组的空位 |
| get_usage_value / set_usage_value | 按 Usage 读写单个数值控件,例如摇杆轴 | |
| get_physical_value / set_physical_value | 按 Usage 读写物理量,例如 Hat 角度 | |
| 物理量与单位 | physical_value / logical_from_physical | Logical 值与物理量互换,应用 Physical 范围和 Unit Exponent |
| decode_unit / effective_physical_range | 展开单位维度,并应用 HID 物理范围缺省规则 | |
| 描述符检查 | lint_descriptor | 检查可编译描述符的映射与结构风险,规则见 lint 说明 |
| JSON 导出 | descriptor_to_json / layout_to_json / report_to_json | 导出版本化描述符与布局、解码组件 |
| lint_to_json | 导出含级别、item 偏移与可选字段索引的 lint 组件 | |
| 位操作 | extract_bits | 提取跨字节、带符号或无符号的位字段 |
| extract_bytes | 按 LSB 位序提取原始字节,支持非字节对齐 | |
| insert_bits / insert_bytes | 在副本中写入整数或原始位字段,保留其他位 |
git clone https://github.com/We1chan/moonhid.git
cd moonhid
moon run src/cmd/main
moon run src/cmd/scenarios
moon run src/cmd/moonhid --target native -- generate keyboard
moon run src/cmd/moonhid --target js -- build examples/declarations/keyboard.json
moon run src/cmd/moonhid --target js -- declare examples/codec/keyboard.hex
moon run src/cmd/moonhid --target native -- encode --kind input --id 0 examples/codec/keyboard.hex examples/codec/keyboard-usages.jsonmoon run src/cmd/moonhid --target native -- generate keyboard
moon run src/cmd/moonhid --target native -- inspect descriptor.hex
moon run src/cmd/moonhid --target js -- inspect descriptor.hex
moon run src/cmd/moonhid --target native -- lint descriptor.hex
moon run src/cmd/moonhid --target native -- encode --kind output --id 0 examples/codec/keyboard.hex examples/codec/keyboard-led-values.json
moon run src/cmd/moonhid --target native -- encode --kind input --id 0 examples/codec/keyboard.hex examples/codec/keyboard-usages.json
moon run src/cmd/moonhid --target js -- encode --kind feature --id 7 examples/codec/vendor-feature.hex examples/codec/vendor-feature-values.json
moon run src/cmd/moonhid --target native -- decode --kind input descriptor.hex report.hex
moon run src/cmd/moonhid --target js -- replay --kind input examples/firmware-ci/descriptor.hex examples/firmware-ci/frames.txt --expect examples/firmware-ci/expected.jsonl| 子命令 | 作用与输出 |
|---|---|
| generate | 从 MoonBit 构建器生成 mouse / keyboard / gamepad / consumer / vendor 描述符,stdout 为十六进制 |
| build / declare(0.6.0 新增) | 声明 JSON v1 → 描述符十六进制,或从描述符导出带原始宽度的声明;不与展示 JSON v2 混用 |
| inspect | 输出完整描述符 JSON v2,其中 lints 数组列出检查结果 |
| lint | 每条检查结果输出一行,例如 warning usage_count @18: ...;没有提示时 stdout 为空,规则见 lint 说明 |
| encode | 按值文件编码 Input / Output / Feature 报告,stdout 为十六进制 |
| decode | 输出带 schema_version: 2 的 wire_hex / decoded 组件;报告文件应包含所需的 Report ID 前缀 |
| replay(0.6.0 新增) | 每行一帧 hex,允许空行与 # 注释;stdout 为带原文件行号的 JSONL,--expect 按 Usage 逐帧断言,见 重放说明 |
moon build src/cmd/moonhid --target native --release
./_build/native/release/build/cmd/moonhid/moonhid.exe lint descriptor.hexnode scripts/build-web.mjs
python3 scripts/serve-web.pymoonhid/
├── moon.mod # 模块配置,source = "src"
├── src/ # MoonBit 源码根目录
│ ├── *.mbt、*_test.mbt # 核心库及同包测试
│ ├── moon.pkg # 核心包依赖
│ ├── pkg.generated.mbti # 核心包公开接口
│ ├── README.mbt.md # 本文,API 示例参与自动测试
│ ├── browser/ # 浏览器 JS 桥接
│ ├── cmd/ # 文件 CLI 与演示入口
│ └── examples/ # MoonBit 合成设备样例包
├── examples/ # 设备回归数据与固件 CI 示例
├── web/ # 检查器页面
├── scripts/ # 构建、验收与打包脚本
├── docs/ # 协议说明与验收记录
└── README.md # 从可执行 README 生成,链接按仓库根目录改写moon check --target all --deny-warn
moon build
moon test
moon test --target native
moon test --target js # 需要 Node.js
node scripts/build-web.mjs
node scripts/test-web.mjs
node scripts/test-usability.mjs
node scripts/test-compatibility.mjs
node scripts/firmware-ci.mjs
moon info
moon fmt --check
node scripts/sync-readme.mjs --check| 标准 | 本项目的做法与证据 |
|---|---|
| MoonBit 为主 | 核心解析、编解码、lint、构建器、CLI、场景逻辑和浏览器桥接使用 MoonBit;JS 处理界面、设备适配和验收编排。设计说明、源码行数统计命令 |
| 仓库公开 | 公开保留开发与交付记录。提交历史、Issues 与里程碑、PR、Release |
| 能够运行 | 一键复现四后端、CLI、网页、离线包和场景演示;可选检查缺依赖明确跳过。验收说明、CI |
| 工作有效 | 仓库创建于 2026-10-01,已有 0.2.0 至 0.6.1 的版本记录,新增工作通过独立 Issue/PR 跟踪。版本记录、四场景示例 |
| 开源合规 | Apache-2.0;核心按公开协议独立实现,记录模板参考和真实描述符来源。LICENSE、真实设备来源 |
| AI 可解释 | 公开参赛者与 AI 工具的分工、质量控制和纠错记录,核心设计有独立说明。AI 协作说明、设计说明 |
pub(all) struct DecodedReport {
kind : ReportKind
report_id : Int
values : Array[DecodedValue]
opaque_values : Array[OpaqueValue]
buffered_values : Array[BufferedValue]
} derive(Eq, Debug)pub struct DescriptorBuilder {
// private fields
}fn DescriptorBuilder::collection(self : DescriptorBuilder, collection_type : Int) -> DescriptorBuilderfn DescriptorBuilder::logical_range(self : DescriptorBuilder, minimum : Int64, maximum : Int64) -> DescriptorBuilderfn DescriptorBuilder::physical_range(self : DescriptorBuilder, minimum : Int64, maximum : Int64) -> DescriptorBuilderfn DescriptorBuilder::short_item(self : DescriptorBuilder, type_code : Int, tag : Int, value : Int64, signed? : Bool) -> DescriptorBuilderfn DescriptorBuilder::usage_range(self : DescriptorBuilder, minimum : Int, maximum : Int) -> DescriptorBuilderpub(all) struct Field {
descriptor_offset : Int
kind : ReportKind
report_id : Int
bit_offset : Int
bit_size : Int
count : Int
flags : Int
logical_min : Int64
logical_max : Int64
physical_min : Int64?
physical_max : Int64?
unit : Int64?
unit_exponent : Int?
collection_index : Int?
usage_spans : Array[UsageSpan]
string_spans : Array[IndexSpan]
designator_spans : Array[IndexSpan]
alternate_usages : Array[Array[UsageSpan]]
} derive(Eq, Debug)pub(all) struct ReportInfo {
kind : ReportKind
report_id : Int
payload_bits : Int
byte_length : Int
data_count : Int
} derive(Eq, Debug)fn active_usages(layout : Layout, kind : ReportKind, bytes : Bytes) -> Result[Array[Usage], Diagnostic]fn clear_usages(layout : Layout, kind : ReportKind, bytes : Bytes, usages : Array[Usage]) -> Result[Bytes, Diagnostic]fn decode_report(layout : Layout, kind : ReportKind, bytes : Bytes) -> Result[DecodedReport, Diagnostic]fn encode_report(layout : Layout, kind : ReportKind, report_id : Int, values : Array[ReportValue], allow_out_of_range? : Bool) -> Result[Bytes, Diagnostic]fn extract_bits(bytes : Bytes, bit_offset : Int, bit_size : Int, signed? : Bool) -> Result[Int64, Diagnostic]fn find_fields(layout : Layout, kind : ReportKind, report_id : Int, usage : Usage) -> Result[Array[FieldRef], Diagnostic]fn format_hex(bytes : Bytes) -> Stringfn get_physical_value(layout : Layout, kind : ReportKind, bytes : Bytes, usage : Usage) -> Result[Double, Diagnostic]fn get_usage_value(layout : Layout, kind : ReportKind, bytes : Bytes, usage : Usage) -> Result[ReportValue, Diagnostic]fn insert_bits(bytes : Bytes, bit_offset : Int, bit_size : Int, value : Int64, signed? : Bool) -> Result[Bytes, Diagnostic]fn insert_bytes(bytes : Bytes, bit_offset : Int, bit_size : Int, value : Bytes) -> Result[Bytes, Diagnostic]fn read_field(layout : Layout, kind : ReportKind, bytes : Bytes, field_index : Int, element_index : Int) -> Result[ReportValue, Diagnostic]fn set_physical_value(layout : Layout, kind : ReportKind, bytes : Bytes, usage : Usage, physical : Double) -> Result[Bytes, Diagnostic]fn set_usage_value(layout : Layout, kind : ReportKind, bytes : Bytes, usage : Usage, value : ReportValue, allow_out_of_range? : Bool) -> Result[Bytes, Diagnostic]fn set_usages(layout : Layout, kind : ReportKind, bytes : Bytes, usages : Array[Usage]) -> Result[Bytes, Diagnostic]fn write_field(layout : Layout, kind : ReportKind, bytes : Bytes, field_index : Int, element_index : Int, value : ReportValue, allow_out_of_range? : Bool) -> Result[Bytes, Diagnostic]Install
Download zipPure MoonBit HID descriptor generation and parsing, report encoding/decoding, and field access.