跳到主要内容

C# 版上位机二次开发教程

视频教程

我们为您准备了视频教程,可以在这里观看全部知识。

C# 版本上位机二次开发教程

在我们开始之前

我们将在这个教程开发一个具有如下功能的程序:

  1. 连接控制器;
  2. 让机器人上电;
  3. 点动机器人;
  4. 让机器人关节插补;
  5. 让机器人直线插补;
  6. 获取机器人位置;
  7. 设置数字输出;
  8. 查询数字输出;

这篇教程将分为 3 个章节:

前置知识

在学习使用 C#进行上位机二次开发前,我们默认您已经具备以下知识:

  1. C#语言
  2. 工业机器人相关基础知识
  3. Visual Studio 编辑器

如果您不具备以上知识,请不要继续本教程。我们给您推荐以下教程:

环境准备

使用 C#进行上位机二次开发,请在 Windows 系统下使用 Visual Studio 进行。

Visual Studio

我们建议您前往 Visual Studio 官网下载Visual Studio Community 2019版本编辑器,网址如下。

免费的 IDE 和开发人员工具 | Visual Studio Community

安装过程中请注意勾选如下两项:

  • .NET 桌面开发
  • 使用 C++ 的桌面开发

SDK

进行二次开发也少不了我们提供的 SDK 文件,请点击下面的链接进行下载。

C# SDK

基础教程

项目初始化

  1. 在 Visual Studio Community 2019(以下简称 VS)中创建新项目,检索语言-C#,环境-桌面。选择Windows 窗体应用(.NET Framework)
  2. 项目名称可以命名为 csharpdemo,为了方便开发调试,请勾选将解决方案和项目放在同一目录中,点击创建即可;
  3. 将 SDK 中提供的Nrc.cs文件放入项目目录中;
  4. 右键解决方案资源管理器窗口中的csharpdemo项目名,选择添加-现有项,在弹出来的窗口中选择刚刚放入目录的Nrc.cs文件;
  5. 在上面的编译一栏中,选择 Debug、Any CPU,点击启动按钮,会编译出一个空白窗口;
  6. 关闭空白窗口后,将 SDK 中依赖目录下的所有文件复制到项目目录的bin/Debug目录下;
  7. 项目初始化完毕。

连接控制器

首先在工具箱窗口中拖入一个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 控件的MouseUpMouseLeave事件函数内容与 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);
}
}
}
}