之前的socket通信,一直是在visual studio的C#平台进行的,服务器端是简单的增量式PID控制器(这个算法,后续应该会再深入学习,然后将算法丰富),可是现在,老师让实现这样一个功能,在另一台计算机上有一搭建好的电机模型,要实时的将电机的速度拿到,作为PID控制器的输入,实时速度与设定值比较,得到的偏差作为PID控制器的输入,PID控制器计算后的输出值再给电机的输入端,从而通过这样的闭环控制,来控制电机(涉及到一个D/A转换,这个现在还没设计。但猜想是不是可以直接通过一个零阶保持器来实现)。
电机的实时速度,需要从simulink模块中提取出来,传给.m文件,从而通过.m文件与C#之间的通信,实现PID控制功能。
C#服务器端代码改变不大,为了方便,直接拿之前做的winform过来,在那基础上改写的,有的代码没用到,也没删掉,核心是Receive( )函数。
C#服务器端winform代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Collections;
using System.IO;
namespace WindowsForms_服务器端
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
double[] nums = new double[3];//存储3个偏差值e(k) e(k-1) e(k-2)
double actual = 0;
double set = 0;
double kp = 0.4;
double ki = 0.53;
double kd = 0.1;
//定义一个空字节数组date作为数据缓冲区,用于缓冲流入和流出的信息
byte[] date = new byte[100];
static Socket newsock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
static Socket client;
ArrayList list = new ArrayList();//存储客户端发来的速度设定值
ArrayList listOut = new ArrayList();//存储PID计算的实际速度值
Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();//创建键值对,存储套接字的ip地址等信息
Thread thConnect;
/// <summary>
/// 建立与客户端的连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonConnect_Click(object sender, EventArgs e)
{
try
{
IPEndPoint Ipep = new IPEndPoint(IPAddress.Any, 125);//指定地址和端口号
newsock.Bind(Ipep);
newsock.Listen(10);//置于监听状态
textState.Text = "等待客户端的连接\n";
thConnect = new Thread(Connect);
thConnect.IsBackground = true;
thConnect.Start();
}
catch
{ }
}
int i = 0;
/// <summary>
/// 不断接收从客户端发来消息的方法
/// </summary>
void Receive()
{
try
{
while (true)
{
int reci = client.Receive(date);
if (date.Length == 0)
{
break;
}
string str = Encoding.ASCII.GetString(date,0,reci);
//if(Encoding.ASCII.GetString(date,0,date.Length)=="a")//证明MATLAB端已经写入成功实时速度
//else
if (str == "a")
{
FileStream fs = new FileStream(@"C:\Users\neu\Desktop\test1.txt", FileMode.Open);
int count = (int)fs.Length;
byte[] readData = new byte[count];
fs.Read(readData, 0, count);
string strNew = Encoding.ASCII.GetString(readData);
fs.Close();//使用完要关闭文件 否则会一直占用资源
//File.Delete(@"C:\Users\neu\Desktop\test1.txt");
actual = double.Parse(strNew);
actual = actual * actual;
date = System.Text.Encoding.UTF8.GetBytes(actual.ToString());
//在这里,下一步要将PID控制输出的速度值,传给MATLAB端 即计算一个,输出一个到MATLAB端
textSend.AppendText(actual.ToString() + "\n");
FileStream fsWrite = new FileStream(@"C:\Users\neu\Desktop\test1.txt", FileMode.OpenOrCreate);
fsWrite.Write(date, 0, date.Length);
fsWrite.Close();
//写入成功后 即PID计算完成后 给MATLAB端发送一信号 告诉它可以往下继续执行了
date = System.Text.Encoding.ASCII.GetBytes("1");
client.Send(date, date.Length, SocketFlags.None);
}
}
}
catch
{ }
}
/// <summary>
/// 服务器端与客户端连接的方法 使一个服务器可以与多个客户端连接
/// </summary>
private void Connect()
{
//接收来自客户端的接入尝试连接,并返回连接客户端的ip地址
while (true)
{
client = newsock.Accept();
IPEndPoint clientep = (IPEndPoint)client.RemoteEndPoint;
//返回客户端的ip地址和端口号
textState.AppendText("与" + clientep.Address + "在" + clientep.Port + "端口连接\n");
dicSocket.Add(clientep.ToString(), client);//将连接的客户端的IP地址添加在键值对集合中
comboBox.Items.Add(clientep);//将客户端的IP地址显示在下拉栏中
comboBox.SelectedIndex = 0;
Thread thReceive = new Thread(Receive);//创建一个新线程 执行Receive()方法
thReceive.IsBackground = true;
thReceive.Start();
}
}
/// <summary>
/// 发送消息给客户端 需自己选择已连接的其中一个客户端
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonSend_Click(object sender, EventArgs e)
{
try
{
string str = textSend.Text;
date = System.Text.Encoding.UTF8.GetBytes(str); //以字节数组的形式 将欢迎信息发送给客户端 注意发送与接收要用相同的编码格式UTF8 否则会乱码
try
{
string ip = comboBox.SelectedItem.ToString();
SendVarMessage(dicSocket[ip], date);
}
catch
{
MessageBox.Show("发送失败,请确认已选择一个客户端");
}
}
catch
{ }
}
/// <summary>
/// 断开与客户端的连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void buttonBreak_Click(object sender, EventArgs e)
{
try
{
client.Close();
newsock.Close();
textState.AppendText("断开连接\n");
this.Close();
}
catch { }
}
/// <summary>
/// 跨线程访问
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_Load(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
}
private void textState_TextChanged(object sender, EventArgs e)
{
}
private void textReceive_TextChanged(object sender, EventArgs e)
{
}
double outSpeed = 0;//PID控制器计算后的输出量,以后要传给模型输入端,以控制模型
/// <summary>
/// 开始进行PID计算
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
}
/// <summary>
/// 发送变长消息方法
/// </summary>
/// <param name="s"></param>
/// <param name="msg"></param>
/// <returns></returns>
private static void SendVarMessage(Socket s, byte[] msg)
{
int offset = 0;
int sent;
int size = msg.Length;
int dataleft = size;
byte[] msgsize = new byte[2];
//将消息的尺寸从整型转换成可以发送的字节型
//因为int型是占4个字节 所以msgsize是4个字节 后边是空字节
msgsize = BitConverter.GetBytes(size);
//发送消息的长度信息
//之前总是乱码出错 客户端接收到的欢迎消息前两个字节是空 后边的两个字符er传送到第二次接收的字节数组中
//因此将er字符转换为int出错 这是因为之前在Send代码中,是将msgsize整个字节数组发送给客户端 所以导致第3 4个空格也发送
//导致发送的信息混乱 这两个空格使发送的信息都往后挪了两个位置 从而乱码
sent = s.Send(msgsize, 0, 2, SocketFlags.None);
while (dataleft > 0)
{
int sent2 = s.Send(msg, offset, dataleft, SocketFlags.None);
//设置偏移量
offset += sent2;
dataleft -= sent2;
}
//return dataleft;
}
/// <summary>
/// 接收变长消息方法
/// </summary>
/// <param name="s"></param>
/// <returns>接收到的信息</returns>
private static byte[] ReceiveVarMessage(object o)//方法的返回值是字节数组 byte[] 存放的是接受到的信息
{
Socket s = o as Socket;
int offset = 0;
int recv;
byte[] msgsize = new byte[2];
//接收2个字节大小的长度信息
recv = s.Receive(msgsize, 0, 2, 0);
//将字节数组的消息长度转换为整型
int size = BitConverter.ToInt16(msgsize, 0);
int dataleft = size;
byte[] msg = new byte[size];
while (dataleft > 0)
{
//接收数据
recv = s.Receive(msg, offset, dataleft, 0);
if (recv == 0)
{
break;
}
offset += recv;
dataleft -= recv;
}
return msg;
}
private void textSend_TextChanged(object sender, EventArgs e)
{
}
}
}
MATLAB .m文件代码:
s = tcpip('127.0.0.1', 125, 'NetworkRole','client'); set(s, 'InputBufferSize', 30); set(s, 'outputBufferSize', 30); set(s,'Timeout',3); fopen(s); b=1; c=1; d=1; a='1.1';%模拟存储电机的实时速度 fid=fopen('C:\Users\neu\Desktop\test1.txt','wb'); fprintf(fid,'%s',a);%将实时速度写在文本文件中,从而服务器端可通过这个文本文件.txt读取到实时速度 fclose(fid); fwrite(s,'a');%发送一个字符(随意的)给PID控制器,表示可以从TXT中读取传入的实时速度值了 pause(1);%暂停一秒 while(b)%while循环是想 只有读到PID控制器计算完成的信号后,再往下执行代码 否则在原地等待 知道读到信号 read=fread(s,1) if read==49 b=0; end end fid2=fopen('C:\Users\neu\Desktop\test1.txt','r');%读取PID控制器输出的计算值 a=fscanf(fid2,'%f')%fscanf读取文本文件(.txt) a是double类型的数据 fclose(fid2);%关闭文件 即释放占用的资源 a='2.2';%模拟存储电机的实时速度 fid=fopen('C:\Users\neu\Desktop\test1.txt','wb'); fprintf(fid,'%s',a);%将实时速度写在文本文件中,从而服务器端可读取到实时速度 fclose(fid); fwrite(s,'a'); pause(1); while(c) read=fread(s,1) if read==49 c=0; end end fid2=fopen('C:\Users\neu\Desktop\test1.txt','r');%读取PID控制器输出的计算值 a=fscanf(fid2,'%f')%fscanf读取文本文件(.txt) a是double类型的数据 fclose(fid2);%关闭文件 即释放占用的资源 a='3.3';%模拟存储电机的实时速度 fid=fopen('C:\Users\neu\Desktop\test1.txt','wb'); fprintf(fid,'%s',a);%将实时速度写在文本文件中,从而服务器端可读取到实时速度 fclose(fid); fwrite(s,'a'); pause(1); while(d) read=fread(s,1) if read==49 d=0; end end fid2=fopen('C:\Users\neu\Desktop\test1.txt','r');%读取PID控制器输出的计算值 a=fscanf(fid2,'%f')%fscanf读取文本文件(.txt) a是double类型的数据 fclose(fid2);%关闭文件 即释放占用的资源 fclose(s)
因为还没实现从simulink仿真模块中拿到模拟电机的实时速度,所以这里是传递了三个给定的参数给C#服务器端,返回的是传递过去数值的平方,即返回值依次是1.21 ,4.84 ,10.89。 从仿真模块中拿到的实时速度,要先存储在一个变量中,然后再发送给服务器端,但是我用fwrite函数,只知道怎样发送一个数值或字符串,怎样将存储数值的变量发送过去还不知道,所以这里是用将实时速度用fprintf函数写入到文本文件.txt中,服务器端再从这个文本文件读数。
下一步,将不必要的代码删掉,缕清楚程序。程序还存在一个问题是,客户端改为MATLAB后,缺少了在线修改参数的模块,因为MATLAB端不是界面,所以,想把之前放在C#客户端的修改参数模块,移至服务器端,服务器端是winform界面,修改很方便,若再编写一个客户端,只是实现修改参数功能,意义不大。