VisualHMI 文件 IO:LUA 読み書き SD 卡/U 盘/Flash 与目录列举

freeFree Technical Resource

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

文件読み書き API 全套:读 txt/csv/bin、写文件、遍历目录、拷贝/Delete。核心思想就一条——流式処理,永远不要让内存里同时存在超过 2KB 的バッファ。HMI 内存极小,违反这条ウォッチドッグ直接复位,デバイス无预警再起動。

1. 存储デバイス标识(M 系列 vs DH 系列)

deviceM 系列DH 系列
SD 卡1:/1.txt/sdcard/1.txt
U 盘2:/1.txt/udisk/1.txt
内部 Flash3:/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
end

6.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
五个必踩的坑:①内存铁律——严禁全局累积大缓冲,一次只留 ≤2KB 块,否则ウォッチドッグ复位;②追加写必须 file_seek(file_size()),HMI 的 append ではない O_APPEND;③路径必须带デバイス前缀和 /——「1:/config.txt」对、「1:config.txt」错;④大文件読み取り循环里要 feed_dog(),回调里别干耗时的事;⑤写文件后立即 file_close(),停電会损坏文件或部分写入。

整理自 广州大彩科技 VisualHMI 开发ドキュメント(hmi-doc.gz-dc.com)LUA Tutorial「文件読み書き」,著作権は大彩科技すべての。

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 *.