项目背景:在产线中,经常出现PLC触摸屏的时间与真实时间不一致的情况,因此需要定时进行时间同步。
实现功能:1、通过S7实现PC与PLC通讯;2、通过创建自定义控件来实现多PLC通讯(将PLC实例到自定义控件中,通过控件来控制PLC通断);3、定时发送心跳信号。
关键代码如下:(文章末尾有源码地址)
1、主窗体代码
public partial class MainWindow : Window
{
/// <summary>
/// 时间同步线程
/// </summary>
Thread threadMain;
/// <summary>
/// 控件合集
/// </summary>
private List<InfoCard> controls = new List<InfoCard>();
private FloatingWindow floatingWindow;
/// <summary>
/// 加载窗体
/// </summary>
public MainWindow()
{
InitializeComponent();
Init();
this.StateChanged += MainWindow_StateChanged;
this.ShowInTaskbar = false;
threadMain = new Thread(ThreadMainMothed);
threadMain.Start();
this.WindowState = WindowState.Maximized;
// 最小化时显示悬浮窗,隐藏主窗口
this.Hide();
if (floatingWindow == null)
{
floatingWindow = new FloatingWindow(this);
// 设置悬浮窗的位置等属性
}
floatingWindow.Show();
}
/// <summary>
/// 窗体状态变化
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MainWindow_StateChanged(object sender, EventArgs e)
{
if (this.WindowState == WindowState.Minimized)
{
// 最小化时显示悬浮窗,隐藏主窗口
this.Hide();
if (floatingWindow == null)
{
floatingWindow = new FloatingWindow(this);
// 设置悬浮窗的位置等属性
}
floatingWindow.Show();
}
else if (this.WindowState == WindowState.Normal)
{
// 恢复正常时隐藏悬浮窗,显示主窗口
if (floatingWindow != null)
{
floatingWindow.Close();
floatingWindow = null;
}
this.Show();
this.Activate();
}
}
/// <summary>
/// 窗体移动
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Border_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
this.DragMove();
}
}
/// <summary>
/// 窗体关闭
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click(object sender, RoutedEventArgs e)
{
//这是最彻底的退出方式,不管什么线程都被强制退出,把程序结束的很干净。
System.Environment.Exit(0);
//this.Close();
}
DataModel.WorkLine workLine = new DataModel.WorkLine();
/// <summary>
/// 初始化加载控件及数据
/// </summary>
private void Init()
{
controls = new List<InfoCard>();
InfoCard infoCard = new InfoCard();
string path = AppDomain.CurrentDomain.BaseDirectory + "DeviceService.xml";
try
{
XmlSerializer xmlSerializer = new XmlSerializer(workLine.GetType());
using (FileStream fileStream = new FileStream(path, FileMode.Open))
{
workLine = (DataModel.WorkLine)xmlSerializer.Deserialize(fileStream);
fileStream.Close();
}
foreach (DataModel.Station station in workLine.Stations)
{
infoCard = new InfoCard();
infoCard.Icon = PackIconMaterialKind.Laptop;
infoCard.StationName = station.StationName;
infoCard.IPAddress = station.IPAddress;
infoCard.FillColor = Brushes.Green;
infoCard.ValueWidth = 30;
infoCard.PLCConnect();
if (infoCard.IsConnected) { }
controls.Add(infoCard);
}
}
catch (Exception ex) { }
// 将控件集合绑定到ItemsControl
itemsControl.ItemsSource = controls;
}
/// <summary>
/// 窗体最小化
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Mini_Click(object sender, RoutedEventArgs e)
{
if (this.WindowState == WindowState.Minimized)
{
this.Hide();
}
else
{
this.WindowState = WindowState.Minimized;
}
}
EnumTimeSync enumTimeSync = EnumTimeSync.Empty;
/// <summary>
/// 时间同步线程对应的主要方法
/// </summary>
private void ThreadMainMothed()
{
while (true)
{
try
{
for (int i = 0; i < controls.Count; i++)
{
if (!controls[i].IsConning)
{
this.Dispatcher.Invoke(() =>
{
if (!controls[i].IsConnected)
{
controls[i].OpenPLC();
}
foreach (var item in workLine.DataPoints)
{
switch (item.PointID)
{
case "TimeSync":
enumTimeSync = EnumTimeSync.TimeSync; break;
case "TimeSign":
enumTimeSync = EnumTimeSync.TimeSign; break;
case "HeartBeat":
enumTimeSync = EnumTimeSync.HeartBeat; break;
default:
enumTimeSync = EnumTimeSync.Empty; break;
}
controls[i].MethodEntry(enumTimeSync, int.Parse(item.DBNumber), int.Parse(item.StartByte), 2);
}
});
}
}
}
catch (Exception ex) { }
Thread.Sleep(100);
}
}
/// <summary>
/// 手动同步时间
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Sync_Click(object sender, RoutedEventArgs e)
{
for (int i = 0; i < controls.Count; i++)
{
if (!controls[i].IsConning)
{
this.Dispatcher.Invoke(() =>
{
controls[i].TimeSyncManual(10100, 0);
});
}
}
}
}
2、悬浮窗口代码
public partial class FloatingWindow : Window
{
Window windowMain;
public FloatingWindow(Window window)
{
InitializeComponent();
var desktopWorkingArea = System.Windows.SystemParameters.WorkArea;
this.Left = desktopWorkingArea.Right - this.Width - 50;
this.Top = desktopWorkingArea.Bottom - this.Height - 50;
this.ShowInTaskbar = false;
windowMain = window;
}
/// <summary>
/// 窗体移动/双击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
this.DragMove();
}
if (e.ClickCount == 2)
{
// 在这里添加双击事件的处理逻辑
if (windowMain != null)
{
windowMain.Show();
windowMain.Activate();
windowMain.WindowState = WindowState.Normal;
}
}
}
/// <summary>
/// 窗体永远在最前
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Deactivated(object sender, EventArgs e)
{
Window window = (Window)sender;
window.Topmost = true;
}
/// <summary>
/// MenuItem的点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = e.Source as MenuItem;
if (menuItem != null)
{
if (menuItem.Header.ToString().Equals("主界面"))
{
// 在这里添加双击事件的处理逻辑
if (windowMain != null)
{
windowMain.Show();
windowMain.Activate();
windowMain.WindowState = WindowState.Normal;
}
}
else if (menuItem.Header.ToString().Equals("备用"))
{
}
else if (menuItem.Header.ToString().Equals("退出"))
{
//这是最彻底的退出方式,不管什么线程都被强制退出,把程序结束的很干净。
System.Environment.Exit(0);
}
}
}
}
3、自定义控件代码
public partial class InfoCard : UserControl
{
public InfoCard()
{
InitializeComponent();
stopwatchHeartBeat.Start();
}
#region 属性
private byte iHart = 1;
private int iOldHour = -1;
public string StationName
{
get { return (string)GetValue(StationNameProperty); }
set { SetValue(StationNameProperty, value); }
}
public static readonly DependencyProperty StationNameProperty = DependencyProperty.Register("StationName", typeof(string), typeof(InfoCard));
public string IPAddress
{
get { return (string)GetValue(IPAddressProperty); }
set { SetValue(IPAddressProperty, value); }
}
public static readonly DependencyProperty IPAddressProperty = DependencyProperty.Register("IPAddress", typeof(string), typeof(InfoCard));
public string Percentage
{
get { return (string)GetValue(PercentageProperty); }
set { SetValue(PercentageProperty, value); }
}
public static readonly DependencyProperty PercentageProperty = DependencyProperty.Register("Percentage", typeof(string), typeof(InfoCard));
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(InfoCard));
public PackIconMaterialKind Icon
{
get { return (PackIconMaterialKind)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(PackIconMaterialKind), typeof(InfoCard));
public SolidColorBrush FillColor
{
get { return (SolidColorBrush)GetValue(FillColorProperty); }
set { SetValue(FillColorProperty, value); }
}
public static readonly DependencyProperty FillColorProperty = DependencyProperty.Register("FillColor", typeof(SolidColorBrush), typeof(InfoCard));
public int ValueWidth
{
get { return (int)GetValue(ValueWidthProperty); }
set { SetValue(ValueWidthProperty, value); }
}
public static readonly DependencyProperty ValueWidthProperty = DependencyProperty.Register("ValueWidth", typeof(int), typeof(InfoCard));
private bool ifConning = false;
#endregion
#region S7PLC
static Plc plc;
/// <summary>
/// 连接PLC
/// </summary>
public void PLCConnect()
{
try
{
OpenPLC();
}
catch (Exception ex) { }
PLCStationRefresh();
}
/// <summary>
/// 断开PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_Disconnect_Click(object sender, RoutedEventArgs e)
{
try
{
if (plc != null)
{
plc.Close();
}
}
catch (Exception ex) { }
PLCStationRefresh();
}
/// <summary>
/// 打开PLC连接
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btn_Connect_Click(object sender, RoutedEventArgs e)
{
try
{
OpenPLC();
}
catch (Exception ex) { }
PLCStationRefresh();
}
/// <summary>
/// PLC状态刷新
/// </summary>
private void PLCStationRefresh()
{
if (plc != null && plc.IsConnected)
{
pro_bar.IsIndeterminate = true;
FillColor = Brushes.Green;
}
else
{
pro_bar.IsIndeterminate = false;
FillColor = Brushes.Red;
}
}
/// <summary>
/// 打开PLC
/// </summary>
public async void OpenPLC()
{
pro_bar.IsIndeterminate = ifConning = true;
try
{
if (plc == null)
plc = new Plc(CpuType.S71500, IPAddress, 0, 0);
if (plc != null && !plc.IsConnected)
await plc.OpenAsync();
//TimeSync();
}
catch (Exception ex) { }
if (plc != null && plc.IsConnected)
pro_bar.IsIndeterminate = true;
else
pro_bar.IsIndeterminate = false;
ifConning = false;
}
/// <summary>
/// 时间同步
/// </summary>
/// <param name="DBNum">DB块地址</param>
/// <param name="startAdr">起始地址</param>
public void TimeSync(int DBNum, int startAdr)
{
try
{
if (iOldHour != DateTime.Now.Hour)
{
iOldHour = DateTime.Now.Hour;
if (plc != null && plc.IsConnected)
{
//String写入
string strTime = DateTime.Now.ToString("yyyyMMddHHmmss");
var temp = Encoding.ASCII.GetBytes(strTime); //将val字符串转换为字符数组
var bytes = S7.Net.Types.S7String.ToByteArray(strTime, temp.Length);
plc.WriteBytes(DataType.DataBlock, DBNum, startAdr, bytes);
TimeSign(DBNum, 22, (byte)1);
}
}
}
catch (Exception ex) { plc = null; }
finally { PLCStationRefresh(); }
}
/// <summary>
/// 手动时间同步
/// </summary>
/// <param name="DBNum">DB块地址</param>
/// <param name="startAdr">起始地址</param>
public void TimeSyncManual(int DBNum, int startAdr)
{
try
{
if (plc != null && plc.IsConnected)
{
//String写入
string strTime = DateTime.Now.ToString("yyyyMMddHHmmss");
var temp = Encoding.ASCII.GetBytes(strTime); //将val字符串转换为字符数组
var bytes = S7.Net.Types.S7String.ToByteArray(strTime, temp.Length);
plc.WriteBytes(DataType.DataBlock, DBNum, startAdr, bytes);
TimeSign(DBNum, 22, (byte)1);
}
}
catch (Exception ex) { plc = null; }
finally { PLCStationRefresh(); }
}
/// <summary>
/// 时间同步信号/心跳信号
/// </summary>
/// <param name="DBNum">DB块地址</param>
/// <param name="startAdr">起始地址</param>
/// <param name="value">信号量</param>
public void TimeSign(int DBNum, int startAdr, byte value)
{
try
{
if (plc != null && plc.IsConnected)
{
plc.Write("DB" + DBNum + ".DBW" + startAdr + ".0", value);
if (value == 1)
stopwatchTimeSign.Restart();
else
stopwatchTimeSign.Stop();
}
}
catch (Exception ex) { plc = null; }
finally { PLCStationRefresh(); }
}
/// <summary>
/// 获取心跳信号值
/// </summary>
public byte GetHartSign
{
get
{
if (iHart == 1)
iHart = 2;
else
iHart = 1;
return iHart;
}
}
/// <summary>
/// PLC是否连接
/// </summary>
public bool IsConnected { get { return plc != null && plc.IsConnected; } }
/// <summary>
/// PLC是否正在连接中
/// </summary>
public bool IsConning { get { return ifConning; } }
Stopwatch stopwatchHeartBeat = new Stopwatch();
Stopwatch stopwatchTimeSign = new Stopwatch();
/// <summary>
/// 循环程序入口
/// </summary>
/// <param name="enumTimeSync">触发类型</param>
/// <param name="DBNum">DB块</param>
/// <param name="startAdr">起始地址</param>
/// <param name="value">值</param>
public void MethodEntry(EnumTimeSync enumTimeSync, int DBNum, int startAdr, byte? value = null)
{
switch (enumTimeSync)
{
case EnumTimeSync.TimeSync:
TimeSync(DBNum, startAdr);
break;
case EnumTimeSync.TimeSign:
if (value != null && stopwatchTimeSign.ElapsedMilliseconds > 3000)
TimeSign(DBNum, startAdr, (byte)value);
break;
case EnumTimeSync.HeartBeat:
if (stopwatchHeartBeat.ElapsedMilliseconds > 3000)
{
TimeSign(DBNum, startAdr, GetHartSign);
stopwatchHeartBeat.Restart();
}
break;
default:
break;
}
}
#endregion
}
以上是关键代码,欢迎给位伙伴交流指正~!
源码连接:多PLC通讯之时间同步心跳机制