如何使用C# 获取目标设备上的读数?
步骤一: 找目标设备的通讯手册
步骤二: 获取报文,使用SSCOM进行测试
步骤三:构建C#代码
步骤一:找到通讯手册


步骤二:使用SSCOM进行测试
百度SSCOM很容易找到安装包。
下载后,选择端口号,打开串口。

如何获取报文呢:
根据文档内容,先得到数据格式


根据文档中每个命令的格式,按照规定的字段(受体头编号、命令、参数等)构造出ASCII字符串,然后计算BCC,最后加上STX、ETX和分隔符,转换成十六进制表示。
STX(Start of Text)是一个控制字符,十六进制为02。所以开头是02

例如第54号命令。与PC建立连接。
可以看到 ,pc到CL-200A,要使用字符 0 0 5 4 1 空格 空格 空格

转换过来就是:
30 30 35 34 31 20 20 20
在加上
ETX: 03
BCC: 30 32
分隔符: 0D 0A

打开SSCOM
点击更多串口设置,设置好数据格式

发送报文

根据通讯手册总结出 :
- 设置PC连接模式(命令54)
02 30 30 35 34 31 20 20 20 03 31 33 0D 0A
- 设置Hold状态(命令55)
02 39 39 35 35 31 20 20 30 03 30 32 0D 0A
- 设置EXT模式(命令40)
02 30 30 34 30 31 30 20 20 03 30 36 0D 0A
- 触发测量(命令40,参数不同)
02 39 39 34 30 32 31 20 20 03 30 34 0D 0A
- 读取Ev、x、y数据(命令02)
02 30 30 30 32 31 32 30 30 03 30 32 0D 0A
然后开始构建C# 程序
步骤三 构建C#
上述报文写成如下格式:
// 命令序列
private readonly byte[] STX = new byte[] { 0x02 };
private readonly byte[] DELIMITER = new byte[] { 0x0D, 0x0A };
// 1. 设置PC连接模式(命令54)
private readonly byte[] setPCModeCmd = new byte[] {
0x30, 0x30, 0x35, 0x34, // "0054"
0x31, 0x20, 0x20, 0x20, // "1 "
0x03 // ETX
};
// 2. 设置Hold状态(命令55)
private readonly byte[] setHoldStatusCmd = new byte[] {
0x39, 0x39, 0x35, 0x35, // "9955"
0x31, 0x20, 0x20, 0x30, // "1 0"
0x03 // ETX
};
// 3. 设置EXT模式(命令40)
private readonly byte[] setEXTModeCmd = new byte[] {
0x30, 0x30, 0x34, 0x30, // "0040"
0x31, 0x30, 0x20, 0x20, // "10 "
0x03 // ETX
};
// 4. 触发测量(命令40,参数不同)
private readonly byte[] triggerMeasurementCmd = new byte[] {
0x39, 0x39, 0x34, 0x30, // "9940"
0x32, 0x31, 0x20, 0x20, // "21 "
0x03 // ETX
};
// 5. 读取Ev、x、y数据(命令02)
private readonly byte[] readDataCmd = new byte[] {
0x30, 0x30, 0x30, 0x32, // "0002"
0x31, 0x32, 0x30, 0x30, // "1200" (CF禁用,校准模式NORM)
0x03 // ETX
};
设置RS485:
public class RS485
{
List<byte> buffer = new List<byte>();
public SerialPort serialPort = null;
public RS485(string comNum, int baudRate)
{
if (serialPort == null)
{
serialPort = new SerialPort();
serialPort.BaudRate = baudRate;
serialPort.DataBits = 8;
serialPort.Parity = Parity.None;
serialPort.PortName = comNum;
serialPort.StopBits = StopBits.One;
serialPort.ReadBufferSize = 1024000;
serialPort.WriteBufferSize = 1024000;
serialPort.ErrorReceived += new SerialErrorReceivedEventHandler(serialPort_ErrorReceived);
serialPort.DataReceived += new SerialDataReceivedEventHandler(serialPort_DataReceived);
serialPort.Open();
}
}
void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
int n = serialPort.BytesToRead;
byte[] readByte = new byte[n];
int data = serialPort.Read(readByte, 0, n);
buffer.AddRange(readByte);
//while (serialPort.BytesToRead > 0)
//{
// int data = serialPort.ReadByte();
// buffer.Add((byte)data);
//}
}
void serialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{
Console.WriteLine("ErrorReceived:" + e.EventType.ToString());
}
将返回的报文 获取其中的 照度值 :
static double ParseIlluminanceFromMessage(string hexMessage)
{
// 1. 去除空格,转换为字节数组
string[] hexBytes = hexMessage.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
byte[] messageBytes = hexBytes.Select(h => Convert.ToByte(h, 16)).ToArray();
// 2. 验证基本格式
if (messageBytes.Length < 9)
throw new ArgumentException("报文太短,不符合CL-200A协议格式");
// 检查STX (0x02)
if (messageBytes[0] != 0x02)
throw new ArgumentException("无效的STX字节");
// 查找ETX (0x03)的位置
int etxIndex = -1;
for (int i = 1; i < messageBytes.Length; i++)
{
if (messageBytes[i] == 0x03)
{
etxIndex = i;
break;
}
}
if (etxIndex == -1)
throw new ArgumentException("未找到ETX字节");
// 3. 解析报文各部分
// 受体头号 (2字节): messageBytes[1..2]
// 命令 (2字节): messageBytes[3..4]
string command = $"{Convert.ToChar(messageBytes[3])}{Convert.ToChar(messageBytes[4])}";
// 状态 (4字节): messageBytes[5..8]
// 检查是否为命令02的响应
if (command != "02")
throw new ArgumentException($"不是命令02的响应 (收到命令: {command})");
// 4. 检查是否为长格式报文 (命令02使用长格式)
// 长格式: STX + 头号(2) + 命令(2) + 状态(4) + 数据(6*3=18) + ETX + BCC(2) + CR + LF
int expectedLength = 1 + 2 + 2 + 4 + 18 + 1 + 2 + 1 + 1; // 32字节
if (messageBytes.Length < expectedLength)
throw new ArgumentException($"报文长度不足,长格式应至少{expectedLength}字节");
// 5. 提取Ev数据 (第一个6字节块)
// 数据起始位置: 跳过STX(1) + 头号(2) + 命令(2) + 状态(4) = 9字节
int dataStartIndex = 9;
// Ev数据块: 6个字节
byte[] evDataBytes = new byte[6];
Array.Copy(messageBytes, dataStartIndex, evDataBytes, 0, 6);
// 6. 解析Ev值
// 格式: 符号(1字节) + 4位数值(4字节) + 指数(1字节)
char sign = Convert.ToChar(evDataBytes[0]);
string valueStr = $"{Convert.ToChar(evDataBytes[1])}{Convert.ToChar(evDataBytes[2])}" +
$"{Convert.ToChar(evDataBytes[3])}{Convert.ToChar(evDataBytes[4])}";
char exponentChar = Convert.ToChar(evDataBytes[5]);
// 7. 解析符号
if (sign != '+' && sign != '-' && sign != '=')
throw new ArgumentException($"无效的符号字符: {sign}");
// 8. 解析数值
if (!int.TryParse(valueStr, out int value))
throw new ArgumentException($"无效的数值: {valueStr}");
// 9. 解析指数
if (!int.TryParse(exponentChar.ToString(), out int exponentCode))
throw new ArgumentException($"无效的指数字符: {exponentChar}");
// 指数映射表 (根据协议第34页)
// '0'->10^-4, '1'->10^-3, '2'->10^-2, '3'->10^-1,
// '4'->10^0, '5'->10^1, '6'->10^2, '7'->10^3, '8'->10^4, '9'->10^5
double exponent = Math.Pow(10, exponentCode - 4);
// 10. 计算照度值
double illuminance = value * exponent;
// 如果符号为负,取负值
if (sign == '-')
illuminance = -illuminance;
// '=' 表示 ±,通常为正,这里按正数处理
return illuminance;
}
完整代码如下:
cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
using System.IO;
using System.IO.Ports;
namespace zhaoduji
{
public partial class Form1 : Form
{
static int count = 1;
RS485 rs485LuminanceMeter;
// 命令序列
private readonly byte[] STX = new byte[] { 0x02 };
private readonly byte[] DELIMITER = new byte[] { 0x0D, 0x0A };
// 1. 设置PC连接模式(命令54)
private readonly byte[] setPCModeCmd = new byte[] {
0x30, 0x30, 0x35, 0x34, // "0054"
0x31, 0x20, 0x20, 0x20, // "1 "
0x03 // ETX
};
// 2. 设置Hold状态(命令55)
private readonly byte[] setHoldStatusCmd = new byte[] {
0x39, 0x39, 0x35, 0x35, // "9955"
0x31, 0x20, 0x20, 0x30, // "1 0"
0x03 // ETX
};
// 3. 设置EXT模式(命令40)
private readonly byte[] setEXTModeCmd = new byte[] {
0x30, 0x30, 0x34, 0x30, // "0040"
0x31, 0x30, 0x20, 0x20, // "10 "
0x03 // ETX
};
// 4. 触发测量(命令40,参数不同)
private readonly byte[] triggerMeasurementCmd = new byte[] {
0x39, 0x39, 0x34, 0x30, // "9940"
0x32, 0x31, 0x20, 0x20, // "21 "
0x03 // ETX
};
// 5. 读取Ev、x、y数据(命令02)
private readonly byte[] readDataCmd = new byte[] {
0x30, 0x30, 0x30, 0x32, // "0002"
0x31, 0x32, 0x30, 0x30, // "1200" (CF禁用,校准模式NORM)
0x03 // ETX
};
public Form1()
{
InitializeComponent();
loadzhaoduji();
}
void loadzhaoduji()
{
try
{
rs485LuminanceMeter = new RS485("COM5", 9600);
rs485LuminanceMeter.serialPort.DataBits = 7;
rs485LuminanceMeter.serialPort.Parity = Parity.Even;
rs485LuminanceMeter.serialPort.StopBits = StopBits.One;
// 发送初始化命令序列
textBox2.AppendText("开始初始化CL-200A..." + "\r\n");
// 1. 设置PC连接模式
textBox2.AppendText("1. 设置PC连接模式..." + "\r\n");
byte[] cmd1 = rs485LuminanceMeter.SendCommand485BCC(STX, setPCModeCmd, DELIMITER);
Thread.Sleep(500);
// 等待并检查响应
List<byte> response1 = rs485LuminanceMeter.ReadIllData();
if (response1.Count > 0)
{
string hexResponse = rs485LuminanceMeter.ByteListToStr(response1);
textBox2.AppendText(" 响应: " + hexResponse + "\r\n");
}
// 2. 设置Hold状态
textBox2.AppendText("2. 设置Hold状态..." + "\r\n");
byte[] cmd2 = rs485LuminanceMeter.SendCommand485BCC(STX, setHoldStatusCmd, DELIMITER);
Thread.Sleep(500);
// 3. 设置EXT模式
textBox2.AppendText("3. 设置EXT模式..." + "\r\n");
byte[] cmd3 = rs485LuminanceMeter.SendCommand485BCC(STX, setEXTModeCmd, DELIMITER);
Thread.Sleep(175);
// 等待并检查响应
List<byte> response3 = rs485LuminanceMeter.ReadIllData();
if (response3.Count > 0)
{
string hexResponse = rs485LuminanceMeter.ByteListToStr(response3);
textBox2.AppendText(" 响应: " + hexResponse + "\r\n");
}
textBox2.AppendText("照度计初始化完成!" + "\r\n");
}
catch (Exception ex)
{
textBox2.AppendText("照度计初始化失败: " + ex.Message + "\r\n");
}
}
static double ParseIlluminanceFromMessage(string hexMessage)
{
// 1. 去除空格,转换为字节数组
string[] hexBytes = hexMessage.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
byte[] messageBytes = hexBytes.Select(h => Convert.ToByte(h, 16)).ToArray();
// 2. 验证基本格式
if (messageBytes.Length < 9)
throw new ArgumentException("报文太短,不符合CL-200A协议格式");
// 检查STX (0x02)
if (messageBytes[0] != 0x02)
throw new ArgumentException("无效的STX字节");
// 查找ETX (0x03)的位置
int etxIndex = -1;
for (int i = 1; i < messageBytes.Length; i++)
{
if (messageBytes[i] == 0x03)
{
etxIndex = i;
break;
}
}
if (etxIndex == -1)
throw new ArgumentException("未找到ETX字节");
// 3. 解析报文各部分
// 受体头号 (2字节): messageBytes[1..2]
// 命令 (2字节): messageBytes[3..4]
string command = $"{Convert.ToChar(messageBytes[3])}{Convert.ToChar(messageBytes[4])}";
// 状态 (4字节): messageBytes[5..8]
// 检查是否为命令02的响应
if (command != "02")
throw new ArgumentException($"不是命令02的响应 (收到命令: {command})");
// 4. 检查是否为长格式报文 (命令02使用长格式)
// 长格式: STX + 头号(2) + 命令(2) + 状态(4) + 数据(6*3=18) + ETX + BCC(2) + CR + LF
int expectedLength = 1 + 2 + 2 + 4 + 18 + 1 + 2 + 1 + 1; // 32字节
if (messageBytes.Length < expectedLength)
throw new ArgumentException($"报文长度不足,长格式应至少{expectedLength}字节");
// 5. 提取Ev数据 (第一个6字节块)
// 数据起始位置: 跳过STX(1) + 头号(2) + 命令(2) + 状态(4) = 9字节
int dataStartIndex = 9;
// Ev数据块: 6个字节
byte[] evDataBytes = new byte[6];
Array.Copy(messageBytes, dataStartIndex, evDataBytes, 0, 6);
// 6. 解析Ev值
// 格式: 符号(1字节) + 4位数值(4字节) + 指数(1字节)
char sign = Convert.ToChar(evDataBytes[0]);
string valueStr = $"{Convert.ToChar(evDataBytes[1])}{Convert.ToChar(evDataBytes[2])}" +
$"{Convert.ToChar(evDataBytes[3])}{Convert.ToChar(evDataBytes[4])}";
char exponentChar = Convert.ToChar(evDataBytes[5]);
// 7. 解析符号
if (sign != '+' && sign != '-' && sign != '=')
throw new ArgumentException($"无效的符号字符: {sign}");
// 8. 解析数值
if (!int.TryParse(valueStr, out int value))
throw new ArgumentException($"无效的数值: {valueStr}");
// 9. 解析指数
if (!int.TryParse(exponentChar.ToString(), out int exponentCode))
throw new ArgumentException($"无效的指数字符: {exponentChar}");
// 指数映射表 (根据协议第34页)
// '0'->10^-4, '1'->10^-3, '2'->10^-2, '3'->10^-1,
// '4'->10^0, '5'->10^1, '6'->10^2, '7'->10^3, '8'->10^4, '9'->10^5
double exponent = Math.Pow(10, exponentCode - 4);
// 10. 计算照度值
double illuminance = value * exponent;
// 如果符号为负,取负值
if (sign == '-')
illuminance = -illuminance;
// '=' 表示 ±,通常为正,这里按正数处理
return illuminance;
}
// 用于从字节列表中解析照度值的辅助方法
private double ParseIlluminanceFromBytes(List<byte> byteList)
{
if (byteList == null || byteList.Count < 32)
{
throw new ArgumentException("接收到的数据长度不足");
}
// 将字节列表转换为十六进制字符串
string hexMessage = "";
for (int i = 0; i < byteList.Count; i++)
{
hexMessage += byteList[i].ToString("X2");
if (i < byteList.Count - 1)
hexMessage += " ";
}
return ParseIlluminanceFromMessage(hexMessage);
}
private bool AdjustMeasurementRange()
{
try
{
textBox2.AppendText("检查并调整测量范围..." + "\r\n");
// 重新设置EXT模式
byte[] cmdExt = rs485LuminanceMeter.SendCommand485BCC(STX, setEXTModeCmd, DELIMITER);
Thread.Sleep(175);
//触发一次测量
byte[] cmdTrigger = rs485LuminanceMeter.SendCommand485BCC(STX, triggerMeasurementCmd, DELIMITER);
Thread.Sleep(500);
return true;
}
catch
{
return false;
}
}
private void button_get_Click(object sender, EventArgs e)
{
try
{
if (rs485LuminanceMeter == null || !rs485LuminanceMeter.serialPort.IsOpen)
{
MessageBox.Show("照度计未连接!");
return;
}
textBox2.AppendText($"第{count}次测量..." + "\r\n");
count++;
// 先调整测量范围
AdjustMeasurementRange();
// 然后进行正常测量
byte[] cmd4 = rs485LuminanceMeter.SendCommand485BCC(STX, triggerMeasurementCmd, DELIMITER);
Thread.Sleep(500);
byte[] cmd5 = rs485LuminanceMeter.SendCommand485BCC(STX, readDataCmd, DELIMITER);
Thread.Sleep(500);
List<byte> response = rs485LuminanceMeter.ReadIllData();
if (response.Count > 0)
{
string hexResponse = rs485LuminanceMeter.ByteListToStr(response);
textBox2.AppendText("接收到的原始数据: " + hexResponse + "\r\n");
try
{
double illuminance = ParseIlluminanceFromBytes(response);
textBox2.AppendText($"照度值 (Ev): {illuminance:F2} lx" + "\r\n");
// 检查并显示状态信息
if (response.Count >= 8)
{
byte errByte = response[5]; // ERR字节
byte rngByte = response[7]; // RNG字节
byte baByte = response[8]; // BA字节
char errChar = Convert.ToChar(errByte);
char rngChar = Convert.ToChar(rngByte);
char baChar = Convert.ToChar(baByte);
textBox2.AppendText($"状态: ERR={errChar}, RNG={rngChar}, BA={baChar}" + "\r\n");
}
}
catch (Exception ex)
{
textBox2.AppendText("解析照度值失败: " + ex.Message + "\r\n");
}
}
else
{
textBox2.AppendText("未收到响应数据!" + "\r\n");
}
}
catch (Exception ex)
{
MessageBox.Show("通讯或解析失败:" + ex.Message);
}
}
}
}
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Threading;
namespace zhaoduji
{
public class RS485
{
List<byte> buffer = new List<byte>();
public SerialPort serialPort = null;
public RS485(string comNum, int baudRate)
{
if (serialPort == null)
{
serialPort = new SerialPort();
serialPort.BaudRate = baudRate;
serialPort.DataBits = 8;
serialPort.Parity = Parity.None;
serialPort.PortName = comNum;
serialPort.StopBits = StopBits.One;
serialPort.ReadBufferSize = 1024000;
serialPort.WriteBufferSize = 1024000;
serialPort.ErrorReceived += new SerialErrorReceivedEventHandler(serialPort_ErrorReceived);
serialPort.DataReceived += new SerialDataReceivedEventHandler(serialPort_DataReceived);
serialPort.Open();
}
}
void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
int n = serialPort.BytesToRead;
byte[] readByte = new byte[n];
int data = serialPort.Read(readByte, 0, n);
buffer.AddRange(readByte);
//while (serialPort.BytesToRead > 0)
//{
// int data = serialPort.ReadByte();
// buffer.Add((byte)data);
//}
}
void serialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{
Console.WriteLine("ErrorReceived:" + e.EventType.ToString());
}
public byte Get_CheckXorBCC(byte[] data)
{
byte CheckCode = 0;
int len = data.Length;
for (int i = 0; i < len; i++)
{
CheckCode ^= data[i];
}
return CheckCode;
}
public byte[] SendCommand485BCC(byte[] STX, byte[] sendData, byte[] DELIMITER)
{
buffer.Clear();
byte BCC = Get_CheckXorBCC(sendData);
int byteHigh = (BCC & 0xf0) >> 4;
int byteLow = BCC & 0x0f;
byte high;
if (byteHigh > 9)
{
high = (byte)(byteHigh + 55);
}
else
{
high = (byte)(byteHigh + 48);
}
byte low;
if (byteLow > 9)
{
low = (byte)(byteLow + 55);
}
else
{
low = (byte)(byteLow + 48);
}
byte[] ARRay = new byte[] { };
byte[] BCCBCC = new byte[] { high, low };
//Array = ARRay.Concat(STX, sendData, high, low, DELIMITER);
ARRay = ARRay.Concat(STX).ToArray();
ARRay = ARRay.Concat(sendData).ToArray();
ARRay = ARRay.Concat(BCCBCC).ToArray();
ARRay = ARRay.Concat(DELIMITER).ToArray();
//byte s = (byte)high;
if (serialPort != null)
{
serialPort.DiscardInBuffer();
//比如那一串报文是0x0102012C000239FE
//我就要设置输入的内容为new byte[] { 0x01, 0x02, 0x01, 0x2c, 0x00, 0x02 }
//newbuffer 里面的内容为{1,2,1,44,0,2,57,254}//最后两位为自动生成的校验码
serialPort.Write(ARRay, 0, ARRay.Length);
}
return ARRay;
}
public List<byte> ReadIllData()
{
List<byte> getBuffer = new List<byte>();
int readcount = 0;
int timeout = 100; // 最大等待次数
// 等待直到收到完整数据或超时
while (readcount < timeout)
{
readcount++;
// 检查是否有数据
if (buffer.Count > 0)
{
// 检查是否收到结束符0x0A
int endIndex = buffer.IndexOf(0x0A);
if (endIndex >= 0)
{
// 找到结束符,提取完整报文
for (int i = 0; i <= endIndex; i++)
{
getBuffer.Add(buffer[i]);
}
// 从buffer中移除已处理的数据
buffer.RemoveRange(0, endIndex + 1);
break;
}
}
Thread.Sleep(10); // 每次等待10ms
}
// 如果超时,但buffer中有数据,也返回
if (getBuffer.Count == 0 && buffer.Count > 0)
{
getBuffer = new List<byte>(buffer);
buffer.Clear();
}
// 清空串口缓冲区
if (serialPort != null)
serialPort.DiscardInBuffer();
return getBuffer;
}
public string ByteListToStr(List<byte> aa)
{
string sss = "";
if (aa.Count > 0)
{
for (int i = 0; i < aa.Count; i++)
{
string asa = aa[i].ToString("x");
sss += asa + " ";
}
}
return sss;
}
}
}
运行:
