在利用 VREP 模型(Coppeliasim) 进行仿真、数字孪生时,一般会通过网络(TCP,UDP,UART)接收大量的外部现场数据。

为了能方便 VREP 的离线开发和调试(不必每次都等编梯形图来发送数据调试或本地模拟数据),有必要暂存接收到的现场数据,待到合适的时候载入 VREP 模型然后再利用,因此,需要将 VREP 启动后整个周期内的数据暂存下来,从而方便后续本地调试

值得一提的是,读写文件暂存数据,可以很方便地用 Python 或者 C++ QT 写插件来实现,VREP 本身也提供了多语言的开发 API 和 SDK。但是考虑到开发和运行环境的一致性,避免到新环境演示时还需要配置一堆依赖和不必要的性能损耗(虽然这里对性能要求不高),以下调研的方案都是围绕 VREP 本身和其官方示例子首推的 lua 语言展开的

概述

要想完成上述流程的需求,主要需要了解 VREP 中如下 API 能力:

  • 在本地读写二进制文件
  • 获取指定路径文件列表
  • 界面能力。由于存储的数据可能较多,需要和用户简单交互:选择载入哪些数据文件,存储到哪个文件夹等

经过查阅 VREP 官方文档发现:

  1. VREP 几乎没有读写本地文件的操作 API, 只能用 sim.persistentDataWrite 将打包好的数据暂存本地缓存中,具体数据文件不可见,会在下次载入环境时自动载入 Then persistent data can also be stored on file, and be automatically reloaded next time CoppeliaSim starts.
  2. 读写文件,获取指定路径文件夹 需要完全借助 lua,VREP 并未提供相关能力
  3. 交互能力较弱。但提供了 SimUI 和 SimTextEditor 等简单的平面UI交互能力

LUA 读写文件

LUA 提供了一些函数来读写文件,下面是一些常用的文件读写函数:

  • open:打开一个文件并返回文件句柄。可以指定文件名、打开模式(读、写、追加等)和字节序(大端或小端)
  • close:关闭一个打开的文件。需要传入文件句柄
  • read:从文件中读取指定数量的字节,并以字符串形式返回。需要传入文件句柄和要读取的字节数
  • write:向文件中写入指定的字符串。需要传入文件句柄和要写入的字符串

读写模式:

“r” : read mode (the default);

“w” : write mode;

“a” : append mode;

“r+” : update mode, all previous data is preserved;

“w+” : update mode, all previous data is erased;

“a+” : append update mode, previous data is preserved, writing is only allowed at the end of file.

下面是一个简单的示例,演示了如何使用这些函数读取和写入文件:

读写文件的路径都是 VREP 安装根目录路径,并非脚本附着的模型所在的路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- 打开文件
local file = io.open("data.txt", "w")

-- 写入数据
file.write(file, "Hello, world!\n")

-- 关闭文件
file.close()

-- 打开文件
file = io.open("data.txt", "r")

-- 读取数据
local data = file.read(file, 1024)

-- 关闭文件
file.close(file)

-- 输出数据
print(data)

在这个示例中,首先使用 simOpenFile 函数打开一个名为 data.txt 的文件,并指定打开模式为写入。然后使用 simWriteFile 函数将字符串 “Hello, world!\n” 写入文件中。然后再次使用 simOpenFile 函数打开文件,但这次指定打开模式为读取。然后使用 simReadFile 函数读取文件中的数据,并将其存储在一个变量中。最后,使用 print 函数将读取的数据输出到控制台。

基于此,简单封装了读写字节文件的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function readByteFile(filename)
    local file = io.open(filename, "rb")
    if file then
        local content = file:read("*all")
        file:close()
        return content
    else
        print('File Empty or not exist.')
    end
    return nil
end

function saveByteFile(directoryPath, filename, byteContent)
    -- Check args and fill them with default value if not exist.
    if directoryPath == nil or directoryPath == {} or directoryPath == '' then
        print('directoryPath is no exist, jumping, please check it for the first')
        return
    end
    if filename == nil then
        filename = 'DemoData.idb' -- give the default filename
    end

    local timestamp = os.date("%Y-%m-%d %H-%M-%S ") -- Must not contain colon character.
    local milliseconds = string.format("%.3f", os.clock()):sub(-3)
    local tmpFilename = directoryPath.. '/' .. timestamp ..milliseconds.. filename

    print(" Saved Data Size: " .. #byteContent.. '\t'..tmpFilename)

    local file = io.open(tmpFilename, "wb")
    if file then
        -- file:write(timestamp .. " " .. byteContent .. "\n")
        file:write(byteContent)
        file:flush() -- Flush the buffer to ensure UDPData is written immediately

        file:close()
        return content
    else
        print("File Saving failed, Please check the filename")
    end
    return nil
end

交互能力 & 基本界面

VREP 交互能力较弱。但提供了 SimUI 和 SimTextEditor 等简单的控件可供直接使用:

  • 通过内嵌 SimTextEditor 结合以下函数弹出文本编辑框,在编辑框输入字符完成交互

  • 利用 SimUI 交互,simUI.fileDialog() 提供了保存文件选择、打开文件选择、文件夹选择等能力

    • simUI.fileDialog(1,’title1’,’./data/’,‘init’,‘idb’,‘idb’) – 保存文件
    • simUI.fileDialog(2,’title2’,’./data/’,‘init’,‘idb’,‘idb’) – 打开文件(可多选)
    • simUI.fileDialog(3,’title3’,’./data/’,‘init’,‘idb’,‘idb’) – 打开文件夹

    上述方法会返回一个字典,其内容是会返回的文件路径:

1
2
3
"LoadingOpenFile: ",{"C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data/2023-04-08-09-41-25A.idb", "C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data/2023-04-08-09-41-26A.idb", "C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data/2023-04-08-09-41-27A.idb", "C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data/2023-04-08-09-41-28A.idb", "C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data/2023-04-08-09-41-29A.idb"}
"LoadingSaveFile: ",{"C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data/A.idb"}
"Loading Dir: ",{"C:/Program Files/CoppeliaRobotics/CoppeliaSimEdu/data"}

SimUI 对话框 Demo

经过测试,SimTextEditor 实现效果和能力非常一般(也是 SimUI 的一部分),具体代码可见模型中的 TextEditorDemo 附着的脚本,SimTextEditor 如下:

1680923942602

而 SimUI 能力丰富,其控件不局限于对话框,甚至还能用 QML 开发新控件,以下是一个结合了 SimUI 和 fileDialog() 的示例,通过对话框完成文件、文件夹的选择:

1680923942602

点击按钮后会返回要保存的文件夹,可通过一些信号量在脚本间传递要保存的目录:sim.setStringSignal("saveDirectoryPath", saveDirectoryPath)

在 UDP 脚本中,检测到保存目录后,对每次收到的数据进行保存(注意这里的保存是不严格的,可能会漏 UDP 包,需要注意数据源的频率和 VREP 协程切换的最小时间片):saveByteFile(sim.getStringSignal("saveDirectoryPath"),"TZ.idb",udpData)

1680936618816

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
-- 完整 SimUI 代码
function initUI()
    xml = [[
<ui closeable="true" on-close="closeEventHandler" resizable="true">
    <!-- label text="Digital Twin Control Panel" wordwrap="true" />
    -->
  
    <tabs>
  
        <tab title="FileDialogDemo">
            <button text="Select open File" on-click="LoadingOpenFile" />
            <button text="Select save File" on-click="LoadingSaveFile" />
            <button text="Select open Directory" on-click="LoadingDir" />
        </tab>
  
        <tab title="SaveFile">
            <button text="Select Data Save Directory" on-click="SetSavingDataDirectory" />
        </tab>
  
        <!--
        <tab title="Play Back">
            <button text="Loading local idb file" on-click="loadingButtonClick" />
            <label text="File Total: " id="3000" wordwrap="true" />
  
            <group layout="form">
                <label text="Progressbar:" />
                <progressbar id="3500" />
            </group>
        </tab>
        -->
  
    </tabs>
</ui>
]]
    ui = simUI.create(xml)
    simUI.setTitle(ui, "Digital Twin Control Panel")
end

function LoadingSaveFile(ui, id)
    print('LoadingSaveFile: ', simUI.fileDialog(1,'title1','.','init','idb','idb'))
end

function LoadingOpenFile(ui, id)
    print('LoadingOpenFile: ', simUI.fileDialog(2,'title2','.','init','idb','idb'))
end

function LoadingDir(ui, id)
    print('Loading Dir: ', simUI.fileDialog(3,'title3','.','init','idb','idb'))
end

function SetSavingDataDirectory(ui, id)
    saveDirectoryPath = simUI.fileDialog(3,'Setting data storing path','.','','','')[1]
    sim.setStringSignal("saveDirectoryPath", saveDirectoryPath)
    print('SetSavingDataDirectory Dir: ', saveDirectoryPath)
end

其他:从指定目录自动读写文件夹列的能力

由于存储了多个数据文件,在仿真调试时可能不必完全载入,可能需要在文件夹中选择部分需要的文件。很遗憾,LUA 本身没有提供读写指定文件夹的能力,需要安装 lfs 拓展才能实现。或者可以粗暴地把需要载入的数据的路径和文件名写到脚本中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local lfs = require("lfs")

function loadingFileListInDir()
    fileListResult = {}
    file_list = lfs.dir(data_dir_path)
    for file_name in file_list do
    for index, file_name in pairs(file_list) do
        -- ignore . and ..
        if file_name ~= '.' and file_name ~= '..' then
            local full_file_path = data_dir_path .. file_name
            print(full_file_path)
            -- table.insert(fileListResult, UTIL.loadLocalIDBFile(full_file_path))
        end
    end
    return fileListResult
end

不过借助于上面的对话框多选,是可以选择要载入的数据的,一旦数据路径拿到,也就用不上 lfs 便利文件夹列表了。

最后即可得到要保存的数据文件:

1680936992311

以上是保存数据文件的过程,载入数据到模型的过程与此类似:

  1. 只需打开文件选择对话框,多选选中要使用的数据源,获取数据绝对路径
  2. 调用 readByteFile() 读取字节
  3. 再按字节内容的实际含义进行转换(反序列化)
  4. 最后通过 sim.readCustomTableDatasim.writeCustomTableDatasim.readCustomDataBlocksim.writeCustomDataBlock 跨子脚本传递数据
  5. 驱动模型即可

方案结论

总的来说,经过测试结论如下:

  • VREP 未提供文件读写能力。在本地读写二进制文件使用 lua 完成
  • VREP SimUI 提供了一定的界面能力。可以通过文件对话框 simUI.fileDialog() 指定数据保存路径、指定载入数据的路径

两者结合,即可完成数字孪生模型在本地的暂存和载入