文件读写 API 全套:读 txt/csv/bin、写文件、遍历目录、拷贝/삭제。核心思想就一条——流式处理,永远不要让内存里同时存在超过 2KB 的缓冲区。HMI 内存极小,违反这条看门狗直接复位,设备无预警重启。
1. 存储设备标识(M 시리즈 vs DH 시리즈)
| 设备 | M 시리즈 | DH 시리즈 |
|---|---|---|
| SD 卡 | 1:/1.txt | /sdcard/1.txt |
| U 盘 | 2:/1.txt | /udisk/1.txt |
| 内部 Flash | 3:/1.txt | /데이터/1.txt |
路径分隔符必须用 /。
2. 文件 API 总表
| 函数 | 매개변수 | 설명 |
|---|---|---|
dofile(name) | lua 文件路径 | 불러오기执行 .lua 模块,大型엔지니어링拆模块 |
list_dir(path) | 目录路径 | 异步扫描目录,结果逐项回调 on_list_dir |
on_list_dir(path,filename,type,fsize) | type 0=文件夹/1=文件;fsize 바이트 | 遍历结果回调,每发现一个条目调一次 |
file_열기(path,mode) | mode 0=FA_READ 只读/1=FA_WRITE 覆盖写/2=FA_WRITE_ADD 追加写 | 열기文件,返回布尔 |
file_close() | - | 닫기文件,쓰기后必须调,防丢데이터 |
file_size() | - | 当前열기文件的总바이트数 |
file_seek(offset) | 바이트偏移,0=文件开头 | 定位读写位置 |
file_read(count) | 1 ≤ count ≤ 2048 | 读바이트数组(table,下标从 1),失败返回 nil |
file_write(데이터) | 바이트 table,1 ≤ #데이터 ≤ 2048 | 写바이트数组,返回布尔 |
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) 后바로분석/체크섬/쓰기/显示,处理完就释放,任何时刻内存里只有一块 ≤2KB 的临时缓冲。写文件同理:每次只准备 ≤2048 바이트的块바로 file_write,不要先拼一个几 MB 的大 table 再分段写(데이터准备阶段就内存溢出了)。
4. 读文件:流式分块 + 回调
官方封装了 file.read_stream(filepath, on_chunk):열기 → 取大小 → 循环 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_데이터(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_데이터(1, g_byte_buffer)
end
5. 写文件
封装 file.write_chunk(filepath, 데이터, mode),mode 1=覆盖写、2=追加写,데이터 支持 string 或바이트 table(≤2048)。
function file.write_chunk(filepath, 데이터, mode)
if mode ~= file.mode.create and mode ~= file.mode.append then return false end
local t = type(데이터)
local len = (t == "string" or t == "table") and #데이터 or -1
if len CHUNK_SIZE then return false end -- 防内存溢出
local ok = file_열기(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(데이터, i) end
else
for i = 1, len do buf[i] = 데이터[i] end
end
local ret = file_write(buf)
file_close()
return ret
end
-- 시나리오1:新建写
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)
-- 시나리오2:追加写
file.write_chunk("1:/"..name..".txt", msg.."n", 2)⚠️ 追加写必须显式 file_seek(file_size())。HMI 的 file_열기(mode=2) 不等于 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, "未知"
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 복사 + 삭제(带进度条)
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) -- 进度条
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 -- 복사
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 -- 삭제
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 튜토리얼「文件读写」,版权归大彩科技所有。
답글 작성