VisualHMI File IO: LUA Read and Write SD Card/U-Disk/Flash and Directory Listing

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

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)

deviceM seriesDH series
SD card1:/1.txt/sdcard/1.txt
U disk2:/1.txt/udisk/1.txt
internal Flash3:/1.txt/data/1.txt

path separator must use '/'.

2. General table of file APIs

FunctionParameterDescription
dofile(name)lua file pathLoad and execute .lua module, split modules for large projects
list_dir(path)directory pathAsynchronously scan directories, callback on_list_dir for each result
on_list_dir(path,filename,type,fsize)type 0=folder/1=file; fsize bytesTraversal 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 appendOpen 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 startPosition for reading and writing
file_read(count)1 ≤ count ≤ 2048Read byte array (table, indices start from 1), return nil on failure
file_write(data)Byte table, 1 ≤ #data ≤ 2048Write byte array, return boolean
file_delete(path)Full pathDelete file, unrecoverable
file_copy(src,dst)Source/Destination full pathCopy file, progress callback on_copy_file_process
on_copy_file_process(status,filesize,transfersize)status 0=failed/1=in progress/2=successfulCopy progress callback, transfersize bytes copied
mkdir(dir)Full directory pathSynchronously 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
end

Pay 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)
  end

5. 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
end

6.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
end
Five pitfalls to avoid:① Memory Iron Law - Strictly prohibit accumulating large buffers globally, only leave ≤2KB blocks at a time, otherwise the watchdog will reset;② Append writes must use file_seek(file_size()), the append function of HMI is not O_APPEND;③ Paths must include a device prefix and a slash- '1:/config.txt' is correct, '1:config.txt' is incorrect;④ In large file read loops, call feed_dog(), and avoid performing time-consuming tasks in callbacks;⑤ Immediately call file_close() after writing to a file, as power loss may corrupt the file or partially written data.

Compiled from Guangzhou Dacai Technology VisualHMI development documentation (hmi-doc.gz-dc.com) LUA tutorial "File Read and Write", copyright reserved by Dacai Technology.

Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC verification and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support

Leave a Reply

Your email address will not be published. Required fields are marked *.