资讯动态

Serial Studio Lua 数据集变换(Per-Dataset Value Transform)开发指南

发布时间:2026/9/17 19:52:33 来源:尧图企业网站定制
Serial Studio Lua 数据集变换Per-Dataset Value Transform开发指南【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio本文面向需要在 Serial Studio 中为数据集Dataset编写 Lua 变换Transform的开发者系统讲解变换函数的运行契约、虚拟数据集Computed vs Regular的取舍、共享表 API、帧元数据、设备写入与仪表盘控制等完整能力并结合仓库源码给出实现级佐证与可直接复用的实战示例。读完本文你将能够为 UART、BLE、MQTT、Modbus、CAN Bus 等任意数据源的数据集编写单元换算、EMA 平滑、校准、迟滞、积分、闭环控制等变换逻辑并理解其在帧流水线中的执行方式。概述为什么需要 Lua 变换Serial Studio 的帧构建FrameBuilder流水线允许为每个数据集挂载一段变换代码在每一帧到达时对解析出的原始值做二次加工。官方提供了 JavaScript 与 Lua 两种脚本语言实现同一套变换 API本文聚焦 Lua 版本对应 JS 版本见 transform_js.md。当你的团队更熟悉 Lua、或者希望利用闭包捕获的局部状态语义例如local ema 0跨帧累积时选择 Lua 是更自然的方式。需要说明的是文档层面的设计目标是Lua 5.4 API 镜像而仓库实际内嵌的执行引擎是LuaJIT见 TransformCompiler.cpp 顶部的#include luajit.h并通过 LuaCompat 垫片补齐兼容性。因此编写时以 Lua 5.1 语法与 LuaJIT 扩展库如bit为准兼容层细节见后文。从源码结构看每个 (source, language) 组合对应一个共享的lua_State引擎数据集变换按 source 分组编译进同一状态由 TransformCompiler.h 中的TransformEngine管理单次调用预算由kTransformWatchdogMs 100毫秒限制见 TransformCompiler.h。核心契约Contract每个变换必须导出一个名为transform的全局函数function transform(value) return value endvalue是 Lua 数值对于字符串数据集则是字符串返回值必须是有限数值finite number。若返回NaN/Inf或任何非数值类型流水线会静默回退到原始值不抛错、不打日志。从源码看变换执行期间的 Lua 错误会调用 TransformCompiler.cpp 中的noteTransformError()记录到错误统计同时该数据集本帧使用原始值。因此错误不应被当作控制流手段详见错误处理小节。Computed 与 Regular 数据集virtual标志的取舍只有当下述条件同时成立时才应在数据集上设置virtual: true该变换没有解析器parser提供的value输入它的输出完全由兄弟数据集、表格table或常量构建例如Power Voltage * Current。换句话说只要变换体内部引用了value参数如单位换算km/h m/s * 3.6、EMA 平滑、标定、死区过滤就必须保持其为普通数据集regular不要动virtual标志。仓库侧对应的属性为changeDrivenTransforms可选启用当其为真时一个计算computed数据集的变换在它所读取的表变量/数据集都没有变化的帧上会被跳过输入依赖发现是自动完成的。该属性在 ProjectModel.h 中声明为Q_PROPERTY(bool changeDrivenTransforms ...)并在 test_change_driven_transforms.py 中有完整的集成测试覆盖包括属性往返序列化、缺省为 false、以及虚拟数据集变换写入 Computed 表变量并统计运行次数的观测路径。隔离机制Isolation每个数据集的 chunk 都加载在**独立的 per-dataset 环境表_ENV沙箱**中因此即便是全局变量——包括function transform本身——在每个数据集之间也是互相私有的顶层local声明则额外享有 chunk 私有性。两个数据集各自定义local ema 0会得到相互独立的状态尽管底层的 Lua 状态在整个数据源source范围内是共享的。local ema 0 local alpha 0.2 function transform(value) ema alpha * value (1 - alpha) * ema return ema end源码实现佐证openSafeLibsForTransform()TransformCompiler.cpp只打开_G、table、string、math、bit五个安全库并显式移除dofile、loadfile、load三个危险全局同时删除string.dump其字节码序列化配合 loader 是经典的沙箱逃逸向量ffi与jit库从不打开。这意味着变换脚本只能访问受控 API不能触碰宿主文件系统或执行任意代码。共享表 APIShared-table API以下函数由宿主注入到每个 Lua 状态中对应实现见 TableScriptBridge.cpp 中的 C 闭包注入段tableGet(tableName, registerName) -- - number | string tableSet(tableName, registerName, v) -- 仅用户表user tables tableHandle(tableName, registerName) -- - handle (number)或 -1加载时解析一次 tableHandleMany(tableName, registers) -- - table of handles tableGetH(handle) -- 按 handle 读取快路径无名称查找 tableSetH(handle, v) -- 按 handle 写入仅 Computed 变量 datasetGetRaw(uniqueId | alias) -- 本帧原始值 datasetGetFinal(uniqueId | alias) -- 更早数据集的最终值参数解析uniqueId与aliasdatasetGetRaw/datasetGetFinal同时接受数值uniqueId或字符串alias字符串参数永远是 alias数值永远是 uniqueId不存在隐式转换。未知的 alias 返回nil并产生一次性警告与未知 uniqueId 的行为一致。源码中的luaDatasetSelector()见 TableScriptBridge.cpp负责在 Lua 线程上区分这两种参数形态。alias 是用户在 Project Editor 中为数据集设置的可选、唯一、自定义名称它能在uniqueId重编号后保持稳定并镜像为__datasets__变量raw:alias/final:alias。下例中的datasetGetRaw(dt_ms)就是通过 alias 读取兄弟数据集。镜像开关的探测规则只要在变换、共享库shared library或解析器脚本中任意一处命名了上述助手函数之一tableGet、tableSet、tableHandle、tableHandleMany、tableGetH、tableSetH、datasetGetRaw、datasetGetFinal就会打开逐帧raw:/final:镜像若整个项目一个都不引用则完全跳过镜像避免无谓开销。运行时拼装的名字如_G[table .. Get]不会被探测到。这一探测逻辑对应 TransformCompiler.cpp 中对变换库与每个数据集变换代码的ScriptApiCall::referencesTableApi()扫描。Handle 快路径如果变换每次调用都命中同一批变量应在顶层local中一次性解析 handle然后用tableGetH/tableSetH替代按名查找local hOffset tableHandle(Calibration, offset) local hScale tableHandle(Calibration, scale) function transform(value) local offset tableGetH(hOffset) or 0 local scale tableGetH(hScale) or 1 return (value - offset) * scale end过期 handle表格定义被编辑之后是安全的 no-op——脚本在下次加载时会重新解析。从 TableScriptBridge.cpp 的注释看tableGetH对 stale 或非法 handle 返回 niltableSetH则直接忽略非 Computed / stale / 非法 handle因此无需显式防御。表格寻址与变量语义位于文件夹内的表格以其完整路径寻址父文件夹标题用/连接再接表格名例如Telemetry/BMS/State顶层表格直接使用裸名。该字符串同样作为project.dataTable.*API 命令的table参数project.dataTable.list会报告每个表格的path。注意路径使用的是文件夹标题因此移动或重命名文件夹会改变路径并使任何针对旧路径解析的 handle 失效。用户表变量分两类Constant常量项目加载时设定不可写Computed计算可从变换中写入。Computed 变量无限期保留最后一次写入的值没有逐帧重置——这正是它们适合承载滤波状态、积分器、边沿计数器和锁存标志的原因。变量的defaultValue是项目加载时的起始值而不是周期性重置值。兼容垫片LuaCompat与解析器 API 相同的兼容层实现见 LuaCompat.hmath.log10(x)与math.pow(a, b)别名完整的bit32库unpack(t)作为table.unpack(t)的别名。这保证了从 Lua 5.2 习惯迁移到 LuaJIT5.1 语义的脚本可以无痛运行。控制台日志Console Logging与解析器相同的 APIprint(...)以及console.log/debug/info/warn/error都会落入应用控制台。其中error总是触发应用通知warn仅在启用Route warnings to notifications时通知默认关闭。脚本日志行还会通过应用的消息处理器镜像到 stdout。由于变换在每一帧对每个数据集都会执行日志必须用局部标志锁存或做频率限制发布型项目中应删除日志调用。实战示例以下示例均来自 transform_lua.md可直接粘贴到数据集变换编辑器中。EMA 平滑local ema 0 local alpha 0.2 function transform(value) ema alpha * value (1 - alpha) * ema return ema end标定Calibrationfunction transform(value) local offset tableGet(Calibration, offset) local scale tableGet(Calibration, scale) return (value - offset) * scale end跨数据集计算function transform(dx) local dt datasetGetRaw(dt_ms) if dt and dt 0 then return (dx / dt) * 1000 end return 0 end迟滞Hysteresislocal last 0 local threshold 0.05 function transform(value) if math.abs(value - last) threshold then last value end return last end基于 Computed 变量的会话级积分器-- Computed 变量 Trip.litresUseddefaultValue 0跨帧持久 -- 因此累计总量在整个会话中持续累加。 function transform(litresPerHour, info) if not info then return tableGet(Trip, litresUsed) or 0 end local prevTs tableGet(Trip, lastTsMs) or info.timestampMs local dtMs info.timestampMs - prevTs tableSet(Trip, lastTsMs, info.timestampMs) local delta (litresPerHour / 3600.0) * (dtMs / 1000.0) local total (tableGet(Trip, litresUsed) or 0) delta tableSet(Trip, litresUsed, total) return total end性能要点Lua 在调用边界上非常快。热路径中应避免string.format除非预期会失败否则避免pcall能使用算术就优先于查表。此外宿主为每次数据集变换设置了 100ms 的看门狗kTransformWatchdogMs见 TransformCompiler.h并与LuaDeadlineHook绑定TransformCompiler.cpp超时/失控脚本会被中断配合 tst_lua_deadline_hook.cpp 与 tst_lua_compat.cpp 的测试保障变换不应试图执行长时间循环。帧元数据第二个info参数变换函数可声明第二个参数以获取当前帧元数据function transform(value, info) -- info.frameNumber : integer每数据源计数器从 1 开始单调递增 -- info.sourceId : integer -- info.timestampMs : integer单调毫秒steady clock非墙钟 end已有的单参数变换无需改动即可继续工作Lua 会忽略多余实参。timestampMs是单调计数器只应用于求差值delta不要用作绝对时间。宿主在编译期通过luaTransformAcceptsInfo()做 arity 探测见 TransformCompiler.cpp判断该变换是否接受info从而决定是否传入第二参数。典型用途——按时间节流设备写入local lastTs 0 function transform(v, info) if info.timestampMs - lastTs 100 then lastTs info.timestampMs deviceWrite(PING\n) end return v end触发项目动作actionFire()actionFire(actionId) - { ok true } | { ok false, error ... }按稳定的actionId不是索引触发项目中的既有动作复用该动作预构建的 payload、编码与定时器模式。调用会记录日志[actionFire] idN indexM ok。宿主通过ActionFireApi::installLua()在 Lua 引导阶段注入TransformCompiler.cpp。设备输出deviceWrite()deviceWrite(data, sourceId?) - { ok true } | { ok false, error ... }data是 Lua 字符串8-bit 干净可携带任意二进制sourceId可选缺省为数据集所属的数据源同步、fire-and-forget不会抛异常调用记录为[deviceWrite] sourceN bytesM writtenK。它用于闭环控制根据传感器值计算设定点并在同一帧写回设备。注入实现见 DeviceWriteApi.cpp。local kp 4.0 function transform(temperature) local sp tableGet(Control, setpoint) or 25.0 local pwm math.max(0, math.min(255, kp * (sp - temperature) 128)) deviceWrite(string.format(PWM%d\n, math.floor(pwm 0.5))) return temperature end注意变换在每帧都会执行因此重复动作必须用局部标志锁存或用计数器做频率限制避免饱和链路。仪表盘控件Dashboard Controls七个 UI 助手函数全部返回{ ok true }或{ ok false, error ... }且均不写日志。同样需要用局部标志锁存使每次调用只在状态迁移时触发一次而不是每帧触发clearPlots() -- 清空 plot/multiplot/FFT/GPS/3D/waterfall setPlotPoints(n) -- 水平采样窗口n 1 setTerminalVisible(visible) -- bool setNotificationLogVisible(visible) -- bool setClockVisible(visible) -- bool setStopwatchVisible(visible) -- bool setActiveWorkspace(idOrName) -- workspaceIdint 1000或标题示例设备重启哨兵值 9999出现时清空绘图历史让新一次启动的图像干净绘制function transform(value) if value 9999 then clearPlots() return 0 end return value end这些调用只影响当前活动的仪表盘窗口不会修改项目文件或用户持久化偏好。错误处理Lua 错误会记录一条看门狗警告本帧改用原始值返回非数值会静默回退不要依赖错误做控制流。错误统计累计次数、最后出错的 uniqueId 与消息由TransformCompiler维护可通过errorCount()、lastErrorDataset()、lastError()查询见 TransformCompiler.h供 1Hz 诊断拉取。若需要进一步了解变换与解析器、JS 变换、表达式变换在帧构建流水线中的编排关系可继续阅读 frame_parser_lua.md 与 transform_js.md。【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价