主题
Lua 脚本 API
📥 下载此API文档:lua-api.md
pico-hid-mapper 内置 Lua 5.4.6 解释器,让你用脚本自定义按键/鼠标行为。典型用途:压枪宏、连招宏、一键丢弃、自动化脚本等。
数据流
USB HID 设备 / 手柄 (Gamepad) / 触摸板 / WebSocket / WebHID (键盘/鼠标/自定义事件)
│
▼
映射引擎 (去抖 / 边沿检测)
│ 按键/按钮事件(仅"边沿":按下、松开)
│ 鼠标移动/滚轮事件
│ 触摸板瞬时快照(on_touchpad,变化时整帧上报)
│ 自定义字符串事件
▼
Lua 引擎 (bitmap 过滤: 只有你声明监听的键/事件才进入 Lua VM)
│
▼
你的 Lua 脚本(多模块项目,按优先级聚合)
on_key / on_mouse_btn / on_mouse_move / on_mouse_wheel
on_custom_event / on_boot_button / on_touchpad / tick
│ 返回 true = 拦截, false = 放行
▼
触摸事件队列 → USB HID 触屏 → 上位机 (手机/电脑)外部文本输入路径(均可触发 on_custom_event(str)):
Web 前端 / LuaEditorDialog → WebSocket CMD 0x01 → ws_ipc → lua_binder_on_custom_event
webctrl WebHID 工具 → HID OUT CMD 0xFA → pio_device → lua_binder_on_custom_event板载 BOOTSEL 按键路径(触发 on_boot_button(pressed)):
core0 BOOTSEL 消抖轮询 (10ms 采样, 连续 3 次一致) → 引擎 IPC → lua_binder_on_boot_button脚本主动输出(两条互不相干的通路,别混淆):
dkb_* / dmo_* → USB HID 键盘 / 鼠标 → 上位机 直通输出:不经过映射引擎,与「映射开关」无关
input_* → 回到上面的「映射引擎」入口 注入:当作真实输入重走一遍管线,会触屏、受开关影响关键:脚本运行在输入热路径上,返回
true会"吃掉"事件让 core 不再做默认处理,返回false则原样放行。 对on_mouse_move/on_mouse_wheel而言,"拦截" = 把位移量(dx/dy 或 wheel) 清零后再交给 core。
脚本模板
运行模式:多模块项目
设备上运行的是一个多模块项目:项目由若干 .lua 模块文件组成,在面板「Lua 工作台 → 模块」页管理(新建 / 重命名 / 复制 / 删除 / 启停 / 排序)。列表顺序 = 优先级 = 执行顺序(排在前面的先执行);停用的模块不装载。所有模块在同一个 Lua VM 里运行,装载规则:
- 每个模块在独立的
_ENV中执行:模块顶层的local、函数和全局变量写都留在模块自己内部;读会穿透到_G(内建 API 与其它模块显式共享的东西)。模块之间默认互相看不见——想让别的模块用到你的函数/数据,必须显式_G.xxx = ...。 - 入口函数按优先级聚合:每个模块按需定义入口函数(见「入口函数」),固件把同名入口按优先级串起来调用:
- 可拦截事件(
on_key/on_mouse_btn/on_mouse_move/on_mouse_wheel/on_gamepad_btn/on_custom_event):按优先级依次调用,首个返回true的模块拦截,排在后面的模块不再收到该事件; - 广播事件(
init/tick/on_touchpad/on_boot_button):每个定义了的模块都会被调用,返回值忽略。
- 可拦截事件(
- 单模块出错不影响别人:某个模块某次调用报错只记日志,其余模块照常运行;全部模块失败才回落默认模板。
- 没有任何模块定义某个入口 → 该入口不安装,对应事件零开销跳过(与单脚本时代"未定义回调"一致)。
单个模块长什么样
每个模块就是一段普通 Lua 源码。你按需定义入口函数(都是可选项),并在顶层用 enable_listen_* 声明你关心哪些事件(多模块下的监听语义见「声明 / 解除监听」):
lua
-- 顶层: 声明要监听的键/鼠标按钮/移动/滚轮 (不声明 → 对应回调永远不触发)
enable_listen_keys(0x13, 0x1E) -- 监听 KEY_P、KEY_1 (HID 键码)
enable_listen_mouse_btn(0x00, 0x01) -- 监听鼠标左键(0x00)、右键(0x01)
-- enable_listen_mouse_move() -- 监听鼠标相对移动
-- enable_listen_mouse_wheel() -- 监听鼠标滚轮
-- 脚本加载/重载时 & 配置/槽位变更时被调用: 在此缓存慢速值
function init()
scale_x, scale_y = get_scale()
view_speed_x, view_speed_y = get_view_speed()
end
-- 按键事件 (仅按下/松开边沿, 无重复)。return true = 拦截, false = 放行
function on_key(keycode, down)
if keycode == 0x13 and down then
local id = touch_down(0x3FFFFFFF, 0x3FFFFFFF) -- 点屏幕中心
touch_up(id)
end
return true
end
-- 鼠标按钮事件
function on_mouse_btn(button, down)
return false
end
-- 鼠标相对移动 (dx,dy 为本次位移像素)。return true = 拦截 (dx,dy 归零)
function on_mouse_move(dx, dy)
return false
end
-- 鼠标滚轮 (wheel 为本次滚动量)。return true = 拦截 (wheel 归零)
function on_mouse_wheel(wheel)
return false
end
-- 手柄按键事件 (仅按下/松开边沿)。return true = 拦截, false = 放行
function on_gamepad_btn(code, pressed)
return false
end
-- 周期回调 (主循环限频调用, 1000Hz)。dt_us = 距上次 tick 的微秒差
function tick(dt_us)
end
-- 自定义字符串事件 (来自 WebSocket / WebHID)
function on_custom_event(str)
return false
end
-- 板载 BOOTSEL 按键事件 (按下/松开边沿, 无需声明监听)
function on_boot_button(pressed)
end执行过程与生命周期
| 时刻 | 发生什么 |
|---|---|
| 项目装载 / 上传新项目 / 热重载 | VM 全新重建 → 按优先级逐个执行每个启用模块的顶层代码(各模块顶层 listen_* 在此生效)→ 安装聚合入口 → 自动调用一次各模块的 init() |
| 收到键盘/鼠标事件 (边沿) | core 去抖后,若该键在监听 bitmap 内 → 同步调用 on_key/on_mouse_btn(多模块按优先级,首个 true 拦截) |
| 收到鼠标移动/滚轮 | 若已(由任一模块)enable_listen_mouse_move/enable_listen_mouse_wheel → 每次移动/滚动同步调用 (非边沿, 连续触发) |
| 主循环 (限频 0.1ms, core1) | 调用 tick(dt_us) (约 10kHz,所有模块都调用) |
| 切换配置槽位 / 下发新配置 | 慢速值 (scale/view_speed) 重算 → 再次调用 init() (VM 不重建, 全局保留) |
| 写入存储 | 将 RAM 里的整份项目镜像持久化到 Flash。写完不重启(RAM 内已生效;写 Flash 期间 USB 总线会短暂中断 ~ms 级) |
要点:
init()是 "(重新)缓存慢速值" 的统一钩子。把get_scale/get_view_speed的结果存进全局变量,后续在on_key/tick里直接读全局 (最快)。- 热重载 (上传新项目) = VM 重建,所有全局/状态清空后按优先级重新执行各模块。
on_key/on_mouse_btn只在边沿触发 (按一次、松一次),不会按住反复触发。想 "按住期间持续动作",用tick+ 一个down标志。on_mouse_move/on_mouse_wheel不是边沿:只要有移动/滚动就会触发 (可能每毫秒多次)。别在里面做重活。- ⚠️ 调试完成后,一定记得点击 "写入存储" 按钮,否则项目会在重启后丢失。
按键体系(三套编号,必读)
固件里同时存在三套按键编号。混用是脚本出错最常见的原因——写任何涉及按键的脚本前,先确认自己在用哪一套。
本文档所有键值一律写作十六进制(
0x前缀),键盘、鼠标、手柄都一样。 只有 bit 位号(bit0)、槽位号、触点编号这类"位置/序号"用十进制。
| 编号 | 格式 | 谁用它 |
|---|---|---|
| 统一 16 位 CODE | 键盘 0x000–0x0FF、鼠标 0x100+按钮、手柄 0x200|按钮 | 配置与映射、动作状态:get_code_map、get_code_action_state、is_code_down,以及 get_key_map_by_index() 返回的 code |
| 各设备原始编号 | 键盘 0x00–0xFE、鼠标 0x00–0x07、手柄 0x01–0x1B | 实时输入:on_key / on_mouse_btn / on_gamepad_btn、enable_listen_*、is_key_down / is_mouse_btn_down / is_gamepad_btn_down、input_* |
| 宿主协议编号 | HID 键码 / 鼠标按钮位 | 打到手机/电脑:dkb_key、dmo_btn |
三种输入的 API 对照
| 能力 | 键盘 | 鼠标 | 手柄 |
|---|---|---|---|
| 声明监听 | enable_listen_keys | enable_listen_mouse_btn | enable_listen_gamepad_btn |
| 事件回调 | on_key(code, down) | on_mouse_btn(btn, down) | on_gamepad_btn(code, down) |
| 按住查询 | is_key_down(code) | is_mouse_btn_down(btn) | is_gamepad_btn_down(btn) |
| 注入引擎 | input_keyboard(code, down) | input_mouse_button(btn, down) | input_gamepad_button(code, down) |
| 查映射 | get_code_map(code) | 同左 | 同左 |
| 动作状态 | get_code_action_state(code) | 同左 | 同左 |
怎么选
- 事件回调永远按设备分:
on_key/on_mouse_btn/on_gamepad_btn各收各的原始编号,回调参数不是统一码。 - 手里已经有统一码(从
get_key_map_by_index()遍历来的、配置里的疾跑键/切换键)→ 用is_code_down(code)。 - 已经知道是哪个设备 → 用分设备接口,编号更直白:
is_gamepad_btn_down(0x01)比is_code_down(0x201)好读。
命名约定:接受统一 CODE 的接口带
*_code*(get_code_map/get_code_action_state/is_code_down);带key/mouse/gamepad的只收该设备自己的原始编号。get_key_map()/get_action_state()是改名前的兼容别名,与新名等价;新脚本请用新名。
常见错误
| 写法 | 结果 |
|---|---|
is_key_down(0x101) | 恒 false —— 它只收键盘键码 |
is_mouse_btn_down(0x101) | 恒 false —— 它只收 0x00–0x07 |
is_gamepad_btn_down(0x201) | 恒 false —— 它只收 0x01–0x1B |
在 on_gamepad_btn 里把 code 直接传给 get_code_map | 查不到 —— 少了 0x200 前缀 |
用 dmo_btn(0x01) / dkb_key(0x101) 混统一码 | 宿主编号是第三套,dmo_btn 只认 0x00–0x04、dkb_key 只认 HID 键码 |
具体键值表见后面的「键码与按钮编号」。
声明 / 解除监听
| 函数 | 说明 |
|---|---|
enable_listen_keys(code1, code2, ...) | 声明 on_key 要接收的 HID 键码 (0x00–0xFF, 可多个)。不声明则 on_key 不触发。 可多次调用 (累加)。每次脚本重载时清空。轮盘方向键 (默认 W/A/S/D, HID 0x1A/0x04/0x16/0x07) 与疾跑键 (默认 Shift 0xE1) 也不例外——想监听它们就得在这里声明。 |
disable_listen_keys(code1, ...) | 解除对指定键码的监听 (可多个);无参数则清空全部键监听。 |
enable_listen_mouse_btn(btn1, btn2, ...) | 声明 on_mouse_btn 要接收的鼠标按钮 (0x00–0x07)。语义同上。 |
disable_listen_mouse_btn(btn1, ...) | 解除对指定按钮的监听 (variadic);无参数则清空全部按钮监听。 |
enable_listen_mouse_move() | 开启 on_mouse_move 监听。 |
disable_listen_mouse_move() | 关闭 on_mouse_move 监听。 |
enable_listen_mouse_wheel() | 开启 on_mouse_wheel 监听。 |
disable_listen_mouse_wheel() | 关闭 on_mouse_wheel 监听。 |
enable_listen_gamepad_btn(code1, code2, ...) | 声明 on_gamepad_btn 要接收的手柄按键码 (0x01–0x1B, 可多个)。语义同上。 |
disable_listen_gamepad_btn(code1, ...) | 解除对指定手柄按键码的监听 (variadic);无参数则清空全部。 |
多模块语义(必读):
- 监听位图是设备级、所有模块共享的,不按模块记账。任意一个模块 enable 了某类事件,该类事件就会进 VM,由聚合入口按优先级分给定义了对应函数的模块(没定义的不调用、零开销;没有任何模块定义则完全不进 VM)。
- 惯例:在模块顶层
enable_listen_*调用一次即可;disable_*几乎用不到——绝大多数脚本只 enable、从不 disable。需要"临时不接管"时,用返回值表达(on_mouse_move返回false)比关监听更安全。- ⚠️
disable_*同样是全局的:disable_listen_keys()(无参)会清空所有模块注册的键监听;disable_listen_keys(0x13)也会解除所有模块对 0x13 的监听。不要在工具/库模块里随手 disable——那会静默关掉其它模块的功能。- 每次项目装载(上传/热重载)时所有监听清空,各模块需重新声明。
入口函数
| 函数 | 参数 | 返回 | 触发时机 |
|---|---|---|---|
init() | 无 | 忽略 | 脚本重载时 + 每次配置/槽位变更时。用于缓存慢速值。 |
on_key(keycode, down) | keycode: HID 键码 (int); down: bool | true=拦截, false=放行 | 仅对 enable_listen_keys 声明过的键、且状态变化 (边沿) 时。 |
on_mouse_btn(button, down) | button: 0x00–0x07; down: bool | 同上 | 仅对 enable_listen_mouse_btn 声明过的按钮、边沿。 |
on_mouse_move(dx, dy) | dx,dy: 本次相对移动像素 (int, 可正可负) | true=拦截 (dx/dy 归零) | 已 enable_listen_mouse_move 且有移动时。 |
on_mouse_wheel(wheel) | wheel: 本次滚轮量 (int, 可正可负) | true=拦截 (wheel 归零) | 已 enable_listen_mouse_wheel 且有滚动时。 |
on_gamepad_btn(code, pressed) | code: 手柄按键码 (0x01–0x1B, 见 手柄按键码表); pressed: bool | true=拦截 | 仅对 enable_listen_gamepad_btn 声明过的按键、边沿。 |
on_custom_event(str) | str: 字符串, 最大 128 字节 (WS) / 60 字节 (HID) | true=拦截 | 外部通过 WebSocket (CMD 0x01) 或 HID CMD 0xFA 发送文本时触发。无需 enable_listen。 |
on_boot_button(pressed) | pressed: bool (true=按下, false=松开) | 忽略 | 板载 BOOTSEL 按键状态变化时触发 (固件消抖确认, 约 20–30ms 延迟)。无需 enable_listen, 无拦截语义, 仅定义了此函数的脚本才会收到事件。 |
on_touchpad(meta, dims, x0, y0, ..., x9, y9) | 见 触摸板 | 忽略 | 手柄触摸板快照:触点数据变化时整帧上报(固件逐字节去重,静止不调用);拔出补发一帧全零。无需 enable_listen, 无拦截语义。 |
tick(dt_us) | dt_us: 距上次 tick 的微秒差 | 忽略 | 主循环限频调用, 约 10kHz (0.1ms 一次)。 |
拦截语义 (重要):
on_key/on_mouse_btn返回true→ core 跳过对该键的全部默认处理 (包括「修饰键+数字行 1~9」切槽热键、鼠标切换键、WASD 轮盘、配置映射)。返回false→ Lua处理完毕后,交给映射继续处理原始逻辑。- 多模块下按优先级依次调用:首个返回
true的模块拦截后,排在后面的模块和 core 默认处理都不再进行;想让后面的模块也看到事件就返回false。广播事件(init/tick/on_touchpad/on_boot_button)不受返回值影响,每个模块都调用。 - 轮盘方向键(以及疾跑键)也走
on_key:映射配置里「轮盘方向键」(WHEEL.WASD) 那四个键 (默认 W/A/S/D) 并不是被 core 提前吃掉的——它们和其它键一样先交给 Lua,on_key返回true时轮盘不会动。反过来, 想让脚本看到它们同样必须enable_listen_keys(...)声明 (不声明就收不到)。默认模板只声明了KEY_P/KEY_1/KEY_2, 所以"按 W 脚本没反应"通常是漏了这一步, 而不是被轮盘拦截。 on_mouse_move/on_mouse_wheel返回true→ 对应位移量清零。移动和滚轮分开判定: 只拦截on_mouse_move不影响同帧滚轮,反之亦然。
触摸输出
| 函数 | 说明 |
|---|---|
id = touch_down(x, y) | 在触摸坐标 (x,y) 按下一个新触点,返回触点 id (整数)。无空闲触点时返回 0xFF。 |
id = touch_move(id, x, y) | 把触点 id 移到 (x,y),返回 id。 |
touch_up(id) | 抬起触点 id。 |
触点最多 12 个。用完务必
touch_up释放,否则touch_down返回0xFF。建议为每个按键维护独立触点 id。
状态 Getter
| 函数 | 返回 | 冷/热 | 用法 |
|---|---|---|---|
get_scale() | scale_x, scale_y | 慢 (配置/槽位变才变) | 在 init() 里缓存进全局。像素→触摸缩放因子。 |
get_view_speed() | view_speed_x, view_speed_y | 慢 | 鼠标控制视角的灵敏度=触摸缩放因子x用户自定义的视角灵敏度。 |
get_vmouse() | x, y, show, down | 热 (每次鼠标移动都变) | 随用随取,别缓存。x,y = 虚拟光标当前屏幕像素坐标。 |
get_screen_size() | w, h | 慢 | 当前配置的屏幕尺寸 (像素)。 |
映射开关 / 输入状态
| 函数 | 返回 / 参数 | 说明 |
|---|---|---|
is_map_on() | bool | 映射是否开启。 |
set_map_on(on) | on: bool | 设置映射开关。切换时自动清理触摸、复位视角/轮盘/vmouse。 |
is_key_down(keycode) | bool | 某键当前是否按下;支持修饰键 (0xE0–0xE7) 与普通键。持续检测建议用 on_key 事件而非轮询。 |
is_mouse_btn_down(btn) | bool | 某鼠标按键 (0x00–0x07) 当前是否按下。 |
is_gamepad_btn_down(btn) | bool | 某手柄按键 (0x01–0x1B) 当前是否按下,编号与 on_gamepad_btn / input_gamepad_button 一致。 |
is_code_down(code) | bool | 按统一 16 位 CODE 查按下状态,跨设备通用(见下)。 |
get_now_ms() | int32 | 开机至今毫秒 (约 24.8 天回绕)。 |
get_rand_num(mini, max) | int32 | [mini, max] 闭区间随机整数。 |
统一 16 位 CODE:
0x000–0x0FF= 键盘键码,0x100+按钮= 鼠标(右键 =0x101),0x200|按钮= 手柄(A =0x201)。 三个is_*_down用各设备的原始编号(is_key_down(0x1A)是 W,is_mouse_btn_down(0x01)是右键,is_gamepad_btn_down(0x01)是 A);is_code_down()用统一码。手里是哪种编号就用对应的那个——配置与映射相关(get_code_map/get_key_map_by_index/get_code_action_state)一律是统一码。
按键映射查询
查询当前配置中某个键的映射信息。坐标返回 触控坐标 (0..TOUCH_MAX),与 get_view_now_pos() / get_wheel_start_pos() 一致。
| 函数 | 返回 / 参数 | 说明 |
|---|---|---|
get_code_map(code) | code: unified keycode (0x00–0xFE=HID 键盘键, 0x100+鼠标按钮, 0x200+手柄按键) | idx, type, flags, coord_count, interval_count / 无映射返回 nil。type: 0=PRESS, 1=MULT_PRESS, 2=SMART_TOGGLE, 3=WHEEL, 4=CLICK, 5=AUTO_FIRE, 6=DRAG, 7=TOGGLE。flags 按 type 划分命名空间: SMART_TOGGLE bit0=RELEASE_MOUSE, bit1=SEPARAT, bit2=TOUCH; WHEEL bit0=松开后关闭映射 (RELEASE_MAP_OFF)。 |
get_key_map_by_index(idx) | idx: 直接 key_maps 索引 (0-63) | code, type, flags, coord_count, interval_count, coord_start, interval_start / 越界返回 nil。用于遍历全部映射。 |
get_key_coord(key_map_idx, step) | 映射索引 + 步号 (0-based) | x, y, r_px (触控坐标 + 像素级随机半径) / 越界返回三个 nil。 |
get_key_interval(key_map_idx, step) | 映射索引 + 步号 (0-based) | interval_ms (毫秒) / 越界返回 nil。AUTO_FIRE 时 step=0=on_ms, step=1=off_ms。 |
命名约定:接受统一 16 位 CODE 的接口一律用
*_code_*/*_code(get_code_map/get_code_action_state/is_code_down), 带key/mouse/gamepad字样的接口只收该设备自己的原始编号。get_key_map()是get_code_map()的旧名,作为兼容别名保留。 另外get_key_map_by_index/get_key_coord/get_key_interval里的key_map指映射条目(不是键盘),它们按索引取值、与设备无关。
典型用法: 由 get_code_map(code) 拿到 coord_count/interval_count, 再用 get_key_coord(idx,i) / get_key_interval(idx,i) 逐条读出。
lua
-- 查看 KEY_P (0x13) 的映射详情
local idx, typ, flags, ccnt, icnt = get_code_map(0x13)
if idx then
print(string.format("P: idx=%d type=%d flags=%d", idx, typ, flags))
for i = 0, ccnt - 1 do
local x, y, r = get_key_coord(idx, i)
print(string.format(" coord[%d]: x=%d y=%d r_px=%d", i, x, y, r))
end
end
-- 遍历全部映射
for i = 0, 63 do
local code, typ = get_key_map_by_index(i)
if code and code ~= 0 then
print(string.format("idx=%d code=0x%x type=%d", i, code, typ))
end
end动作运行状态
查询映射引擎此刻正在执行的按键动作(运行态,不是配置,也不是输入状态)。
用途:判断 TOGGLE / 连发 / 自动释放这类动作的触点是否按着。注意 is_key_down() 只反映按键本身—— TOGGLE 松开按键后触点照样保持,两者并不等价。
| 函数 | 参数 | 说明 |
|---|---|---|
get_code_action_state(code) | code: 统一 16 位 CODE | state, type, touch_ids, step, total, timer_us / 该键当前没有进行中的动作返回 nil。旧名 get_action_state() 作为兼容别名保留。 |
code 编法与 get_code_map() 一致:0x000-0x0FF = 键盘键码,0x100+按钮 = 鼠标键(右键 = 0x101),0x200+按钮 = 手柄键。
| 字段 | 含义 |
|---|---|
state | 1=ACTIVE(动作推进中;TOGGLE 下即"触点保持按下")、2=HELD(等松键)、3=RELEASING(反向释放中) |
type | ACTION_* 编号,与 get_code_map() 的 type 同口径 |
touch_ids | 位图,bit N = 本动作正持有触点 N;非 0 即"触点按着" |
step / total | 分段动作(MULT_PRESS / DRAG / AUTO_FIRE / SHORT_LONG)的进度 |
timer_us | 剩余计时(微秒),无计时为 0 |
该查询与映射引擎同核(core1),在
on_key/on_mouse_btn/tick里直接调用即可,无需加锁。 想做"按住期间持续动作"就用tick轮询它;只关心边沿则在自己的on_*里查一次。
lua
local st, typ, touches = get_code_action_state(0x101) -- 右键绑定的 TOGGLE
if st == nil then
print("该键没有进行中的动作")
elseif touches ~= 0 then
print("触点按着", string.format("0x%X", touches))
end输入注入
从 Lua 主动向映射引擎注入键盘/鼠标事件,走完整的 core 映射管线 (键状态去重、按键映射、触屏输出等),效果等同于真实 USB 外设产生的输入。注入期间跳过 Lua 拦截 (on_key/on_mouse_btn 等不会触发),防止递归死循环。
| 函数 | 参数 | 说明 |
|---|---|---|
input_keyboard(keycode, down) | keycode: HID 键码 (0x00–0xFE); down: bool | 注入键盘按下/释放。keycode 为 0x00 或 >0xFE 则静默忽略。 |
input_mouse_button(button, down) | button: 0x00–0x07; down: bool | 注入鼠标按键。button 越界静默忽略。 |
input_mouse_move(dx, dy[, wheel]) | dx,dy: 相对位移 (像素); wheel: 滚轮 (默认 0) | 注入鼠标移动/滚轮。效果取决于 map_on: 映射开→移动视角; 映射关→移动 vmouse。 |
input_gamepad_button(code, down) | code: 手柄按键码 (0x01–0x1B, 见 手柄按键码表); down: bool | 注入手柄按键。code 越界 (≤0 或 >0x1B) 静默忽略。 |
注意: 注入的键事件会真实修改键状态 bitmap (
key_state[])。若注入input_keyboard(0x15, true)后未注入对应的false,映射引擎将认为该键一直处于按下状态,导致动作卡在 HOLD 阶段。
lua
-- 注入一次 R 键按下并释放
input_keyboard(0x15, true) -- KEY_R down
input_keyboard(0x15, false) -- KEY_R up
-- 注入鼠标左键点击
input_mouse_button(0x00, true)
input_mouse_button(0x00, false)
-- 注入鼠标向右移动 100 像素
input_mouse_move(100, 0)键盘输出(打到手机/电脑)
dkb_ = direct keyboard(直接键盘输出)。与上面的 input_keyboard 不同:这一组不经过映射引擎,直接把按键作为一台 USB 键盘发给手机/电脑。 需要先在「参数配置 → 键盘直通」打开并重启设备(dkb_is_on() 可查询当前是否可用;开关关闭时这些函数不报错,只是什么都不做)。
直通输出不受「映射开关」影响:
map_on只决定物理键鼠是走触屏映射、还是(开关打开时)直通给上位机;dkb_*任何时候都照发。
| 函数 | 参数 | 说明 |
|---|---|---|
dkb_key(keycode, down) | keycode: 0x04–0x73 或 0xE0–0xE7; down: bool | 按下 / 抬起一个键。修饰键(0xE0–0xE7)同样适用。范围外的键码会被忽略。 |
dkb_release_all() | 无 | 抬起所有键与修饰键。脚本异常或退出前调用可防"卡键"。 |
dkb_led() | 无 | 读手机/电脑写回的键盘灯状态:bit0 Num、bit1 Caps、bit2 Scroll、bit3 Compose、bit4 Kana。 |
dkb_is_on() | 无 | 键盘输出是否已启用(开关打开且设备已重启后为 true)。 |
dkb_state() | 无 | 一次拿到整份按键状态,见下(返回 4 个整数)。 |
dkb_is_down(keycode) | keycode: 同上 | 该键当前是否按下(true/false)。范围同 dkb_key。 |
键码范围:只支持键盘区的
0x04–0x73(字母、数字、F1–F24、方向键、编辑键等)与修饰键0xE0–0xE7。 媒体键等更靠后的键码不支持,会被忽略。
dkb_state()的四个整数:拼起来是 16 字节(小端)——第 1 字节是修饰键位图(bit0 左Ctrl…bit7 右GUI), 第 2–15 字节是 112 位键位图,第 16 字节恒为 0。键keycode(0x04–0x73)对应第8 + (keycode - 0x04)位; 修饰键0xE0–0xE7对应第keycode - 0xE0位。lualocal words = {dkb_state()} -- words[1]..words[4] local n = 0x1A - 0x04 -- W 键 local w = words[(8 + n) // 32 + 1] -- 落在哪个整数 local down = (w >> ((8 + n) % 32)) & 1 == 1单独查一个键用
dkb_is_down()更省事(Lua 5.4 支持&、>>等位运算)。
lua
-- 向手机输入 "Hi"(H=0x0B, i=0x0C,Shift 用修饰键 0xE1)
-- 按下与抬起必须自己配对;间隔太短可能被宿主吃掉,需要明显按下时长就分两步 + 协程 sleep
if dkb_is_on() then
dkb_key(0xE1, true) -- 左 Shift 按下
dkb_key(0x0B, true) -- H 按下
dkb_key(0x0B, false) -- H 抬起
dkb_key(0xE1, false) -- Shift 抬起
dkb_key(0x0C, true); dkb_key(0x0C, false) -- i
end
-- 组合键 Ctrl+C
dkb_key(0xE0, true); dkb_key(0x06, true); dkb_key(0x06, false); dkb_key(0xE0, false)想要"按住 30ms 再松"这种时序,用「异步动作序列」里的协程:
spawn(function() dkb_key(0x04, true); sleep_ms(30); dkb_key(0x04, false) end)。
鼠标直通(打到手机/电脑)
dmo_ = direct mouse output(直接鼠标输出)。与上面的 input_mouse_move 不同:这一组不经过映射引擎,直接把位移、滚轮、按键作为一台 USB 鼠标发给手机/电脑——脚本给多少像素光标就走多少(不乘"鼠标速度"倍率)。
只在「光标显示方式 → 直接鼠标」或「目标平台 → 苹果」(苹果模式下除「关闭」外的选择都按直接鼠标运行)下可用;切换后设备热重载 USB 生效。未启用时这些函数不报错,只是什么都不做。
| 函数 | 参数 | 说明 |
|---|---|---|
dmo_move(dx, dy[, wheel]) | 相对位移(像素,正 dx = 右、正 dy = 下);wheel 缺省 0,正 = 上滚 | 移动光标 / 滚动。纯滚动写 dmo_move(0, 0, -1)。 |
dmo_btn(button, down) | button: 0x00=左 0x01=右 0x02=中 0x03=后退 0x04=前进 | 按下 / 抬起一个鼠标键。 |
dmo_release_all() | 无 | 抬起 dmo_ 按下过的所有键(插在设备上的物理鼠标不受影响)。 |
dmo_is_on() | 无 | 鼠标直通是否可用。 |
dmo_state() | 无 | 按键位图整数:bit0 左、bit1 右、bit2 中、bit3 后退、bit4 前进。 |
dmo_is_down(button) | button: 0x00–0x04 | 该鼠标键当前是否按下。 |
按键状态与物理鼠标各记一份:脚本按住的键不会被物理鼠标的同键操作带掉,反之亦然。按下与抬起同样要自己配对。
dmo_btn只认 5 个真实鼠标键(0x00–0x04):滚轮伪键编号(0x05/0x06)在这里不接受,滚轮走dmo_move的第三个参数。 直通输出同样不受「映射开关」影响;dmo_*任何时候都照发(未启用时除外)。
dkb_state()/dkb_is_down()/dmo_state()/dmo_is_down()查的是发给手机/电脑的状态 (含直通进来的物理键鼠);想查"引擎收到了哪些键",用is_key_down()/is_mouse_btn_down()。
lua
-- 光标右移 200 像素,再点一下左键
if dmo_is_on() then
dmo_move(200, 0)
dmo_btn(0x00, true)
dmo_btn(0x00, false)
end
-- 滚轮下滚 3 格
dmo_move(0, 0, -3)槽位操作
| 函数 | 返回 | 说明 |
|---|---|---|
get_current_slot() | int (0-8) | 当前激活的配置槽位索引。恒返回 0-8 有效值。 |
set_current_slot(idx) | 无 | 切换到槽位 idx (0-8)。非法值或空槽时保持当前槽位不变;目标槽为当前槽时自动跳过。请求异步应用、无间隔限制,连续调用时每次都会实际切换(原子标志仅保留最新目标)。调用后立即 get_current_slot() 可能仍返回旧值(应用是异步的)。效果与槽位热键(修饰键 + 数字行 1~9,1 = 槽 0)一致。 |
临时叠加层
叠加层临时使用另一个配置槽位的按键映射,不切换基础配置。坐标按当前基础层的坐标系解释;屏幕尺寸、视角、轮盘、陀螺仪等其它配置继续来自基础层。切换基础配置槽位时会清除叠加层。
| 函数 | 返回 | 说明 |
|---|---|---|
load_overlay(slot) | 无 | 异步加载槽位 slot (0-8) 的键盘、鼠标按键和手柄按键映射,包括坐标、随机半径、间隔和动作参数。 |
clear_overlay() | 无 | 异步清除叠加层;后续新按键使用基础层。已经开始的动作仍按按下时的来源层完成。 |
overlay_is_active() | bool | 当前是否已应用叠加层。 |
overlay_get_slot() | int (0-8) / nil | 当前叠加层的槽位索引;未启用时返回 nil。 |
对于每个新按键,叠加层映射优先;叠加层没有该按键时回退基础层。松开沿用按下时选中的映射。叠加层支持所有现有按键动作类型。请求异步执行,紧接 load_overlay() 查询状态可能仍读到旧状态。
空槽、无效槽位配置或运行时缓冲区繁忙时会拒绝加载,保留当前叠加层并在设备日志中记录原因。只有 clear_overlay() 会主动清除当前叠加层。加载槽位仍遵守设备激活限制。
示例:按住手柄 EXTRA1 时加载槽位索引 2,松开时清除叠加层。槽位索引为 0-8;如果你的“配置2”指从 1 开始数的第二个槽位,应使用索引 1。
lua
local EXTRA1 = 0x14
enable_listen_gamepad_btn(EXTRA1)
function on_gamepad_btn(code, pressed)
if code ~= EXTRA1 then return false end
if pressed then load_overlay(2) else clear_overlay() end
return true
end视角 (View)
坐标均为触控坐标 (0..TOUCH_MAX)。
| 函数 | 说明 |
|---|---|
get_view_id() / set_view_id(id) | 当前视角触点 id (0xFF = 未触摸)。 |
get_view_now_pos() → x, y | 当前视角坐标。 |
move_view_offset(x, y) | 以偏移量移动视角。仅当 map_on 且已有视角触点时生效。建议使用方式为 鼠标移动 x (scaleX, yscaleY)。 |
get_view_start_pos() / set_view_start_pos(x, y) | 视角起始点。 |
get_view_speed() / set_view_speed(x, y) | 视角控制速度 (灵敏度)。 |
set_view_auto_center(on) | 视角越界时是否回到 start_pos。默认 true。 |
set_view_auto_release_timeout(ms) | 视角触点无移动多久 (毫秒) 后自动释放;0 = 永不自动释放。默认 400。 |
set_gyro_enable(on) / is_gyro_enable() | 陀螺仪总开关(默认开,运行时开关,重启恢复开)。关闭 = 全局屏蔽陀螺仪:视角辅助、生效时机、开镜/开火倍率、飞鼠全部不生效;打开后各按自己的配置生效(配置开关关着就还是关)。触摸板通道 on_touchpad 不受此开关影响。 |
get_gyro_state() → bitmap | 陀螺仪运行状态位图:bit0 总开关开|bit1 配置开关开|bit2 六轴在线|bit3 生效时机门内|bit4 开镜/开火倍率窗口(LT/RT 按住,任何模式)|bit5 视角模式工作中(映射开启且时机门内)|bit6 飞鼠就绪(切换键已绑定且已开启)|bit7 飞鼠工作中(映射关闭 + 飞鼠开 + 六轴在线)。视角(bit5)与飞鼠(bit7)由映射开关互斥;其余位是"就绪/窗口"状态而非工作状态。 |
虚拟光标 (vmouse)
坐标均为真实屏幕像素;触点 id 由内部管理,故封装为函数。
| 函数 | 说明 |
|---|---|
get_vmouse_state() → show, down, x, y | vmouse 状态。 |
set_vmouse_show(on) / set_vmouse_down(on) | 设置显示 / 按下。 |
move_vmouse_pos_offset(x, y) | 以偏移量移动 vmouse (x,y 为屏幕像素偏移,直接累加)。 |
move_vmouse_pos_target(x, y) | 移动 vmouse 到目标触控坐标 (内部 /scale 转为屏幕像素)。 |
轮盘 (Wheel)
中心为触控坐标,半径为归一化 float (相对屏幕宽)。
| 函数 | 返回 | 说明 |
|---|---|---|
get_wheel_start_pos() | x, y | 轮盘中心坐标。 |
get_wheel_range() | range, shift_range | 普通半径 / shift 疾跑半径。 |
get_wheel_shift_range_type() | enable, hold_or_click, keep_on_released | enable = 是否开启 shift 疾跑; hold_or_click = true 切换 (toggle) / false 按住 (hold); keep_on_released = 切换模式下松开 WASD 是否保持疾跑状态 (仅 toggle 模式有意义)。 |
调试输出
| 函数 | 说明 |
|---|---|
print(...) | 输出到 WebSocket 调试通道,带 [lua] 前缀。未连接 WS 客户端时丢弃。 |
warn(...) | 同 print,带 [lua warn] 前缀。 |
板载 RGB LED
控制设备面板上的 WS2812B RGB 指示灯。
| 函数 | 说明 |
|---|---|
set_led(r, g, b) | 设置 LED 颜色 (每通道 0-255)。负数跳过该通道 (保持原值);正数 >255 自动钳位到 255。set_led(0,0,0) 等效熄灭。 |
led_off() | 熄灭 LED。 |
按通道部分更新:set_led 的三个参数分别对应红、绿、蓝。想只改一个通道,把另外两个传负数即可。
lua
-- 全通道设置
set_led(255, 0, 0) -- 红
set_led(0, 255, 0) -- 绿
set_led(0, 0, 255) -- 蓝
-- 只改红色,绿蓝不变
set_led(255, -1, -1)
-- 只改绿色 (值 >255 自动钳位)
set_led(-1, 300, -1)
-- 改红和蓝,绿不变
set_led(128, -1, 64)
-- 熄灭两种方式
set_led(0, 0, 0)
led_off()典型用途:用 LED 颜色指示脚本状态 (如
init()亮绿、出错亮红、on_key闪烁等)。
状态屏 (lcd_line)
设备外接的 ST7789 状态屏(240×240)底部是脚本自定义区(两行文本),用 lcd_line 设置内容:
| 函数 | 说明 |
|---|---|
lcd_line(1, "文本") | 写第 1 行(主行,大字号)。 |
lcd_line(2, "文本") | 写第 2 行(次行,小字号)。 |
- 超长自动截断;写入不是立即绘制,屏在下一次刷新扫描时更新(约 0.1~0.4 秒)。
- 传空串清空该行。
- 仅状态屏固件(
--board lcd/lcd-dev构建且屏幕正常点亮)提供;普通/蓝牙固件里没有这个函数(调用会报 attempt to call nil)。 - 文本随整行一起重绘,频繁更新(如每帧改数字)没有意义,建议在状态变化时才写。
lua
function init()
lcd_line(1, "压枪 M416 · 垂直 62")
lcd_line(2, "配置:满配 M4")
end坐标系与换算
固件内部使用两套坐标体系:
屏幕像素
get_screen_size() 返回手机实际分辨率 (如 1080×2400)。get_vmouse() 返回的光标坐标也是这个坐标系 → 范围 0..screen_w / 0..screen_h。
触控坐标
touch_down/move/up、get_view_now_pos() 等使用触控坐标: 范围 0..0x7FFFFFFE 。无论手机分辨率多少,触控坐标范围永远不变。屏幕正中心永远是 0x3FFFFFFF (= 0x7FFFFFFE / 2)。
换算
lua
-- 像素 → 触控 (乘 scale, 最常用)
touch_x = pixel_x * scale_x
touch_y = pixel_y * scale_y
-- 触控 → 像素 (除 scale, 少数 API 用到)
pixel_x = touch_x / scale_xscale_x/scale_y 用 get_scale() 取,建议在 init() 缓存进全局。
各 API 坐标系速查
| API | 坐标系 | 说明 |
|---|---|---|
get_vmouse() / get_vmouse_state() | 屏幕像素 | 虚拟光标位置 |
move_vmouse_pos_offset | 屏幕像素 | 偏移量直接累加 |
move_vmouse_pos_target | 屏幕像素 | 传入坐标,直接移动光标到目标位置 |
touch_down/move/up | 触控坐标 | 最终输出,必须用触控坐标 |
get_view_now_pos / move_view_offset | 触控坐标 | 视角系统 |
get_view_start_pos / set_view_start_pos | 触控坐标 | 视角起始点 |
get_wheel_start_pos | 触控坐标 | 轮盘中心 |
get_key_coord | 触控坐标 | 按键映射的坐标 (x,y,r_px) |
get_screen_size() | 屏幕像素 | 手机实际分辨率 |
示例:在手机屏幕中心点一下
lua
function init()
scale_x, scale_y = get_scale()
sw, sh = get_screen_size()
end
-- 手机屏幕中心(像素) → 触控坐标
local cx_px = sw / 2 -- 如 540 (1080 宽屏)
local cy_px = sh / 2 -- 如 1200
local id = touch_down(cx_px * scale_x, cy_px * scale_y)
-- 等价于: touch_down(0x3FFFFFFF, 0x3FFFFFFF)
touch_up(id)键码与按钮编号
键盘键码 = 标准 USB HID Usage ID
键盘键码直接采用 USB HID Usage ID(Keyboard/Keypad 页),与标准 HID 键盘描述符逐字节一致,没有私有换算。常见锚点:
| 键 | 码 | 键 | 码 |
|---|---|---|---|
| A–Z | 0x04–0x1D | 1–9 / 0 | 0x1E–0x26 / 0x27 |
| Enter / Esc / Backspace | 0x28 / 0x29 / 0x2A | Tab / Space | 0x2B / 0x2C |
| F1–F12 | 0x3A–0x45 | F13–F24 | 0x68–0x73 |
| 方向键 → ← ↓ ↑ | 0x4F–0x52 | 小键盘 | 0x53–0x63 |
| 左/右 Ctrl | 0xE0 / 0xE4 | 左/右 GUI | 0xE3 / 0xE7 |
修饰键按 Ctrl / Shift / Alt / GUI 顺序,左 0xE0–0xE3、右 0xE4–0xE7。
表中没列的键(- = [ ] \ ; ' , . /、CapsLock、Insert/Home/PageUp/Delete/End/PageDown、国际键等)按标准 HID 表取值即可,全部宏名见 defines.h。
唯一的例外:
0xE8–0xFB不是标准 HID,是本固件扩展的媒体键:0xE8播放/暂停、0xE9停止、0xEA上一曲、0xEB下一曲、0xEC弹出、0xED音量+、0xEE音量−、0xEF静音、0xF0WWW、0xF1后退、0xF2前进、0xF3停止、0xF4查找、0xF5/0xF6滚轮上/下、0xF7编辑、0xF8睡眠、0xF9咖啡、0xFA刷新、0xFB计算器。
鼠标按钮 (on_mouse_btn 的 button):
| 值 | 按钮 |
|---|---|
0x00 | 左键 |
0x01 | 右键 |
0x02 | 中键 |
0x03 | 后退 |
0x04 | 前进 |
手柄按键码
| 码 | 按键 | 码 | 按键 |
|---|---|---|---|
0x01 | A / Cross | 0x10 | 十字键 上 |
0x02 | B / Circle | 0x11 | 十字键 下 |
0x03 | X / Square | 0x12 | 十字键 左 |
0x04 | Y / Triangle | 0x13 | 十字键 右 |
0x05 | LB / L1 | 0x14–0x1B | EXTRA_1–8 |
0x06 | RB / R1 | ||
0x07 | LT / L2 | ||
0x08 | RT / R2 | ||
0x09 | SELECT / Back / Share | ||
0x0A | START / Options | ||
0x0B | LS / L3 (左摇杆按下) | ||
0x0C | RS / R3 (右摇杆按下) | ||
0x0D | HOME / Guide / PS | ||
0x0E | MISC (触控板 / Mute) |
三套编号的分工、API 对照与常见错误见前面的「按键体系」一节。 完整键码表见 HID 键码参考。
性能模型与规则
on_key/on_mouse_btn跑在输入热路径上 — 每次有按键/鼠标事件时调用,必须保持极快,不要在里面做重循环/大分配。- 只有声明过的键/按钮才进 VM (bitmap 预过滤),没声明零开销跳过。多模块下判据是"任一模块声明了该类事件 且 存在模块定义了对应入口"——两个条件都不满足时 VM 完全不进入。
- 开启
on_mouse_move的代价模型:开启后每次移动报文都进一次 VM(1kHz 鼠标 = 每秒 1000 次)。240MHz 下一次轻量回调(读参、做加减、调一次输出 API)约 5–15µs,1kHz 下约 1% core1 占用——正常使用完全无感;但不要在 handler 里建表/大循环,8kHz 高回报率鼠标下重 handler 会显著吃满 core1(手感变"发涩"而不是线性变慢)。 - 慢/热分离: 慢速值 (scale、view_speed) 在
init()缓存进全局,以减少每次调用的开销;热值 (vmouse) 随用随取。 - 数字是 32 位 (固件设了
LUA_32BITS): 整数 int32、浮点 float32。别用超过 2^31 的整数。 - 单核单线程 (引擎在 core1,同核串行): 脚本内无需考虑并发。
- 没有阻塞 sleep: 回调必须立刻返回。如果需要使用sleep,用协程 (见下文)。
- 单次进入 VM 有 10ms 墙钟预算: 固件每 2 万条指令轮询一次执行耗时,单次回调(含该次回调内恢复运行的所有协程分片)累计超过 10ms 即抛错中断,日志报
script exceeded 10000us step budget (infinite loop?)。正常回调只有几十~几百条指令(微秒级);协程每片都极短,长期运行不受影响。真正的死循环(如while true do end)会在预算内被拦下。多模块下同一次事件的聚合调用算"一次进入 VM":该事件里所有模块的 handler 共享这 10ms——排在后面的模块可能被前面的重 handler 挤掉预算,别让任何单个回调做重活。 on_touchpad自带固件级节流: 与上次分发的快照逐字节相同就不调用(手指静止零开销),但滑动时约等于手柄报文率(可达数百 Hz)——回调内只做差分/比较这类轻量计算,输出动作交给input_*或状态位,别在里面 print 每一帧。- 脚本大小 ≤ 64KB。
完整示例
跑马灯 (LED 渐变循环)
利用 tick(dt_us) 驱动,在颜色之间平滑过渡(线性插值),每 1s 完成一次颜色切换。调节 MAX_BRIGHT 控制最大亮度。
lua
-- 跑马灯:红→绿→蓝 平滑渐变,每 1s 过渡完成
local COLORS = {
{255, 0, 0}, -- 红
{0, 255, 0}, -- 绿
{0, 0, 255}, -- 蓝
}
local MAX_BRIGHT = 20 -- 最大亮度 (0-255),越小越暗
local idx = 1
local acc = 0
local TRANSITION = 1000000 -- 1s (微秒)
local cr, cg, cb = 0, 0, 0
function init()
set_led(MAX_BRIGHT, 0, 0)
cr, cg, cb = MAX_BRIGHT, 0, 0
end
function tick(dt_us)
acc = acc + dt_us
if acc >= TRANSITION then
acc = acc - TRANSITION
idx = idx + 1
if idx > #COLORS then idx = 1 end
end
local t = acc / TRANSITION
local prev = idx - 1
if prev < 1 then prev = #COLORS end
local r1 = math.floor(COLORS[prev][1] * MAX_BRIGHT / 255 + 0.5)
local g1 = math.floor(COLORS[prev][2] * MAX_BRIGHT / 255 + 0.5)
local b1 = math.floor(COLORS[prev][3] * MAX_BRIGHT / 255 + 0.5)
local r2 = math.floor(COLORS[idx][1] * MAX_BRIGHT / 255 + 0.5)
local g2 = math.floor(COLORS[idx][2] * MAX_BRIGHT / 255 + 0.5)
local b2 = math.floor(COLORS[idx][3] * MAX_BRIGHT / 255 + 0.5)
local nr = math.floor(r1 + (r2 - r1) * t + 0.5)
local ng = math.floor(g1 + (g2 - g1) * t + 0.5)
local nb = math.floor(b1 + (b2 - b1) * t + 0.5)
if nr ~= cr or ng ~= cg or nb ~= cb then
cr, cg, cb = nr, ng, nb
set_led(cr, cg, cb)
end
end提示:改
MAX_BRIGHT调亮度,改COLORS表自定义颜色序列,改TRANSITION调过渡速度。相邻颜色之间线性插值,无跳变。
按键点击 (每键独立触点)
lua
enable_listen_keys(0x13, 0x1E, 0x1F) -- KEY_P, KEY_1, KEY_2
local CENTER = 0x3FFFFFFF -- 屏幕中心 (触摸坐标)
local touches = {} -- [keycode] = touch_id
function on_key(keycode, down)
if down then
touches[keycode] = touch_down(CENTER, CENTER)
else
local id = touches[keycode]
if id and id ~= 0xFF then
touch_up(id)
touches[keycode] = nil
end
end
return true
end连点 Autofire (按住 R → 每 50ms 在光标处点一下)
lua
enable_listen_keys(0x15) -- KEY_R
local firing = false
local acc = 0 -- 距上次开火累计的微秒
local INTERVAL = 50000 -- 50ms (微秒)
function init()
scale_x, scale_y = get_scale()
end
function on_key(keycode, down)
firing = down
if down then acc = INTERVAL end -- 按下即刻先点一次
return true
end
function tick(dt_us)
if not firing then return end
acc = acc + dt_us
if acc >= INTERVAL then
acc = acc - INTERVAL
local mx, my = get_vmouse()
local id = touch_down(mx * scale_x, my * scale_y)
if id ~= 0xFF then touch_up(id) end
end
end右键长按拖动 (触点跟随光标)
lua
enable_listen_mouse_btn(0x01) -- 鼠标右键
local drag = nil
function init()
scale_x, scale_y = get_scale()
end
function on_mouse_btn(button, down)
local mx, my = get_vmouse()
if down then
drag = touch_down(mx * scale_x, my * scale_y)
elseif drag then
touch_up(drag); drag = nil
end
return true
end
function tick(dt_us)
if drag then
local mx, my = get_vmouse()
touch_move(drag, mx * scale_x, my * scale_y)
end
end接管鼠标移动 (中键拖动)
on_mouse_move 返回 true 会把本次 dx/dy 清零,core 的默认视角处理被跳过。下例:按住中键期间把移动转成 "在光标处拖动一个触点"。
lua
enable_listen_mouse_btn(0x02) -- 中键
enable_listen_mouse_move() -- 接管移动
local dragging = false
local drag_id = 0xFF
local drag_x = 0
local drag_y = 0
function init()
scale_x, scale_y = get_scale()
end
function on_mouse_btn(button, down)
if button == 0x02 then
dragging = down
if not down and drag_id ~= 0xFF then
touch_up(drag_id); drag_id = 0xFF
end
end
return false
end
function on_mouse_move(dx, dy)
if not dragging then return false end
local mx, my = get_vmouse()
if drag_id == 0xFF then
drag_id = touch_down(mx * scale_x, my * scale_y)
drag_x = mx * scale_x
drag_y = my * scale_y
else
drag_x = drag_x + dx * scale_x
drag_y = drag_y + dy * scale_y
touch_move(drag_id,drag_x , drag_y)
end
return true -- 吃掉本次移动
end注意: 一旦
on_mouse_move返回true,core 的 vmouse 光标就不会随这次移动更新。若逻辑依赖光标位置,要么放行 (return false) 让 core 推进 vmouse,要么自行累加dx/dy维护坐标。
测量鼠标移动距离(按住左键拖动)
按住鼠标左键并拖动,松开后打印鼠标累计位移。可用于测量视角灵敏度。
lua
-- 测量鼠标移动距离脚本(监听鼠标左键)
enable_listen_mouse_btn(0x00) -- 鼠标左键
enable_listen_mouse_move()
local recording = false
local total_dx = 0
local total_dy = 0
function init()
recording = false
total_dx = 0
total_dy = 0
end
function on_mouse_btn(button, down)
if button == 0x00 then
if down and is_map_on() then
recording = true
total_dx = 0
total_dy = 0
print("测量开始(左键按住),移动鼠标...")
elseif not down and recording then
recording = false
print(string.format(
"测量结束 | 总位移: dx=%d, dy=%d",
total_dx, total_dy
))
total_dx = 0
total_dy = 0
end
end
return false -- 放行左键,不影响正常操作(例如开火)
end
function on_mouse_move(dx, dy)
if recording then
total_dx = total_dx + dx
total_dy = total_dy + dy
end
return false -- 放行移动,不影响视角控制
end采集压枪数据和压枪数据重放
监听鼠标移动事件,并以100hz将鼠标移动打印在控制台
lua
enable_listen_mouse_btn(0x00)
enable_listen_mouse_move()
-- 全局状态变量
local is_recording = false -- 记录开关状态
local total_dx = 0 -- 累计 X 轴位移
local total_dy = 0 -- 累计 Y 轴位移
local acc_us = 0 -- 时间累加器 (微秒)
local INTERVAL_US = 10000 -- 100Hz 对应的时间间隔:10,000 微秒 (10 毫秒)
-- 脚本初始化/热重载回调
function init()
is_recording = false
total_dx = 0
total_dy = 0
acc_us = 0
end
-- 鼠标按键事件 (边沿触发)
function on_mouse_btn(button, down)
-- 仅响应鼠标左键 (button 0x00) 且需映射开启 (is_map_on)
if button == 0x00 and is_map_on() then
if down then
-- 按下左键:开始记录并重置数据
is_recording = true
total_dx = 0
total_dy = 0
acc_us = 0
print(">>> [压枪记录] 开始记录数据 (100Hz) <<<")
else
-- 松开左键:停止记录并打印最终汇总
if is_recording then
is_recording = false
print(string.format(">>> [压枪记录] 停止记录 | 最终总位移 -> X: %d, Y: %d <<<", total_dx, total_dy))
end
end
end
-- 返回 false 放行按键,确保游戏内正常开火;若设为 true 会吃掉左键点击
return false
end
-- 鼠标相对移动事件 (连续触发)
function on_mouse_move(dx, dy)
-- 仅在记录开启状态下累加鼠标移动值
if is_recording then
total_dx = total_dx + dx
total_dy = total_dy + dy
end
-- 返回 false 放行移动,确保视角能正常移动;若设为 true 视角会被卡住
return false
end
-- 周期回调 (约 1000Hz 频率调用, dt_us 为距上次调用的微秒差)
function tick(dt_us)
if not is_recording then return end
acc_us = acc_us + dt_us
-- 当时间累积达到 10ms (10,000µs) 时打印一次,实现 100Hz 打印频率
if acc_us >= INTERVAL_US then
acc_us = acc_us - INTERVAL_US
print(string.format("[压枪数据] 累计总位移 -> dX: %d | dY: %d", total_dx, total_dy))
total_dx = 0
total_dy = 0
end
end将录制的鼠标位移数据,在按下左键的时候重放
lua
enable_listen_mouse_btn(0x00)
-- -----------------------------------------------------------------
-- 数据压缩区:一对一对填即可 [dx1, dy1, dx2, dy2, dx3, dy3, ...]
-- -----------------------------------------------------------------
local RECOIL_DATA = {
0,1,1,8,1,11,1,7,3,20,2,19,1,24,3,30,2,34,3,42,3,46,3,57,1,52,2,60,2,63,2,66,3,67,3,68,3,70,2,71,3,72,3,70,3,69,4,71,4,68,4,67,4,66,4,66,3,66,3,65,2,65,2,66,0,66,1,65,1,64,2,64,1,25,3,101,1,62,1,62,0,61,-2,62,-1,61,-3,60,-2,61,-2,60,-1,61,-3,61,-3,62,-3,61,-3,62,-3,63,-2,62,-2,62,-2,62,-2,63,0,25,-1,100,-1,64,-1,64,-1,70,-2,61,0,58,-1,63,0,62,-1,63,0,63,1,65,1,66,0,65,0,66,1,66,1,66,0,66,2,67,2,59,3,71,2,63,3,63,2,63,3,64,2,61,2,60,1,60,1,60,2,60
}
local is_firing = false
local current_step = 1
local acc_us = 0
local INTERVAL_US = 10000 -- 10ms (100Hz)
local view_speed_x = 1
local view_speed_y = 1
function init()
is_firing = false
current_step = 1
acc_us = 0
view_speed_x , view_speed_y = get_view_speed()
end
function on_mouse_btn(button, down)
if button == 0x00 and is_map_on() then
if down then
is_firing = true
current_step = 1
acc_us = 0
else
is_firing = false
end
end
return false
end
function tick(dt_us)
if not is_firing or #RECOIL_DATA == 0 then return end
acc_us = acc_us + dt_us
if acc_us >= INTERVAL_US then
acc_us = acc_us - INTERVAL_US
-- 自动按步骤计算出数组中 dx 和 dy 的位置
local idx = (current_step - 1) * 2 + 1
if idx <= #RECOIL_DATA then
local dx = RECOIL_DATA[idx]
local dy = RECOIL_DATA[idx + 1]
move_view_offset(dx * view_speed_x , dy * view_speed_y )
current_step = current_step + 1
else
-- 数据用尽后维持最后一帧的下压幅度
local last_dx = RECOIL_DATA[#RECOIL_DATA - 1]
local last_dy = RECOIL_DATA[#RECOIL_DATA]
move_view_offset(last_dx * view_speed_x, last_dy * view_speed_y )
end
end
end异步动作序列 (协程 coroutine)
引擎没有阻塞式 sleep。想写 "按下 → 等 50ms → 滑动 → 等 30ms → 抬起" 这种带时序的顺序逻辑,用固件内建的 spawn + sleep(协程):调度器已内建、全模块共享,不用再自己维护 tasks 表和 tick 里的排水循环。 例如vmouse模式下,右键将物品拖动到丢弃区域
lua
enable_listen_mouse_btn(0x01) -- 右键
local scale_x, scale_y = 1, 1
function init()
scale_x, scale_y = get_scale()
end
function on_mouse_btn(button, down)
if button == 0x01 and down then
local mx, my = get_vmouse() -- 快照按下瞬间的光标位置
spawn(function() -- 内建全局:任何回调/init/顶层都能调
local id = touch_down(mx * scale_x, my * scale_y)
if id == 0xFF then return end -- 没有空闲触点, 放弃
sleep_ms(50) -- 停 50ms
touch_move(id, 0x3FFFFFFF, 0x3FFFFFFF) -- 滑到屏幕中心
sleep_ms(30) -- 停 30ms
touch_up(id)
end)
end
return false
end内建 API(固件自动注入,无需定义):
| 函数 | 说明 |
|---|---|
spawn(fn) | 把函数包成协程入队,下个 tick 开始跑;报错会记日志 [lua_task] 模块名: ... 并移除,不影响其它任务 |
sleep_ms(ms) | 只能在 spawn 的协程里调用:暂停 ms 毫秒后继续 |
sleep_us(us) | 同上,单位微秒 |
sleep | = sleep_ms 的别名(毫秒语义) |
模式要点:
spawn(fn)把任意 "动作序列函数" 变成异步任务;函数里用sleep_ms/sleep_us表达等待。带时序的顺序逻辑(连点、按住 N ms、逐字打字…)优先用异步,不要写成 tick 状态机。sleep*在回调/tick 里直接调用会报错("sleep 只能在 spawn 启动的协程里调用")——回调必须立刻返回。- 唤醒粒度 = tick 周期:实际等待向上取整到下一次调度,
sleep_us(1)不会真在 1µs 后继续。常规时序用sleep_ms就够。 - 协程里每一步仍要轻量 — 别在一步里做重循环。等待外部条件时也一样:
while not cond() do sleep_ms(10) end(每 10ms 醒来查一次),不要写无sleep的忙等——单次进入 VM 的 10ms 墙钟预算会被它烧穿。 - 协程可以长期运行(宏循环这类),每片耗时极短即可;预算按"单次 tick 内所有分片累计"计,正常分片远碰不到。
- 任务数上限 32。
coroutine标准库可用 (create/resume/yield/status)。旧脚本自带 spawn/sleep 定义的(local 或全局)会遮蔽内建,行为不变——旧 µs 语义的自定义sleep继续按微秒等。
键鼠直通 (dkb_ / dmo_)
把 Pico 当成一台「键盘 + 鼠标」用:脚本直接在上位机(手机/电脑)上敲键、移动光标。与前面的触屏示例完全独立,不进触屏链路。
前置(面板里改完必须重启设备):
- 键盘:「参数配置 → 键盘直通」打开
- 鼠标:「参数配置 → 光标显示方式 → 直接鼠标」,或「PICO 系统配置 → 目标平台 → 苹果」
用法:在面板底部「发送文本到 Lua」输入下面任一指令(也可经 WebSocket CMD 0x01 发送):
| 指令 | 效果 |
|---|---|
type:hello | 在上位机逐字打出 hello |
click | 光标右移 100 像素后点一下左键 |
wheel:-3 | 光标不动,向下滚 3 格 |
state | 回传一行直通状态(键盘 4 个位图整数 + 鼠标位图) |
lua
-- ═══ 键鼠直通示例 ═══
-- 直通输出不经过映射引擎:这里的按键/位移不会变成触屏事件,也不受「映射开关」影响。
local TEXT_KEYS = { -- 演示用:字母/数字/空格/回车 (HID 键码)
["a"] = 0x04, ["b"] = 0x05, ["c"] = 0x06, ["d"] = 0x07, ["e"] = 0x08,
["f"] = 0x09, ["g"] = 0x0A, ["h"] = 0x0B, ["i"] = 0x0C, ["j"] = 0x0D,
["k"] = 0x0E, ["l"] = 0x0F, ["m"] = 0x10, ["n"] = 0x11, ["o"] = 0x12,
["p"] = 0x13, ["q"] = 0x14, ["r"] = 0x15, ["s"] = 0x16, ["t"] = 0x17,
["u"] = 0x18, ["v"] = 0x19, ["w"] = 0x1A, ["x"] = 0x1B, ["y"] = 0x1C,
["z"] = 0x1D, ["1"] = 0x1E, ["2"] = 0x1F, ["3"] = 0x20, ["4"] = 0x21,
["5"] = 0x22, ["6"] = 0x23, ["7"] = 0x24, ["8"] = 0x25, ["9"] = 0x26,
["0"] = 0x27, ["\r"] = 0x28, [" "] = 0x2C,
}
-- ── 协程调度(spawn/sleep_ms)为固件内建,见上一节「异步动作序列」──
-- ── 逐字打字:按下 → 停 15ms → 抬起 → 停 15ms ──
local function type_text(s)
spawn(function()
for i = 1, #s do
local k = TEXT_KEYS[s:sub(i, i)]
if k then
dkb_key(k, true)
sleep_ms(15)
dkb_key(k, false)
sleep_ms(15)
end
end
dkb_release_all() -- 兜底: 中途出错也不会在上位机卡键
end)
end
-- ── 移动 + 单击 ──
local function click_at_offset(dx, dy)
spawn(function()
dmo_move(dx, dy)
sleep_ms(20) -- 等光标到位再点
dmo_btn(0x00, true) -- 0 = 左键
sleep_ms(30) -- 按住 30ms, 太短会被上位机忽略
dmo_btn(0x00, false)
end)
end
function on_custom_event(str)
local cmd, arg = string.match(str, "^([%a_]+):?(.*)$")
if cmd == "type" then
if not dkb_is_on() then
warn("键盘直通未启用: 请在「参数配置 → 键盘直通」打开后重启设备")
return true
end
type_text(arg)
elseif cmd == "click" then
if not dmo_is_on() then
warn("鼠标直通未启用: 请选「光标显示方式 → 直接鼠标」或「目标平台 → 苹果」后重启设备")
return true
end
click_at_offset(100, 0)
elseif cmd == "wheel" then
dmo_move(0, 0, tonumber(arg) or -1) -- 正 = 上滚
elseif cmd == "state" then
local w = { dkb_state() } -- 4 个整数 = 16 字节小端位图
print(string.format("[直通] 键盘 %08X %08X %08X %08X | A键=%s | 鼠标位图=%d | 左键=%s",
w[1], w[2], w[3], w[4],
tostring(dkb_is_down(0x04)), -- 0x04 = A
dmo_state(),
tostring(dmo_is_down(0x00))))
else
warn("unknown cmd: " .. str)
end
return true
end模式要点:
- 两个直通开关不归脚本管:脚本只能用
dkb_is_on()/dmo_is_on()查询;未启用时这些函数不报错,什么都不做(所以示例里先查再发,并在日志里说明原因)。 - 按键必须自己配对按下/抬起(没有
*_tap);要时序就照上一节的协程写法。dkb_release_all()/dmo_release_all()适合放在序列末尾兜底。 - 物理键鼠与脚本各记一份按键状态:直通开关打开时,插在设备上的物理键盘也会直通给上位机(映射关闭时),但这与脚本按下的键互不干扰。
dkb_state()/dkb_is_down()/dmo_state()/dmo_is_down()查的是发给上位机的状态(含直通进来的物理键鼠);查"引擎收到了哪些键"用is_key_down()/is_mouse_btn_down()。
外部指令控制 (on_custom_event + 参数解码 + print 回传)
on_custom_event 是与外界双向通信的核心入口。外部可通过 WebSocket 或 HID 发送文本指令,脚本解码参数执行,再用 print() 回传结果。无需 enable_listen — 只要定义了 on_custom_event 就会触发。
lua
-- 指令格式:
-- "tap(540,1200)" → 在 (540,1200) 点一下
-- "swipe(100,800,900,800)" → 从 (100,800) 滑到 (900,800)
-- "getpos" → 返回当前 vmouse 位置
function init()
scale_x, scale_y = get_scale()
end
function on_custom_event(str)
-- 解码 "tap(x,y)" 指令
local sx, sy = str:match("tap%((%d+),(%d+)%)")
if sx then
local x, y = tonumber(sx), tonumber(sy)
local id = touch_down(x * scale_x, y * scale_y)
if id ~= 0xFF then
touch_up(id)
print("tap ok: (" .. x .. "," .. y .. ")")
else
warn("tap fail: no free touch point")
end
return true
end
-- 解码 "swipe(x1,y1,x2,y2)" 指令
local x1, y1, x2, y2 = str:match("swipe%((%d+),(%d+),(%d+),(%d+)%)")
if x1 then
local id = touch_down(tonumber(x1) * scale_x, tonumber(y1) * scale_y)
if id == 0xFF then
warn("swipe fail: no free touch point")
return true
end
touch_move(id, tonumber(x2) * scale_x, tonumber(y2) * scale_y)
touch_up(id)
print("swipe ok: (" .. x1 .. "," .. y1 .. ") -> (" .. x2 .. "," .. y2 .. ")")
return true
end
-- 查询指令 "getpos"
if str == "getpos" then
local mx, my, show, down = get_vmouse()
print("vmouse: x=" .. mx .. " y=" .. my .. " show=" .. tostring(show) .. " down=" .. tostring(down))
return true
end
warn("unknown cmd: " .. str)
return true
end发送端示例:
bash
# WebSocket CLI
echo -ne '\x01tap(540,1200)' | websocat ws://192.168.73.1:80/ws# HID OUT 帧格式 (CMD 0xFA)
[55 AA][0D][FA]tap(540,1200)WS 路径最大 128 字节,HID 路径最大 60 字节。超出截断。
BOOTSEL 按键 (on_boot_button)
板载 BOOTSEL 按键可作为脚本输入:短按/长按识别、无外接按键实现运行时切换。on_boot_button 是按下/松开边沿触发,"按住期间"的状态要用 tick + 计时标志。
lua
-- 短按 BOOTSEL → 循环切换配置槽位 (0-8);长按 1 秒 → LED 变红提示 (松开恢复)
local press_us = nil -- 已按下的持续时间 (µs), nil = 未按下
local long_fired = false
function on_boot_button(pressed)
if pressed then
press_us = 0
long_fired = false
else
if press_us and not long_fired then
-- 短按: 切到下一个槽位, 效果同槽位热键(修饰键+数字行 1~9)
set_current_slot((get_current_slot() + 1) % 9)
end
press_us = nil
led_off()
end
end
function tick(dt_us)
if press_us then
press_us = press_us + dt_us
if press_us >= 1000000 and not long_fired then
long_fired = true
set_led(255, 0, 0) -- 长按 1s: LED 变红作为反馈
end
end
end事件由 core0 消抖后经 IPC 投递 (约 20–30ms 延迟),按下与松开各触发一次
on_boot_button。无需enable_listen— 只要定义了此函数就会触发。
触摸板 (on_touchpad)
手柄触摸板(DS5 / DS Edge 及将来的触摸板设备)的瞬时快照通道:每次触点数据变化时调用一次,上报全部槽位的当前状态。无需 enable_listen(未定义此函数时零开销),无拦截语义(返回值被忽略)。
lua
-- meta 位域:bit0-9 = 触点 i 在位|bit10-13 = 设备最大点数|bit14-19 = 来源枚举(0=DS5/DS Edge)|其余保留
-- dims 位域:bit0-15 = 宽 W|bit16-31 = 高 H(DS5 为 1920 | 943<<16)
-- x0..y9 : 各槽位坐标,**触摸板原始分辨率**(固件零换算,宽高比保真);
-- 无效槽位恒 0。设备拔出时补发一帧全零快照("设备已离开"信号)。
function on_touchpad(meta, dims, x0, y0, x1, y1, x2, y2, x3, y3,
x4, y4, x5, y5, x6, y6, x7, y7, x8, y8, x9, y9) end说明:
- 固件对快照逐字节去重:与上次分发的快照完全相同就不调用——手指静止时不会刷屏;反过来,滑动的每一帧都会进来(约等于手柄报文率),回调里保持轻量。
- 脚本可以只声明需要的参数(尾部多余的槽位实参被 Lua 自动丢弃),要全收就用
...。 - 触摸板的物理按压(Pad Click)不在此通道——它走
on_gamepad_btn的BTN_MISC(0x0E)。 - 调试建议:先用
print(string.format("%d %d %d,%d", meta, dims, x0, y0))看原始流,确认去重与坐标行为后再往上叠逻辑。
例 1:光标控制(等同鼠标输入)
按槽位差分出位移,注入 input_mouse_move——映射关闭时移动虚拟光标,映射开启时移动视角(与真实鼠标同管线):
lua
-- 触摸板 → 相对鼠标(笔记本触摸板手感)。DS5 有两个槽位,这里只用槽 0。
local prev_x, prev_y = nil, nil
local SENS = 3 -- 灵敏度倍率(原始分辨率 1920×943 的坐标差直接乘)
function on_touchpad(meta, dims, x0, y0)
if meta & 0x1 ~= 0 then -- 槽 0 有手指
if prev_x then
input_mouse_move((x0 - prev_x) * SENS, (y0 - prev_y) * SENS)
end
prev_x, prev_y = x0, y0
else
prev_x, prev_y = nil, nil -- 抬起:清基线,防止下次落指跳变
end
end例 2:双指捏合 / 张开(简单手势)
手势开始记两指距离,任一指抬起时比较起止距离——一个手势只报一次,不用计时器:
lua
-- 距离用平方值即可比大小;阈值"×4"= 距离拉大一倍 / 缩小到一半
local d0, d1, seeing = 0, 0, false
function on_touchpad(meta, dims, x0, y0, x1, y1)
if meta & 0x3 == 0x3 then -- 两指都在
local dx, dy = x0 - x1, y0 - y1
local d = dx * dx + dy * dy
if not seeing then d0 = d end -- 手势开始时的距离
d1 = d
seeing = true
elseif seeing then -- 任一指抬起 = 手势结束
if d1 > d0 * 4 then
print("张开") -- 这里换成任意动作
elseif d1 * 4 < d0 then
print("捏合")
end
seeing = false
end
end例 3:触摸板观测(两个坐标 + 触摸板按压)
最基础的调参脚本:触点坐标和物理按压直接打印到日志页,粘贴即可运行:
lua
-- 触点坐标:固件已去重,变化即来;全部抬起时打印一次"抬起"
function on_touchpad(meta, dims, x0, y0, x1, y1)
local c = meta & 0x3FF
if c == 0 then print("TP 抬起")
elseif c == 1 then print(string.format("P1=(%d,%d)", x0, y0))
elseif c == 2 then print(string.format("P2=(%d,%d)", x1, y1))
else print(string.format("P1=(%d,%d) P2=(%d,%d)", x0, y0, x1, y1))
end
end
-- 触摸板按压(Pad Click)是手柄按键 BTN_MISC(0x0E),走按键边沿
enable_listen_gamepad_btn(0x0E)
function on_gamepad_btn(code, pressed)
if code == 0x0E then print(pressed and "TP 按下" or "TP 松开") end
end空模板
lua
function init() end
function on_key(keycode, down) return false end
function on_mouse_btn(button, down) return false end
function on_custom_event(str) return false end
function on_boot_button(pressed) end
function tick(dt_us) end上传与调试
上传项目
面板「Lua 工作台」里编辑的是整份多模块项目:
- 点击加载到内存 → 整份项目经
POST /lua/fs/image一次性写入设备 RAM,立即热重载生效; - 调试完成后,点击写入存储 →
POST /lua/save把 RAM 镜像持久化到 flash(不重启,RAM 内早已生效),断电不丢。
HTTP 层面对应:
| 端点 | 说明 |
|---|---|
GET /lua/fs/image | 读回当前 RAM 里的整份项目镜像(二进制) |
POST /lua/fs/image | 整镜像流式写入并热重载(未激活 403) |
POST /lua/save | 把 RAM 镜像持久化到 flash |
GET /lua/info | flash 存储元信息(kind=fs 为多模块格式) |
GET /lua/get、POST /lua/set | 旧单脚本接口已退役:恒返回 410 Gone,请改用 /lua/fs/image |
查看状态
GET /lua/state 返回运行状态:
loaded=1 source=modules modules=3→ 多模块项目装载成功(modules=实际装载的模块数)。loaded=0 source=default error=...→ 全部模块失败,设备回落默认模板;error=是首个失败的报错(文本自带模块.lua:行号,面板日志里可点击跳转)。entry=on_key tick ...→ 聚合后实际安装的入口函数 (确认函数名没写错)。
运行期日志
脚本里的 print(...) / warn(...) 统一经 WebSocket 调试通道下发。连接 WS 客户端即可实时查看。print 带 [lua] 前缀,warn 带 [lua warn] 前缀。多模块下 print 不带模块名——建议脚本自己加前缀区分来源(如 print("[aim] ..."));报错则自带 模块.lua:行号。
运行期日志
脚本里的 print(...) / warn(...) 统一经 WebSocket 调试通道下发。连接 WS 客户端即可实时查看。print 带 [lua] 前缀,warn 带 [lua warn] 前缀。
双向通信:
warn是脚本主动向主机推送消息的唯一通道。配合on_custom_event接收指令,可构建 "主机发指令 → Lua 执行 → print 回报结果" 的完整交互。
常见坑
- 定义了
on_key却没enable_listen_keys→ 永远不触发。必须声明监听。 - 在工具/库模块里随手
disable_listen_*→ 监听位图是全局的,会把其它模块注册的监听一起清掉(无参清空全部)。只 enable、基本不 disable;想"不接管"就用返回false表达。 - 以为模块之间能互相看到函数 → 每个模块独立
_ENV,顶层local/函数/全局写都在自己模块里;共享要显式_G.xxx = ...,读穿透_G。 - 重模块挤压轻模块 → 同一次事件的聚合调用共享 10ms VM 预算,某个模块的回调做重活会挤压同事件里排在后面的模块。
print分不清是哪个模块说的 → 输出不带模块名,自己加前缀;报错自带模块.lua:行号。- 以为轮盘方向键 (默认 W/A/S/D) 被"提前吃掉" → 不是。这四个键与疾跑键和其它键完全一样:先进
on_key,返回false才轮到 core 驱动轮盘 (顺序见「拦截语义」)。脚本收不到它们,只可能是漏了enable_listen_keys(0x1A, 0x04, 0x16, 0x07);反过来on_key返回true时轮盘不会动。 - 把缓存值写成
init()内部的local→on_key/tick看不到。要写全局或文件顶层local。 - 忘了
touch_up→ 触点 (最多 10) 很快耗尽,touch_down返回0xFF。 - 在
on_key/tick里做重活 → 输入卡顿。 - 当成 64 位整数用 → 实际 32 位,大数溢出。
- 想"按住持续触发"却只写
on_key→ 边沿只触发一次;用tick+down标志。 - 多个键共用一个触点变量 → 会串。用
touches[keycode]表分开管理。 on_custom_event里忘了return true→ core 虽无默认处理,但返回false表示 "未处理"。- 字符串超上限 →
on_custom_event的str会被截断 (WS:128B / HID:60B)。长指令用简短格式或拆分发送。 - 手柄 SELECT+RS 组合键 → 同时按下 SELECT + RS (右摇杆按下) 会切换映射开关(与槽位切槽热键不同,这是开/关切换)。切换时虚拟光标自动显示/隐藏。此行为在
on_gamepad_btn之前执行,Lua 无法拦截这个组合键。