news 2026/9/18 22:09:21

Serial Studio 数据集值变换(Dataset Transforms)完全指南:Lua / JavaScript / Expression 三语言标定、滤波与闭环控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Serial Studio 数据集值变换(Dataset Transforms)完全指南:Lua / JavaScript / Expression 三语言标定、滤波与闭环控制

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 end

JavaScript:

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 开始
pienaninf常量

同一数据源的其它数据集通过其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是自然对数,degrad在弧度与度之间转换。从源码看,这一函数表在 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 = 256kMaxNodes = 512kMaxStackDepth = 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 end

alphaema是 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) end

Lua 中在 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.valueobj: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 LibraryJavaScript Library;变换编辑器也有一个Open Lua Library/Open JavaScript Library按钮,可直接跳到当前语言对应的库。API 同样暴露它们(project.transformLibrary.getproject.transformLibrary.setproject.transformLibrary.dryRun,各带可选的language参数luajs)。

-- 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 中compileLuaLibrarykTransformLibraryErrorId的错误记录实现。

数据集参数(Dataset parameters)

每个数据集可携带命名参数,其变换以params读取。在变换对话框的Parameters表中逐行添加名称与值,或通过 API(project.dataset.update {transformParams: {...}})。值是有类型的:数字文本变成数字,truefalse变成布尔值,其它内容保持字符串。

-- 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 完全相同。tablereg是字符串。数据集查找接受整数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) end

JavaScript 示例:同样思路。

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 end
function 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 failure

deviceWrite同步、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

行为等同于用户按下动作的按钮,包括运行动作的定时器(AutoStartRepeatNTimes等)。调用记录为[actionFire] id=N index=M okactionFire也可在帧解析器与画布脚本中使用。

控制仪表板

变换还可以驱动一小批仪表板辅助函数: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。

使用变换对话框

  1. 在 Project Editor 树中选择一个数据集;
  2. 点击数据集工具栏中的Transform按钮;
  3. 对话框打开(标题为"Dataset Value Transform",设置后为"Transform — <dataset title>"),包含:
    • Language selector。Lua、JavaScript 或 Expression。默认使用数据源的帧解析器语言,但每个数据集可自选;
    • Template dropdown。34 个现成变换模板。Expression 下禁用;
    • Code editor。语法高亮,带自动补全;
    • Parameters table。变换以params.<name>读取的命名值;
    • Test area。输入原始值、点击 Test、查看变换输出。测试会先加载共享库与参数表,因此调用库函数的变换测试时与实际运行一致;
  4. 编写或挑选一个transform(value)函数;
  5. 点击Apply将变换保存到数据集。

当你在尚无变换的数据集上打开编辑器时,它预填了一段多行注释,解释transform(value)如何工作以及顶层local/var状态如何被捕获。该占位文本不是真正的变换。若未定义transform()函数就点击 Apply,占位文本被丢弃、数据集继续显示原始值。清空代码行为相同:不保存任何内容,对话框关闭。但写入非空文本或注释却从未定义transform()则不同:Apply 会拒绝,显示 "The value transform must define a transform(value) function.",对话框保持打开,直到你定义transform()或点击 Clear。

切换语言

切换语言下拉框时,编辑器会自动加载新语言的等价模板(若当前代码匹配某个已知模板)。自定义代码保持不变,仅切换语法高亮器。

内置模板(34 个)

变换编辑器包含 34 个现成模板。从 Template 下拉框选择后即载入编辑器供调参。

标定与转换

模板说明
Linear Calibrationy = slope × value + offset。传感器标定。
Polynomial (2nd order)y = a×x² + b×x + c。非线性响应曲线。
Map Range[inMin, inMax]重新缩放到[outMin, outMax]
ADC to Voltage10 位 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 Degreesdeg = rad × 180/π。
Degrees to Radiansrad = 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 ThermistorNTC 电阻转温度(°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 end

alphaema都在文件顶部声明为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

规则与限制

  1. 函数必须命名为transform(区分大小写)。
  2. 它接受一个必需参数(value),加一个可选第二参数(info),携带帧元数据(frameNumbersourceIdtimestampMs)。
  3. 它必须返回数字,或对文本数据集返回字符串(返回的字符串被保留为数据集值)。
  4. 返回nilNaNInfinity时回退到原始值;脚本错误同样回退(两种语言都会记录日志;只有非错误的nil/NaN/Infinity回退是静默的)。
  5. 每个数据集自选变换语言(Lua、JavaScript 或 Expression);同一数据源上的数据集可以混用。数据源的帧解析器语言只是未选择时的默认值。规则 1-4 描述transform()函数,适用于 Lua 与 JavaScript;Expression 数据集没有函数,发布其单个表达式的值。
  6. 同一数据源上的数据集共享一个底层脚本引擎,但每个数据集的顶层状态互相隔离。JavaScript 中用var声明有状态变量:隐式(未声明)全局会写入共享引擎并泄漏到其它数据集。Lua 中每个数据集 chunk 有自己的环境,因此裸全局(以及 chunk 顶层的function foo() end)保持每数据集隔离;仍推荐local以保持清晰并与 JavaScript 规则一致。
  7. 引擎是沙箱化的:无文件 I/O、无网络、无操作系统命令。从源码看,Lua 沙箱只打开_Gtablestringmathbit五个安全库,并显式清除dofileloadfileloadstring.dump,且从不打开ffijit(TransformCompiler.cpp)。
  8. 变换在每个传入帧上运行,因此请保持其快速。避免无界循环或重计算。每次调用的看门狗会在 100 ms 后终止变换(Lua:调用前武装 deadline;JavaScript:每帧武装一次 interrupt),这一阈值正是源码中的kTransformWatchdogMs = 100(TransformCompiler.h)。超时变换记录警告并回退到原始值,同规则 4。

相关文档

  • SerialStudio-SDK.md:变换可调用的完整辅助函数面(tableGet/tableSetdeviceWriteactionFire、通知、协议编码器)。
  • 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 22:06:57

C语言回调函数实战:从函数指针到工程级应用

我最早对回调函数有“顿悟感”&#xff0c;是在维护一个串口通信模块的时候。那会儿协议解析、数据分包、命令分发全写在一个循环里&#xff0c;每加一个功能就要改主逻辑&#xff0c;眼看着代码越来越像一团打了结的耳机线。后来把“收到数据之后干什么”这个动作抽出来&#…

作者头像 李华
网站建设 2026/9/18 22:06:45

Hugo 模板函数 crypto.MD5 完全指南:md5 哈希与 Gravatar 头像实战

Hugo 模板函数 crypto.MD5 完全指南&#xff1a;md5 哈希与 Gravatar 头像实战 【免费下载链接】hugo The world’s fastest framework for building websites. 项目地址: https://gitcode.com/gh_mirrors/hu/hugo crypto.MD5 是 Hugo 模板系统中 crypto 命名空间下的哈…

作者头像 李华
网站建设 2026/9/18 22:06:34

BusyBox根文件系统/dev目录创建:静态mknod、devtmpfs、mdev三方案详解

做嵌入式Linux的兄弟应该都干过这事&#xff1a;往板子上烧完内核&#xff0c;手搓了一个BusyBox根文件系统&#xff0c;结果启动到一半卡在“Creating 5 entries in /dev”或者挂载根文件系统之后VFS报一堆节点不存在&#xff0c;console登录不了&#xff0c;串口一片死寂。这…

作者头像 李华
网站建设 2026/9/18 22:06:34

Gyroflow 镜头校准 5 步指南:自制一份精准镜头配置文件

Gyroflow 镜头校准 5 步指南&#xff1a;自制一份精准镜头配置文件 【免费下载链接】gyroflow Video stabilization using gyroscope data 项目地址: https://gitcode.com/GitHub_Trending/gy/gyroflow Gyroflow 用陀螺仪数据为视频防抖&#xff0c;而防抖后画面是否变形…

作者头像 李华