主题
fdb — 数据存储
模块名:fdb
基于 FlashDB 的掉电不丢失存储,提供两套独立能力:
- KVDB(键值库):按键名存取配置、校准参数、累计量。
- TSDB(时序库):按时间戳追加采样数据,按时间范围回查。
全部是库级函数,没有对象。数据库的初始化在固件侧完成,Lua 里不需要也没有 kvdb_init 之类的函数。
函数总览
键值库(KVDB)
| 函数 | 作用 |
|---|---|
fdb.kv_set(string, integer) | 写入键值,支持数字/字符串/布尔/table |
fdb.kv_get(string) | 按键读值,可取 table 里的某个字段 |
fdb.kv_del(string) | 按键删除 |
fdb.kv_iter | 创建遍历迭代器 |
fdb.kv_next | 取迭代器的下一个键名 |
时序库(TSDB)
| 函数 | 作用 |
|---|---|
fdb.ts_add(integer) | 追加一条时序数据 |
fdb.ts_select(integer, integer) | 按时间范围查询,逐条回调 |
本模块不注册常量。
fdb.kv_set
lua
fdb.kv_set(key, value) --> boolean写入一个键值对。值的类型会被记录,kv_get 时按原类型返回。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | key | string | 是 | — | 键名 |
| 2 | value | number | string | boolean | table | 是 | — | 值。table 会先用 json.encode 序列化后存储 |
返回值
| 类型 | 说明 |
|---|---|
| boolean | 是否写入成功 |
示例
lua
fdb.kv_set("site", "workshop-A")
fdb.kv_set("calib", 1.0234)
fdb.kv_set("enabled", true)
fdb.kv_set("pid", { p = 2.0, i = 0.3, d = 0.1 }) -- table 走 JSON说明
- Flash 有擦写寿命。不要在高频定时器里反复写同一个键,只在值真正变化时写。
fdb.kv_get
lua
fdb.kv_get(key, [field]) --> value | nil按键名读值。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | key | string | 是 | — | 键名 |
| 2 | field | string | 否 | "" | 当值是 table(JSON)时,只取这个字段 |
返回值
| 类型 | 说明 |
|---|---|
| number / string / boolean / table | 按写入时的类型还原 |
| nil | 键不存在或读取失败 |
示例
lua
local site = fdb.kv_get("site")
local pid = fdb.kv_get("pid") -- 整个 table
local kp = fdb.kv_get("pid", "p") -- 只取 p 字段
if kp then ctrl = pid.creat(kp, ...) end说明
- 读不到时返回
nil,使用前需判空。
fdb.kv_del
lua
fdb.kv_del(key) --> boolean删除一个键。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | key | string | 是 | — | 键名 |
返回值
| 类型 | 说明 |
|---|---|
| boolean | 是否删除成功 |
fdb.kv_iter
lua
fdb.kv_iter() --> iter | nil创建一个 KVDB 迭代器,用于遍历所有键。
参数
无参数。
返回值
| 类型 | 说明 |
|---|---|
| userdata | 迭代器 |
| nil | 创建失败 |
fdb.kv_next
lua
fdb.kv_next(iter) --> string | nil取迭代器的下一个键名。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | iter | userdata | 是 | — | fdb.kv_iter() 返回的迭代器 |
返回值
| 类型 | 说明 |
|---|---|
| string | 下一个键名 |
| nil | 已遍历完 |
示例
lua
local iter = fdb.kv_iter()
if iter then
while true do
local k = fdb.kv_next(iter)
if not k then break end
print(k, fdb.kv_get(k))
end
endfdb.ts_add
lua
fdb.ts_add(key, value) --> boolean向时序库追加一条数据。时间戳由固件自动打上。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | key | string | 是 | — | 点位名,会随值一起存入记录,供 ts_select 按点位过滤(最长 255 字节) |
| 2 | value | number | string | boolean | table | 是 | — | 值,类型规则同 kv_set |
返回值
| 类型 | 说明 |
|---|---|
| boolean | 是否写入成功 |
示例
lua
timer.start(60000, function() -- 每分钟各存一条,用 key 区分点位
fdb.ts_add("temp", plc.df(100))
fdb.ts_add("press", plc.df(102))
end)说明
key会与值一起写进每条记录,ts_select可据此只取某个点位(见下)。- 新版固件能自动读出旧版固件写的老数据,老数据没有 key,在
ts_select里key回调参数为nil。 - 每条记录会多占
2 + key 长度个字节。key 应尽量简短(如"t1"而不是"workshop1_temperature"),否则会明显增加存储占用。 - 反过来不成立:新版固件写的数据,旧版固件读不出来,值会返回
nil。降级固件前请注意。 - 时序库是环形存储,写满后覆盖最旧的数据。采样周期需与存储容量匹配,容量不足以保存长周期的秒级数据。
fdb.ts_select
lua
fdb.ts_select(start_time, end_time, callback) --> boolean -- 不过滤,返回全部点位
fdb.ts_select(start_time, end_time, key, callback) --> boolean -- 只返回匹配 key 的记录按时间范围查询时序库,每查到一条就调一次回调。可选传入 key 只查某个点位。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | start_time | integer | 是 | — | 起始 Unix 时间戳(秒) |
| 2 | end_time | integer | 是 | — | 结束 Unix 时间戳(秒) |
| 3 | key | string | 否 | — | 传入则只回调 key 匹配的记录;省略则返回全部。老格式(无 key)记录在指定 key 时一律被跳过 |
| — | callback | function | 是 | — | 逐条回调 (timestamp, value, key)。第 3 个参数是记录里的点位名,老数据为 nil |
返回值
| 类型 | 说明 |
|---|---|
| boolean | 是否查到了数据(读取长度 > 0) |
示例
lua
local now = os.time()
-- 查全部点位
fdb.ts_select(now - 3600, now, function(ts, value, key)
print(os.date("%H:%M:%S", ts), key, value)
end)
-- 只查温度这一个点位
fdb.ts_select(now - 3600, now, "temp", function(ts, value)
print(os.date("%H:%M:%S", ts), value)
end)说明
- 回调是同步调用的,
ts_select返回时所有回调都已执行完。范围内数据较多时会占用较长时间,不要在高频定时器里查询大范围。 - 过滤是读取时按记录里的 key 逐条比对,不是索引查找。范围内记录较多时仍会遍历全部记录,只是不对不匹配的记录触发回调。
- 返回值只表示"这段时间内读到过数据",不表示过滤后有命中。判断是否命中需要在回调里自行计数。
常见用法
开机加载参数,参数变化时才保存
lua
-- 开机取回上次保存的调节参数,没存过就用默认值
local kp = fdb.kv_get("gain", "p") or 2.0
local ki = fdb.kv_get("gain", "i") or 0.3
local kd = fdb.kv_get("gain", "d") or 0.1
-- 交给梯形图使用:放大 100 倍存进 D 区
plc.d(500, math.floor(kp * 100))
plc.d(501, math.floor(ki * 100))
plc.d(502, math.floor(kd * 100))
-- 上位机改完参数后调一次,只在变化时写 Flash
function save_gain(p, i, d)
fdb.kv_set("gain", { p = p, i = i, d = d })
end本地缓存历史曲线,供上位机回查
lua
timer.start(60000, function()
fdb.ts_add("temp", plc.df(100))
fdb.ts_add("press", plc.df(102))
end)
-- 上位机通过 MQTT 请求近一小时的温度曲线
mqtt.reg(mqttc, mqtt.RECVDATA, function(topic, payload)
if topic:match("history$") then
local now, rows = os.time(), {}
fdb.ts_select(now - 3600, now, "temp", function(ts, v) -- 只取 temp 这一路
rows[#rows + 1] = { t = ts, v = v }
end)
mqtt.publish(mqttc, "data/history", json.encode(rows), 0, 0)
end
end)注意事项
- Flash 擦写次数有限,不要在秒级定时器里写 KV。配置类数据只在变化时写。
kv_get读不到返回nil,应配合or 默认值或显式判空。- 时序库容量有限、写满覆盖最旧数据,长期历史请上传到服务器保存。
ts_select是同步遍历,大范围查询会阻塞主循环。- 个别裁剪过的固件可能未启用本模块,此时脚本里访问
fdb会得到 nil 并报 "attempt to index a nil value"。
例程
统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。
例程 1:开机加载参数,变化时保存
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / 软PLC模拟器 / 上位机 | 依赖模块:fdb | 难度:★☆☆
场景:PID 参数掉电不丢,开机取回,没存过用默认值。
完整代码(可直接下载运行):
lua
local kp = fdb.kv_get("gain", "p") or 2.0
local ki = fdb.kv_get("gain", "i") or 0.3
local kd = fdb.kv_get("gain", "d") or 0.1
print("kp=", kp, "ki=", ki, "kd=", kd)
-- 参数变化时才写 Flash
function save_gain(p, i, d)
fdb.kv_set("gain", { p = p, i = i, d = d })
end要点:kv_get 读不到返回 nil,用 or 默认值 兜底;table 走 JSON 存取。
验证方法:存一次后重启,参数还在。
注意事项:Flash 有擦写寿命,别在秒级定时器反复写同一键;只读变化写。
例程 2:时序库存曲线 + 回查
适用:同上 | 依赖模块:fdb json mqtt plc | 难度:★★☆
场景:每分钟存温度/压力,上位机要近一小时曲线时回查。
完整代码(可直接下载运行):
lua
timer.start(60000, function()
fdb.ts_add("temp", plc.df(100))
fdb.ts_add("press", plc.df(102))
end)
mqtt.reg(mqttc, mqtt.RECVDATA, function(topic, payload)
if topic:match("history$") then
local now, rows = os.time(), {}
fdb.ts_select(now - 3600, now, "temp", function(ts, v)
rows[#rows + 1] = { t = ts, v = v }
end)
mqtt.publish(mqttc, "data/history", json.encode(rows), 0, 0)
end
end)要点:ts_add 自动打时间戳,key 短小省空间;ts_select 同步遍历回调。
验证方法:请求 history,收到一小时温度点数组。
注意事项:ts_select 同步阻塞主循环,别查太大范围;时序库环形覆盖,长历史上传服务器。
例程 3:注意事项——kv_get 返回 nil 判空
适用:同上 | 依赖模块:fdb | 难度:★☆☆
完整代码(可直接下载运行):
lua
local v = fdb.kv_get("site")
print(v:upper()) -- ❌ v 为 nil 时报错
local v2 = fdb.kv_get("site") or "default"
print(v2:upper()) -- ✅ 先兜底要点:kv_get 读不到返回 nil,使用前必须判空或 or。
验证方法:键不存在时第一段报错,第二段正常。
注意事项:个别裁剪固件未启用本模块,访问 fdb 为 nil 会报错。