文件読み書き API 全套:读 txt/csv/bin、写文件、遍历目录、拷贝/Delete。核心思想就一条——流式処理,永远不要让内存里同时存在超过 2KB 的バッファ。HMI 内存极小,违反这条ウォッチドッグ直接复位,デバイス无预警再起動。
1. 存储デバイス标识(M 系列 vs DH 系列)
| device | M 系列 | DH 系列 |
|---|---|---|
| SD 卡 | 1:/1.txt | /sdcard/1.txt |
| U 盘 | 2:/1.txt | /udisk/1.txt |
| 内部 Flash | 3:/1.txt | /data/1.txt |
路径分隔符必须用 /。
2. 文件 API 总表
| 函数 | parameter | 説明 |
|---|---|---|
dofile(name) | lua 文件路径 | 加载执行 .lua モジュール,大型工程拆モジュール |
list_dir(path) | 目录路径 | 异步扫描目录,结果逐项回调 on_list_dir |
on_list_dir(path,filename,type,fsize) | type 0=文件夹/1=文件;fsize byte | 遍历结果回调,每发现一个条目调一次 |
file_open(path,mode) | mode 0=FA_READ 読み取り専用/1=FA_WRITE カバー写/2=FA_WRITE_ADD 追加写 | 打开文件,戻る布尔 |
file_close() | - | 关闭文件,写入后必须调,防丢データ |
file_size() | - | 当前打开文件的总Bytesの数 |
file_seek(offset) | バイト偏移,0=文件开头 | ポジショニング読み書き位置 |
file_read(count) | 1 ≤ count ≤ 2048 | 读Bytesの配列(table,下标从 1),失敗戻る nil |
file_write(data) | byte table,1 ≤ #data ≤ 2048 | 写Bytesの配列,戻る布尔 |
file_delete(path) | 完全路径 | 削除文件,不可恢复 |
file_copy(src,dst) | 源/目标完全路径 | コピー文件,进度回调 on_copy_file_process |
on_copy_file_process(status,filesize,transfersize) | status 0失敗/1進行中/2成功 | 拷贝进度回调,transfersize 已コピーバイト |
mkdir(dir) | 完全目录路径 | 同步作成文件夹 |
3. 🔴 内存セキュリティ铁律(不看必翻车)
严禁把多次読み取り的データ累积存进全局变量或长生命周期バッファ。典型エラー:第一次读 2KB 存全局 table,后面每次读再追加——処理大文件时内存持续增长,直接触发ウォッチドッグ复位。
正确姿势:流式処理——每次 file_read(2048) 后立即解析/check/写入/表示,処理完就释放,任何时刻内存里只有一块 ≤2KB 的临时缓冲。写文件同理:每次只准备 ≤2048 bytes的块立即 file_write,不要先拼一个几 MB 的大 table 再セグメント化写(データ准备阶段就内存溢出了)。
4. 读文件:流式分块 + 回调
官方封装了 file.read_stream(filepath, on_chunk):open → 取大小 → 循环 seek+read(≤2048) → 每块调 on_chunk(chunk, offset, is_last) → 关文件。回调戻る false 可提前终止,大文件循环里调 feed_dog() 喂狗。
4.1 读 txt 逐行表示
if addr == 0x1501 then
local idx = get_uint16(vtype, addr)
if idx == 0 then return end
local path = (idx == 1) and "1:/test1.txt" or "1:/test2.txt"
record_clear(0)
g_line_buffer = "" -- 行缓冲
file.read_stream(path, function(chunk, offset, is_last)
local str = ""
for _, b in ipairs(chunk) do str = str .. string.char(b) end
g_line_buffer = g_line_buffer .. str
local lines = my_split(g_line_buffer, "n") -- 按行分割
g_line_buffer = lines[#lines] -- 保留不完全行(可能跨块)
for i = 1, #lines - 1 do
if #lines[i] > 0 then record_write_string(0, lines[i]) end
end
return true
end)
if string.len(g_line_buffer) > 0 then -- 最后一行
record_write_string(0, g_line_buffer)
end
end注意跨块行缓冲:换行符可能正好落在两块之间,所以要维护 g_line_buffer 把不完全行留到下一块。
4.2 读 bin 按 16 バイト一行
elseif addr == 0x1502 then
local idx = get_uint16(vtype, addr)
if idx == 0 then return end
local path = (idx == 1) and "1:/test1.bin" or "1:/test2.bin"
record_clear(1)
g_byte_buffer = {}
file.read_stream(path, function(chunk, offset, is_last)
for _, b in ipairs(chunk) do
table.insert(g_byte_buffer, b)
if #g_byte_buffer >= 16 then -- 每满 16 バイト送信一行
local line = {}
for i = 1, 16 do line[i] = g_byte_buffer[i] end
record_write_data(1, line)
for i = 1, 16 do table.remove(g_byte_buffer, 1) end
end
end
return true
end)
if #g_byte_buffer > 0 then -- 末尾不足 16 バイト补 0
while #g_byte_buffer < 16 do table.insert(g_byte_buffer, 0) end
record_write_data(1, g_byte_buffer)
end
5. 写文件
封装 file.write_chunk(filepath, data, mode),mode 1=カバー写、2=追加写,data サポート string 或バイト table(≤2048)。
function file.write_chunk(filepath, data, mode)
if mode ~= file.mode.create and mode ~= file.mode.append then return false end
local t = type(data)
local len = (t == "string" or t == "table") and #data or -1
if len CHUNK_SIZE then return false end -- 防内存溢出
local ok = file_open(filepath, mode)
if not ok then return false end
if mode == file.mode.append then -- 追加モード必须手动ポジショニング!
local size = file_size()
if not file_seek(size) then file_close(); return false end
end
local buf = {}
if t == "string" then
for i = 1, len do buf[i] = string.byte(data, i) end
else
for i = 1, len do buf[i] = data[i] end
end
local ret = file_write(buf)
file_close()
return ret
end
-- Scenarios1:新建写
local name = get_string(VT_LW, 0x1510)
local msg = get_string(VT_LW, 0x1520)
if not name or #name == 0 or not msg then return end
file.write_chunk("1:/"..name..".txt", msg.."n", 1)
-- Scenarios2:追加写
file.write_chunk("1:/"..name..".txt", msg.."n", 2)⚠️ 追加写必须显式 file_seek(file_size())。HMI 的 file_open(mode=2) not equal to POSIX 的 O_APPEND,不会自动ポジショニング到末尾——不 seek 就从偏移 0 开始写,把原文件开头カバー掉。
6. 遍历 + 拷贝削除
6.1 遍历 SD 根目录表示到データ記録控件
function get_extension(filename)
local dot = string.find(filename, ".", 1, true)
if dot then
return string.sub(filename, 1, dot-1), string.sub(filename, dot+1):lower()
end
return filename, "Unknown"
end
function on_list_dir(path, filename, type, fsize)
local msg
if type == 0 then
msg = filename..";文件夹;;"
else
local sz
if fsize >= 1024^3 then sz = string.format("%.2fG", fsize/1024^3)
elseif fsize >= 1024^2 then sz = string.format("%.2fM", fsize/1024^2)
elseif fsize >= 1024 then sz = string.format("%.2fKB", fsize/1024)
else sz = fsize.."byte" end
local name, ext = get_extension(filename)
msg = name..";"..ext..";"..sz..";"
end
record_write_string(2, msg)
end
-- リフレッシュ按钮 LW1504=1
function on_update(slave, vtype, addr)
if vtype == VT_LW then
if addr == 0x1504 and get_uint16(vtype, addr) == 1 then
record_clear(2)
list_dir("1:") -- 遍历 SD 根目录
end
end
end6.2 Copy + Delete(带プログレスバー)
function on_copy_file_process(status, filesize, transfersize)
if status == 0 then
set_uint16(VT_LW, 0x1507, 3) -- 失敗
elseif status == 1 then
local p = math.floor(transfersize * 100 / filesize)
set_uint16(VT_LW, 0x1506, p) -- progress bar
refresh_screen()
elseif status == 2 then
set_uint16(VT_LW, 0x1506, 100)
set_uint16(VT_LW, 0x1507, 2) -- 成功
refresh_screen()
end
end
function on_update(slave, vtype, addr)
if vtype == VT_LW and addr == 0x1505 then
local mode = get_uint16(vtype, addr)
if mode == 1 then -- Copy
if file.exists("1:/1.txt") then
file_copy("1:/1.txt", "1:/1_copy.txt")
else
set_uint16(VT_LW, 0x1507, 1)
end
mkdir("1:/test")
elseif mode == 2 then -- Delete
file_delete("1:/test")
if file.exists("1:/1_copy.txt") then
file_delete("1:/1_copy.txt")
if not file.exists("1:/1_copy.txt") then
set_uint16(VT_LW, 0x1507, 4)
else
set_uint16(VT_LW, 0x1507, 5)
end
else
set_uint16(VT_LW, 0x1507, 1)
end
end
refresh_screen()
end
end
整理自 广州大彩科技 VisualHMI 开发ドキュメント(hmi-doc.gz-dc.com)LUA Tutorial「文件読み書き」,著作権は大彩科技すべての。
Leave a Reply