在利用 VREP 模型(Coppeliasim) 进行仿真、数字孪生时,一般会通过网络(TCP,UDP,UART)接收大量的外部现场数据。
为了能方便 VREP 的离线开发和调试(不必每次都等编梯形图来发送数据调试或本地模拟数据),有必要暂存接收到的现场数据,待到合适的时候载入 VREP 模型然后再利用,因此,需要将 VREP 启动后整个周期内的数据暂存下来,从而方便后续本地调试
值得一提的是,读写文件暂存数据,可以很方便地用 Python 或者 C++ QT 写插件来实现,VREP 本身也提供了多语言的开发 API 和 SDK。但是考虑到开发和运行环境的一致性,避免到新环境演示时还需要配置一堆依赖和不必要的性能损耗(虽然这里对性能要求不高),以下调研的方案都是围绕 VREP 本身和其官方示例子首推的 lua 语言展开的
要想完成上述流程的需求,主要需要了解 VREP 中如下 API 能力:
- 在本地读写二进制文件
- 获取指定路径文件列表
- 界面能力。由于存储的数据可能较多,需要和用户简单交互:选择载入哪些数据文件,存储到哪个文件夹等
经过查阅 VREP 官方文档发现:
- VREP 几乎没有读写本地文件的操作 API, 只能用 sim.persistentDataWrite 将打包好的数据暂存本地缓存中,具体数据文件不可见,会在下次载入环境时自动载入 Then persistent data can also be stored on file, and be automatically reloaded next time CoppeliaSim starts.
- 读写文件,获取指定路径文件夹 需要完全借助 lua,VREP 并未提供相关能力
- 交互能力较弱。但提供了 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 如下:
而 SimUI 能力丰富,其控件不局限于对话框,甚至还能用 QML 开发新控件,以下是一个结合了 SimUI 和 fileDialog() 的示例,通过对话框完成文件、文件夹的选择:
点击按钮后会返回要保存的文件夹,可通过一些信号量在脚本间传递要保存的目录:sim.setStringSignal("saveDirectoryPath", saveDirectoryPath)
在 UDP 脚本中,检测到保存目录后,对每次收到的数据进行保存(注意这里的保存是不严格的,可能会漏 UDP 包,需要注意数据源的频率和 VREP 协程切换的最小时间片):saveByteFile(sim.getStringSignal("saveDirectoryPath"),"TZ.idb",udpData)
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 便利文件夹列表了。
最后即可得到要保存的数据文件:
以上是保存数据文件的过程,载入数据到模型的过程与此类似:
- 只需打开文件选择对话框,多选选中要使用的数据源,获取数据绝对路径
- 调用 readByteFile() 读取字节
- 再按字节内容的实际含义进行转换(反序列化)
- 最后通过 sim.readCustomTableData、 sim.writeCustomTableData、sim.readCustomDataBlock、sim.writeCustomDataBlock 跨子脚本传递数据
- 驱动模型即可
方案结论#
总的来说,经过测试结论如下:
- VREP 未提供文件读写能力。在本地读写二进制文件使用 lua 完成
- VREP SimUI 提供了一定的界面能力。可以通过文件对话框 simUI.fileDialog() 指定数据保存路径、指定载入数据的路径
两者结合,即可完成数字孪生模型在本地的暂存和载入