通过前面6章,实现了winform中安装NanUI或者winformium、调用了HTML页面、与HTML页面通过Handler及注入进行了互动。
在第5章的时候,提到过“项目功能预想”,有5个主要功能。这章主要尝试第一个功能实现
一、实现“通过CS读取COM口数据,放到页面上,通过页面对COM口进行互动”
1、基础的类
public class RuanBaseClass
{
/// <summary>
/// 检测是否,带上原因公共构造
/// </summary>
public class TestBoolAndString
{
/// <summary>
/// 检测是否成功,默认为否,True 是警报,False 不警报
/// </summary>
public bool IsTestOk { get; set; } = false;
/// <summary>
/// 检测结果内容,默认为空
/// </summary>
public string TestRueseltString { get; set; } = "";
/// <summary>
/// 检测结果的JSON格式
/// </summary>
public string RuseltJsonString
{
get { return "{IsTestOK:" + this.IsTestOk.ToString() + ",TestRueseltString:" + this.TestRueseltString + "}"; ; }
}
}
/// <summary>
/// 传递页面的类型枚举下。随时更新
/// </summary>
public enum WindowPageType : int
{
/// <summary>
/// 主页面
/// </summary>
SystemIndex = 1,
/// <summary>
/// 主页面的JS弹窗
/// </summary>
SystemIndexMessPageJS = 2,
}
public class SeralPortsHelper
{
#region 下拉框相关设置
/// <summary>
/// CS中设置串口号的下拉框
/// </summary>
/// <param name="obj"></param>
public void SetPortNameValues(ComboBox obj, string ComName)
{
obj.Items.Clear();
foreach (string str in SerialPort.GetPortNames())
{
obj.Items.Add(str);
if (str == ComName)
{
obj.SelectedItem = ComName;
}
}
}
/// <summary>
/// CS中设置波特率的下拉框
/// </summary>
public void SetBauRateValues(ComboBox obj, string BrvValue)
{
obj.Items.Clear();
foreach (SerialPortBaudRates rate in Enum.GetValues(typeof(SerialPortBaudRates)))
{
obj.Items.Add(((int)rate).ToString());
if (((int)rate).ToString() == BrvValue)
{
obj.SelectedItem = BrvValue;
}
}
}
/// <summary>
/// CS中设置数据位的下拉框
/// </summary>
public void SetDataBitsValues(ComboBox obj, string BitValue)
{
obj.Items.Clear();
foreach (SerialPortDatabits databit in Enum.GetValues(typeof(SerialPortDatabits)))
{
obj.Items.Add(((int)databit).ToString());
if (((int)databit).ToString() == BitValue)
{
obj.SelectedItem = BitValue.ToString();
}
}
}
/// <summary>
/// CS中设置校验位列表的下拉框
/// </summary>
public void SetParityValues(ComboBox obj, string parityValue)
{
obj.Items.Clear();
foreach (string str in Enum.GetNames(typeof(Parity)))
{
obj.Items.Add(str);
if (str == parityValue)
{
obj.SelectedItem = str;
}
}
}
/// <summary>
/// CS中设置停止位的下拉框
/// </summary>
public void SetStopBitValues(ComboBox obj, string stopbitvalue)
{
obj.Items.Clear();
foreach (int str in Enum.GetValues(typeof(StopBits)))
{
obj.Items.Add(str);
if (str.ToString() == stopbitvalue)
{
obj.SelectedItem = str;
}
}
}
#endregion
#region 格式化数据
/// <summary>
/// 通过字符串设置串口名称,其实是验证下是否包含这个名字的端口号。
/// </summary>
/// <param name="searialPortStringName"></param>
/// <returns>TestBoolAndString格式的结果,第一个值是是否成功,第二个是具体原因或返回值</returns>
public TestBoolAndString GetAndCheckSerialPortName(string searialPortStringName)
{
TestBoolAndString _testBoolAndString = new TestBoolAndString();
_testBoolAndString.IsTestOk = false;
_testBoolAndString.TestRueseltString = "正在对【"+ searialPortStringName + "】进行设置。";
foreach (string str in SerialPort.GetPortNames())
{
if (str == searialPortStringName)
{
_testBoolAndString.IsTestOk = true;
_testBoolAndString.TestRueseltString = searialPortStringName;
}
}
if (!_testBoolAndString.IsTestOk)
{
_testBoolAndString.TestRueseltString = "设置COM口不存在,请检查。";
}
return _testBoolAndString;
}
/// <summary>
/// 通过字符串获取停止位
/// </summary>
/// <param name="StopBitsText"></param>
/// <returns></returns>
public StopBits GetStopBits(string StopBitsText)
{
switch (StopBitsText.Trim().ToLower())
{
case "none": return System.IO.Ports.StopBits.One;
case "one": return System.IO.Ports.StopBits.One;
case "onePointFive": return System.IO.Ports.StopBits.OnePointFive;
case "two": return System.IO.Ports.StopBits.Two;
}
return StopBits.One;
}
/// <summary>
/// 获取数据位,默认为8,异常为8
/// </summary>
/// <param name="DatabitString">字符串的数据位</param>
/// <returns></returns>
public int GetDatabits(string DatabitString)
{
int tmpdata = 8;
if (DatabitString == "5")
{
tmpdata = 5;
}
else if (DatabitString == "6")
{
tmpdata = 6;
}
else if (DatabitString == "7")
{
tmpdata = 7;
}
else if (DatabitString == "8")
{
tmpdata = 8;
}
else
{
tmpdata = 8;
}
return tmpdata;
}
/// <summary>
/// 获取数据位,默认为8,异常为8
/// </summary>
/// <param name="GetDatabits">数字的数据位</param>
/// <returns></returns>
public int GetDatabits(int DatabitString)
{
int tmpdata = 8;
if (DatabitString == 5)
{
tmpdata = 5;
}
else if (DatabitString == 6)
{
tmpdata = 6;
}
else if (DatabitString == 7)
{
tmpdata = 7;
}
else if (DatabitString == 8)
{
tmpdata = 8;
}
else
{
tmpdata = 8;
}
return tmpdata;
}
/// <summary>
/// 将字符串转化为串口奇偶校验
/// </summary>
/// <param name="ParityString">输入的字符串奇偶</param>
/// <returns></returns>
public Parity GetParity(string ParityString)
{
if (ParityString == "Mark")
{
return System.IO.Ports.Parity.Mark;
}
else if (ParityString == "Odd")
{
return System.IO.Ports.Parity.Odd;
}
else if (ParityString == "Space")
{
return System.IO.Ports.Parity.Space;
}
else if (ParityString == "Even")
{
return System.IO.Ports.Parity.Even;
}
else
{
return System.IO.Ports.Parity.None;
}
}
/// <summary>
///通过数字,格式化波特率,默认或出错为9600
/// </summary>
public int GetBaudRates(int BaudRatesString)
{
int tempRate = 9600;
if ((int)BaudRatesString > 0)
{
tempRate = (int)BaudRatesString;
}
return tempRate;
}
/// <summary>
///通过字符串格式化波特率,默认或出错为9600
/// </summary>
public int GetBaudRates(string BaudRatesString)
{
int tempRate = 9600;
if (Convert.ToInt32(BaudRatesString) > 0)
{
tempRate = Convert.ToInt32(BaudRatesString);
}
return tempRate;
}
/// <summary>
/// 将字符串转化为HOMS握手规则
/// </summary>
/// <param name="HandshakeString"></param>
/// <returns></returns>
public System.IO.Ports.Handshake GetHandshake(string HandshakeText)
{
switch (HandshakeText.Trim().ToLower())
{
case "none": return System.IO.Ports.Handshake.None;
case "xonxoff": return System.IO.Ports.Handshake.XOnXOff;
case "requesttosend": return System.IO.Ports.Handshake.RequestToSend;
case "requesttosendxonxoff": return System.IO.Ports.Handshake.RequestToSendXOnXOff;
}
return System.IO.Ports.Handshake.None;
}
#endregion
#region 串口数据规整
/// <summary>
/// 串口数据位列表(5,6,7,8)
/// </summary>
public enum SerialPortDatabits : int
{
FiveBits = 5,
SixBits = 6,
SeventBits = 7,
EightBits = 8
}
/// <summary>
/// 串口波特率列表。
/// 75,110,150,300,600,1200,2400,4800,9600,14400,19200,28800,38400,56000,57600,
/// 115200,128000,230400,256000
/// </summary>
public enum SerialPortBaudRates : int
{
BaudRate_75 = 75,
BaudRate_110 = 110,
BaudRate_150 = 150,
BaudRate_300 = 300,
BaudRate_600 = 600,
BaudRate_1200 = 1200,
BaudRate_2400 = 2400,
BaudRate_4800 = 4800,
BaudRate_9600 = 9600,
BaudRate_14400 = 14400,
BaudRate_19200 = 19200,
BaudRate_28800 = 28800,
BaudRate_38400 = 38400,
BaudRate_56000 = 56000,
BaudRate_57600 = 57600,
BaudRate_115200 = 115200,
BaudRate_128000 = 128000,
BaudRate_230400 = 230400,
BaudRate_256000 = 256000
}
/// <summary>
/// 设置握手类型
/// </summary>
public enum SerialPortHandshake : int
{
None = 0, //没有用于握手的控件。
XOnXOff = 1,//使用 XON/XOFF 软件控制协议。 发送 XOFF 控制以停止数据传输。 发送 XON 控制以继续传输。 使用这些软件控制,而不是使用请求发送(RTS) 和清除发送(CTS) 硬件控制。
RequestToSend = 2,//使用请求发送(RTS) 硬件流控制。 RTS 发出信号,指出数据可用于传输。 如果输入缓冲区已满,RTS 行将被设置为 false。 当输入缓冲区中有更多可用空间时,RTS 行将被设置为 true。
RequestToSendXOnXOff = 3,//同时使用请求发送(RTS) 硬件控制和 XON/XOFF 软件控制。
}
/// <summary>
/// 设置停止位
/// </summary>
public enum SerialPortStopBits : int
{
None = 0,
One = 1,
Two = 2,
OnePointFive = 3
}
/// <summary>
/// 设置奇偶校验位
/// </summary>
public enum SerialPortParity : int
{
//
// 摘要:
// 没有奇偶校验检查时发生。
None = 0,
//
// 摘要:
// 设置奇偶校验位,以便设置了位数为奇数。
Odd = 1,
//
// 摘要:
// 设置奇偶校验位,以便设置了位的计数为偶数。
Even = 2,
//
// 摘要:
// 将奇偶校验位设置为 1。
Mark = 3,
//
// 摘要:
// 将奇偶校验位设置为 0。
Space = 4
}
#endregion
}
}
2、CS中关键代码( SystemWindow: Formium)的CS代码中
在之前项目的项目中,主要增加了下面代码
(1)在代码开始前
/// <summary>
/// 获取下基础,这个窗口要调取的页面
/// </summary>
private RuanBaseClass.WindowPageType SystemMainPage = RuanBaseClass.WindowPageType.SystemIndex;
/// <summary>
/// new出个串口对象的新类。
/// </summary>
private RuanBaseClass.SeralPortsHelper thisPageSerialPort = new RuanBaseClass.SeralPortsHelper();
/// <summary>
/// 定义本页面的COM口
/// </summary>
private SerialPort serialPort = new SerialPort();
/// <summary>
/// 对外的字符串
/// </summary>
public string serialPortGetString = "等待数据传输中。。。";
(2)在页面加载的时候(Window_Loaded)
//获取加载的页面对象
var frame = e.Browser.GetMainFrame();
//开始注入JS前,获取注入的页面
var hbrjso = BeginRegisterJavaScriptObject(frame);
//这里建立obj,创建注入的对象,需要了解对应页面的JS的方法名。下面进行obj的创建
//下面引用,阮2024.8.30.自己写的主页注入的方法,与主页一致。尝试
var obj = RegisteredWindowPageJS(SystemMainPage);
RegisterJavaScriptObject(hbrjso, "AllJsOfIndex", obj);
//把第三个参数(仪器com口的JS,注入到页面中并在页面中的名字是DeviceSerialPort)
RegisterJavaScriptObject(hbrjso, "DeviceSerialPort", RegisteredDeviceSerialPortJS());
//结束注册
EndRegisterJavaScriptObject(hbrjso);
(3)新增COM口的JS注册方法
/// <summary>
/// 阮。2024.9.4 开始编写,把仪器中COM口的事情,注入到页面的JS
/// </summary>
/// <param name="systemMainPage">注入的页面</param>
/// <returns>返回一个JS</returns>
private JavaScriptObject RegisteredDeviceSerialPortJS()
{
//定义返回的JsObject
var _regObg = new JavaScriptObject();
var _GetComRuselt = new RuanBaseClass.TestBoolAndString();
//给页面调取串口数据定义个获取值的属性
_regObg.DefineProperty("SerialPortValue", () => serialPortGetString);
//获取设备COM口
_regObg.Add("GetDeviceSerialPortNameJS", args =>
{
_GetComRuselt.IsTestOk = false;
_GetComRuselt.TestRueseltString = "未找到COM口";
if (SerialPort.GetPortNames().Length > 0)
{
_GetComRuselt.IsTestOk = true;
_GetComRuselt.TestRueseltString = string.Join(",", SerialPort.GetPortNames());
}
return _GetComRuselt.RuseltJsonString;
});
//设置SerialPort参数,并尝试打开。
_regObg.Add("SetSerialPortTryOpen", async (args, promise) =>
{
var retval = string.Join(",", args.Select(x => x.GetString()));
JsonDocument doc = JsonDocument.Parse(retval);
JsonElement root = doc.RootElement;
string portname = root.GetProperty("PortName").GetString();
string portBaudRate = root.GetProperty("BaudRate").GetString();
string portDataBits = root.GetProperty("DataBits").GetString();
string portParity = root.GetProperty("Parity").GetString();
string portStopBits = root.GetProperty("StopBits").GetString();
string portOpen = root.GetProperty("Open").GetString();
//输入参数正确
if (portname != "" && portBaudRate != "" && portDataBits != "" && portParity != "" && portStopBits != "" && portOpen != "" )
{
//如果设置参数是打开
if (portOpen == "true" || portOpen == "True" || portOpen == "TRUE")
{
_GetComRuselt = thisPageSerialPort.GetAndCheckSerialPortName(portname);
//检测名字通过,检测名字并尝试打开
if (_GetComRuselt.IsTestOk)
{
try
{
if (serialPort.IsOpen)
{
serialPort.Close();
serialPort.PortName = portname;
serialPort.BaudRate = thisPageSerialPort.GetBaudRates(portBaudRate);
serialPort.DataBits = thisPageSerialPort.GetDatabits(portDataBits);
serialPort.Parity = thisPageSerialPort.GetParity(portParity);
serialPort.StopBits = thisPageSerialPort.GetStopBits(portStopBits);
serialPort.DataReceived += AllPort_DataReceived; // 绑定数据接收事件
serialPort.Open();
promise.Resolve("检测端口正常,此次关闭后重新打开");
}
else
{
serialPort.PortName = portname;
serialPort.BaudRate = thisPageSerialPort.GetBaudRates(portBaudRate);
serialPort.DataBits = thisPageSerialPort.GetDatabits(portDataBits);
serialPort.Parity = thisPageSerialPort.GetParity(portParity);
serialPort.StopBits = thisPageSerialPort.GetStopBits(portStopBits);
serialPort.DataReceived += AllPort_DataReceived; // 绑定数据接收事件
serialPort.Open();
promise.Resolve("检测端口正常,刚刚打开");
}
}
catch (Exception e)
{
string _msg = _GetComRuselt.RuseltJsonString + "\r\n 系统警报:\r\n" + e.ToString();
promise.Reject(_msg);
}
}
else
{
promise.Reject(_GetComRuselt.RuseltJsonString);
}
}
//不打开,只是测试
else
{
try
{
_GetComRuselt = thisPageSerialPort.GetAndCheckSerialPortName(portname);
thisPageSerialPort.GetBaudRates(portBaudRate);
thisPageSerialPort.GetDatabits(portDataBits);
thisPageSerialPort.GetParity(portParity);
thisPageSerialPort.GetStopBits(portStopBits);
promise.Resolve("检测端口正常。");
}
catch (Exception e)
{
string _msg = _GetComRuselt.RuseltJsonString + "\r\n 系统警报:\r\n" + e.ToString();
promise.Reject(_msg);
}
}
}
else
{
_GetComRuselt.IsTestOk = false;
_GetComRuselt.TestRueseltString = "你输入的值为:"+ retval.ToString() + "\r\n SerialPoart 参数错误:PortName 、 BaudRate、 DataBits 、 Parity 、 StopBits 、 Open ";
promise.Reject(_GetComRuselt.RuseltJsonString);
}
});
return _regObg;
}
3、在HTML页面中的关键JS( index.html)的JS脚本中
主要增加了按钮,并点击按钮执行GetComPageTest(),页面增加DIV,通过innerHTML展示出来,并不断刷新。
<h1>-----COM口测试-阮------</h1>
<button name="testlogin" onClick="GetComPageTest()">获取COM口测试</button>
<div id="myDiv">这里展示数据</div>
<h1>----COM口测试结束--------</h1>
//关于获取CS的COM口的测试
function GetComPageTest() {
var ComString = window.external.DeviceSerialPort.GetDeviceSerialPortNameJS();
alert("获取COM口名称"+ComString);
const PortObj = {
PortName:"COM4",
BaudRate: "9600",
DataBits: "8",
Parity: "None",
StopBits: "1",
Open: "true"
}
//将COM口配置组成JSON
var PortObjS = JSON.stringify(PortObj);
//获取CS执行COM口操作的promise
var promise = window.external.DeviceSerialPort.SetSerialPortTryOpen(PortObjS);
//将结果展示出来
promise.then(res => { alert(res) }).catch(err => { alert(err) });
}
//设置获取数据的时间间隔,毫秒
var t = setInterval('GetComdata()',100)
function GetComdata() {
document.getElementById('myDiv').innerHTML = window.external.DeviceSerialPort.SerialPortValue;
}
看下实现功能的视频
CSND-COM与html-6
二、此次探坑总结
主要遇到的坑
1、serialPort.IsOpen (不同实例中的拒绝访问报错)
new了serialPort 后,本想的是点击开始获取数据,再次点击停止获取数据。
把new serialPort 放到点击按钮后执行的注入JS 代码中,意味着每次点击开始获取数据,则新new中,运行过程中不报错,多次点击按钮后,报个“COM口拒绝访问”的错。后查相关资料,由于注入JS,打开COM口放在了子线程中,每次点击都new出一个。
也就是说第一次点击按钮正常运行。第二次再点击按钮,则对已经开始采集的COM,再次open。
实际代码用 isopen=true,表面看是正常,实际第二次点击按钮后new出来的COM口,并未打开,所以在打断点的时候,会让人误以为第一COM口没有打开。
解决办法就是点击开始采集按钮的时候,不要new出新的COM口,而是对已有的COM进行操作,也就是把New COM口放外面去。
2、页面与CS的通讯(约定JSON)
在项目实现过程中,JS给CS传递了个object对象,但如何获取其内容,尝试了很多方法都不满意。考虑到JS与CS对于数组的读写编辑删除方面,虽然能将对象进行传递,但不能相互混用。结合将来也必定有其他对象的传递,比如XML等。为此约定了相互传递,还是以JSON为主。
本案例,JS定义:
const PortObj = {
PortName:"COM4",
BaudRate: "9600",
DataBits: "8",
Parity: "None",
StopBits: "1",
Open: "true"
}
在CS中,要读取和写入COM口名称等参数,也按JSON进行约定,因此有了
var _GetComRuselt = new RuanBaseClass.TestBoolAndString();
JsonDocument doc = JsonDocument.Parse(retval);
JsonElement root = doc.RootElement;
的代码,同样JS中也有JSON的操作,需要注意的是JSON的解析,需要WIN7\IE8\ .net6.0\等要求,在开发中要特别注意。
3、JS注入对象,Promise 相关。
CS代码中,异步调用返回,作者采用promis,Jsobj.Add(..., async (args, promise) ..
在对promis不了解情况下,坑中挖掘2天。
各位看官移步
前端 JS 经典:Promise 详解_js promise-CSDN博客,这里讲的比较明确。
winformium作者,在注入JS代码中采用了该前台很经典的异步方法。
大概意思:CS异步调用,有完成与否、成功、失败、失败后如何操作等。JS调用CS的异步操作,CS中随时调整promise的状态,则JS可以同步获取异步调用情况,而不需要始终等待CS执行完毕后的返回值。
如果不采用这个方法,而逐一进行代码,JS和CS的异步调用,将变得超级复杂。为此,作者案例采用该方法,极大简化了异步调用的代码复杂度。赞一个。
PS,在作者的反馈群中,有大神提出了CS中用task。的确功能类似,但JS和CS如此方便的知晓相互调用状态,还是推荐作者promise的方法。