在工业自动化和物联网领域,串口设备(如PLC、传感器等)常常需要通过网络进行远程监控和通信。基于TCP的串口转网口工具可以帮助我们实现这种需求,同时支持Modbus协议的读写操作,非常适合在具体工业场景中使用。本文将详细介绍基于C#的串口转网口工具的开发过程,包括功能设计、代码实现以及配置管理。
配置串口参数(如波特率、数据位、停止位、校验位等)。
支持Modbus协议,读取串口设备中的寄存器数据。
网络通信支持:
配置TCP监听地址和端口。
支持多个客户端连接,实现数据转发功能。
心跳检测:
支持心跳包功能,确保网络连接的可靠性。
配置管理:
实现配置文件管理,保存和加载串口和TCP服务器的设置。
可视化界面:
提供用户界面,直观展示串口、网口状态和传输数据。
1. 主要组件
串口通信模块:
使用SerialPort类管理串口通信。
使用Modbus.Device库处理Modbus协议。
TCP服务模块:
使用TcpListener和TcpClient实现TCP通信。
支持多客户端连接管理。
心跳包机制:
使用Timer控件定时发送心跳包。
配置管理:使用ConfigurationManager管理配置文件。
2. 数据流向
串口到网口:串口接收到数据后,转发给所有已连接的TCP客户端。
网口到串口:TCP客户端发送的数据通过串口发送。
Modbus协议处理:根据Modbus协议读取串口设备的数据,并转发给TCP客户端。
指示灯:显示串口和网口的状态(绿色为正常,红色为异常)。
串口配置:设置串口参数。
服务器配置:设置TCP服务器的IP地址和端口。
心跳设置:设置心跳包的间隔、内容和HEX模式。
2. 代码实现
2.1 初始化与配置加载
private bool Init()
{
try
{
// 加载串口配置
for (int i = 1; i <= 8; i++)
{
cbPortName.Items.Add("COM" + i);
cbBTL.Items.Add((1200 * Math.Pow(2, i - 1)).ToString());
if (i >= 5)
{
cbSJW.Items.Add(i.ToString());
}
if (i >= 1 && i <= 5)
{
cbJYW.Items.Add(Enum.GetName(typeof(Parity), i - 1));
}
if (i >= 1 && i <= 4)
{
cbSZW.Items.Add(Enum.GetName(typeof(StopBits), i - 1));
}
}
cbPortName.SelectedItem = ConfigurationManager.AppSettings["SerialPort"];
cbBTL.SelectedItem = ConfigurationManager.AppSettings["BaudRate"];
cbSJW.SelectedItem = ConfigurationManager.AppSettings["DataBits"];
cbJYW.SelectedItem = ConfigurationManager.AppSettings["Parity"];
cbSZW.SelectedItem = ConfigurationManager.AppSettings["StopBits"];
// 配置串口
serialPort = new SerialPort();
serialPort.PortName = cbPortName.SelectedItem.ToString();
serialPort.BaudRate = Convert.ToInt32(cbBTL.SelectedItem.ToString());
serialPort.DataBits = Convert.ToInt32(cbSJW.SelectedItem);
serialPort.Parity = (Parity)Enum.Parse(typeof(Parity), cbJYW.SelectedItem.ToString());
serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbSZW.SelectedItem.ToString());
// 配置服务器
tbIP.Text = ConfigurationManager.AppSettings["IPAddress"];
tbDK.Text = ConfigurationManager.AppSettings["Port"];
IPAddress ip = IPAddress.Parse(tbIP.Text.Trim());
int port = Convert.ToInt32(tbDK.Text.Trim());
tcpListener = new TcpListener(ip, port);
// 心跳包配置
radioButton1.Checked = bool.Parse(ConfigurationManager.AppSettings["HeartOn"]);
radioButton2.Checked = !bool.Parse(ConfigurationManager.AppSettings["HeartOn"]);
tbXTJG.Text = ConfigurationManager.AppSettings["HeartTime"];
tbXTNR.Text = ConfigurationManager.AppSettings["HeartContent"];
checkBox1.Checked = bool.Parse(ConfigurationManager.AppSettings["HeartHex"]);
timer1.Interval = Convert.ToInt32(tbXTJG.Text);
timer1.Tick += Timer1_Tick;
return true;
}
catch (Exception ex)
{
MessageBox.Show($"出错了: {ex.Message}");
return false;
}
}
2.2串口与Modbus初始化
private void StartSerialPort()
{
try
{
serialPort.Open();
master = ModbusSerialMaster.CreateRtu(serialPort);
panel1.BackColor = Color.Lime; // 状态灯显示绿色
}
catch (Exception ex)
{
panel1.BackColor = Color.Red; // 状态灯显示红色
MessageBox.Show($"启动串口失败: {ex.Message}");
}
}
2.4 数据转发逻辑
private void AcceptData()
{
cts = new CancellationTokenSource();
Task.Run(() =>
{
while (!cts.IsCancellationRequested)
{
try
{
TcpClient client = tcpListener.AcceptTcpClient();
clients.Add(client);
Task.Run(async () =>
{
NetworkStream stream = client.GetStream();
while (!cts.IsCancellationRequested)
{
try
{
byte[] buffer = new byte[client.Available];
int length = await stream.ReadAsync(buffer, 0, buffer.Length);
string message = Encoding.UTF8.GetString(buffer, 0, length).Trim();
byte[] responseData = null;
switch (message)
{
case "温度值":
responseData = ReadWD();
break;
case "含水率":
responseData = ReadHS();
break;
case "电导率":
responseData = ReadDD();
break;
case "PH值":
responseData = ReadPH();
break;
case "盐度":
responseData = ReadYD();
break;
case "TDS":
responseData = ReadTDS();
break;
default:
// 如果不是特定命令,将消息转发给串口
serialPort.Write(buffer, 0, length);
break;
}
if (responseData != null)
{
await stream.WriteAsync(responseData, 0, responseData.Length);
}
Invoke((Action)(async () =>
{
tbread.Text = (Convert.ToInt32(tbread.Text) + length).ToString();
panel3.BackColor = Color.Lime;
await Task.Delay(100);
panel3.BackColor = Color.White;
}));
}
catch (Exception ex)
{
MessageBox.Show($"出错: {ex.Message}");
break;
}
}
});
}
catch (Exception ex)
{
MessageBox.Show($"客户端连接异常: {ex.Message}");
}
}
});
}
2.5 心跳包机制
private void Timer1_Tick(object sender, EventArgs e)
{
if (clients.Count == 0)
{
return;
}
byte[] buffer;
if (checkBox1.Checked) // 十六进制
{
string[] hexs = tbXTNR.Text.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
buffer = new byte[hexs.Length];
for (int i = 0; i < hexs.Length; i++)
{
try
{
buffer[i] = Convert.ToByte(hexs[i], 16);
}
catch (FormatException)
{
MessageBox.Show("十六进制格式错误");
return;
}
}
}
else
{
buffer = Encoding.UTF8.GetBytes(tbXTNR.Text.Trim());
}
for (int i = 0; i < clients.Count; i++)
{
if (clients[i].Connected)
{
NetworkStream stream = clients[i].GetStream();
stream.Write(buffer, 0, buffer.Length);
}
}
}
2.6 Modbus读取实现
private byte[] ReadWD()
{
try
{
ushort[] data = master.ReadHoldingRegisters(1, 0x01, 1);
float temperature = data[0] / 10F;
return Encoding.UTF8.GetBytes(temperature.ToString("N1"));
}
catch (Exception ex)
{
MessageBox.Show($"读取温度值出错: {ex.Message}");
return new byte[0];
}
}
3. 配置文件管理
使用app.config文件保存设置:
//这里可以更改为自己的网络ip地址
4.保存配置
private void btnSave_Click(object sender, EventArgs e)
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["SerialPort"].Value = cbPortName.SelectedItem.ToString();
config.AppSettings.Settings["BaudRate"].Value = cbBTL.SelectedItem.ToString();
config.AppSettings.Settings["DataBits"].Value = cbSJW.SelectedItem.ToString();
config.AppSettings.Settings["Parity"].Value = cbJYW.SelectedItem.ToString();
config.AppSettings.Settings["StopBits"].Value = cbSZW.SelectedItem.ToString();
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
MessageBox.Show("串口保存配置成功,重启查看效果");
}
注意串口配对和配置情况
例如(COM1->COM2) 两两一组
波特率、数据位等信息要保持一致
服务器的配置可以根据自己的网络ip进行更改
并在资源管理器中点击引用 添加这两个.netget包
六、总结
本项目基于C#实现了一个完整的串口转网口工具,支持串口Modbus协议读写、TCP多客户端通信以及心跳机制,同时提供了配置文件管理功能。该工具适用于工业自动化、物联网等领域,具有较高的实用性和可扩展性。
完整的代码如下
using Modbus.Device;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO.Ports;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 串口转网口
{
public partial class Form1 : Form
{
//串口网口都支持
IModbusMaster master;
SerialPort serialPort;
TcpListener tcpListener;
CancellationTokenSource cts;
List clients = new List();
public Form1()
{
InitializeComponent();
}
#region 窗口加载负责对通信基础进行初始化
private void Form1_Load(object sender, EventArgs e)
{
if (!Init())
{
// 关窗体
Close();
return;
}
// 启动串口
StartSerialPort();
StartTcpListener();
}
private void StartTcpListener()
{
try
{
tcpListener.Start();
panel2.BackColor = Color.Lime;
// 服务器启动后需要开启一个循环来接受新的客户端连接
AcceptData();
timer1.Enabled = radioButton1.Checked;
}
catch (Exception ex)
{
panel2.BackColor = Color.Red;
MessageBox.Show("启动TCP监听器失败: " + ex.Message);
}
}
private void AcceptData()
{
cts = new CancellationTokenSource();
Task.Run(() =>
{
while (!cts.IsCancellationRequested)
{
try
{
// 等待客户端连接
TcpClient client = tcpListener.AcceptTcpClient();
//这里添加到list里面是为了当串口收到消息时,能够找到在线的客户端,并发送消息
clients.Add(client);
//接收客户端的消息转发给串口
Task.Run(() =>
{
while (!cts.IsCancellationRequested)
{
try
{
byte[] buffer = new byte[client.Available];
NetworkStream stream = client.GetStream();
int length = stream.Read(buffer, 0, buffer.Length);
string message = Encoding.UTF8.GetString(buffer, 0, length).Trim();
// 检查客户端发送的消息并读取相应的数据
byte[] responseData = null;
switch (message)
{
case "温度值":
responseData = ReadWD();
break;
case "含水率":
responseData = ReadHS();
break;
case "电导率":
responseData = ReadDD();
break;
case "PH值":
responseData = ReadPH();
break;
case "盐度":
responseData = ReadYD();
break;
case "TDS":
responseData = ReadTDS();
break;
default:
// 如果不是特定命令,将消息转发给串口
serialPort.Write(buffer, 0, length);
break;
}
if (responseData != null)
{
stream.Write(responseData, 0, responseData.Length);
}
Invoke((Action)(async () =>
{
tbread.Text = (Convert.ToInt32(tbread.Text) + length).ToString();
//模拟信号灯,有数据进来频闪
panel3.BackColor = Color.Lime;
await Task.Delay(100);
panel3.BackColor = Color.White;
}));
}
catch (Exception ex)
{
MessageBox.Show("出错: " + ex.Message);
break;
}
}
}, cts.Token);
}
catch (Exception ex)
{
}
}
}, cts.Token);
}
private void StartSerialPort()
{
try
{
serialPort.Open();
// 创建Modbus Master实例
master = ModbusSerialMaster.CreateRtu(serialPort);
panel1.BackColor = Color.Lime;
}
catch (Exception ex)
{
panel1.BackColor = Color.Red;
MessageBox.Show("启动串口失败: " + ex.Message);
}
}
private bool Init()
{
try
{
for (int i = 1; i <= 8; i++)
{
cbPortName.Items.Add("COM" + i);
cbBTL.Items.Add((1200 * Math.Pow(2, i - 1)).ToString());
if (i >= 5)
{
cbSJW.Items.Add(i.ToString());
}
if (i >= 1 && i <= 5)
{
cbJYW.Items.Add(Enum.GetName(typeof(Parity), i - 1));
}
if (i >= 1 && i <= 4)
{
cbSZW.Items.Add(Enum.GetName(typeof(StopBits), i - 1));
}
}
cbPortName.SelectedItem = ConfigurationManager.AppSettings["SerialPort"];
cbBTL.SelectedItem = ConfigurationManager.AppSettings["BaudRate"];
cbSJW.SelectedItem = ConfigurationManager.AppSettings["DataBits"];
cbJYW.SelectedItem = ConfigurationManager.AppSettings["Parity"];
cbSZW.SelectedItem = ConfigurationManager.AppSettings["StopBits"];
// 配置串口
serialPort = new SerialPort();
serialPort.PortName = cbPortName.SelectedItem.ToString();
serialPort.BaudRate = Convert.ToInt32(cbBTL.SelectedItem.ToString());
serialPort.DataBits = Convert.ToInt32(cbSJW.SelectedItem);
serialPort.Parity = (Parity)Enum.Parse(typeof(Parity), cbJYW.SelectedItem.ToString());
serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), cbSZW.SelectedItem.ToString());
// 配置服务器
tbIP.Text = ConfigurationManager.AppSettings["IPAddress"];
tbDK.Text = ConfigurationManager.AppSettings["Port"];
// 初始化服务器信息
IPAddress ip = IPAddress.Parse(tbIP.Text.Trim());
int port = Convert.ToInt32(tbDK.Text.Trim());
tcpListener = new TcpListener(ip, port);
// 心跳包配置
radioButton1.Checked = bool.Parse(ConfigurationManager.AppSettings["HeartOn"]);
radioButton2.Checked = !bool.Parse(ConfigurationManager.AppSettings["HeartOn"]); // 确保两个单选按钮互斥
tbXTJG.Text = ConfigurationManager.AppSettings["HeartTime"];
tbXTNR.Text = ConfigurationManager.AppSettings["HeartContent"];
checkBox1.Checked = bool.Parse(ConfigurationManager.AppSettings["HeartHex"]);
// 心跳包通过Timer控件来控制
timer1.Interval = Convert.ToInt32(tbXTJG.Text);
timer1.Tick += Timer1_Tick;
return true;
}
catch (Exception ex)
{
MessageBox.Show("出错了: " + ex.Message);
return false;
}
}
private void Timer1_Tick(object sender, EventArgs e)
{
if (clients.Count == 0)
{
return;
}
byte[] buffer;
if (checkBox1.Checked) // 十六进制
{
string[] hexs = tbXTNR.Text.Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries);
buffer = new byte[hexs.Length];
for (int i = 0; i < hexs.Length; i++)
{
try
{
buffer[i] = Convert.ToByte(hexs[i], 16);
}
catch (FormatException)
{
MessageBox.Show("十六进制格式错误");
return;
}
}
}
else
{
// 心跳内容为字符串
buffer = Encoding.UTF8.GetBytes(tbXTNR.Text.Trim());
}
for (int i = 0; i < clients.Count; i++)
{
if (clients[i].Connected)
{
NetworkStream stream = clients[i].GetStream();
stream.Write(buffer, 0, buffer.Length);
}
}
}
private byte[] ReadWD()
{
try
{
ushort[] data = master.ReadHoldingRegisters(1, 0x01, 1);
float temperature = data[0] / 10F;
return Encoding.UTF8.GetBytes(temperature.ToString("N1"));
}
catch (Exception ex)
{
MessageBox.Show("读取温度值出错: " + ex.Message);
return new byte[0]; // 返回空字节数组
}
}
private byte[] ReadHS()
{
try
{
ushort[] data = master.ReadHoldingRegisters(1, 0x00, 1);
float humidity = data[0] / 10F;
string debugMsg = $"读取含水率: 原始值={data[0]}, 计算值={humidity}";
Console.WriteLine(debugMsg); // 在输出窗口查看
return Encoding.UTF8.GetBytes(humidity.ToString("N1"));
}
catch (Exception ex)
{
Console.WriteLine($"读取含水率出错: {ex.Message}");
return new byte[0];
}
}
private byte[] ReadDD()
{
try
{
// 假设电导率在Modbus地址0x02
ushort[] data = master.ReadHoldingRegisters(1, 0x02, 1);
float conductivity = data[0] / 10F;
return Encoding.UTF8.GetBytes(conductivity.ToString("N1"));
}
catch (Exception ex)
{
MessageBox.Show("读取电导率出错: " + ex.Message);
return new byte[0]; // 返回空字节数组
}
}
private byte[] ReadPH()
{
try
{
ushort[] data = master.ReadHoldingRegisters(1, 0x03, 1);
float conductivity = data[0] / 10F;
return Encoding.UTF8.GetBytes(conductivity.ToString("N1"));
}
catch (Exception ex)
{
MessageBox.Show("读取电导率出错: " + ex.Message);
return new byte[0]; // 返回空字节数组
}
}
private byte[] ReadYD()
{
try
{
ushort[] data = master.ReadHoldingRegisters(1, 0x07, 1);
float conductivity = data[0] / 10F;
return Encoding.UTF8.GetBytes(conductivity.ToString("N1"));
}
catch (Exception ex)
{
MessageBox.Show("读取电导率出错: " + ex.Message);
return new byte[0]; // 返回空字节数组
}
}
private byte[] ReadTDS()
{
try
{
ushort[] data = master.ReadHoldingRegisters(1, 0x08, 1);
float conductivity = data[0] / 10F;
return Encoding.UTF8.GetBytes(conductivity.ToString("N1"));
}
catch (Exception ex)
{
MessageBox.Show("读取电导率出错: " + ex.Message);
return new byte[0]; // 返回空字节数组
}
}
#endregion
#region 串口保存
private void btnSave_Click(object sender, EventArgs e)
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["SerialPort"].Value = cbPortName.SelectedItem.ToString();
config.AppSettings.Settings["BaudRate"].Value = cbBTL.SelectedItem.ToString();
config.AppSettings.Settings["DataBits"].Value = cbSJW.SelectedItem.ToString();
config.AppSettings.Settings["Parity"].Value = cbJYW.SelectedItem.ToString();
config.AppSettings.Settings["StopBits"].Value = cbSZW.SelectedItem.ToString();
config.Save(ConfigurationSaveMode.Modified);
ConfigurationManager.RefreshSection("appSettings");
MessageBox.Show("串口保存配置成功,重启查看效果");
}
private void btnSave2_Click(object sender, EventArgs e)
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["IPAddress"].Value = tbIP.Text.Trim();
config.AppSettings.Settings["Port"].Value = tbDK.Text.Trim();
config.Save();
MessageBox.Show("服务器保存配置成功,重启查看效果");
}
private void btnSave3_Click(object sender, EventArgs e)
{
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["HeartOn"].Value = radioButton1.Checked ? "true" : "false";
config.AppSettings.Settings["HeartTime"].Value = tbXTJG.Text.Trim();
config.AppSettings.Settings["HeartContent"].Value = tbXTNR.Text.Trim();
config.AppSettings.Settings["HeartHex"].Value = checkBox1.Checked.ToString();
config.Save();
MessageBox.Show("心跳包保存配置成功,重启查看效果");
}
#endregion
#region 心跳开关
private void radioButton1_CheckedChanged(object sender, EventArgs e)
{
timer1.Enabled = radioButton1.Checked;
}
private void radioButton2_CheckedChanged(object sender, EventArgs e)
{
timer1.Enabled = radioButton2.Checked;
}
#endregion
private void btnread_Click(object sender, EventArgs e)
{
}
}
}