Serial Studio 数据集值变换(Dataset Transforms)完全指南:Lua / JavaScript / Expression 三语言标定、滤波与闭环控制
【免费下载链接】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 Value Transforms)功能的完整技术指南。该功能允许为每个数据集定义可选的transform(value)函数,在原始解析值进入仪表板之前完成标定、单位换算、滤波与信号调理。读完本文,你将掌握三种变换语言(Lua、JavaScript、Expression)的完整语法与函数表、跨帧持久状态、共享表读写 API、deviceWrite()闭环控制、actionFire()动作触发,以及 34 个内置模板的选型与使用,并能据此在实际项目中落地传感器标定、噪声抑制与自动控制等场景。
概述:变换在数据管线中的位置
帧解析器parse(frame)产出一个原始值数组,每个值通过Frame Index映射到对应数据集。在值到达仪表板之前,一个可选的变换函数可以修改它:
parse(frame) → Raw Value → transform(value) → Dashboard变换的典型用途:
- 设备发送原始 ADC 计数,需要标定(斜率 + 偏移);
- 需要单位换算(摄氏度到华氏度、弧度到度);
- 信号有噪声,需要滤波(滑动平均、EMA、低通);
- 需要派生值(变化率、累计值、dB 转换);
- 传感器非线性需要多项式校正或查表。
变换是可选的,未定义变换的数据集直接显示原始解析值。变换还可以读写共享表(shared tables)——项目期定义的常量与跨帧保持的计算变量,这正是把标定值同时传给多个通道、让一个变换计算的值被另一个消费、或为积分器/滤波器/锁存器保持状态的机制。完整参考见 Data-Tables.md(即 Variables 文档)。
transform()函数签名与输入输出
Lua:
function transform(value) return value * 0.01 + 273.15 endJavaScript:
function transform(value) { return value * 0.01 + 273.15; }输入:value参数是已从帧解析并按 Frame Index 映射到本数据集的原始值。数值型解析值以浮点数到达(Luanumber/ JSnumber),非数值则以字符串到达(Luastring/ JSstring)。非数值数据集的值仍以字符串形式传入变换;若变换返回数字,数据集变为数值型;若返回字符串,则保留字符串值。
输出:函数返回一个数字,或对文本数据集返回字符串(返回的字符串被保留为数据集值)。返回值会替换所有位置的原始值:仪表板控件、绘图、CSV 导出、MDF4 导出以及 API。
错误与回退:若函数返回nil(Lua)、undefined/NaN/Infinity(JS),或发生错误,原始值保持不变,数据流不被中断。JS 异常与 Lua 错误都会向应用日志写入一条带错误信息和数据集 ID 的警告;只有非错误的回退情形(nil/undefined/NaN/Infinity)是静默的。
Expression 变换:无脚本引擎的算术表达式
语言下拉框中还有第三种语言:Expression。它没有函数、没有语句、没有脚本引擎,编辑器只保存一个算术表达式,其值即为数据集的新读数:
v * 0.01 + 273.15表达式被编译为一个不分配内存的扁平程序(postfix 栈机),是标度、限幅与通道组合这类常见场景下成本最低的选项。需要状态、多行分支、表访问或deviceWrite时,改用 Lua 或 JavaScript。以#开头的行是注释,一直注释到行尾。
Expression 输入
| 名称 | 含义 |
|---|---|
v | 本样本的原始值 |
t | 帧时间戳(秒),基于应用单调时钟(与 JS 变换的info.timestampMs同源)。流启动时不是从零开始:计算经过时间请用dt,或减去首个样本的t |
dt | 距上一个样本的秒数 |
n | 样本索引,从 0 开始 |
pi、e、nan、inf | 常量 |
同一数据源的其它数据集通过其Script Alias读取——即datasetGetRaw和 API 接受的同一标识符。数据集标题不可用于解析:标题是显示文本、可随意更改,而别名是稳定的。
v * shunt_current没有别名的数据集通过其dataset ID寻址,并用花括号包裹(因为裸数字是数值常量):
v * {12}花括号也可以承载包含空格的别名。若项目定义了本身全是数字的别名,则别名优先于同数字的 ID。兄弟数据集持有其最新发布值,因此在树中较后处理的数据集会贡献其上一帧的样本值——请在 Project Editor 中据此安排数据集顺序。
sample(name, k)读取某数据集往前k个样本的值,最多回溯 256:
v - sample(shunt_current, 1)名称可以是裸写或花括号包裹,但绝不能加引号。
Expression 的共享表访问
表达式通过table(name, variable)读取表变量,与 Lua/JS 的tableGet访问的是同一批常量与计算变量:
v * table(calibration, voltage_scale)两个名称均可裸写,含空格时用花括号。变量在表达式编译时解析为句柄,因此每次样本读取只是一次数组索引查找;未知变量是编译错误,并会指出是哪个"表.变量"对。该访问是只读的——从表达式写变量会引入执行顺序依赖,那是 Lua/JS 变换配tableSet的用武之地。计算变量带有与兄弟数据集相同的处理顺序警告:表达式看到的是最后一次写入的值,若该变量在本帧较晚位置才被写入,则看到的是上一帧的值。
table()仅在帧管线上可用。运行 stream 通道的数据源(如音频等密集数据源,或强制开启 stream 通道的数据源)上,使用table()的表达式会编译失败,因为变量存储属于另一线程,逐样本跨线程读取不在支持范围内。
Expression 运算符与函数
运算符(从最松到最紧):||、&&、==!=、<<=>>=、+-、*/%、^(右结合)、一元-和!,以及条件condition ? a : b。
| 元数 | 函数 |
|---|---|
| 一元 | absfloorceilroundsqrtcbrtexploglog10log2sincostanasinacosatansinhcoshtanhdegrad |
| 二元 | minmaxpowatan2hypot |
| 三元 | clamplerp |
log是自然对数,deg与rad在弧度与度之间转换。从源码看,这一函数表在 ExpressionTransform.cpp 中以FunctionSpec表的形式实现,每个函数直接映射到 IEEE double 实现(如clamp(a, lo, hi)即fmin(fmax(a, lo), hi)),与文档表格完全一致。
Expression 示例
# Scale and offset an ADC reading v * 0.01 + 273.15# Limit to a range clamp(v, 0, 100)# Ratio of two channels, guarding the divide bus_voltage == 0 ? 0 : v / bus_voltage# Difference from one sample ago v - sample(flow, 1)Expression 限制
一个表达式最多编译 512 个操作(expression too long),历史最多回溯 256 个样本。编译错误只上报一次,此后数据集发布其原始值。这些上限在源码中即 ExpressionTransform.h 的kMaxHistory = 256、kMaxNodes = 512、kMaxStackDepth = 64常量。Expression 的 Template 下拉框被禁用:内置模板都是 Lua 与 JavaScript 代码,没有 Expression 等价物。
跨帧持久状态
在变换代码顶层(transform()函数之外)声明的变量在帧与帧之间持久存在——这正是滤波器、累加器等有状态变换保持状态的机制。
关键规则:在文件顶部使用local(Lua)或var(JavaScript)。不要依赖裸全局变量。Serial Studio 隔离每个数据集各自的顶层状态,因此两个使用同一模板的数据集(例如两条通道上的两个 EMA)不会互相覆盖变量。
Lua:在文件顶部声明local:
local alpha = 0.1 local ema function transform(value) if ema == nil then ema = value end ema = alpha * value + (1 - alpha) * ema return ema endalpha与ema是 chunk 局部变量。Lua 将它们捕获为transform闭包的上值(upvalue),因此在调用之间存活,且对该数据集私有。同一数据源上另一个带自己local ema的数据集不会看到或覆盖此值。
JavaScript:在文件顶部声明var:
var alpha = 0.1; var ema; function transform(value) { if (ema === undefined) ema = value; ema = alpha * value + (1 - alpha) * ema; return ema; }Serial Studio 在编译时把每个 JS 变换包裹进一个 IIFE((function(params) { ... })(...),见 TransformCompiler.cpp),因此顶层var声明被限定在该数据集的闭包内,而不是共享引擎的全局对象上——与 Lua 获得同样的隔离,无需额外操作。
不要这样做
JavaScript 中要避免裸全局:隐式全局会写入共享引擎的全局对象,与同一数据源上的其它数据集冲突。Lua 中每个数据集 chunk 有自己私有的环境,裸全局保持每数据集隔离、不会冲突;但为了清晰并遵循 JS 规则,仍推荐声明local:
-- Lua: this works (ema is isolated to this dataset), but local is clearer function transform(value) ema = ema or value ema = 0.1 * value + 0.9 * ema return ema end// WRONG in JavaScript: ema without var is an implicit global // on the shared engine, so another dataset with the same mistake // would clobber it. Always declare var at the top. function transform(value) { if (typeof ema === 'undefined') ema = value; ema = 0.1 * value + 0.9 * ema; return ema; }始终在文件顶部用local/var声明有状态变量,这也让代码更易读:扫一眼文件顶部即可看出哪些变量携带状态。
辅助函数
JavaScript 中,在文件顶部定义的辅助函数(如function clamp(x, lo, hi) { ... })同样被 IIFE 闭包捕获、每数据集私有,可以放心使用。Lua 中请用local function定义辅助函数以共享隔离:
local function clamp(x, lo, hi) if x < lo then return lo end if x > hi then return hi end return x end function transform(value) return clamp(value, 0, 100) endLua 中在 chunk 顶层写普通function foo() end定义的是全局,但该全局仍留在本数据集私有环境中,不会与其它数据集冲突;不过出于清晰起见仍推荐加local function前缀。
计算型数据集(Computed datasets)
数据集可以在 Project Editor 中标记为computed。计算型数据集没有 Frame Index:传入帧不为其提供任何数据,其值完全由transform()函数计算,典型方式是读取其它数据集或表变量。此时传入的value参数恒为0:
function transform(value) local a = datasetGetFinal(10) -- reads the final value of dataset with unique ID 10 local b = datasetGetFinal(11) return (a + b) / 2 -- average of two channels end计算型数据集用于派生指标(平均值、比率、总和、占总量的百分比),它们应显示在仪表板上并随原始通道一起导出,但不存在于线格式(wire format)中。
自动检测:不必手动勾选复选框——项目加载时,任何变换代码从不引用自由标识符value(区别于something.value或obj:value)的数据集都会被自动标记为 computed,其 Frame Index 字段随即变灰。若希望数据集保持由传入帧驱动,请确保变换至少读取一次value。
状态何时重置
持久状态(包括顶层 Lua/JS 上值以及计算型表变量)在以下情况被清除:
- 设备断开(变换引擎被销毁);
- 用户在变换编辑器中点击Apply(引擎用全新状态重新编译);
- 项目重新加载或保存变更。
因此滤波器和累加器在每次新连接会话时从零开始,这通常是期望的行为。在连接会话内,计算型变量与变换上值可无限期保持值,不会在帧之间被清除。
共享库与数据集参数
大型项目会在许多数据集中重复同一公式:例如 40 个 RTD 通道,每个都携带一份相同的转换,仅三个常量不同。两个项目级特性可消除这种复制。
共享库(Shared libraries)
项目可以持有一个 Lua chunk(项目内所有 Lua 变换都可调用)和一个 JavaScript chunk(项目内所有 JS 变换都可调用)。两者都位于 Project Editor 树顶部的Project Scripts下、Control Loop 旁边,即Lua Library与JavaScript Library;变换编辑器也有一个Open Lua Library/Open JavaScript Library按钮,可直接跳到当前语言对应的库。API 同样暴露它们(project.transformLibrary.get、project.transformLibrary.set、project.transformLibrary.dryRun,各带可选的language参数lua或js)。
-- Shared library function rtd(raw, p) return (raw * p.scale + p.offset) / p.r0 end该 chunk 在数据集变换编译之前运行一次,写入每个数据源变换状态的全局表。其顶层函数通过普通全局查找对所有 Lua 变换可见;变换自己的local值仍对该变换私有。由于库在编译期求值,调用库函数与调用本地辅助函数成本相同。
JavaScript 库工作原理相同:它被求值一次到每个数据源变换引擎的全局对象中,其顶层函数对该数据源的所有 JS 变换闭包可见。每种库只被同语言的变换看到;Expression 变换没有可调用的函数。
若库编译或运行失败,变换仍可在没有它的情况下编译:调用缺失函数的数数据集回退到其原始值,失败出现在 Problem Center 的"the shared Lua library"或"the shared JavaScript library"条目下。修复库后,引擎会在 Apply 时重新编译,无需重新连接。从源码看,这一失败-回退路径由 TransformCompiler.cpp 中compileLuaLibrary对kTransformLibraryErrorId的错误记录实现。
数据集参数(Dataset parameters)
每个数据集可携带命名参数,其变换以params读取。在变换对话框的Parameters表中逐行添加名称与值,或通过 API(project.dataset.update {transformParams: {...}})。值是有类型的:数字文本变成数字,true与false变成布尔值,其它内容保持字符串。
-- Dataset transform: the same three lines on every RTD channel function transform(value) return rtd(value, params) end在一个数据集上设置transformParams为{"scale": 0.1, "offset": -40, "r0": 100}、在下一个上设置{"scale": 0.1, "offset": -40, "r0": 1000},两个通道共享一个公式,仅表格行不同。
params在 Lua 与 JavaScript 中均可用:Lua 中它是变换环境中的表(params.r0),JavaScript 中它是传给变换闭包的对象(params.r0)。两者都在编译期固定:编辑参数会重新编译变换。Expression 变换不读params;请改用表变量。
共享表 API(Table API)
Lua 与 JavaScript 变换有四个内置函数用于读写项目的共享表。Expression 变换改用table(name, variable)读取变量且不能写入。表的完整说明见 Data-Tables.md,本节从变换视角记录 API 表面。
| 函数 | 返回 | 用途 |
|---|---|---|
tableGet(table, reg) | number、string 或 nil/undefined | 读取用户定义变量 |
tableSet(table, reg, value) | 无 | 写入计算变量(常量只读) |
datasetGetRaw(uniqueId \| alias) | number、string 或 nil/undefined | 当前帧中任一数据集的原始(变换前)值 |
datasetGetFinal(uniqueId \| alias) | number、string 或 nil/undefined | 当前帧中已处理数据集的最终(变换后)值 |
Lua 与 JavaScript 的 API 完全相同。table与reg是字符串。数据集查找接受整数uniqueId(Project Editor 中每个数据集旁显示的数字)或数据集别名字符串(Script Alias字段)。数字参数总是 uniqueId,字符串总是别名,因此datasetGetRaw(128)与datasetGetRaw("128")是不同查找。未知别名返回nil/undefined并带一次性控制台警告,与未知 uniqueId 相同。
对每个值都访问同一批变量的变换,可把名字一次性解析为句柄并用tableGetH/tableSetH避免每次调用的名字查找。详见 SerialStudio-SDK.md 的 Fast table access with handles。
Lua 示例:用项目级标定因子缩放电压读数:
function transform(value) local k = tableGet("calibration", "voltage_scale") -- constant return value * (k or 1.0) endJavaScript 示例:同样思路。
function transform(value) { var k = tableGet("calibration", "voltage_scale"); return value * (k !== undefined ? k : 1.0); }写入计算变量:一个变换发布,另一个消费:
-- Dataset 10 (processed first): publish the total current function transform(value) tableSet("runtime", "total_current", value) return value end-- Dataset 20 (processed later): compute power from current × voltage function transform(value) local i = tableGet("runtime", "total_current") or 0 return value * i end处理顺序
变换按顺序应用:组按帧顺序,数据集按组内顺序。在单帧内:
datasetGetRaw(uid)只对当前数据集及其之前已处理的数据集返回本帧原始值。更靠后的数据集仍持有上一帧的原始值,因为原始值与最终值是逐数据集增量写入的,而非预扫描一次性写入;datasetGetFinal(uid)只对已变换的数据集有效(同组内靠前的数据集,或更早组的任何数据集);- 计算变量保持其最后写入的值,因此一帧中写入的值在下一帧仍可见,非常适合积分器、导数与锁存标志。若希望变量每帧从零开始,请在较早变换的顶部自行写重置值。
如果需要数据集 B 消费数据集 A 的最终值,请确保 A 在 Project Editor 树中位于 B 之前。
变更驱动变换(Change-Driven Transforms,可选开启)
默认情况下,每个数据集的变换在每帧都运行。对于大型表驱动项目——帧解析器写表变量、许多计算数据集各读一个变量——意味着一个设备的帧会重新运行每个数据集的变换,即使其数据未变化。
Change-Driven Transforms开关开启更快模式:计算数据集的变换仅在其读取的某个变量(或数据集)自上次运行以来发生变化时才运行;否则该帧保持其之前的输出。Serial Studio 自动发现每个变换读取了什么,因此脚本无需任何改动。仪表板两种模式下显示相同的值;该选项只是跳过未变化值的重算。
在 Project Editor 中开启:选择项目根节点,点击 Project Title 旁边的Settings按钮(扳手图标),在弹出框中切换Change-Driven Transforms(与 Time Range 和 Point Count 并列)。
该选项默认关闭,按项目保存。对于帧丢失严重的重型表驱动仪表板,请开启它。有两点须知:
- 逐样本滤波器(EMA、死区)此后按输入的真实新样本步进一次,而非按全局帧步进——这通常正是你想要的;
- 若计算数据集的输出依赖 Serial Studio 无法视为输入的东西——直接在变换内读取墙钟时间(
Date.now()、os.time())而非经表或frameInfo传入的值——请为该项目保持关闭,或让这类数据集保持非计算型,因为变换可能不会仅因时间流逝而重跑。
帧元数据:第二个frameInfo参数
每个变换都可以声明第二个参数,携带该值所属帧的元数据。单参数变换(function transform(value))继续工作且无额外成本:引擎在编译期检查每个变换的参数个数,不使用信息时完全跳过 info 表/对象的构建。这一"按参数个数探测"在源码中有明确实现——Lua 通过luaTransformAcceptsInfo读取nparams,JS 通过函数length属性判断(TransformCompiler.cpp 与 L553)。
function transform(value, info) -- info.frameNumber : integer, monotonic counter per source (starts at 1) -- info.sourceId : integer, the source the dataset belongs to -- info.timestampMs : integer, monotonic milliseconds (steady clock) return value endfunction transform(value, info) { // info.frameNumber : number // info.sourceId : number // info.timestampMs : number return value; }info.timestampMs是取自 OS 稳定时钟的单调毫秒计数器。它在相邻帧之间递增,但不是墙钟时间、也不匹配Date.now()。请用它计算差值(info.timestampMs - lastTs),而不是"现在几点"。info.frameNumber按数据源计数,断开或项目重载后从 1 重新开始。
示例:限制控制更新的频率
一个调整设备设定点的变换,无论帧率多高,每秒最多执行十次:
local lastTs = 0 function transform(temperature, info) if info.timestampMs - lastTs >= 100 then lastTs = info.timestampMs local sp = tableGet("Control", "setpoint") or 25.0 deviceWrite(string.format("SP=%.2f\n", sp)) end return temperature end示例:每 50 帧请求一次状态推送
function transform(value, info) { if (info.frameNumber % 50 === 0) deviceWrite("STATUS?\n"); return value; }写回设备:deviceWrite()
变换可以通过deviceWrite(data, sourceId?)向已连接设备发送字节。预期用途是闭环控制:读取传感器值、计算设定点或修正量,并一步推回设备。
签名
deviceWrite(data, sourceId?) data: string (Lua) / string 或 byte 数组 (JavaScript) sourceId: 可选数字;默认是变换所属数据集的源 returns: { ok = true } on success { ok = false, error = "..." } on failuredeviceWrite是同步、fire-and-forget的:它立即把字节推给驱动。它不阻塞等待回复,也不抛异常。任何失败都变成{ ok = false, error = "..." }。每次调用都会记录到应用日志:[deviceWrite] source=<id> bytes=<n> written=<n>,便于验证控制命令是否按预期发出。
Lua 示例:PWM 控制器
local kp = 4.0 local setpoint = 25.0 function transform(sensor_temp) local error = setpoint - sensor_temp local pwm = math.max(0, math.min(255, kp * error + 128)) deviceWrite(string.format("PWM=%d\n", math.floor(pwm + 0.5))) return sensor_temp end该变换原样返回原始温度(仪表板仍显示测量值),并在每帧把计算出的 PWM 占空比写回设备。
JavaScript 示例:报警锁存
let triggered = false; function transform(value) { if (!triggered && value > 100) { const r = deviceWrite("ALARM=1\n"); if (r.ok) triggered = true; else console.warn("alarm write failed:", r.error); } return value; }triggered上值保证报警条件锁存后deviceWrite不会在后续每帧重复触发。
定向到另一数据源
传入显式sourceId可写入不同数据源。当遥测来自一个数据源、设备的命令通道在另一个数据源时很有用:
function transform(value) if value < 5 then deviceWrite("REQ_FULL_REPORT\n", 0) -- ask source 0 for a full status frame end return value end失败模式
deviceWrite从不抛异常。可能的error值:
"device not connected or write failed":目标源没有活动驱动,或驱动的write()返回 0/负数;"deviceWrite: data is empty":负载为零字节;"deviceWrite: data must be a string"(Lua)/"... string or byte array"(JS);"deviceWrite: sourceId must be a number"。
何时不该用
- 用户点击的按钮或滑块请用Output Widget。变换每帧都运行;Output Widget 在用户操作时运行;
- 不要从计算数据集
deviceWrite,除非你理解其执行顺序。计算数据集像任何其它数据集一样按树顺序处理(并非自动最后处理),因此计算数据集只能看到它之前数据集的最终值。请把它放在输入之后。它仍然每帧触发; - 不要每帧写大负载。采集管线的变换阶段与数据源内所有数据集共享;频繁的
deviceWrite会饱和链路。
触发动作:actionFire()
变换还可以触发项目中已定义的任何Action(见 Actions.md)。复用现有动作(连同其预构建负载、编码与定时模式),而不是在deviceWrite中硬编码字节。
actionFire(actionId) actionId: integer (the action's stable identifier) returns: { ok = true } | { ok = false, error = "..." }actionId是动作的actionId字段:项目文件存储、API 返回的同一个持久整数。它不是动作列表中的位置。可用project.action.list(MCP)查找,或在 Project Editor 的 Actions 面板读取。
local triggered = false function transform(value, info) if not triggered and value > 100 then local r = actionFire(7) if r.ok then triggered = true end end return value end行为等同于用户按下动作的按钮,包括运行动作的定时器(AutoStart、RepeatNTimes等)。调用记录为[actionFire] id=N index=M ok。actionFire也可在帧解析器与画布脚本中使用。
控制仪表板
变换还可以驱动一小批仪表板辅助函数:clearPlots()、setPlotPoints(n)、setTerminalVisible(bool)、setNotificationLogVisible(bool)、setClockVisible(bool)、setStopwatchVisible(bool)与setActiveWorkspace(idOrName)。它们与在解析器中的行为相同,返回相同的{ ok, error }形状。变换中的典型模式是"在状态转换时触发一次",例如:
function transform(value) if value >= 9999 then -- device reboot sentinel clearPlots() return 0 end return value end完整参考,包括参数类型与更长的示例(GPS 修复重置、模式驱动的工作区切换、专注模式):见 JavaScript-API.md 的 Controlling the dashboard。
使用变换对话框
- 在 Project Editor 树中选择一个数据集;
- 点击数据集工具栏中的Transform按钮;
- 对话框打开(标题为"Dataset Value Transform",设置后为"Transform — <dataset title>"),包含:
- Language selector。Lua、JavaScript 或 Expression。默认使用数据源的帧解析器语言,但每个数据集可自选;
- Template dropdown。34 个现成变换模板。Expression 下禁用;
- Code editor。语法高亮,带自动补全;
- Parameters table。变换以
params.<name>读取的命名值; - Test area。输入原始值、点击 Test、查看变换输出。测试会先加载共享库与参数表,因此调用库函数的变换测试时与实际运行一致;
- 编写或挑选一个
transform(value)函数; - 点击Apply将变换保存到数据集。
当你在尚无变换的数据集上打开编辑器时,它预填了一段多行注释,解释transform(value)如何工作以及顶层local/var状态如何被捕获。该占位文本不是真正的变换。若未定义transform()函数就点击 Apply,占位文本被丢弃、数据集继续显示原始值。清空代码行为相同:不保存任何内容,对话框关闭。但写入非空文本或注释却从未定义transform()则不同:Apply 会拒绝,显示 "The value transform must define a transform(value) function.",对话框保持打开,直到你定义transform()或点击 Clear。
切换语言
切换语言下拉框时,编辑器会自动加载新语言的等价模板(若当前代码匹配某个已知模板)。自定义代码保持不变,仅切换语法高亮器。
内置模板(34 个)
变换编辑器包含 34 个现成模板。从 Template 下拉框选择后即载入编辑器供调参。
标定与转换
| 模板 | 说明 |
|---|---|
| Linear Calibration | y = slope × value + offset。传感器标定。 |
| Polynomial (2nd order) | y = a×x² + b×x + c。非线性响应曲线。 |
| Map Range | 从[inMin, inMax]重新缩放到[outMin, outMax]。 |
| ADC to Voltage | 10 位 ADC 计数转电压(3.3 V 基准)。 |
| Calibration from Data Table | 从表变量读取斜率/偏移并应用。 |
平滑滤波
| 模板 | 说明 |
|---|---|
| Moving Average | 通过环形缓冲区对最近 N 个样本取平均。 |
| Exponential Moving Average (EMA) | 加权平均,可调响应度(alpha)。 |
| Low-Pass Filter | 一阶 IIR,可通过 alpha 调节截止。 |
| High-Pass Filter | 一阶 IIR。去除直流漂移与慢速偏移。 |
| Median Filter | 滚动窗口取中值。对尖峰离群值稳健。 |
| Kalman Filter (1D) | 标量卡尔曼滤波,可调 Q(过程)与 R(测量)噪声。 |
统计
| 模板 | 说明 |
|---|---|
| Rolling RMS | 最近 N 个样本的均方根。适用于交流信号、振动、音频。 |
| Running Minimum | 变换启动以来观察到的最小值。 |
| Running Maximum | 变换启动以来观察到的最大值。 |
| Running Accumulator | 离散积分(运行总和)。 |
| Rate of Change | 离散微分(value - previous)。 |
信号整形
| 模板 | 说明 |
|---|---|
| Clamp | 将输出限制在[min, max]。 |
| Dead Zone | 抑制零附近的小值。 |
| Slew-Rate Limiter | 限制相邻样本间值的变化幅度。 |
| Auto-Zero / Tare | 对前 N 个样本取平均,再从之后每个值中减去该偏差。 |
| Schmitt Trigger | 滞回比较器。带独立上升/下降阈值,输出 0/1。 |
角度数学
| 模板 | 说明 |
|---|---|
| Unwrap Angle | 消除 ±360° 跳变,使输出跨边界连续。 |
| Integrate Rate to Angle | 以固定采样率将角速率(deg/s)积分为绝对角度。 |
| Radians to Degrees | deg = rad × 180/π。 |
| Degrees to Radians | rad = deg × π/180。 |
单位换算
| 模板 | 说明 |
|---|---|
| Celsius to Fahrenheit | °F = °C × 9/5 + 32。 |
| Fahrenheit to Celsius | °C = (°F - 32) × 5/9。 |
| Kelvin to Celsius | °C = K - 273.15。 |
| Logarithmic (dB) | 将线性幅度转换为分贝。 |
逻辑与位运算
| 模板 | 说明 |
|---|---|
| Bit Extract | 从打包状态字中提取单个位(0 或 1)。 |
| Absolute Value | 无符号幅值。 |
| Invert Sign | 取反。 |
| Round to N Decimals | 精度控制。 |
传感器
| 模板 | 说明 |
|---|---|
| Steinhart-Hart Thermistor | NTC 电阻转温度(°C)。 |
模板清单由 ScriptTemplates.h 中的清单机制加载(loadScriptTemplateManifest+templateResourcePath),在 DatasetTransformEditor.cpp 中通过buildTemplates()填充下拉框,选中后经onTemplateSelected载入对应语言的代码。
变换如何融入数据管线
Serial Studio 按清晰管线处理数据。知道变换所处位置,有助于决定什么放进parse()、什么放进transform():
| 阶段 | 函数 | 作用域 | 目的 |
|---|---|---|---|
| 帧解析器 | parse(frame) | 每数据源 | 将原始字节解码为值数组 |
| 数据集变换 | transform(value) | 每数据集 | 将原始值转换为工程单位 |
| 仪表板 | (无) | 每控件 | 显示最终值 |
经验法则:
- 帧解析器负责协议解码:字节提取、位操作、CRC 校验、多消息状态机。它处理线格式;
- 数据集变换负责值的调理:标定、单位换算、滤波、派生计算。它处理物理含义。
这一分离让解析器专注于协议逻辑、变换专注于传感器特性。你可以跨项目复用同一个解析器,只改每数据集变换即可适配不同标定。从实现层面看,变换编译按(数据源, 语言)聚合成共享引擎,每个引擎内再按数据集缓存函数引用(TransformCompiler.cpp),这正是"同源数据集可混用三种语言、共享引擎、但状态互相隔离"的底层机制。
实战示例
示例 1:线性标定
压力传感器输出原始 ADC 计数(0 到 4095)。数据手册:0 = 0 PSI,4095 = 100 PSI:
function transform(value) return value * 100 / 4095 end示例 2:噪声温度传感器
RTD 传感器在真值附近波动 ±0.5°C。用 EMA 平滑:
local alpha = 0.15 local ema function transform(value) if ema == nil then ema = value end ema = alpha * value + (1 - alpha) * ema return ema endalpha与ema都在文件顶部声明为local,因此 Lua 将它们捕获为transform闭包的上值。状态在帧间持续且对该数据集私有。同一数据源上的另一个数据集可以使用同一 EMA 模板并带自己的local ema而不受干扰。
示例 3:罗盘航向归一化
磁力计以弧度报告航向。转换为度并限制在 0 到 360:
function transform(value) local degrees = value * 180 / math.pi return degrees % 360 end示例 4:带死区的电池电压
电池监视器在断开连接时围绕 0 V 波动。抑制 0.5 V 以下的噪声:
function transform(value) if value < 0.5 then return 0 end return value end示例 5:速度从 m/s 转为 km/h
function transform(value) return value * 3.6 end规则与限制
- 函数必须命名为
transform(区分大小写)。 - 它接受一个必需参数(
value),加一个可选第二参数(info),携带帧元数据(frameNumber、sourceId、timestampMs)。 - 它必须返回数字,或对文本数据集返回字符串(返回的字符串被保留为数据集值)。
- 返回
nil、NaN或Infinity时回退到原始值;脚本错误同样回退(两种语言都会记录日志;只有非错误的nil/NaN/Infinity回退是静默的)。 - 每个数据集自选变换语言(Lua、JavaScript 或 Expression);同一数据源上的数据集可以混用。数据源的帧解析器语言只是未选择时的默认值。规则 1-4 描述
transform()函数,适用于 Lua 与 JavaScript;Expression 数据集没有函数,发布其单个表达式的值。 - 同一数据源上的数据集共享一个底层脚本引擎,但每个数据集的顶层状态互相隔离。JavaScript 中用
var声明有状态变量:隐式(未声明)全局会写入共享引擎并泄漏到其它数据集。Lua 中每个数据集 chunk 有自己的环境,因此裸全局(以及 chunk 顶层的function foo() end)保持每数据集隔离;仍推荐local以保持清晰并与 JavaScript 规则一致。 - 引擎是沙箱化的:无文件 I/O、无网络、无操作系统命令。从源码看,Lua 沙箱只打开
_G、table、string、math、bit五个安全库,并显式清除dofile、loadfile、load与string.dump,且从不打开ffi与jit(TransformCompiler.cpp)。 - 变换在每个传入帧上运行,因此请保持其快速。避免无界循环或重计算。每次调用的看门狗会在 100 ms 后终止变换(Lua:调用前武装 deadline;JavaScript:每帧武装一次 interrupt),这一阈值正是源码中的
kTransformWatchdogMs = 100(TransformCompiler.h)。超时变换记录警告并回退到原始值,同规则 4。
相关文档
- SerialStudio-SDK.md:变换可调用的完整辅助函数面(
tableGet/tableSet、deviceWrite、actionFire、通知、协议编码器)。 - JavaScript-API.md:向变换供值的
parse(frame)函数。 - Data-Tables.md:所有变换可用的共享常量与计算变量。
- Data-Flow.md:数据如何从设备经解析、变换进入仪表板。
- Project-Editor.md:配置数据集、变换与表的位置。
- Widget-Reference.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),仅供参考