Lua移植为机器人编程语言


主页      博客      (1)- Qt+Lua环境搭建      (2)- 读写C变量      (3)- 调用C函数      (4)- 运行Lua脚本      (5)- 文件操作      (6)- 编辑器高亮显示      (7)- 行号更新      (8)- Lua与C共享数组     


本项目是为公司机器人控制器扩展用户脚本写的Lua移植集成,原本机器人运动控制是专门开发的指令及编程环境,其中没有预留可以用户写自定义函数的方法。过去机器人应用多是简单的搬运、焊接、装配、喷涂等,都是点到点运动,并且也没有复杂的控制逻辑。
但是现在有客户应用在医疗机器人上面,现有方案使用的是Kuka机器人,其中有许多已经写好用户函数,其功能大多不涉及运动控制,只是逻辑比较复杂。现有架构已经定型,无法实现自定义函数功能,如果替代Kuka的话,要么是完全重写底层架构,要么就需要针对客户所需的功能进行底层开发集成,优点则是效率高,但是这样做的话有几个缺点:
(1) 极大的增加底层复杂性;
(2) 降低控制器稳定性;
(3)由于客户也无法给出具体需要多少函数和具体功能需求,所以导致底层开发同事也不能有效的满足实际情况。
原来见过优傲UR机器人除了常规的指令外,有一个脚本解释器,而越疆DOBOT则直接采用Lua脚本和scratch图形化编程,其他厂家则基本是底层架构支持自定义函数功能。 综合比较,最终确定采用Lua脚本单独做用户自定义函数扩展,而运动控制继续使用当前编程环境。
-------------------------------------------------------------------
原项目直接在机器人底层中嵌入了Lua函数包,所有的变量或者函数功能都是已经在底层写好,Lua脚本只需要用户在电脑端写好脚本程序,然后通过U盘或者ssh远程传输脚本文件到控制器中,最后在控制程序或者后台任务程序中添加调用Lua脚本指令即可。
此文主要示例下如何使用Qt做编辑器界面(包含Lua文件打开/读写/保存、关键字高亮、行号更新等功能),调用集成的Lua库,运行脚本实现读写C中的变量/共享数组,调用C中的系统函数。

=======================================================================

(1) Qt+Lua环境搭建
我另外一篇blog中有相关的 Lua学习记录
或者参考 Lua开发环境搭建
网上有人已经将各个版本的Lua编译好,可以直接下载对应的文件,就无需自己编译了。
具体可以参考这个链接: Lua的依赖库dll、解释器lua.exe、编译器luac.exe
此处仅仅简单记录一下我的Qt+Lua的编程环境搭建步骤:
步骤一: 使用VS2015编译出Lua 5.4.3的库文件,(参考 搭建C++调用Lua环境 )。
(1):从 Lua官网 下载源代码:

(2): 打开VS2015 创建一个新的工程,这里我创建的是一个win32控制台应用项目(Win32 console application):

(3): 确定后下一步,选择DLL和空项目,点击完成:

(4): 添加源文件,右键选择添加Add->现有项ExistingItem,选择Lua 源代码文件夹里的所有内容,去掉里面的lua.c 和luac.c:

(5): 打开项目属性,在c++高级里把编译改为C代码(/TC): :

(5): 或者修改属性-> C/C++ -> Preprocessor -> Preprocessor Definitions 为 LUA_BUILD_AS_DLL:

(6): 直接编译即可,注意选择所需的输出的平台(x86/x64)和配置(release/debug),编译结果如下:

(7): 编译后会生成.lib和dll库,.lib用于开发链接使用,dll用于运行使用:

(8): (可选)提取出以后调用dll/lib库所需头文件(lauxlib.h/lua.h/lua.hpp/luaconf.h/lualib.h):

注:除lua.hpp都是C风格头文件,如果要在c++中使用记住加上extern "C",或者直接使用lua.hpp。其实lua.hpp就是加了extern "C"。

至此,库编译就结束了。
-------------------------------------------------------------------
步骤二:测试调用Lua DLL:
(1):我使用的是vs2015写的测试用例,首先建立一个空的C++控制台应用程序,然后在里面创建一个lua-test.cpp文件和一个test.lua文件,目录结构如下:

(2):添加项目包含目录和依赖项。我是把安装的Lua文件直接拷贝到新建的项目工程内的,这样做的好处,是方便把测试工程给大家,不需要安装Lua,工程就可以直接运行。
此处使用前一步中编译出的dll/lib和部分h头文件。



(3):在test.lua文件内添加如下代码:
print "Hello, Lua!"
(4):在lua-test.cpp文件内添加如下代码:
#include <iostream>
#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int main(int argc, char *argv[])
{
    lua_State *L = luaL_newstate();
	luaL_openlibs(L);
	luaL_dofile(L, "test.lua");
	lua_close(L);
	system("pause");
	return 0;
}


(5):如果一切顺利,此时你点击运行(F5),应该会弹窗如下窗口,说明你环境搭建成功了。


-------------------------------------------------------------------

=======================================================================
(2) 读写C变量
我另外一篇blog中有相关的 Lua学习记录
或者参考 Lua开发环境搭建
具体使用方法就不详细写了,可以参考上面两个链接,此处仅简单写下代码。
test.lua中脚本如下:
print("Lua: cvar = " .. cvar)
cvar = 321;
print("Lua:Change cvar = " .. cvar)

C代码如下所示:
#include <iostream>
#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}

int main(int argc, char *argv[])
{
    lua_State *L = luaL_newstate();
	luaL_openlibs(L);
	int cvar = 1;
	lua_pushnumber(L, cvar);     // push cvar to top of stack
	lua_setglobal(L, "cvar");    // name the top of stack
	cout << "C: cvar = " << cvar << endl;
	if (luaL_loadfile(L, "test.lua")
		|| lua_pcall(L, 0, 0, 0))
	{
		const char * error = lua_tostring(L, -1);
		cout << string(error) << endl;
	}
	lua_getglobal(L, "cvar");         //从lua变量空间将var1变量放入栈中
	if (!lua_isnumber(L, -1))         // 通常需要对栈中元素类型做下检测
		printf("is not number");
	cvar = lua_tonumber(L, -1);       // 从栈中读取刚才压入的变量,此时并不会将变量出栈
	cout << "C:AfterRunLua: cvar = " << cvar << endl;
	lua_close(L);
	system("pause");
	return 0;
}

点击运行(F5),应该会弹窗如下窗口,


=======================================================================
(3) 调用C函数
我另外一篇blog中有相关的 Lua学习记录
或者参考 Lua开发环境搭建
具体使用方法就不详细写了,可以参考上面两个链接,此处仅简单写下代码。
一: 下面这个例子仅仅是Lua脚本中调用C中函数输出信息:
test.lua中脚本如下:
fun()

C代码如下所示:
#include <iostream>
#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
LUALIB_API int fun(lua_State *L)
{
	if (L == nullptr)return 0;
	cout << "this is a message from C++" << endl;
	return 0;
}
int main(int argc, char *argv[])
{
    lua_State *L = luaL_newstate();
	luaL_openlibs(L);
	lua_pushcfunction(L, fun);     // push cvar to top of stack
	lua_setglobal(L, "fun");    // name the top of stack
	if (luaL_loadfile(L, "test.lua")
		|| lua_pcall(L, 0, 0, 0))
	{
		const char * error = lua_tostring(L, -1);
		cout << string(error) << endl;
	}
	lua_close(L);
	system("pause");
	return 0;
}

点击运行(F5),应该会弹窗如下窗口,


-------------------------------------------------------------------

一: 下面这个例子是Lua脚本中调用C中函数进行计算:
test.lua中脚本如下:
local a,s = add_sub(100,1)
print("addResult = " .. a .. "; subResult = " .. s)

C代码如下所示:
#include <iostream>
#include <stdio.h>
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
LUALIB_API int add_sub(lua_State *L)
{
	if (L == nullptr)
	{
		return 0;
	}
	int n = lua_gettop(L);
	if (n != 2)
	{
		lua_pushstring(L, "Error:invalid number of parameters.");
		lua_error(L);
		return 0;
	}
	
	int firstValue = luaL_checkinteger(L, -2);
	int secondValue = luaL_checkinteger(L, -1);
	cout << "-------- C++ output ---------" << endl;
	cout << "firstValue = " << firstValue << "; secondValue = " << secondValue << endl;
	cout << "-----------------------------" << endl;
	int firstResult = firstValue + secondValue;
	int secondResult = firstValue - secondValue;
	lua_pushnumber(L, firstResult);
	lua_pushnumber(L, secondResult);
	// this function has two value return to lua
	return 2;
}
int main(int argc, char *argv[])
{
    lua_State *L = luaL_newstate();
	luaL_openlibs(L);
	lua_pushcfunction(L, add_sub);     // push cvar to top of stack
	lua_setglobal(L, "add_sub");    // name the top of stack
	if (luaL_loadfile(L, "test.lua")
		|| lua_pcall(L, 0, 0, 0))
	{
		const char * error = lua_tostring(L, -1);
		cout << string(error) << endl;
	}
	lua_close(L);
	system("pause");
	return 0;
}

点击运行(F5),应该会弹窗如下窗口,


=======================================================================
(4) 运行Lua脚本
运行脚本基本二种方法:dostring() 和 dofile(); 有时为了更好的控制程序运行和错误检测输出,一般将上面两个函数进行分解为:loadstring()/loadfile()/pcall()。
比如:
const char* cLuaCode = "print(\"Hello,Lua!\")";
size_t len = strlen(cLuaCode);
if(luaL_loadstring(L, cLuaCode) != 0) {
  cout << "load_error:" << (lua_tostring(L, -1)) << endl;
}
if(lua_pcall(L, 0, LUA_MULTRET, 0) != 0) {
  cout << "pcall_error:" << (lua_tostring(L, -1)) << endl;
}   

if (luaL_loadfile(L, "test.lua")
    || lua_pcall(L, 0, 0, 0))
{
    const char * error = lua_tostring(L, -1);
    cout << string(error) << endl;
}

=======================================================================
(5) 文件操作
此处简单介绍下Qt中文件操作方法,具体可以参考Qt官网文档 QFile Class
1、文件操作 QFile
QFile 类提供了 一个用于读/写文件的接口,它可以用来读/写文本文件、二进制文件和 Qt 资源的 I/0 设备。
一般在构建 QFile 对象时便指定文件名,当然也可以使用 setFileName ()进行设置。可以使用 exists() 来检查文件是否存在使用 remove()来删除一个文件 。
一个文件可以使用 open() 打开,使用 close()关闭,使用 flush()刷新 ,文件的读写可以使用 read ()、 readLine ()、 readAll ()和 write (),可以使用自size()函数来获取文件的大小,使用 seek ()来定位到文件 的任意位置,使用 pos ()来获取当前的位置,使用 atEnd ()来判断是否到达了文件的末尾 。
还是可以用以下学过FILE,fstream这些来进行文件操作
打开并写入文件,文件操作流程 打开>写入/读取>关闭
访问一个设备以前,需要使用 open ()函数打开该设备,而且必须指定正确的打开模式,不同的打开模 式之间可以使用“|”符号同时使用。
打开设备后可以使用 write()或者 putChar()来进行写人,可以使用seek函数进行文件指针移动(如果 有),使用read ()、 readLine ()或者 readAll ()进行读取,最后使用 close()关闭设备。
 //----已读方式打开文件--------//
    QFile   file(filcnamc);//填充文件路径
    file.open(QIODevice::ReadOnly);//以读的方式打开文件
    //-------一次性全部读出文件内容------------------//
    QByteArray arary=file.readAll();//一次性全部读出文件内容
    //--------------------------------------//
    
    //----------------每次读取一行--------------//
      QByteArray arary_2;
      while (!file.atEnd()) {   
      //file.atEnd()判断还有没有可以读取的也就是判断是否为空
      arary_2+=file.readLine();//读取一行
      }
    //-------------------关闭文件-----------------------//
      file.close();//关闭文件
    //------------以写入的方式打开文件------------------//
    file.open(QIODevice::Append);//以在末尾追加写入的方式打开文件
    file.write("I love you!");//写入的内容
    file.close();//关闭文件

其他操作
    获取的文件大小:size()
    更改文件的名字:rename("新名字")
    删除文件:remove()
    获取一行中的某个位子的字符:先用readLine()读完这行,然后通过 string的mid函数处理
    mid(5,1)从第五个位置,获取1个字符
    移动文件指针:seek(5)

手动获取文件路径:
#include <QFileDialog> 
QString filcnamc=QFileDialog::getOpenFileName(this,"Open Files","C:");

获取文件属性
#include<QFileInfo>    //查看文件属性
#include<QDebug>    
#include<QDateTime>    //用来输出文件生成时间
 
 
2代码
QFile file("test.lua");
QFileInfo  info(file);
qDebug() << "绝对路径: " << info.absoluteFilePath();
qDebug() <<"文件名:" << info.fileName();
qDebug() << "后缀名:" << info.suffix();
qDebug() << "创建时间" << info.created();
qDebug() << "创建时间" << info.created().toString("yyyy.MM.dd hh:mm:ss");
qDebug() << "文件大小:" << info.size();

定位读取:
    QFile file("test.lua");
    QFileInfo  info(file);
    file.open(QIODevice::ReadOnly);             //以只读的方式打开文件
    file.seek(0);                               //定位文件指针位置为0
    QByteArray  array=file.read(5);             //读取到第5个
    qDebug() << "前5个字节: " << array;
    qDebug() << "当前位置: " << file.pos();
    file.seek(15);
    array=file.read(5);
    qDebug() << "第16到第20个字节:" << array;
    file.close();

内容文件和文件夹的显示和监视文件或者文件夹有无发生改变
#include<QDebug>
#include<QDir>                  //操作文件夹
#include<QFileSystemWatcher>    //文件夹监视器
 
1.显示选顶文件夹下的所有文件和目录
 
 QDir myDir("D:\\xnj");//设置当前文件夹   
 myDir.absolutePath();//显示当前路劲下的文件和文件夹
 myDir.entryList();//显示
 
2.指定文件后缀名显示选顶文件夹下的文件
 QDir myDir("D:\\xnj");//设置当前文件夹   
 myDir.setNameFilters(QStringList("*.cpp"));//只显示后缀名为.cpp的
 myDir.entryList();//显示 
 
3.在当前选定目录下生成文件夹
QDir myDir("D:\\xnj");//设置当前文件夹   
myDir.mkdir("mydir");  //在当前路径创建一个文件夹
 
4.文件文件夹监视器
//-------------------声明一个槽函数------------------------//   
   private slots:   
 void showMessage(const QString  &path);//文件监控槽函数 
//--------------------------------------------------------//
 
//-------------------定义槽函数----------------------------//
void MainWindow::showMessage(const QString &path)//文件夹监视槽函数
{
    if(path=="D:\\xnj\\mydir")
    {
       //文件夹
    }
    else if(path=="D:\\xnj\\yy.txt")
    {
        //文件 
    }  
}
 
//--------------------操作代码--------------------------//
 QDir myDir("D:\\xnj");//设置当前文件夹   
  
 connect(&myWatcher,&QFileSystemWatcher::directoryChanged,this,&MainWindow::showMessage);//监视文件夹有没有发生改变
   connect(&myWatcher,&QFileSystemWatcher::fileChanged,this,&MainWindow::showMessage);
//监视文件有没有发生改变
    myWatcher.addPath("D:\\xnj\\mydir");
    myWatcher.addPath("D:\\xnj\\yy.txt");
//------------------------------------------------------//

文本流与数据流(传输文件)
#include<QTextStream>  //文本流
#include<QDataStream>  //数据流
 
1.文本流的读取
 
//-----------------------------文本流读取---------------------------------------------//
QString filcnamc=QFileDialog::getOpenFileName(this,"打开文件","C:\\Users\\ym\\Desktop\\zy"); QFile   file(filcnamc);//填充文件路径
file.open(QIODevice::ReadOnly);//以读的方式打开文件
 QTextStream  stream(&file);
 stream.setCodec("utf-8");
 QByteArray arary_2;
 while (!stream.atEnd())  //stream.atEnd()判断还有没有可以读取的也就是判断是否为空
 {   
 
   arary_2+=stream.readLine();//读取一行
  
 }
 
file.close();
//----------------------------数据流读取写------------------------------------------//
 
 QFile file_2("C:\\Users\\1.txt");
     file_2.open(QIODevice::WriteOnly);//以写的方式打开
     QDataStream  data_stream(&file);//创建数据流
     
     data_stream<<QString("jfesfesf")<<(qint32)65;
     file_2.close();
     file_2.open(QIODevice::WriteOnly);//以读取的方式打开 
     QDataStream  data_in_stream(&file);//创建数据流
     QString str;
     qint32 n;
    data_in_stream >> str >> n;
//-----------------------------------------------------------------------------------//
总结:我本流与操作系统有关系可能会出现乱码,大小端不一致,而数据流与操作系统无关所以适合文件传输
 


=======================================================================
(6) 编辑器高亮显示
本来计划直接使用VS Code或者Notepad++进行Lua脚本的编辑,然后程序中打开对应的文件开始运行就可以了。
后来想着将脚本文件各种操作都做在Qt中,打开、编辑、传输、调试等等,所以准备做个Lua界面编辑器,网上查了很多Qt扩展插件,比如:QScintilla/textosaurus/notepad等,但是因为移植有点麻烦,并且要么功能太多,要么不稳定,或者有些功能还无法修改实现。最终还是采用Qt的Syntax Highlighter进行关键字高亮显示,而自动补全提示目前暂时没有实现。
Syntax Highlighter具体使用方法和案例完全可以参考文档 Syntax Highlighter Example
对于Lua要修改的容易出错是多行注释,因为在Lua中需要“--[[” 和 “]]”进行配对实现多行注释,所以要使用\转意符。 比如下述代码:
    multiLineCommentFormat.setForeground(Qt::green);
    commentStartExpression = QRegularExpression(QStringLiteral("--\\[\\["));
    commentEndExpression = QRegularExpression(QStringLiteral("--\\]\\]|\\]\\]"));

其他要修改的地方就是将例程中C++关键字,替换成Lua所使用的关键字。
具体效果如下图所示:


=======================================================================
(7) 行号更新
此处使用了两个QTextEdit进行同步更新,根据脚本编辑区txtLuaScript的信号(textChanged、cursorPositionChanged)更新txtLineNum中的内容,具体代码可参考下面:
void MainWindow::updateLineNum()
{
    // change lineNum area width
    int digits = 1;
    int lines = totalLines;
    while (lines >= 10)
    {
        lines /= 10;
        ++digits;
    }
    int lineNumAreaWidth = 15 + fontMetrics().horizontalAdvance(QLatin1Char('8')) * digits;
    ui->txtLineNum->setMinimumSize(lineNumAreaWidth ,600);

    // update line number of left-linenum-area
    QString strLineNum = "";
    for(int i=1;i<totalLines;i++)
        strLineNum += QString::number(i) + '\n';
    ui->txtLineNum->setText(strLineNum);
    ui->txtLineNum->verticalScrollBar()->value() ui->txtLineNum->verticalScrollBar()->setValue(ui->txtLuaScript->verticalScrollBar()->value());
}
void MainWindow::on_txtLuaScript_textChanged()
{
    totalLines = ui->txtLuaScript->document()->lineCount();
    updateLineNum();
}

void MainWindow::on_txtLuaScript_cursorPositionChanged()
{    ui->txtLineNum->verticalScrollBar()->setValue(ui->txtLuaScript->verticalScrollBar()->value());
}


=======================================================================
(8)- Lua与C共享数组
因为机器人编程中需要读写很多的系统变量,最简单的方法就是使用前面所写的 (2)- 读写C变量 ,但是因为很多变量在C中是数组,如果按2中所示,需要将所有变量都一个个的注册,非常麻烦。
后来想直接将C中的数组共享给Lua,这样在脚本中读写就更简单了,网上查询了差不多2周时间(能力有限:-(...),找到一个帖子 share-array-between-lua-and-c ,完全就是我想要的效果.
注意:帖子中的Answer代码需要根据自己的版本进行修改一下。
此处分享转载如下:

I have really Googled this question but I never really got an solution.

I want to share an Array between C and Lua, for performance I will avoid copying Arrays to and from Lua.

So I want to pass a pointer to the Array from C to Lua. And then from Lua I want to set/modify values in this array directly.


Example in C code

I want to define my array

int mydata[] = {1,2,3,4} 

set it global to access it from Lua with the name mydata.


In Lua

I want to change the values like this

mydata[3] = 9

and when I return to C, mydata[3] is 9 because it is a pointer to the array.

How is this possible?


------------ Downside is the best answer ------------------

You can expose arbitrary data to Lua via userdata. If you give your userdata values a metatable, you can define the behavior for various operators/operations on those userdata. In this case, we want to expose an array to Lua and define what to do in the case of array[index] and array[index] = value.

We expose the array to Lua by creating a userdata buffer large enough to hold the address of the array. We define the indexing/assignment behavior by created a metatable with the __index and __newindex methods.

Below is a complete, working example that exposes a static array to Lua. Your program will probably have some other call for returning the array to Lua. Note, there's no boundschecking at all; if you try to index outside the array bounds, you'll crash. To make this more robust, you'd want to change the userdata to a structure which has the array pointer and the array size, so you can do boundschecking.

#include "lauxlib.h"

// metatable method for handling "array[index]"
static int array_index (lua_State* L) { 
   int** parray = luaL_checkudata(L, 1, "array");
   int index = luaL_checkint(L, 2);
   lua_pushnumber(L, (*parray)[index-1]);
   return 1; 
}

// metatable method for handle "array[index] = value"
static int array_newindex (lua_State* L) { 
   int** parray = luaL_checkudata(L, 1, "array");
   int index = luaL_checkint(L, 2);
   int value = luaL_checkint(L, 3);
   (*parray)[index-1] = value;
   return 0; 
}

// create a metatable for our array type
static void create_array_type(lua_State* L) {
   static const struct luaL_reg array[] = {
      { "__index",  array_index  },
      { "__newindex",  array_newindex  },
      NULL, NULL
   };
   luaL_newmetatable(L, "array");
   luaL_openlib(L, NULL, array, 0);
}

// expose an array to lua, by storing it in a userdata with the array metatable
static int expose_array(lua_State* L, int array[]) {
   int** parray = lua_newuserdata(L, sizeof(int**));
   *parray = array;
   luaL_getmetatable(L, "array");
   lua_setmetatable(L, -2);
   return 1;
}

// test data
int mydata[] = { 1, 2, 3, 4 };

// test routine which exposes our test array to Lua 
static int getarray (lua_State* L) { 
   return expose_array( L, mydata ); 
}

int __declspec(dllexport) __cdecl luaopen_array (lua_State* L) {
   create_array_type(L);

   // make our test routine available to Lua
   lua_register(L, "array", getarray);
   return 0;
}

Usage:

require 'array'

foo = array()
print(foo) -- userdata

-- initial values set in C
print(foo[1])
print(foo[2])
print(foo[3])
print(foo[4])

-- change some values
foo[1] = 2112
foo[2] = 5150
foo[4] = 777

-- see changes
print(foo[1])
print(foo[2])
print(foo[3])
print(foo[4])

=======================================================================
(1) 利用直方图对汽车车牌进行字符分割

=======================================================================       
=========================================================================================