Tip:No Ego
Some programmers have a huge problem: their own ego. But there is no time for developing an ego. There is no time for being a rockstar.
Who is it who decides about your quality as programmer? You? No. The others? Probably. But can you really compare an Apple with a Banana? No. You are an individual. You cannot compare your whole self with another human being. You can only compare a few facettes.
A facet is nothing what you can be proud of. You are good at Java? Cool. The other guy is not as good as you, but better with bowling. Is Java more important than bowling? It depends on the situation. Probably you earn more money with Java, but the other guy might have more fun in life because of his bowling friends.
Can you really be proud because you are a geek? Programmers with ego don’t learn. Learn from everybody, from the experienced and from the noobs at the same time.
Kodo Sawaki once said: you are not important.
Think about it.
——The 10 rules of a Zen programmer
零、背景:为什么要造这个轮子
传统的桌面应用大多数是低代码例如 WinForm、WPF、QT 等基于现有的组件进行拖拽式开发,如果没有特别去优化改善界面,用户体验感是很差的,因此衍生出一种嵌入式浏览器方案 CEF,尝试使用现有的前端技术去解决桌面 UI 问题。
基于这个背景下,本文从学习研究的角度实现一个示例以探索 CEF 解决方案在工业领域的应用,现模拟一个工业调试设备的场景,例如从称重机中获取重量、发送亮灯信号、控制电路开关等。串口调试工具用于检验硬件设备是否能够正常运作,如下图所示:
- Step1、界面上选择设备的串口参数
- Step2、根据串口参数连接到设备
- Step3、读取并解析设备返回的数据
- Step4、将数据回显到界面上
- Step5、根据界面的数据判断设备运行情况
一、技术栈
Vite + Vue3 + TS + WebSocket+ ElementUI(plus) + .NET Framework 4.7.2 + WPF + SQLITE3,开发环境为 Win10,VS2019,VS Code。
二、后端设计与实现
开发环境(补充)
1、WS服务器类WebSocketServer
安装 fleck 库,这里使用的版本是 1.2.0,
using Fleck;
using System.Diagnostics;
namespace SerialDevTool.WS
{
class MyWebSocketServer
{
/// <summary>
/// 运行 WS 服务器
/// </summary>
public static void Run()
{
FleckLog.Level = LogLevel.Debug;
var server = new WebSocketServer("ws://127.0.0.1:3000");
server.Start(socket =>
{
// 建立连接
socket.OnOpen = () =>
{
Debug.WriteLine("客户端连接成功");
};
// 关闭连接
socket.OnClose = () =>
{
Debug.WriteLine("客户端已经关闭");
};
// 收到消息
socket.OnMessage = message =>
{
Debug.WriteLine(string.Format("收到客户端信息:{0}",message));
socket.Send(message);
};
// 发生错误
socket.OnError = exception => {
Debug.WriteLine(string.Format("发生错误:{0}",exception.Message));
};
});
Debug.WriteLine("WS服务器已启动");
}
}
}
这里我们创建了一个 WS 服务器,地址为 ws://127.0.0.1:3000 ,并且实现了 OnOpen、OnClose 、OnMessage、OnError 对应的方法,启动方式如下,
Task.Run(() =>
{
MyWebSocketServer.Run();
});
使用 Postman 测试 WS,点击左上角 File–> New,选择 WebSocket,
可以看到,Postman 向服务器发送 hello world,服务器也向 Postman 返回 hello world,
2、串口通讯工具类SerialPortlUtil
using System;
using System.Diagnostics;
using System.IO.Ports;
namespace SerialDevTool.Utils
{
/// <summary>
/// 串口工具类
/// </summary>
public class SerialPortlUtil
{
/// <summary>
/// 默认偏移
/// </summary>
private static readonly int OFFSET = 0;
/// <summary>
/// 默认数据位
/// </summary>
private static readonly int COUNT = 8;
/// <summary>
/// 默认超时时间,单位 ms
/// </summary>
private static readonly int DEFAULT_TIMEOUT = 500;
/// <summary>
/// 默认COM口
/// </summary>
private static readonly string DEFAULT_COM = "COM1";
/// <summary>
/// 默认波特率
/// </summary>
private static readonly int DEFAULT_BAUDRATE = 9600;
/// <summary>
/// 默认校验位
/// </summary>
private static readonly Parity DEFAULT_PARITY = Parity.None;
/// <summary>
/// 默认数据位
/// </summary>
private static readonly int DEFAULT_DATABITS = 8;
/// <summary>
/// 默认停止位
/// </summary>
private static readonly StopBits DEFAULT_STOPBITS = StopBits.One;
/// <summary>
/// 获取默认串口实例
/// </summary>
public static SerialPort GetDefaultSerialPortInstance()
{
return GetSerialPortInstance(DEFAULT_COM);
}
/// <summary>
/// 获取串口实例
/// </summary>
/// <param name="com"></param>
/// <returns></returns>
public static SerialPort GetSerialPortInstance(string com)
{
// COM1,9600,0,8,1
if (com.Contains(","))
{
string[] comParams = com.Split(new string[] { "," }, StringSplitOptions.None);
return new SerialPort(comParams[0], int.Parse(comParams[1]), GetParity(comParams[2]), int.Parse(comParams[3]), GetStopBits(comParams[4]))
{
ReadTimeout = DEFAULT_TIMEOUT,
WriteTimeout = DEFAULT_TIMEOUT
};
}
// COM1
return new SerialPort(com, DEFAULT_BAUDRATE, DEFAULT_PARITY, DEFAULT_DATABITS, DEFAULT_STOPBITS)
{
ReadTimeout = DEFAULT_TIMEOUT,
WriteTimeout = DEFAULT_TIMEOUT
};
}
/// <summary>
/// 解析停止位
/// </summary>
/// <param name="stopBits"></param>
/// <returns></returns>
public static StopBits GetStopBits(string stopBits)
{
switch (stopBits)
{
case "0":
{
return StopBits.None;
}
case "1":
{
return StopBits.One;
}
case "2":
{
return StopBits.Two;
}
case "3":
{
return StopBits.OnePointFive;
}
default:
return StopBits.One;
}
}
/// <summary>
/// 解析校验位
/// </summary>
/// <param name="parity"></param>
/// <returns></returns>
public static Parity GetParity(string parity)
{
switch (parity)
{
case "0":
{
return Parity.None;
}
case "1":
{
return Parity.Odd;
}
case "2":
{
return Parity.Even;
}
case "3":
{
return Parity.Mark;
}
case "4":
{
return Parity.Space;
}
default:
return Parity.None;
}
}
/// <summary>
/// 写入 8 位字节数据
/// </summary>
/// <param name="serialPort"></param>
/// <param name="buffer"></param>
public static void Write(SerialPort serialPort, byte[] buffer)
{
try
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
serialPort.Write(buffer, OFFSET, COUNT);
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("Write Exception: {0}", ex.Message));
}
}
/// <summary>
/// 将指定的字符串和 System.IO.Ports.SerialPort.NewLine 值写入输出缓冲区。
/// </summary>
/// <param name="serialPort"></param>
/// <param name="text"></param>
public static void WriteLine(SerialPort serialPort, string text)
{
try
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
serialPort.WriteLine(text);
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("WriteLine Exception: {0}", ex.Message));
}
}
/// <summary>
/// 读 8 位字节数据
/// </summary>
/// <param name="serialPort"></param>
/// <param name="buffer"></param>
public static int Read(SerialPort serialPort, byte[] buffer)
{
try
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
return serialPort.Read(buffer, OFFSET, COUNT);
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("Read Exception: {0}", ex.Message));
}
return 0;
}
/// <summary>
/// 一直读取到输入缓冲区中的 System.IO.Ports.SerialPort.NewLine 值。
/// </summary>
/// <param name="serialPort"></param>
/// <returns></returns>
public static string ReadLine(SerialPort serialPort)
{
string line = "";
try
{
if (!serialPort.IsOpen)
{
serialPort.Open();
}
line = serialPort.ReadLine();
}
catch (Exception ex)
{
Debug.WriteLine(string.Format("ReadLine Exception: {0}", ex.Message));
}
return line;
}
}
}
使用虚拟串口测试,
/// <summary>
/// 测试串口通讯
/// </summary>
private void TestSerialPort()
{
Task.Run(() =>
{
SerialPort readCom = SerialPortlUtil.GetSerialPortInstance("COM6");
int length = 0;
while (true)
{
byte[] buffer = new byte[8];
length = SerialPortlUtil.Read(readCom, buffer);
if (length > 0)
{
Debug.Write("receive: ");
for (int i = 0; i < length; i++)
{
Debug.Write(string.Format("{0} ", buffer[i]));
}
Debug.Write("\n");
Thread.Sleep(1000);
}
}
});
Task.Run(() =>
{
SerialPort writeCom = SerialPortlUtil.GetSerialPortInstance("COM5");
while (true)
{
SerialPortlUtil.Write(writeCom, new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 });
Thread.Sleep(500);
}
});
}
这里的虚拟串口 COM5 每 500ms 向缓存区写入数据,COM6 每 1000ms 从缓存区中读取数据,SerialPort 读写数据类型均支持 Byte、Char、String,
3、将串口通讯绑定到WS方法
using Fleck;
using SerialDevTool.Utils;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
namespace SerialDevTool.WS
{
class MyWebSocketServer
{
/// <summary>
/// 写标志
/// </summary>
private const string WRITE_FLAG = "##WRITE##";
private readonly string[] WRITE_FLAG_SEPARATOR = new string[] { WRITE_FLAG };
/// <summary>
/// 打开串口标志
/// </summary>
private const string OPEN_FLAG = "##OPEN##";
private readonly string[] OPEN_FLAG_SEPARATOR = new string[] { OPEN_FLAG };
/// <summary>
/// 关闭串口标志
/// </summary>
private const string CLOSE_FLAG = "##CLOSE##";
private readonly string[] CLOSE_FLAG_SEPARATOR = new string[] { CLOSE_FLAG };
/// <summary>
/// 当前连接的 socket
/// </summary>
private Dictionary<string,IWebSocketConnection> _webSocketDic;
/// <summary>
/// 当前连接的串口
/// </summary>
private Dictionary<string, SerialPort> _serialPortDic;
public MyWebSocketServer(){
this._webSocketDic = new Dictionary<string, IWebSocketConnection>();
this._ser