Nmodbus4 是C# 常用的通信类库,但是关于其断线重连的资料却很少,今天实验断线重连成功,特来记录一下。为了简单举例,这里的PLC变量都使用短整形ushort,提前与PLC约定放缩倍数,在配置文件中使用scale放缩系数得到正确值
json 配置文件:
{
"ip" : "127.0.0.1",
"port" : "502",
"slaveNo" : "1",
"address" : "0",
"varNum" : "10",
"actualDatas" : [ {
"id" : 1,
"name" : "Var1",
"description" : "Short1",
"insertTime" : "",
"createTime" : "",
"varAddress" : "0",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 2,
"name" : "Var2",
"description" : "Short2",
"insertTime" : "",
"createTime" : "",
"varAddress" : "1",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 3,
"name" : "Var3",
"description" : "Short3",
"insertTime" : "",
"createTime" : "",
"varAddress" : "2",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 4,
"name" : "Var4",
"description" : "Short4",
"insertTime" : "",
"createTime" : "",
"varAddress" : "3",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 5,
"name" : "Var5",
"description" : "Short5",
"insertTime" : "",
"createTime" : "",
"varAddress" : "4",
"dataType" : "Short",
"scale" : "0.5",
"value" : ""
}, {
"id" : 6,
"name" : "Var6",
"description" : "Short6",
"insertTime" : "",
"createTime" : "",
"varAddress" : "5",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 7,
"name" : "Var7",
"description" : "Short7",
"insertTime" : "",
"createTime" : "",
"varAddress" : "6",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 8,
"name" : "Var8",
"description" : "Short8",
"insertTime" : "",
"createTime" : "",
"varAddress" : "7",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 9,
"name" : "Var9",
"description" : "Short9",
"insertTime" : "",
"createTime" : "",
"varAddress" : "8",
"dataType" : "Short",
"scale" : "1",
"value" : ""
}, {
"id" : 10,
"name" : "Var10",
"description" : "Short10",
"insertTime" : "",
"createTime" : "",
"varAddress" : "9",
"dataType" : "Short",
"scale" : "1",
"value" : ""
} ]
}
实体类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.Entities
{
public class CfgJson
{
public string Ip { get; set; }
public string Port { get; set; }
public string SlaveNo { get; set; }
public string Address { get; set; }
public string varNum { get; set; }
public List<ActualData> ActualDatas { get; set; }
}
}
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.Entities
{
public class ActualData
{
//主键,自增
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
public string Name { get; set; }
public string Value { get; set; }
[SugarColumn(IsNullable = true)]//可以为NULL
public string InsertTime { get; set; }
[SugarColumn(IsNullable = true)]//可以为NULL
public string CreateTime { get; set; }
public string Description { get; set; }
public string DataType { get; set; }
public string Scale { get; set; }
public string VarAddress { get; set; }
}
}
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.Entities
{
public class Log
{
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
[SugarColumn(IsNullable = true)]//可以为NULL
public DateTime Date { get; set; }
public string Level { get; set; }
[SugarColumn(IsNullable = true)]//可以为NULL
public string Logger { get; set; }
[SugarColumn(IsNullable = true)]//可以为NULL
public string Message { get; set; }
}
}
Modbus帮助类:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.Modbus
{
public interface IModbusHelper
{
string Conn();
string DisConn();
void readHoldingRegisters(ObservableCollection<string> colorList, ObservableCollection<string> varList);
string writeHoldingRegisters(string varAddress, string value);
void Insert();
}
}
using log4net;
using Modbus.Device;
using Newtonsoft.Json;
using PrismFWDemon.Entities;
using PrismFWDemon.SqlSugar.Impl;
using PrismFWDemon.ViewModels;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace PrismFWDemon.Modbus.Impl
{
public class ModbusHelper:IModbusHelper
{
bool last = false;
bool trigIn = false;
bool trigQ = false;
private string ip;
private string port;
private List<ActualData> actualDatas = new List<ActualData>();
public List<ActualData> ActualDatas
{
get { return actualDatas; }
set { actualDatas = value; }
}
/**
* modubs从站ID
*/
private string slaveNo;
private bool isConnected = false;
private bool isFirstConn = true;
private int errorTimes = 0;
CancellationTokenSource cts = new CancellationTokenSource();
/**
* 起始地址
*/
private string address;
//变量个数
private string varNum;
private TcpClient tcpClient = null;
private ModbusIpMaster master;
//配置文件地址
private const string jsonfile = "D:\\modbuscfg1.json";
public string Jsonfile
{
get { return jsonfile; }
}
//读取变量
private string str;
private CfgJson cfgJson;
private SqlSugarHelper sqlSugarHelper;
public ModbusHelper(SqlSugarHelper sqlSugarHelper)
{
this.sqlSugarHelper = sqlSugarHelper;
str = File.ReadAllText(jsonfile);
cfgJson = JsonConvert.DeserializeObject<CfgJson>(str);
this.ip = cfgJson.Ip;
this.port = cfgJson.Port;
this.slaveNo = cfgJson.SlaveNo;
this.address = cfgJson.Address;
this.varNum = cfgJson.varNum;
this.actualDatas = cfgJson.ActualDatas;
//this.Conn();
//this.readHoldingRegisters();
}
public string Conn()
{
try
{
tcpClient = new TcpClient();
tcpClient.Connect(IPAddress.Parse(ip), int.Parse(port));
master = ModbusIpMaster.CreateIp(tcpClient);
isConnected = true;
master.Transport.ReadTimeout = 1000;//读取数据超时时间为设定值
master.Transport.WriteTimeout = 1000;//写入数据超时时间为设定值
master.Transport.Retries = 5;
master.Transport.WaitToRetryMilliseconds = 2500;
return "OK";
}
catch (Exception ex)
{
LogHelper.Info(ex.Message);
LogHelper.Error(ex.Message);
return ex.Message;
}
}
public string DisConn()
{
if (master != null)
{
master.Dispose();
tcpClient.Dispose();
tcpClient.Close();
return "OK";
}
else
{
return "Failed";
}
}
public void readHoldingRegisters(ObservableCollection<string> colorList, ObservableCollection<string> varList)
{
Task.Run(async () =>
{
while (!cts.IsCancellationRequested)
{
if (isConnected == true)
{
ushort[] des = null;
await Task.Delay(500);
try
{
des = master.ReadHoldingRegisters(ushort.Parse(address), ushort.Parse(varNum));
}
catch (Exception ex)
{
LogHelper.Info(ex.Message);
LogHelper.Error(ex.Message);
}
//读出的数据不为空,则解析数据,存储
if (des != null)
{
errorTimes = 0;
for (int i = 0; i < actualDatas.Count; i++)
{
actualDatas[i].Value = ((float)des[i] * float.Parse(actualDatas[i].Scale)).ToString();
actualDatas[i].CreateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
varList[i] = actualDatas[i].Value;
if (varList[i] == "1")
{
colorList[i] = "Green";
}
else if (varList[i] == "2")
{
colorList[i] = "Orange";
}
else if (varList[i] == "3")
{
colorList[i] = "Red";
}
}
//按事件插入数据库
Insert();
}
else
{
errorTimes++;
await Task.Delay(500);
if (errorTimes >= 3)
{
isConnected = false;
}
}
}
else
{
if (!isFirstConn)
{
await Task.Delay(1000);
tcpClient?.Close();
//已经测试,不行
//tcpClient.Dispose();
//master?.Transport.Dispose();
//master?.Dispose();
}
try
{
//首次连接,或多次连接
tcpClient = new TcpClient();
tcpClient.Connect(IPAddress.Parse(ip), int.Parse(port));
//先将master置为空,再断线重连
master = null;
master = ModbusIpMaster.CreateIp(tcpClient);
master.Transport.ReadTimeout = 1000;//读取数据超时时间为设定值
master.Transport.WriteTimeout = 1000;//写入数据超时时间为设定值
master.Transport.Retries = 5;
master.Transport.WaitToRetryMilliseconds = 2500;
isConnected = true;
isFirstConn = false;
}
catch (Exception ex)
{
LogHelper.Info(ex.Message);
LogHelper.Error(ex.Message);
}
}
}
},cts.Token);
}
public string writeHoldingRegisters(string varAddress, string value)
{
master.WriteSingleRegister(ushort.Parse(varAddress), ushort.Parse(value));
return "OK";
}
public void Insert()
{
trigIn = this.actualDatas[9].Value.Equals("10");
trigQ = (trigIn & !last);
last = trigIn;
if (trigQ)
{
try
{
InsertDataBase();
}
catch (Exception ex)
{
LogHelper.Info(ex.Message);
LogHelper.Error(ex.Message);
}
}
}
//插入数据工具类
public int InsertDataBase()
{
string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
foreach (var item in this.actualDatas)
{
item.InsertTime = now;
}
int count = this.sqlSugarHelper.db.Insertable(this.actualDatas).ExecuteCommand();
return count;
}
}
}
ViewModel:
using Prism.Commands;
using Prism.Mvvm;
using PrismFWDemon.Modbus.Impl;
using PrismFWDemon.SqlSugar;
using PrismFWDemon.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.ViewModels
{
public class TdResponsitoryViewModel: BindableBase
{
//ObservableCollection 可以通知到内容变化,List不行
private ObservableCollection<string> varList = new ObservableCollection<string>()
{
"var1","var2","var3","var4","var5","var6","var7","var8","var9","var10"
};
public ObservableCollection<string> VarList
{
get { return varList; }
set { varList = value; RaisePropertyChanged(); }
}
//private string color1_1 = "Gary";
//public string Color1_1
//{
// get { return color1_1; }
// set { color1_1 = value; }
//}
private ObservableCollection<string> colors=new ObservableCollection<string>()
{
"Gray","Gray","Gray","Gray","Gray","Gray","Gray","Gray","Gray","Gray"
};
public ObservableCollection<string> Colors
{
get { return colors; }
set { colors = value; RaisePropertyChanged(); }
}
public DelegateCommand OpenCargoA { get; private set; }
private void CargoA()
{
var window = new AddCargoAView();
window.ShowDialog();
}
private ModbusHelper modbusHelper;
public ModbusHelper ModbusHelper
{
get { return modbusHelper; }
set { modbusHelper = value; RaisePropertyChanged(); }
}
public TdResponsitoryViewModel(ModbusHelper modbusHelper)
{
this.modbusHelper = modbusHelper;
this.modbusHelper.readHoldingRegisters(this.Colors,this.VarList);
OpenCargoA = new DelegateCommand(CargoA);
}
}
}
view:
<UserControl x:Class="PrismFWDemon.Views.TdResponsitoryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PrismFWDemon.Views"
xmlns:prism="http://prismlibrary.com/"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<md:TransitioningContent OpeningEffect="{md:TransitionEffect Kind=ExpandIn}">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Grid.Row="0">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[0]}"
Command="{Binding OpenCargoA}"
Content="仓库1-1" />
</Grid>
<Grid Grid.Column="0" Grid.Row="1">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[1]}"
Command="{Binding SendMsg}"
Content="仓库2-1"/>
</Grid>
<Grid Grid.Column="0" Grid.Row="2">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[2]}"
Command="{Binding SendMsg}"
Content="仓库3-1" />
</Grid>
<Grid Grid.Column="0" Grid.Row="3">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[3]}"
Command="{Binding SendMsg}"
Content="仓库4-1" />
</Grid>
<Grid Grid.Column="1" Grid.Row="0">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[4]}"
Command="{Binding SendMsg}"
Content="仓库1-2" />
</Grid>
<Grid Grid.Column="1" Grid.Row="1">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[5]}"
Command="{Binding SendMsg}"
Content="仓库2-2" />
</Grid>
<Grid Grid.Column="1" Grid.Row="2">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[6]}"
Command="{Binding SendMsg}"
Content="仓库2-3" />
</Grid>
<Grid Grid.Column="1" Grid.Row="3">
<Button
Margin="20,20,20,20"
Height="80"
Background="{Binding Colors[7]}"
Command="{Binding SendMsg}"
Content="仓库2-4" />
</Grid>
<Grid Grid.Column="2" Grid.Row="0">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[0]}" />
</Grid>
<Grid Grid.Column="2" Grid.Row="1">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[1]}" />
</Grid>
<Grid Grid.Column="2" Grid.Row="2">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[2]}" />
</Grid>
<Grid Grid.Column="2" Grid.Row="3">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[3]}" />
</Grid>
<Grid Grid.Column="3" Grid.Row="0">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[4]}" />
</Grid>
<Grid Grid.Column="3" Grid.Row="1">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[5]}" />
</Grid>
<Grid Grid.Column="3" Grid.Row="2">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[6]}" />
</Grid>
<Grid Grid.Column="3" Grid.Row="3">
<Button
Margin="20,20,20,20"
Height="80"
Command="{Binding SendMsg}"
Content="{Binding VarList[7]}" />
</Grid>
</Grid>
</Grid>
</md:TransitioningContent>
</UserControl>
依赖注入:
using Prism.DryIoc;
using Prism.Ioc;
using PrismFWDemon.Common;
using PrismFWDemon.Modbus;
using PrismFWDemon.Modbus.Impl;
using PrismFWDemon.SqlSugar;
using PrismFWDemon.SqlSugar.Impl;
using PrismFWDemon.ViewModels;
using PrismFWDemon.Views;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace PrismFWDemon
{
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
///
/*
App.xaml 添加 xmlns:prism="http://prismlibrary.com/" 命名空间
修改继承 PrismApplication
App.xaml修改为prism:PrismApplication
创建 启动项
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
创建IOC容器
protected override Window CreateShell() //启动页
protected override void RegisterTypes 依赖注入
*/
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainView>();
}
protected override void OnInitialized()
{
var service = App.Current.MainWindow.DataContext as IConfigureService;
if (service != null)
service.Configure();
base.OnInitialized();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//注册导航
containerRegistry.RegisterForNavigation<ServiceView, ServiceViewModel>(); //PageA 区域名称,别名
containerRegistry.RegisterForNavigation<ClientView, ClientViewModel>();
containerRegistry.RegisterForNavigation<MsgView, MsgViewModel>();
containerRegistry.RegisterForNavigation<TdResponsitoryView, TdResponsitoryViewModel>();
//注册对话框
containerRegistry.RegisterDialog<MsgView, MsgViewModel>();//也可以加别名("Question")
containerRegistry.Register<ISqlSugarHelper, SqlSugarHelper>();
containerRegistry.RegisterSingleton<ISqlSugarService, SqlSugarService>();
containerRegistry.Register<IModbusHelper, ModbusHelper>();
//containerRegistry.Register<ILogHelper, LogHelper>();
}
}
}
using log4net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.SqlSugar.Impl
{
public class LogHelper
{
public static void Error(string message, Exception ex)
{
ILog log = LogManager.GetLogger("Error");
if (log.IsErrorEnabled)
{
log.Error(message, ex);
}
}
public static void Error(string message)
{
ILog log = LogManager.GetLogger("Error");
if (log.IsErrorEnabled)
{
log.Error(message);
}
}
public static void Info(string message)
{
ILog log = LogManager.GetLogger("Info");
if (log.IsInfoEnabled)
{
log.Info(message);
}
}
}
}
using SqlSugar;
namespace PrismFWDemon.SqlSugar.Impl
{
public class SqlSugarHelper: ISqlSugarHelper
{
private SqlSugarService sqlSugarService;
public SqlSugarHelper(SqlSugarService sqlSugarService)
{
this.sqlSugarService = sqlSugarService;
}
public SqlSugarClient db
{
get => new SqlSugarClient(new ConnectionConfig()
{
ConnectionString = this.sqlSugarService.ConnectionStr,
DbType = DbType.Sqlite, //必填, 数据库类型
IsAutoCloseConnection = true, //默认false, 时候知道关闭数据库连接, 设置为true无需使用using或者Close操作
InitKeyType = InitKeyType.Attribute //默认SystemTable, codefist需要使用Attribute
});
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PrismFWDemon.SqlSugar.Impl
{
public class SqlSugarService : ISqlSugarService
{
private string connectionStr = "Data Source=MyDb.db";
public string ConnectionStr
{
get { return connectionStr; }
set { connectionStr = value; }
}
}
}
效果:
断线: