Complete set of file read-write APIs: reading txt/csv/bin files, writing files, traversing directories, copying/deleting. The core idea is one -stream processing, never allow more than 2KB of buffers to exist in memory at the same time. HMI memory is extremely small, violating this rule will directly reset the watchdog, and the device will restart without warning.
1. Storage device identification (M series vs DH series)
| device | M series | DH series |
|---|---|---|
| SD card | 1:/1.txt | /sdcard/1.txt |
| U disk | 2:/1.txt | /udisk/1.txt |
| internal Flash | 3:/1.txt | /data/1.txt |
path separator must use '/'.
2. General table of file APIs
| Function | Parameter | Description |
|---|---|---|
dofile(name) | lua file path | Load and execute .lua module, split modules for large projects |
list_dir(path) | directory path | Asynchronously scan directories, callback on_list_dir for each result |
on_list_dir(path,filename,type,fsize) | type 0=folder/1=file; fsize bytes | Traversal result callback, called once for each entry found |
file_open(path,mode) | mode 0=FA_READ read-only/1=FA_WRITE overwrite/2=FA_WRITE_ADD append | Open file, return boolean |
file_close() | - | Close file, must be called after writing to prevent data loss |
file_size() | - | Total bytes of the currently opened file |
file_seek(offset) | Byte offset, 0=file start | Position for reading and writing |
file_read(count) | 1 ≤ count ≤ 2048 | Read byte array (table, indices start from 1), return nil on failure |
file_write(data) | Byte table, 1 ≤ #data ≤ 2048 | Write byte array, return boolean |
file_delete(path) | Full path | Delete file, unrecoverable |
file_copy(src,dst) | Source/Destination full path | Copy file, progress callback on_copy_file_process |
on_copy_file_process(status,filesize,transfersize) | status 0=failed/1=in progress/2=successful | Copy progress callback, transfersize bytes copied |
mkdir(dir) | Full directory path | Synchronously create folders |
3. 🔴 Iron law of memory safety (ignore at your peril)
It is strictly prohibited to accumulate data read multiple times into global variables or long-lived buffers.Typical error: Read 2KB into a global table for the first time, and append each subsequent read - this will continuously grow memory when processing large files, triggering a watchdog reset.
Correct approach:Stream processing- Parse/verify/write/display immediately after each file_read(2048), release after processing, and only have a temporary buffer of ≤2KB in memory at any given time. The same applies to writing files: prepare only blocks of ≤2048 bytes and immediately file_write, do not assemble a large table of several MBs and write in segments (memory overflow during the data preparation stage).
4. Reading files: Stream chunking + callback
Officially encapsulatedfile.read_stream(filepath, on_chunk): Open → Get size → Loop seek+read(≤2048) → Call on_chunk(chunk, offset, is_last) for each chunk → Close file. If the callback returns false, terminate early, and call feed_dog() to feed the watchdog in large file loops.
4.1 Read txt and display line by line
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
endPay attention to cross-block line buffering: The newline character may fall between two blocks, so maintain the g_line_buffer to keep incomplete lines for the next block.
4.2 Read bin and treat each 16 bytes as a line
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)
end5. Write to file
encapsulationfile.write_chunk(filepath, data, mode), mode 1=overwrite, 2=append, data supports string or byte 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
-- 场景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)⚠️ Append writing must explicitly use file_seek(file_size()).HMI's file_open(mode=2) is not equivalent to POSIX's O_APPEND and does not automatically position to the end - without seeking, it starts writing from offset 0, overwriting the beginning of the original file.
6. Traversal + copy and delete
6.1 Traverse the SD root directory and display to the data recording control
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 Copy + delete (with progress bar)
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
endCompiled from Guangzhou Dacai Technology VisualHMI development documentation (hmi-doc.gz-dc.com) LUA tutorial "File Read and Write", copyright reserved by Dacai Technology.
Leave a Reply