C# 版上位机二次开发教程
视频教程
我们为您准备了视频教程,可以在这里观看全部知识。
在我们开始之前
我们将在这个教程开发一个具有如下功能的程序:
- 连接控制器;
- 让机器人上电;
- 点动机器人;
- 让机器人关节插补;
- 让机器人直线插补;
- 获取机器人位置;
- 设置数字输出;
- 查询数字输出;
这篇教程将分为 3 个章节:
前置知识
在学习使用 C#进行上位机二次开发前,我们默认您已经具备以下知识:
- C#语言
- 工业机器人相关基础知识
- Visual Studio 编辑器
如果您不具备以上知识,请不要继续本教程。我们给您推荐以下教程:
环境准备
使用 C#进行上位机二次开发,请在 Windows 系统下使用 Visual Studio 进行。
Visual Studio
我们建议您前往 Visual Studio 官网下载Visual Studio Community 2019版本编辑器,网址如下。
免费的 IDE 和开发人员工具 | Visual Studio Community安装过程中请注意勾选如下两项:
- .NET 桌面开发
- 使用 C++ 的桌面开发
SDK
进行二次开发也少不了我们提供的 SDK 文件,请点击下面的链接进行下载。
基础教程
项目初始化
- 在 Visual Studio Community 2019(以下简称 VS)中创建新项目,检索语言-C#,环境-桌面。选择Windows 窗体应用(.NET Framework);
- 项目名称可以命名为 csharpdemo,为了方便开发调试,请勾选将解决方案和项目放在同一目录中,点击创建即可;
- 将 SDK 中提供的Nrc.cs文件放入项目目录中;
- 右键解决方案资源管理器窗口中的csharpdemo项目名,选择添加-现有项,在弹出来的窗口中选择刚刚放入目录的Nrc.cs文件;
- 在上面的编译一栏中,选择 Debug、Any CPU,点击启动按钮,会编译出一个空白窗口;
- 关闭空白窗口后,将 SDK 中依赖目录下的所有文件复制到项目目录的bin/Debug目录下;
- 项目初始化完毕。
连接控制器
首先在工具箱窗口中拖入一个TextBox控件。如果工具箱窗口消失,请在视图菜单中打开它。在属性窗口中设置 TextBox 控件的属性如下:
- Name:ipAddress
再拖入一个Button控件,设置属性如下:
- Name:connectButton
- Text:连接控制器
选中 Button 控件后,点击属性窗口中闪电图标的事件按钮,双击其中Click,新建按钮的 Click 事件对应的函数connectButton_Click()
,其代码如下:
private void connectButton_Click(object sender, EventArgs e)
{
var ip = ipAddress.Text; // 获取ipAddress控件中的值
if (Nrc.connect_robot(ip)) //调用connect_robot(),返回值为bool
{
connectButton.Text = "连接成功";
}
else
{
connectButton.Text = "连接失败";
}
}
这里面使用到了 API 中的connect_robot()
,传入了参数 ip。返回值为 bool 类型,true
表示连接成功,false
表示连接失败。
在这里使用 if 来判断,如果连接成功,则将 connectButton 的显示值变为连接成功
,否则变为连接失败
。
机器人上电
机器人需要上电后才可以进行点动、运行等操作。
拖入一个Button控件,设置属性如下:
- Name:enableButton
- Text:机器人上电
新建控件的 Click 事件enableButton_Click()
,其代码如下:
private void enableButton_Click(object sender, EventArgs e)
{
if(enableButton.Text == "机器人上电") {
if (Nrc.servo_power_on()) //调用servo_power_on()
{
enableButton.Text = "机器人下电";
}
else
{
return;
}
}else if(enableButton.Text == "机器人下电")
{
Nrc.servo_power_off(); //调用servo_power_off()
}
}
这里使用到了 APi 中的servo_power_on()
和servo_power_off()
。
servo_power_on()
返回值为 bool 类型,true
表示上电成功,false
表示不成功。
这里使用 if 来判断,如果上电成功则将显示字符改变为机器人下电
,否则不执行操作。
外面的 if 表示如果按钮的字符为机器人上电
,则执行上电操作,如果为机器人下电
,则执行下电操作。
机器人下电使用servo_power_off()
,没有返回值。
查看轴的当前位置
在点动之前,我们需要先看到轴的当前位置,这样在点动过程中也能看到值的实时变化值。
拖入一个Label
控件,设置属性如下:
- Name:j1value
- Text:无
Text 的无表示当没有获取到值时,默认的显示值为“无”。
定义一个 getPosition()方法,用来获取当前位置
private void getPosition()
{
var position = new double[] { 0, 0, 0, 0, 0, 0 }; //定义数组变量用来接收位置值,6个轴,所以数组长度为6
Nrc.get_current_position(position); //调用get_current_position()
j1value.Text = position[0].ToString(); //需要将double值转换为string才能在label中显示
}
这里使用了get_current_position()
,根据 API 文档我们看到需要传入一个数组变量来接收位置值。由于机器人有 6 个轴,我们定义了一个长度为 6 的数组变量。
下面需要使用定时器功能,用来每隔一段时间获取一次当前位置值。
在Form1
类中定义一个定时器 getPositionTimer
static System.Windows.Forms.Timer getPositionTimer = new System.Windows.Forms.Timer();
我们需要在控制器连接成功后才可以开始获取,所以修改 connectButton 的 Click 事件函数如下:
private void connectButton_Click(object sender, EventArgs e)
{
var ip = ipAddress.Text;
if (Nrc.connect_robot(ip))
{
connectButton.Text = "连接成功";
//设置定时器每隔一段时间执行函数handleGetPositionTimer()
getPositionTimer.Tick += new EventHandler(handleGetPositionTimer)//定时器,每500ms执行一次handleGetPositionTimer
getPositionTimer.Interval = 500; //500ms
getPositionTimer.Start();
}
else
{
connectButton.Text = "连接失败";
}
}
这里我们将定时器绑定了handleGetPositionTimer
这个方法,每隔 500ms 执行一次。所以我们需要定义一下:
// 注意要传入参数 Object myObject,EventArgs myEventArgs
private void handleGetPositionTimer(Object myObject,EventArgs myEventArgs)
{
getPosition();
}
这样,连接控制器后,每隔 500ms,就会执行一次handleGetPositionTimer()
方法,也就是会执行一次getPosition()
,将当前位置中的 1 轴的值放在j1value
这个 Label 中显示出来。
反方向点动按钮
下面我们制作点动按钮。
拖入一个Button控件,设置属性如下:
- Name:j1re
- Text:1 轴 -
点动需要每 200ms 调用一次,否则系统会判定为连接断开,机器人停止。所以点动也需要使用定时器功能。在Form1
类中定义一个定时器 jogTimer
static System.Timers.Timer jogTimer = new System.Timers.Timer();
由于点动是一个过程,不能使用 Click 事件,所以新建控件MouseDown(鼠标按下后)事件的函数,代码如下:
private void j1re_MouseDown(object sender, MouseEventArgs e) // 鼠标按下时
{
jogTimer.Elapsed += new ElapsedEventHandler((ss, ee) => jogging(ss, ee, 1, false)); //点动,false表示反方向
jogTimer.Interval = 200;//点动需要200毫秒发一次,否则会判断中断点动
jogTimer.Start();
}
这里定时器每隔 200ms 会执行一次jogging()
方法,并传入了 1、false 两个参数,分别表示 1 轴,反方向。
我们需要定义jogging()
方法如下
private void jogging(Object source,ElapsedEventArgs e,int axis,bool direction)
{
Nrc.start_jogging(axis, direction);
}
这里调用了start_jogging()
,传入的两个参数分别表示轴和方向,方向使用 bool 类型表示,true 表示正方向,false 表示反方向。
我们还需要新建 j1re 控件的MouseUp*(鼠标抬起)事件的函数j1re_MouseUp()
,代码如下:
private void j1re_MouseUp(object sender, MouseEventArgs e) //鼠标抬起时
{
jogTimer.Stop();
Nrc.stop_jogging(1); //停止点动1轴
}
这里有两步操作,停止 jogTimer 定时器,不再调用start_jogging()
,然后调用stop_jogging()
,传入参数 1,表示停止点动 1 轴。
为了防止鼠标按下时离开按钮机器人还在动,可以再新建 j1re 控件的MouseLeave(鼠标离开)事件的函数j1re_MouseLeave()
,其内容与j1re_MouseUp()
相同,代码如下:
private void j1re_MouseLeave(object sender, EventArgs e) //鼠标按下的同时离开
{
jogTimer.Stop();
Nrc.stop_jogging(1);
}
现在可以点击上面的启动按钮调试一下,在输入框中输入 IP 地址,点击连接控制器,显示连接成功后,点击机器人上电按钮,按钮变为了机器人下电,这时点击 1 轴 -,如果数值在减少,松开后数值停止改变,恭喜你成功了!
正方向点动按钮
再拖入一个Button控件,修改属性如下:
- Name:j1po
- Text:1 轴 +
它的MouseDown事件函数代码如下:
private void j1po_MouseDown(object sender, MouseEventArgs e) // 鼠标按下时
{
jogTimer.Elapsed += new ElapsedEventHandler((ss, ee) => jogging(ss, ee, 1, true)); //点动,false表示反方向
jogTimer.Interval = 200;//点动需要200毫秒发一次,否则会判断中断点动
jogTimer.Start();
}
可以看到它和j1po_MouseDown()
仅仅传给jogging()
方法的参数,从false
变为了true
,表示正向运动。
j1po 控件的MouseUp和MouseLeave事件函数内容与 j1re 相同,在此不做赘述。
关节插补按钮
下面我们制作关节插补和直线插补的功能。选择坐标系,设置好速度、1-6 轴的值后,点击关节插补按钮,机器人做关节插补运动到设置的目标值。点击直线插补按钮,机器人做直线插补运动到设置的目标值。
首先拖入一个ComboBox控件,用来选择坐标系,设置属性如下:
- Name:coordBox
然后右键控件,点击编辑项,在其中写入四个坐标系,每个坐标系一行:
- 关节坐标
- 直角坐标
- 工具坐标
- 用户坐标
拖入 7 个TextBox控件,分别用来填写运行速度和 1-6 轴的目标值。分别设置它们的Name属性值如下:
- speed
- pos1
- pos2
- pos3
- pos4
- pos5
- pos6
拖入 8 个Label控件,分别与 ComboBox 和 7 个 TextBox 控件对齐,分别设置它们的 Text 属性如下:
- 坐标系
- 速度
- 1 轴
- 2 轴
- 3 轴
- 4 轴
- 5 轴
- 6 轴
拖入一个Button控件,设置属性如下:
- Name:movjButton
- Text:关节插补
新建 movjButton 的Click事件函数movjButton_Click()
,代码如下
private void movjButton_Click(object sender, EventArgs e)
{
// pos1.text是个string,要转成double
var pos = new double[] { Convert.ToDouble(pos1.Text), Convert.ToDouble(pos2.Text), Convert.ToDouble(pos3.Text), Convert.ToDouble(pos4.Text), Convert.ToDouble(pos5.Text), Convert.ToDouble(pos6.Text) };
//速度要转成int
//坐标系0-关节,1-直角,2-工具,3-用户,这里的selectedIndex和定义的数字正好相等
Nrc.robot_movj(pos,Convert.ToInt32(speed.Text),coordBox.SelectedIndex);
}
这里调用了robot_movj()
,需要传入 3 个参数-位置、速度、坐标系,分别为 double[]、int、int 类型。
首先我们定义一个 pos 数组变量,它的值为 pos1-pos6 输入框(TextBox)的值,但是由于 TextBox 控件返回的是 string 类型,所以我们需要使用Convert.ToDouble()
方法来将 string 转换为 double 类型存入数组,然后传入robot_movj()
。
使用Convert.ToInt32()
方法将 speed 输入框的值转换为 int,传入robot_movj()
,速度针对关节插补和直线插补的含义不同
- 直线插补:毫米每秒,1-9999
- 关节插补:关节最大速度的百分比,1-100
根据 ComboBox 的特征,我们选中下拉框中第 1-n 项时,其传回的 SelectedIndex 值为 0-(n-1)。而我们定义的坐标系也正好为 0-关节,1-直角,2-工具,3-用户。所以我们直接将 coordBox 控件的SelectedIndex值传入robot_movj()
。
这样关节插补的按钮就制作完成了。
下面我们在 j1value 这个 Label 控件下面新拖入 5 个 Label 控件,分别表示 2-6 轴的值,分别设置其 Name 属性为 j2value-j6value,修改getPosition()
代码如下:
private void getPosition()
{
var position = new double[] { 0, 0, 0, 0, 0, 0 };
Nrc.get_current_position(position);
j1value.Text = position[0].ToString();
j2value.Text = position[1].ToString();
j3value.Text = position[2].ToString();
j4value.Text = position[3].ToString();
j5value.Text = position[4].ToString();
j6value.Text = position[5].ToString();
}
点击启动按钮进行调试,机器人上电后,选择关节坐标系,速度填写 20,1 轴-6 轴分别填写 10,20,30,40,50,60,点击关节插补按钮。如果 j1value-j6value 控件显示机器人正在向目标值运动,并最终到达目标值,那么恭喜你,成功制作了关节插补功能!
直线插补按钮
直线插补与关节插补按钮的功能相同。我们拖入一个Button控件,设置其属性如下:
- Name:movlButton
- Text:直线插补
新建其 Click 事件函数movlButton_Click()
,代码如下
private void movlButton_Click(object sender, EventArgs e)
{
// pos1.text是个string,要转成double
var pos = new double[] { Convert.ToDouble(pos1.Text), Convert.ToDouble(pos2.Text), Convert.ToDouble(pos3.Text), Convert.ToDouble(pos4.Text), Convert.ToDouble(pos5.Text), Convert.ToDouble(pos6.Text) };
//速度要转成int
//坐标系0-关节,1-直角,2-工具,3-用户,这里的selectedIndex和定义的数字正好相等
Nrc.robot_movl(pos, Convert.ToInt32(speed.Text), coordBox.SelectedIndex);
}
可以看到,和关节插补按钮仅仅是调用的 API 不同,改为调用robot_movl()
,传入的参数相同。
获取数字输出
获取数字输出的值,我们建议使用CheckBox控件,这样在查看输出值的过程中,也可以通过勾选来设置输出值。
拖入一个CheckBox控件来查看数字输出端口 1 的值,设置属性如下:
- Name:dout1
- Text:DOUT 1
然后我们定义一个getDout()
方法,用来获取当前数字输出的值:
private void getDout()
{
var dou = new double[16];
Nrc.get_dout(dou);
dout1.Checked = toBool(dou[0]);
}
这里用到了get_dout()
,需要传入一个 double 数组变量来接收每个端口输出值,我们使用的 IO 板有 16 个输出端口,所以数组长度为 16。这里每个端口会有 3 种值
- -1 - 没有 IO 板
- 0 - 输出低电平
- 1 - 输出高电平
由于 CheckBox 控件勾选和未勾选的状态为 bool 类型,我们需要自己定义一个toBool()
方法将值从 double 转换为 bool 类型:
private bool toBool(double value)
{
if(value == 0 || value == -1)
{
return false;
}
else
{
return true;
}
}
然后可以在连接成功的地方调用getDout()
,因为输出状态不会由于外部环境改变,所以不需要实时获取。
设置数字输出
定义setDout()
方法来设置输出值,传入两个 int 类型的值,port、value:
private void setDout(int port,int value)
{
Nrc.set_dout(port, value);
}
这里使用了set_dout()
,需要传入两个 int 类型的值,分别为端口号和输出值,输出值 0 表示低电平,1 表示高电平。将 port 和 value 传入。
新建 dout1 控件的CheckedChanged(改变勾选)事件函数dout1_CheckedChanged()
,代码如下
private void dout1_CheckedChanged(object sender, EventArgs e)
{
if (dout1.Checked)
{
setDout(1, 1);
}
else
{
setDout(1, 0);
}
}
使用 if 来判断,如果勾选框状态为true
,也就是勾选状态,那么设置端口 1 的值为高电平,否则设置端口 1 的值为低电平。
初期完成!
那么到此为止,我们的项目已经大致完成!现在 Form1.cs 的代码如下
using NbtRobot;
using System;
using System.Timers;
using System.Windows.Forms;
namespace csharpdemo
{
public partial class Form1 : Form
{
static System.Windows.Forms.Timer getPositionTimer = new System.Windows.Forms.Timer();
static System.Timers.Timer jogTimer = new System.Timers.Timer();
public Form1()
{
InitializeComponent();
}
private void connectButton_Click(object sender, EventArgs e)
{
var ip = ipAddress.Text;
if (Nrc.connect_robot(ip))
{
connectButton.Text = "连接成功";
getDout();
getPositionTimer.Tick += new EventHandler(handleGetPositionTimer); //定时器,每500ms执行一次handleGetPositionTimer
getPositionTimer.Interval = 500; //500ms
getPositionTimer.Start();
}
else
{
connectButton.Text = "连接失败";
}
}
private void handleGetPositionTimer(Object myObject,EventArgs myEventArgs)
{
getPosition();
}
private bool toBool(double value)
{
if(value == 0 || value == -1)
{
return false;
}
else
{
return true;
}
}
private void getDout()
{
var dou = new double[16];
Nrc.get_dout(dou);
dout1.Checked = toBool(dou[0]);
}
private void setDout(int port,int value)
{
Nrc.set_dout(port, value);
}
private void getPosition()
{
var position = new double[] { 0, 0, 0, 0, 0, 0 };
Nrc.get_current_position(position);
j1value.Text = position[0].ToString();
j2value.Text = position[1].ToString();
j3value.Text = position[2].ToString();
j4value.Text = position[3].ToString();
j5value.Text = position[4].ToString();
j6value.Text = position[5].ToString();
}
private void enableButton_Click(object sender, EventArgs e)
{
if(enableButton.Text == "机器人上电") {
if (Nrc.servo_power_on())
{
enableButton.Text = "机器人下电";
}
else
{
return;
}
}else if(enableButton.Text == "机器人下电")
{
Nrc.servo_power_off();
}
}
private void jogging(Object source,ElapsedEventArgs e,int axis,bool direction)
{
Nrc.start_jogging(axis, direction);
}
private void j1re_MouseDown(object sender, MouseEventArgs e) // 鼠标按下时
{
jogTimer.Elapsed += new ElapsedEventHandler((s, ee) => jogging(s, ee, 1, false)); //点动,false表示反方向
jogTimer.Interval = 200;//点动需要200毫秒发一次,否则会判断中断点动
jogTimer.Start();
}
private void j1re_MouseUp(object sender, MouseEventArgs e) //鼠标抬起时
{
jogTimer.Stop();
Nrc.stop_jogging(1); //停止点动1轴
}
private void j1re_MouseLeave(object sender, EventArgs e) //鼠标按下的同时离开
{
jogTimer.Stop();
Nrc.stop_jogging(1);
}
private void j1po_MouseDown(object sender, MouseEventArgs e)
{
jogTimer.Elapsed += new ElapsedEventHandler((s, ee) => jogging(s, ee, 1, true)); //点动,true表示正方向
jogTimer.Interval = 200;//点动需要200毫秒发一次,否则会判断中断点动
jogTimer.Start();
}
private void j1po_MouseLeave(object sender, EventArgs e)
{
jogTimer.Stop();
Nrc.stop_jogging(1);
}
private void j1po_MouseUp(object sender, MouseEventArgs e)
{
jogTimer.Stop();
Nrc.stop_jogging(1);
}
private void movjButton_Click(object sender, EventArgs e)
{
// pos1.text是个string,要转成double
var pos = new double[] { Convert.ToDouble(pos1.Text), Convert.ToDouble(pos2.Text), Convert.ToDouble(pos3.Text), Convert.ToDouble(pos4.Text), Convert.ToDouble(pos5.Text), Convert.ToDouble(pos6.Text) };
//速度要转成int
//坐标系0-关节,1-直角,2-工具,3-用户,这里的selectedIndex和定义的数字正好相等
Nrc.robot_movj(pos,Convert.ToInt32(speed.Text),coordBox.SelectedIndex);
}
private void movlButton_Click(object sender, EventArgs e)
{
// pos1.text是个string,要转成double
var pos = new double[] { Convert.ToDouble(pos1.Text), Convert.ToDouble(pos2.Text), Convert.ToDouble(pos3.Text), Convert.ToDouble(pos4.Text), Convert.ToDouble(pos5.Text), Convert.ToDouble(pos6.Text) };
//速度要转成int
//坐标系0-关节,1-直角,2-工具,3-用户,这里的selectedIndex和定义的数字正好相等
Nrc.robot_movl(pos, Convert.ToInt32(speed.Text), coordBox.SelectedIndex);
}
private void dout1_CheckedChanged(object sender, EventArgs e)
{
if (dout1.Checked)
{
setDout(1, 1);
}
else
{
setDout(1, 0);
}
}
}
}