using HslCommunication;
using HslCommunication.Profinet.Melsec;
using MySqlConnector;
using System;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Timers;
using System.Data.SqlClient;
using NLog;
using Timer = System.Windows.Forms.Timer;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Reflection;
namespace OvenData
{
public partial class Form1 : Form
{
// PLC连接管理优化 - 改为长连接模式
private MelsecMcNet plc;
private readonly object plcLock = new object();
private const int MaxConnectionRetries = 100000;
private const int ConnectionRetryDelay = 20000; // 10秒
private bool isPlcConnecting = false;
private DateTime lastHeartbeatTime = DateTime.MinValue;
private const int HeartbeatInterval = 30000; // 30秒心跳包
private DateTime lastSuccessfulConnectionTime = DateTime.MinValue;
private int consecutiveFailedConnections = 0;
private int totalFailedConnections = 0;
private int totalSuccessfulConnections = 0;
private int currentConnectionRetryDelay = ConnectionRetryDelay;
private bool isFirstConnectionAttempt = true;
private bool isHeartbeatActive = false; // 新增:心跳活动状态
// 数据库连接优化
private string connectionString;
private ConcurrentQueue<DbCommandData> dbCommandQueue = new ConcurrentQueue<DbCommandData>();
private const int MaxQueueSize = 1000; // 限制队列最大长度
private System.Timers.Timer dbBatchTimer;
// 定时器优化
private System.Timers.Timer dataCollectionTimer;
private System.Timers.Timer connectionCheckTimer;
private System.Timers.Timer heartbeatTimer;
private DateTime nextTempReadTime = DateTime.MinValue;
private DateTime nextUnitReadTime = DateTime.MinValue;
private bool isPLCConnected = false;
// UI组件
private Label statusLabel;
private Timer uiUpdateTimer;
private Label connectionStatusLabel;
private Label lastConnectionLabel;
private Label retryDelayLabel;
private Label dataCollectionStatusLabel;
private Label lastDataCollectionLabel;
// 日志
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// 系统托盘
private NotifyIcon trayIcon;
private ContextMenuStrip trayMenu;
// 报警状态跟踪
private Dictionary<string, bool> alarmStatusDict = new Dictionary<string, bool>();
private System.Timers.Timer alarmCheckTimer;
// 数据采集标记
private DateTime lastTempCollectionTime = DateTime.MinValue;
private DateTime lastUnitCollectionTime = DateTime.MinValue;
private readonly object collectionLock = new object();
// 用于跟踪上次采集是否成功
private bool lastTemperatureCollectionSucceeded = true;
private bool lastUnitDataCollectionSucceeded = true;
// 重试计数
private int temperatureRetryCount = 0;
private int unitDataRetryCount = 0;
private const int MaxRetries = 3; // 最大重试次数
private const int RetryDelayBase = 20000; // 基础重试延迟(毫秒)
// 程序运行状态
private bool isShuttingDown = false;
private DateTime lastSuccessfulDataCollectionTime = DateTime.MinValue;
private int totalDataCollections = 0;
private int successfulDataCollections = 0;
// 采集时间间隔定义
private static readonly TimeSpan TemperatureCollectionInterval = TimeSpan.FromMinutes(5);
private static readonly TimeSpan UnitDataCollectionInterval = TimeSpan.FromHours(1);
public Form1()
{
InitializeComponent();
InitializeComponents();
InitializeTimers();
InitializeTrayIcon();
InitializeAlarmStatus();
// 注册窗体事件
this.Resize += Form1_Resize;
this.FormClosing += Form1_FormClosing;
// 启动时立即尝试连接
Task.Run(async () => await CheckConnectionAsync());
// 记录程序启动
Logger.Info("低温烤箱数据采集系统已启动");
}
private void InitializeAlarmStatus()
{
// 初始化常见报警点位的状态跟踪
for (int i = 0; i < 6; i++)
{
alarmStatusDict[$"D7500.{i}"] = false; // 仓高温
alarmStatusDict[$"D7500.{i + 16}"] = false; // 仓低温
alarmStatusDict[$"D7500.{i + 32}"] = false; // 仓温度异常
alarmStatusDict[$"D7500.{i + 48}"] = false; // 仓超温保护
alarmStatusDict[$"D7500.{i + 64}"] = false; // 仓风压低
alarmStatusDict[$"D7513.{i}"] = false; // 仓循环风机故障
alarmStatusDict[$"D7513.{i + 24}"] = false; // 仓发热体故障
alarmStatusDict[$"D7513.{i + 48}"] = false; // 仓跳闸
}
// 其他报警点位
alarmStatusDict["D7521.0"] = false; // 下位要板超时
alarmStatusDict["D7521.1"] = false; // 定位出错
alarmStatusDict["D7521.2"] = false; // 前门上升报警
}
private void InitializeTrayIcon()
{
// 创建托盘菜单
trayMenu = new ContextMenuStrip();
// 创建菜单项
var showMenuItem = new ToolStripMenuItem("显示窗口");
showMenuItem.Click += (s, e) =>
{
this.Show();
this.WindowState = FormWindowState.Normal;
this.Activate();
};
var exitMenuItem = new ToolStripMenuItem("退出程序");
exitMenuItem.Click += (s, e) =>
{
isShuttingDown = true;
Application.Exit();
};
// 添加菜单项到托盘菜单
trayMenu.Items.Add(showMenuItem);
trayMenu.Items.Add(exitMenuItem);
// 创建托盘图标
trayIcon = new NotifyIcon
{
Text = "低温烤箱数据采集",
Icon = GetAppIcon(),
ContextMenuStrip = trayMenu,
Visible = true
};
// 双击托盘图标显示窗口
trayIcon.DoubleClick += (s, e) =>
{
this.Show();
this.WindowState = FormWindowState.Normal;
this.Activate();
};
}
// 获取应用程序图标
private Icon GetAppIcon()
{
try
{
return Properties.Resources.kaoxiang1;
}
catch (Exception ex)
{
Logger.Warn(ex, "无法加载自定义图标,使用系统默认图标");
return SystemIcons.Information;
}
}
private void InitializeComponents()
{
// 初始化PLC连接
plc = new MelsecMcNet("192.168.38.174", 8001)
{
ConnectTimeOut = 10000,
ReceiveTimeOut = 10000
};
// 数据库连接字符串
connectionString = new MySqlConnectionStringBuilder
{
Server = "192.168.238.247",
Database = "Oven_Data",
UserID = "root",
Password = "RelianceERP2013&&",
Pooling = true,
MinimumPoolSize = 1,
MaximumPoolSize = 10,
ConnectionTimeout = 15,
ConnectionLifeTime = 300,
Keepalive = 30,
AllowUserVariables = true
}.ToString();
// 状态标签
statusLabel = new Label
{
Text = "PLC连接状态: 初始化中...",
AutoSize = true,
Location = new Point(12, 12),
Font = new Font("Microsoft YaHei", 10F, FontStyle.Bold),
ForeColor = Color.Blue
};
this.Controls.Add(statusLabel);
// 添加队列状态标签
Label queueStatusLabel = new Label
{
Name = "queueStatusLabel",
Text = "数据库队列: 0条",
AutoSize = true,
Location = new Point(12, 40),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(queueStatusLabel);
// 添加连接状态标签
connectionStatusLabel = new Label
{
Name = "connectionStatusLabel",
Text = "连接状态: 等待连接",
AutoSize = true,
Location = new Point(12, 70),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(connectionStatusLabel);
// 添加最后连接时间标签
lastConnectionLabel = new Label
{
Name = "lastConnectionLabel",
Text = "最后成功连接: 从未",
AutoSize = true,
Location = new Point(12, 100),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(lastConnectionLabel);
// 添加重试延迟标签
retryDelayLabel = new Label
{
Name = "retryDelayLabel",
Text = "重试延迟: 5秒",
AutoSize = true,
Location = new Point(12, 130),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(retryDelayLabel);
// 添加数据采集状态标签
dataCollectionStatusLabel = new Label
{
Name = "dataCollectionStatusLabel",
Text = "数据采集: 等待中",
AutoSize = true,
Location = new Point(12, 160),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(dataCollectionStatusLabel);
// 添加最后数据采集时间标签
lastDataCollectionLabel = new Label
{
Name = "lastDataCollectionLabel",
Text = "最后采集: 从未",
AutoSize = true,
Location = new Point(12, 190),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(lastDataCollectionLabel);
// 添加系统状态标签
Label systemStatusLabel = new Label
{
Name = "systemStatusLabel",
Text = "系统状态: 正常运行",
AutoSize = true,
Location = new Point(12, 220),
Font = new Font("Microsoft YaHei", 9F),
ForeColor = Color.Gray
};
this.Controls.Add(systemStatusLabel);
}
private void InitializeTimers()
{
// UI更新定时器
uiUpdateTimer = new Timer { Interval = 1000 };
uiUpdateTimer.Tick += UiUpdateTimer_Tick;
uiUpdateTimer.Start();
// 连接检查定时器
connectionCheckTimer = new System.Timers.Timer(5000) { AutoReset = true };
connectionCheckTimer.Elapsed += async (s, e) => await CheckConnectionAsync();
connectionCheckTimer.Start();
// 心跳包定时器
heartbeatTimer = new System.Timers.Timer(HeartbeatInterval) { AutoReset = true };
heartbeatTimer.Elapsed += async (s, e) => await SendHeartbeatAsync();
heartbeatTimer.Start();
// 报警检查定时器
alarmCheckTimer = new System.Timers.Timer(1000) { AutoReset = true };
alarmCheckTimer.Elapsed += async (s, e) => await CheckAndSaveAlarmDataAsync();
alarmCheckTimer.Start();
// 数据采集定时器 - 改为一次性触发
dataCollectionTimer = new System.Timers.Timer
{
AutoReset = false, // 只触发一次
Enabled = false // 初始禁用
};
dataCollectionTimer.Elapsed += async (s, e) => await CollectDataAsync();
// 数据库批量处理定时器
dbBatchTimer = new System.Timers.Timer(2000) { AutoReset = true };
dbBatchTimer.Elapsed += async (s, e) => await ProcessDbQueueAsync();
dbBatchTimer.Start();
// 初始化下一次采集时间
InitializeNextCollectionTimes();
// 启动精确的定时器调度
ScheduleNextDataCollection();
}
private void InitializeNextCollectionTimes()
{
DateTime now = DateTime.Now;
// 计算温度采集时间点(从当前时间开始的5分钟间隔)
nextTempReadTime = CalculateNextTempTime(now);
// 计算单位数据采集时间点(从当前时间开始的下一个整点)
nextUnitReadTime = CalculateNextUnitTime(now);
Logger.Info($"初始化采集时间: 温度采集 {nextTempReadTime}, 单位数据采集 {nextUnitReadTime}");
}
private DateTime CalculateNextTempTime(DateTime now)
{
// 计算从当前时间开始的下一个5分钟点
int minutes = now.Minute;
int nextMinute = ((minutes / 5) + 1) * 5;
if (nextMinute >= 60)
{
// 下一个小时的0分钟
return new DateTime(now.Year, now.Month, now.Day, now.Hour + 1, 0, 0);
}
else
{
return new DateTime(now.Year, now.Month, now.Day, now.Hour, nextMinute, 0);
}
}
private DateTime CalculateNextUnitTime(DateTime now)
{
// 计算从当前时间开始的下一个整点
if (now.Minute > 0 || now.Second > 0 || now.Millisecond > 0)
{
return new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0).AddHours(1);
}
else
{
return now;
}
}
private DateTime CalculateExpectedLastCollectionTime(DateTime now, TimeSpan interval)
{
// 计算理论上的上次采集时间点
long ticksSinceEpoch = now.Ticks;
long intervalTicks = interval.Ticks;
long remainder = ticksSinceEpoch % intervalTicks;
return new DateTime(ticksSinceEpoch - remainder);
}
private async Task CheckConnectionAsync()
{
if (isPlcConnecting || isShuttingDown) return;
try
{
isPlcConnecting = true;
bool connected = await Task.Run(() =>
{
lock (plcLock)
{
if (!isPLCConnected)
{
// 计算指数退避重试延迟(增加最大延迟到5分钟)
if (consecutiveFailedConnections > 0)
{
currentConnectionRetryDelay = Math.Min(
ConnectionRetryDelay * (int)Math.Pow(2, Math.Min(consecutiveFailedConnections, 8)), // 最大延迟约5分钟
300000);
}
else
{
currentConnectionRetryDelay = ConnectionRetryDelay;
}
Logger.Info($"尝试连接PLC (第{consecutiveFailedConnections + 1}次尝试, 延迟: {currentConnectionRetryDelay / 1000}秒)...");
UpdateConnectionStatusLabel($"正在连接PLC (第{consecutiveFailedConnections + 1}次尝试)...");
UpdateRetryDelayLabel($"{currentConnectionRetryDelay / 1000}秒");
// 等待指定延迟
if (!isFirstConnectionAttempt && consecutiveFailedConnections > 0)
{
System.Threading.Thread.Sleep(currentConnectionRetryDelay);
}
isFirstConnectionAttempt = false;
var result = plc.ConnectServer();
if (result.IsSuccess)
{
// 连接成功,重置所有状态
ResetAllStates();
Logger.Info($"PLC连接成功! 这是第{totalSuccessfulConnections}次成功连接");
return true;
}
consecutiveFailedConnections++;
totalFailedConnections++;
Logger.Warn($"PLC连接失败: {result.Message}");
UpdateConnectionStatusLabel($"连接失败: {result.Message}");
UpdateLastConnectionLabel("从未");
return false;
}
// 已连接状态下检查心跳
if ((DateTime.Now - lastHeartbeatTime).TotalMilliseconds > HeartbeatInterval * 2)
{
Logger.Warn("心跳超时,尝试重连PLC...");
UpdateConnectionStatusLabel("心跳超时,正在重连...");
plc.ConnectClose();
var result = plc.ConnectServer();
if (result.IsSuccess)
{
ResetAllStates();
Logger.Info("PLC重连成功");
return true;
}
Logger.Warn($"PLC重连失败: {result.Message}");
UpdateConnectionStatusLabel($"重连失败: {result.Message}");
isPLCConnected = false;
return false;
}
return true;
}
});
isPLCConnected = connected;
}
catch (Exception ex)
{
Logger.Error(ex, "PLC连接检查失败");
isPLCConnected = false;
}
finally
{
isPlcConnecting = false;
}
}
private void ResetAllStates()
{
// 重置连接相关状态
consecutiveFailedConnections = 0;
currentConnectionRetryDelay = ConnectionRetryDelay;
lastSuccessfulConnectionTime = DateTime.Now;
isHeartbeatActive = true;
lastHeartbeatTime = DateTime.Now;
isPLCConnected = true;
// 重置采集相关状态
ResetCollectionState();
// 记录连接成功
totalSuccessfulConnections++;
}
private void ResetCollectionState()
{
lock (collectionLock)
{
// 重置采集时间和重试计数
lastTempCollectionTime = DateTime.MinValue;
lastUnitCollectionTime = DateTime.MinValue;
temperatureRetryCount = 0;
unitDataRetryCount = 0;
lastTemperatureCollectionSucceeded = true;
lastUnitDataCollectionSucceeded = true;
// 重新计算采集时间点(关键修复)
ResetCollectionTimes();
// 立即调度下一次采集
ScheduleNextDataCollection();
Logger.Info("数据采集状态已重置");
}
}
private void ResetCollectionTimes()
{
DateTime now = DateTime.Now;
// 温度采集时间点:从当前时间开始计算下一个5分钟间隔
nextTempReadTime = CalculateNextTempTime(now);
// 单位数据采集时间点:从当前时间开始计算下一个整点
nextUnitReadTime = CalculateNextUnitTime(now);
Logger.Info($"重置采集时间: 温度采集 {nextTempReadTime}, 单位数据采集 {nextUnitReadTime}");
}
private void UpdateConnectionStatusLabel(string status)
{
if (connectionStatusLabel.InvokeRequired)
{
connectionStatusLabel.BeginInvoke(new Action(() => UpdateConnectionStatusLabel(status)));
return;
}
connectionStatusLabel.Text = $"连接状态: {status}";
connectionStatusLabel.ForeColor = isPLCConnected ? Color.Green : Color.Red;
}
private void UpdateLastConnectionLabel(string timeText)
{
if (lastConnectionLabel.InvokeRequired)
{
lastConnectionLabel.BeginInvoke(new Action(() => UpdateLastConnectionLabel(timeText)));
return;
}
if (lastSuccessfulConnectionTime != DateTime.MinValue)
{
lastConnectionLabel.Text = $"最后成功连接: {lastSuccessfulConnectionTime:yyyy-MM-dd HH:mm:ss}";
}
else
{
lastConnectionLabel.Text = $"最后成功连接: {timeText}";
}
}
private void UpdateRetryDelayLabel(string delayText)
{
if (retryDelayLabel.InvokeRequired)
{
retryDelayLabel.BeginInvoke(new Action(() => UpdateRetryDelayLabel(delayText)));
return;
}
retryDelayLabel.Text = $"重试延迟: {delayText}";
}
private void UpdateDataCollectionStatusLabel(string status)
{
if (dataCollectionStatusLabel.InvokeRequired)
{
dataCollectionStatusLabel.BeginInvoke(new Action(() => UpdateDataCollectionStatusLabel(status)));
return;
}
dataCollectionStatusLabel.Text = $"数据采集: {status}";
dataCollectionStatusLabel.ForeColor = lastTemperatureCollectionSucceeded && lastUnitDataCollectionSucceeded ?
Color.Green : Color.Orange;
}
private void UpdateLastDataCollectionLabel(string timeText)
{
if (lastDataCollectionLabel.InvokeRequired)
{
lastDataCollectionLabel.BeginInvoke(new Action(() => UpdateLastDataCollectionLabel(timeText)));
return;
}
if (lastSuccessfulDataCollectionTime != DateTime.MinValue)
{
lastDataCollectionLabel.Text = $"最后采集: {lastSuccessfulDataCollectionTime:yyyy-MM-dd HH:mm:ss}";
}
else
{
lastDataCollectionLabel.Text = $"最后采集: {timeText}";
}
}
private async Task SendHeartbeatAsync()
{
if (!isPLCConnected || isShuttingDown || !isHeartbeatActive) return;
try
{
await Task.Run(() =>
{
lock (plcLock)
{
// 读取一个固定点位作为心跳检测
var result = plc.ReadBool("M8000");
if (result.IsSuccess)
{
lastHeartbeatTime = DateTime.Now;
Logger.Debug("心跳包发送成功");
}
else
{
Logger.Warn($"心跳包发送失败: {result.Message}");
isPLCConnected = false;
isHeartbeatActive = false;
UpdateConnectionStatusLabel("心跳检测失败");
}
}
});
}
catch (Exception ex)
{
Logger.Error(ex, "心跳包发送异常");
isPLCConnected = false;
isHeartbeatActive = false;
UpdateConnectionStatusLabel("心跳检测异常");
}
}
private async Task CollectDataAsync()
{
if (isShuttingDown) return;
if (!isPLCConnected)
{
// 重新调度下一次采集
ScheduleNextDataCollection();
return;
}
DateTime now = DateTime.Now;
bool temperatureSucceeded = false;
bool unitDataSucceeded = false;
bool anyCollectionAttempted = false;
bool allCollectionsSucceeded = true;
try
{
UpdateDataCollectionStatusLabel("正在采集...");
// 温度数据采集
if (ShouldCollectTemperature(now))
{
anyCollectionAttempted = true;
temperatureSucceeded = await ReadAndSaveTemperatureDataAsync();
if (temperatureSucceeded)
{
lock (collectionLock)
{
lastTempCollectionTime = now;
temperatureRetryCount = 0; // 重置重试计数
Logger.Info($"温度数据采集完成: {now}");
// 更新下一次温度采集时间
nextTempReadTime = CalculateNextTempTime(now);
Logger.Info($"下一次温度采集时间更新为: {nextTempReadTime}");
}
}
else
{
temperatureRetryCount++;
Logger.Warn($"温度数据采集失败 (重试{temperatureRetryCount}/{MaxRetries}): {now}");
allCollectionsSucceeded = false;
}
}
// 其他数据采集
if (ShouldCollectUnitData(now))
{
anyCollectionAttempted = true;
unitDataSucceeded = await ReadAndSaveOtherUnitDataAsync();
if (unitDataSucceeded)
{
lock (collectionLock)
{
lastUnitCollectionTime = now;
unitDataRetryCount = 0; // 重置重试计数
Logger.Info($"单位数据采集完成: {now}");
// 更新下一次单位数据采集时间
nextUnitReadTime = CalculateNextUnitTime(now);
Logger.Info($"下一次单位数据采集时间更新为: {nextUnitReadTime}");
}
}
else
{
unitDataRetryCount++;
Logger.Warn($"单位数据采集失败 (重试{unitDataRetryCount}/{MaxRetries}): {now}");
allCollectionsSucceeded = false;
}
}
// 记录本次采集结果
lastTemperatureCollectionSucceeded = temperatureSucceeded;
lastUnitDataCollectionSucceeded = unitDataSucceeded;
// 更新成功采集时间
if (anyCollectionAttempted && allCollectionsSucceeded)
{
lastSuccessfulDataCollectionTime = now;
successfulDataCollections++;
}
totalDataCollections++;
}
catch (Exception ex)
{
Logger.Error(ex, "数据采集失败");
allCollectionsSucceeded = false;
}
finally
{
// 根据采集结果动态调整定时器
ScheduleNextDataCollection(temperatureSucceeded, unitDataSucceeded);
// 更新UI状态
UpdateDataCollectionStatusLabel(allCollectionsSucceeded ? "成功" : "部分失败");
UpdateLastDataCollectionLabel(lastSuccessfulDataCollectionTime == DateTime.MinValue ? "从未" :
lastSuccessfulDataCollectionTime.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
// 判断是否应该采集温度数据
private bool ShouldCollectTemperature(DateTime now)
{
lock (collectionLock)
{
// 检查是否到达下一次采集时间
if (now < nextTempReadTime)
return false;
// 计算理论上的上次采集时间
DateTime expectedLastCollection = CalculateExpectedLastCollectionTime(now, TemperatureCollectionInterval);
// 检查是否已经在当前时间间隔内采集过
if (lastTempCollectionTime > expectedLastCollection)
{
Logger.Debug($"跳过温度数据采集: 上次采集时间 {lastTempCollectionTime}, 当前时间 {now}");
return false;
}
return true;
}
}
// 判断是否应该采集单位数据
private bool ShouldCollectUnitData(DateTime now)
{
lock (collectionLock)
{
// 检查是否到达下一次采集时间
if (now < nextUnitReadTime)
return false;
// 计算理论上的上次采集时间
DateTime expectedLastCollection = CalculateExpectedLastCollectionTime(now, UnitDataCollectionInterval);
// 检查是否已经在当前时间间隔内采集过
if (lastUnitCollectionTime > expectedLastCollection)
{
Logger.Debug($"跳过单位数据采集: 上次采集时间 {lastUnitCollectionTime}, 当前时间 {now}");
return false;
}
return true;
}
}
private async Task<bool> ReadAndSaveTemperatureDataAsync()
{
if (isShuttingDown) return false;
try
{
// 批量读取温度数据
var readTasks = new[]
{
ReadPlcDataAsync<short>("D7007", 6), // 当前温度
ReadPlcDataAsync<short>("D7102", 6) // 目标温度
};
await Task.WhenAll(readTasks);
var currentTemps = readTasks[0].Result;
var targetTemps = readTasks[1].Result;
// 批量添加到数据库队列
for (int i = 0; i < 6; i++)
{
if (currentTemps != null && i < currentTemps.Length)
{
double value = currentTemps[i] / 10.0;
EnqueueDbCommand(
"INSERT INTO temperature_data (variable_name, address, value, unit, timestamp) VALUES (@var, @addr, @val, @unit, NOW())",
new { var = $"{i + 1}仓温度当前值", addr = $"D700{i + 7}", val = value, unit = "℃" });
}
if (targetTemps != null && i < targetTemps.Length)
{
double value = targetTemps[i] / 10.0;
EnqueueDbCommand(
"INSERT INTO temperature_data (variable_name, address, value, unit, timestamp) VALUES (@var, @addr, @val, @unit, NOW())",
new { var = $"{i + 1}仓温度目标值", addr = $"D710{i + 2}", val = value, unit = "℃" });
}
}
Logger.Info("温度数据采集完成");
return true;
}
catch (Exception ex)
{
Logger.Error(ex, "温度数据采集失败");
return false;
}
}
private async Task<bool> ReadAndSaveOtherUnitDataAsync()
{
if (isShuttingDown) return false;
try
{
// 使用并行读取提高效率
var tasks = new[]
{
ReadAndSaveUnitDataAsync("D7202", "累计待机时间", "H", 0.1),
ReadAndSaveUnitDataAsync("D7206", "累计运行时间", "H", 0.1),
ReadAndSaveUnitDataAsync("D7210", "累计故障时间", "H", 0.1),
ReadAndSaveUnitDataAsync("D7214", "累计维修时间", "H", 0.1),
ReadAndSaveUnitDataAsync("D7218", "累计保养时间", "H", 0.1),
ReadAndSaveUnitDataAsync("D7222", "累计产量", "片", 1),
ReadAndSaveUnitDataAsync("D7226", "累计用电", "KWH", 1)
};
await Task.WhenAll(tasks);
Logger.Info("单位数据采集完成");
return true;
}
catch (Exception ex)
{
Logger.Error(ex, "单位数据采集失败");
return false;
}
}
private async Task ReadAndSaveUnitDataAsync(string address, string name, string unit, double factor)
{
if (isShuttingDown) return;
var value = await ReadPlcDataAsync<int>(address, 1);
if (value != null && value.Length > 0)
{
double actualValue = value[0] * factor;
EnqueueDbCommand(
"INSERT INTO other_data (variable_name, address, value, unit, timestamp) VALUES (@var, @addr, @val, @unit, NOW())",
new { var = name, addr = address, val = actualValue, unit = unit });
}
}
private async Task CheckAndSaveAlarmDataAsync()
{
if (!isPLCConnected || isShuttingDown) return;
try
{
// 并行读取报警区域
var alarmTasks = new[]
{
ProcessAlarmRegionAsync("D7500.0", 48, new[]
{
("仓高温", 0, 6),
("仓低温", 16, 6),
("仓温度异常", 32, 6),
("仓超温保护", 48, 6),
("仓风压低", 64, 6)
}),
ProcessAlarmRegionAsync("D7513.0", 48, new[]
{
("仓循环风机故障", 0, 6),
("仓发热体故障", 24, 6),
("仓跳闸", 48, 6)
}),
ProcessAlarmRegionAsync("D7521.0", 16, new[]
{
("下位要板超时", 0, 1),
("定位出错", 1, 1),
("前门上升报警", 2, 1)
})
};
await Task.WhenAll(alarmTasks);
}
catch (Exception ex)
{
Logger.Error(ex, "报警信息检查失败");
throw;
}
}
private async Task ProcessAlarmRegionAsync(string startAddress, ushort bitCount, (string, int, int)[] alarmDefinitions)
{
if (isShuttingDown) return;
var alarms = await ReadPlcDataAsync<bool>(startAddress, bitCount);
if (alarms == null) return;
foreach (var (baseName, startBit, count) in alarmDefinitions)
{
for (int i = 0; i < count; i++)
{
int index = startBit + i;
if (index < alarms.Length)
{
string alarmName = count > 1 ? $"{i + 1}{baseName}" : baseName;
string fullAddress = $"{startAddress.Substring(0, startAddress.Length - 1)}{index}";
// 检查报警状态变化
bool currentStatus = alarms[index];
bool previousStatus;
lock (alarmStatusDict)
{
alarmStatusDict.TryGetValue(fullAddress, out previousStatus);
// 状态变化时记录事件
if (currentStatus != previousStatus)
{
alarmStatusDict[fullAddress] = currentStatus;
if (currentStatus)
{
// 报警触发
EnqueueDbCommand(
"INSERT INTO alarm_data (alarm_name, address, status, timestamp) VALUES (@name, @addr, 1, NOW())",
new { name = alarmName, addr = fullAddress });
Logger.Warn($"报警触发: {alarmName} ({fullAddress})");
// 显示托盘通知
ShowAlarmNotification(alarmName);
}
else
{
// 报警恢复
EnqueueDbCommand(
"INSERT INTO alarm_data (alarm_name, address, status, timestamp) VALUES (@name, @addr, 0, NOW())",
new { name = alarmName, addr = fullAddress });
Logger.Info($"报警恢复: {alarmName} ({fullAddress})");
}
}
}
}
}
}
}
private void ShowAlarmNotification(string alarmName)
{
if (trayIcon != null)
{
trayIcon.ShowBalloonTip(
5000,
"低温烤箱报警",
$"报警触发: {alarmName}",
ToolTipIcon.Warning);
}
}
private async Task<T[]> ReadPlcDataAsync<T>(string address, int length) where T : struct
{
if (isShuttingDown) return null;
int retries = 0;
OperateResult<T[]> result = null;
while (retries < MaxConnectionRetries)
{
try
{
lock (plcLock)
{
// 确保连接已建立
if (!isPLCConnected)
{
var connectResult = plc.ConnectServer();
if (!connectResult.IsSuccess)
{
Logger.Error($"PLC连接失败: {connectResult.Message}");
throw new Exception($"PLC连接失败: {connectResult.Message}");
}
isPLCConnected = true;
isHeartbeatActive = true;
lastHeartbeatTime = DateTime.Now;
}
// 根据数据类型选择不同的读取方法
if (typeof(T) == typeof(bool))
{
// 处理位地址读取
if (address.Contains("."))
{
string[] parts = address.Split('.');
string baseAddress = parts[0];
int bitOffset = int.Parse(parts[1]);
// 计算需要读取的字节数
int bytesToRead = (bitOffset + length + 7) / 8;
var byteResult = plc.Read(baseAddress, (ushort)bytesToRead);
if (byteResult.IsSuccess)
{
bool[] bits = new bool[length];
for (int i = 0; i < length; i++)
{
int byteIndex = (bitOffset + i) / 8;
int bitIndex = (bitOffset + i) % 8;
bits[i] = (byteResult.Content[byteIndex] & (1 << bitIndex)) != 0;
}
return bits as dynamic;
}
}
else
{
result = plc.ReadBool(address, (ushort)length) as OperateResult<T[]>;
}
}
else if (typeof(T) == typeof(short))
{
result = plc.ReadInt16(address, (ushort)length) as OperateResult<T[]>;
}
else if (typeof(T) == typeof(int))
{
// 支持批量读取多个int
if (length > 1)
{
result = plc.ReadInt32(address, (ushort)length) as OperateResult<T[]>;
}
else
{
var singleResult = plc.ReadInt32(address);
if (singleResult.IsSuccess)
{
return new[] { singleResult.Content } as dynamic;
}
}
}
else if (typeof(T) == typeof(float))
{
result = plc.ReadFloat(address, (ushort)length) as OperateResult<T[]>;
}
if (result != null && result.IsSuccess)
{
lastHeartbeatTime = DateTime.Now;
return result.Content;
}
Logger.Warn($"PLC数据读取失败: 地址 {address}, 错误: {result?.Message}");
}
}
catch (Exception ex)
{
retries++;
if (retries >= MaxConnectionRetries)
{
Logger.Error(ex, $"PLC数据读取失败: 地址 {address}, 已重试 {retries} 次");
isPLCConnected = false;
isHeartbeatActive = false;
throw;
}
Logger.Warn($"PLC数据读取重试 ({retries}/{MaxConnectionRetries}): {ex.Message}");
await Task.Delay(ConnectionRetryDelay);
}
}
return null;
}
private void EnqueueDbCommand(string sql, object parameters)
{
if (isShuttingDown) return;
// 限制队列大小,防止内存溢出
if (dbCommandQueue.Count >= MaxQueueSize)
{
Logger.Warn("数据库队列已满,丢弃命令: " + sql);
return;
}
// 验证参数类型
if (parameters == null)
{
Logger.Error("数据库命令参数为空: " + sql);
return;
}
// 检查参数属性与SQL参数是否匹配
var paramType = parameters.GetType();
var sqlParams = sql.Split('@').Skip(1).Select(p => p.Split(' ', ',', ')', ';').First()).ToList();
foreach (var sqlParam in sqlParams)
{
if (paramType.GetProperty(sqlParam) == null)
{
Logger.Warn($"SQL参数与对象属性不匹配: @{sqlParam} 在 {paramType.Name} 中未找到");
}
}
dbCommandQueue.Enqueue(new DbCommandData { Sql = sql, Parameters = parameters });
}
private async Task ProcessDbQueueAsync()
{
if (isShuttingDown || dbCommandQueue.IsEmpty) return;
var batch = new List<DbCommandData>();
int dequeueCount = Math.Min(100, dbCommandQueue.Count);
for (int i = 0; i < dequeueCount; i++)
{
if (dbCommandQueue.TryDequeue(out var cmd))
{
batch.Add(cmd);
}
}
if (batch.Count == 0) return;
try
{
using (var conn = new MySqlConnection(connectionString))
{
await conn.OpenAsync();
using (var transaction = await conn.BeginTransactionAsync())
{
try
{
foreach (var cmd in batch)
{
using (var dbCmd = new MySqlCommand(cmd.Sql, conn, transaction))
{
// 使用强类型参数
foreach (var prop in cmd.Parameters.GetType().GetProperties())
{
object value = prop.GetValue(cmd.Parameters);
dbCmd.Parameters.AddWithValue($"@{prop.Name}", value ?? DBNull.Value);
}
await dbCmd.ExecuteNonQueryAsync();
}
}
await transaction.CommitAsync();
Logger.Debug($"成功批量写入数据库: {batch.Count} 条记录");
}
catch (Exception ex)
{
Logger.Error(ex, "数据库事务执行失败,回滚操作");
await transaction.RollbackAsync();
throw;
}
}
}
}
catch (Exception ex)
{
Logger.Error(ex, "数据库批量操作失败");
// 将失败的命令重新加入队列
foreach (var cmd in batch)
{
dbCommandQueue.Enqueue(cmd);
}
}
}
private void UiUpdateTimer_Tick(object sender, EventArgs e)
{
UpdateStatusLabel();
UpdateQueueStatusLabel();
UpdateLastConnectionLabel("");
UpdateLastDataCollectionLabel("");
// 更新系统状态
var systemStatusLabel = Controls.Find("systemStatusLabel", true).FirstOrDefault() as Label;
if (systemStatusLabel != null)
{
if (systemStatusLabel.InvokeRequired)
{
systemStatusLabel.BeginInvoke(new Action(() =>
{
string status = $"系统状态: 正常运行 (连接: {(isPLCConnected ? "已连接" : "未连接")}, 心跳: {(isHeartbeatActive ? "活跃" : "未激活")})";
systemStatusLabel.Text = status;
systemStatusLabel.ForeColor = isPLCConnected ? Color.Green : Color.Red;
}));
}
}
}
private void UpdateStatusLabel()
{
if (statusLabel.InvokeRequired)
{
statusLabel.BeginInvoke(new Action(UpdateStatusLabel));
return;
}
statusLabel.Text = isPLCConnected
? "PLC连接状态: 已连接 (正常采集数据)"
: "PLC连接状态: 未连接 (等待设备开启)";
statusLabel.ForeColor = isPLCConnected ? Color.Green : Color.Red;
if (trayIcon != null)
{
trayIcon.Text = isPLCConnected
? "低温烤箱数据采集程序 - 已连接"
: "低温烤箱数据采集程序 - 未连接";
trayIcon.Icon = isPLCConnected
? GetAppIcon()
: SystemIcons.Warning;
}
}
private void UpdateQueueStatusLabel()
{
var queueLabel = Controls.Find("queueStatusLabel", true).FirstOrDefault() as Label;
if (queueLabel == null) return;
if (queueLabel.InvokeRequired)
{
queueLabel.BeginInvoke(new Action(UpdateQueueStatusLabel));
return;
}
int queueCount = dbCommandQueue.Count;
queueLabel.Text = $"数据库队列: {queueCount}条";
// 根据队列长度设置不同的颜色
if (queueCount > 500)
{
queueLabel.ForeColor = Color.Red;
}
else if (queueCount > 200)
{
queueLabel.ForeColor = Color.Orange;
}
else
{
queueLabel.ForeColor = Color.Gray;
}
}
private void ScheduleNextDataCollection(bool? temperatureSuccess = null, bool? unitDataSuccess = null)
{
if (isShuttingDown) return;
DateTime now = DateTime.Now;
TimeSpan delay = TimeSpan.Zero;
// 如果温度采集失败且在重试次数内
if (temperatureSuccess.HasValue && !temperatureSuccess.Value && temperatureRetryCount < MaxRetries)
{
int retryDelay = RetryDelayBase * (int)Math.Pow(2, temperatureRetryCount);
delay = TimeSpan.FromMilliseconds(retryDelay);
Logger.Info($"温度数据采集失败,{retryDelay / 1000}秒后重试 (第{temperatureRetryCount + 1}/{MaxRetries}次)");
}
// 如果单位数据采集失败且在重试次数内
else if (unitDataSuccess.HasValue && !unitDataSuccess.Value && unitDataRetryCount < MaxRetries)
{
int retryDelay = RetryDelayBase * (int)Math.Pow(2, unitDataRetryCount);
delay = TimeSpan.FromMilliseconds(retryDelay);
Logger.Info($"单位数据采集失败,{retryDelay / 1000}秒后重试 (第{unitDataRetryCount + 1}/{MaxRetries}次)");
}
else
{
// 正常情况下,计算下次采集时间
DateTime nextCollectionTime = DateTime.MaxValue;
// 取温度和单位数据采集时间的最小值
if (nextTempReadTime > now)
{
nextCollectionTime = nextTempReadTime;
}
if (nextUnitReadTime > now && nextUnitReadTime < nextCollectionTime)
{
nextCollectionTime = nextUnitReadTime;
}
delay = nextCollectionTime - now;
// 关键修复:如果下次采集时间超过1小时,说明时间计算可能有问题,重新计算
if (delay.TotalHours > 1)
{
Logger.Warn($"检测到过长的采集延迟: {delay.TotalHours}小时,重新计算采集时间");
ResetCollectionTimes();
ScheduleNextDataCollection();
return;
}
// 确保延迟为正
if (delay.TotalMilliseconds < 0)
{
// 检查是否有应该采集但未采集的数据
bool shouldCollectTemp = ShouldCollectTemperature(now);
bool shouldCollectUnit = ShouldCollectUnitData(now);
if (shouldCollectTemp || shouldCollectUnit)
{
Logger.Info("检测到需要立即采集的数据,触发采集");
delay = TimeSpan.Zero;
}
else
{
// 重新计算采集时间
Logger.Info("检测到数据采集时间计算错误,重新计算采集时间");
ResetCollectionTimes();
ScheduleNextDataCollection();
return;
}
}
}
// 设置定时器
dataCollectionTimer.Interval = Math.Max(1000, delay.TotalMilliseconds); // 最小延迟1秒
dataCollectionTimer.Enabled = true;
Logger.Info($"下次数据采集时间: {DateTime.Now.Add(delay):yyyy-MM-dd HH:mm:ss} (约{delay.TotalSeconds:F2}秒后)");
}
private void Form1_Resize(object sender, EventArgs e)
{
if (WindowState == FormWindowState.Minimized)
{
this.Hide();
//trayIcon.ShowBalloonTip(5000, "低温烤箱数据采集", "程序已最小化到系统托盘", ToolTipIcon.Info);
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
{
// 阻止默认关闭行为
e.Cancel = true;
this.Hide();
//trayIcon.ShowBalloonTip(5000, "低温烤箱数据采集", "程序已最小化到系统托盘", ToolTipIcon.Info);
return;
}
// 其他关闭原因(如应用程序退出)
isShuttingDown = true;
// 停止所有定时器
if (dataCollectionTimer != null) dataCollectionTimer.Stop();
if (connectionCheckTimer != null) connectionCheckTimer.Stop();
if (heartbeatTimer != null) heartbeatTimer.Stop();
if (alarmCheckTimer != null) alarmCheckTimer.Stop();
if (dbBatchTimer != null) dbBatchTimer.Stop();
if (uiUpdateTimer != null) uiUpdateTimer.Stop();
// 关闭PLC连接
try
{
if (plc != null && isPLCConnected)
{
plc.ConnectClose();
Logger.Info("PLC连接已关闭");
}
}
catch (Exception ex)
{
Logger.Error(ex, "关闭PLC连接时出错");
}
// 处理剩余的数据库命令
try
{
if (dbCommandQueue.Count > 0)
{
Logger.Info($"正在处理剩余的 {dbCommandQueue.Count} 条数据库命令...");
ProcessDbQueueAsync().Wait();
Logger.Info("所有数据库命令已处理完毕");
}
}
catch (Exception ex)
{
Logger.Error(ex, "处理剩余数据库命令时出错");
}
// 隐藏托盘图标
if (trayIcon != null)
{
trayIcon.Visible = false;
}
Logger.Info("低温烤箱数据采集系统已关闭");
}
}
// 数据库命令数据类
public class DbCommandData
{
public string Sql { get; set; }
public object Parameters { get; set; }
}
}实现重连后继续读取数据