using DOAN.Infrastructure.PLC; using DOAN.Model.PBL; using DOAN.ServiceCore.Signalr; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; namespace DOAN.ServiceCore { /// /// 永驻线程 /// 功能:检测传感器信号,判断箱子数 /// public class DoanBackgroundService2 : BaseService, IHostedService, IDisposable { private readonly IHubContext notificationHubContext; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private Task _executingTask; private PLCTool pLCTool; private List storagelocationList = new List(); private List pointPositionList = new List(); private List buttonPositionList = new List(); private Timer refreshTimer; public DoanBackgroundService2(IHubContext hubContext) { notificationHubContext = hubContext; } public async Task StartAsync(CancellationToken cancellationToken) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PCL定时任务开启!"); pLCTool = new PLCTool(); bool isConnected = pLCTool.ConnectPLC(); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PLC初始连接状态: {(isConnected ? "成功" : "失败")}"); // 初始化料架层和点位表数据 await RefreshData(null); // 启动后台任务 _executingTask = ExecuteAsync(_cancellationTokenSource.Token); // 设置定时器每10分钟刷新一次料架层和点位表 .FromMinutes(1) refreshTimer = new Timer(RefreshDataTimerCallback, null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); await (_executingTask.IsCompleted ? _executingTask : Task.CompletedTask); } public async Task StopAsync(CancellationToken cancellationToken) { // 请求取消后台任务 _cancellationTokenSource.Cancel(); // 停止定时器 refreshTimer?.Change(Timeout.Infinite, Timeout.Infinite); refreshTimer?.Dispose(); // 等待后台任务完成 await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)); } private static bool GetInvertedBit(byte b, int position) { if (position < 0 || position > 7) { throw new ArgumentOutOfRangeException( nameof(position), "Position must be between 0 and 7" + position ); } byte mask = (byte)(1 << position); bool isSet = (b & mask) != 0; return !isSet; // 返回取反后的值 } /// /// 功能:检测传感器信号,判断箱子数并更新库存及日志 /// /// 取消令牌 /// private async Task ExecuteAsync(CancellationToken stoppingToken) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PCL定时任务ExecuteAsync!"); try { while (!stoppingToken.IsCancellationRequested) { // 检查PLC连接状态,如果断开则尝试重连 if (!pLCTool.IsConnected()) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 检测到PLC连接断开,尝试重连..."); // 由于PLCTool中的ReconnectPLC是无限重连的,这里不需要额外的延迟 if (!pLCTool.ReconnectPLC()) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PLC重连失败,跳过本次循环"); continue; } Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PLC重连成功"); } // 读取PLC I/O状态(13个字节) byte[] plcSensorValues; try { // 读取1开头100,101。。。等全部地址数据 plcSensorValues = pLCTool.ReadAllValue("VB100", 13); if (plcSensorValues != null) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 成功读取PLC数据,字节数: {plcSensorValues.Length}"); } } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 读取PLC数据异常: {ex.Message}\n堆栈跟踪: {ex.StackTrace}"); continue; } // 读取按钮PLC I/O状态 foreach (PlcButton button in buttonPositionList) { int row = button.Address - 100; int col = button.Index; if ( plcSensorValues != null && row >= 0 && row < plcSensorValues.Length && !GetInvertedBit(plcSensorValues[row], col) ) { // 按钮按下灭灯 if (button.Code == "灭灯按钮") { var offLightstoragelocation = await Context .Queryable() .Where(it => it.Id == button.StoragelocationId) .FirstAsync(); bool isSuccess = pLCTool.WriteBit( offLightstoragelocation.PlcAddress, false ); if (isSuccess) { offLightstoragelocation.IsLight = 0; await Context .Updateable(offLightstoragelocation) .ExecuteCommandAsync(); //灭灯日志 Light_Log light_Log = new Light_Log { Id = SnowFlakeSingle.Instance.NextId().ToString(), LightOperation = 2, LayerNum = offLightstoragelocation.LayerNum, ShelfCode = offLightstoragelocation.RackCode, IsSuccess = isSuccess, Operationer = "按钮手动灭灯", CreatedTime = DateTime.Now }; await Context.Insertable(light_Log).ExecuteCommandAsync(); } await notificationHubContext.Clients.All.SendAsync( "PBL_storagelocation_change", "手动灭灯" ); } // 镜体补料按钮 if (button.Code == "镜体补料按钮") { bool lightStatus = pLCTool.ReadBit("V208.4"); // 原本灭灯,亮灯补料 if (!lightStatus) { AlarmLog alarmLog = new AlarmLog { Id = SnowFlakeSingle.Instance.NextId().ToString(), Name = "镜体需要补料", Code = "镜体补料", StoragelocationId = 0, Type = 2, Status = 1, ActionTime = DateTime.Now, EndTime = DateTime.Now }; await Context.Insertable(alarmLog).ExecuteCommandAsync(); var alarmData = new { RackCode = "镜体需要补料", LayerNum = 1, CurrentPackageCount = 0, AlarmThreshold = 1, ActionTime = DateTime .Now.ToString("yyyy-MM-dd HH:mm:ss") .ToString(), }; string alarmMessage = System.Text.Json.JsonSerializer.Serialize( alarmData ); await notificationHubContext.Clients.All.SendAsync( "PBL_lack_alarm", alarmMessage ); } else { // 原本亮灯,按一下灭灯 pLCTool.WriteBit("V208.4", false); } } } } var updateStoragelocationList = new List(); var inventoryLogs = new List(); foreach (var layerItem in storagelocationList) { // 获取这个料架层的点位表 var layerPoints = await Context .Queryable() .Where(it => it.FkStorageId == layerItem.Id) .ToListAsync(); if (layerPoints.Any()) { int currentPackageCount = 0; // 默认最小值为0 foreach (var point in layerPoints) { int row = point.ByteNum - 100; int col = point.BitNum - 1; if ( plcSensorValues != null && row >= 0 && row < plcSensorValues.Length ) { currentPackageCount += GetInvertedBit(plcSensorValues[row], col) ? 1 : 0; } } // 检查箱子数量变化并记录日志 int previousPackageCount = layerItem.PackageNum.GetValueOrDefault(); if (currentPackageCount > previousPackageCount) { UpdateAndLog( layerItem, currentPackageCount, 2, "出库", inventoryLogs ); updateStoragelocationList.Add(layerItem); } else if (currentPackageCount < previousPackageCount) { UpdateAndLog( layerItem, currentPackageCount, 1, "入库", inventoryLogs ); updateStoragelocationList.Add(layerItem); } // 补料报警触发 if ( layerItem.IsLackAlarm == 1 && currentPackageCount < layerItem.AlarmNum ) { // 是否已经报警过,并且 bool hasLastAlarm = await Context .Queryable() .Where(it => it.StoragelocationId == layerItem.Id) .Where(it => it.Type == 1) .Where(it => it.Status == 1) .Where(it => DateTime.Now <= it.EndTime) .OrderBy(it => it.ActionTime, OrderByType.Desc) .AnyAsync(); if (!hasLastAlarm) { string layerName = layerItem.LayerNum == 1 ? "上层" : "中层"; AlarmLog alarmLog = new AlarmLog { Id = SnowFlakeSingle.Instance.NextId().ToString(), Name = $"镜壳需补料:{layerItem.RackCode} {layerName} 阈值{layerItem.AlarmNum}", Code = "镜壳补料", StoragelocationId = layerItem.Id, Type = 1, Status = 1, ActionTime = DateTime.Now, // 过X分钟超时jishi EndTime = DateTime.Now.AddMinutes(2) }; await Context.Insertable(alarmLog).ExecuteCommandAsync(); var alarmData = new { RackCode = layerItem.RackCode, LayerNum = layerItem.LayerNum, CurrentPackageCount = currentPackageCount, AlarmThreshold = layerItem.AlarmNum, ActionTime = DateTime .Now.ToString("yyyy-MM-dd HH:mm:ss") .ToString(), }; string alarmMessage = System.Text.Json.JsonSerializer.Serialize( alarmData ); await notificationHubContext.Clients.All.SendAsync( "PBL_lack_alarm", alarmMessage ); } } } } // 更新库存 if (updateStoragelocationList.Any()) { await Context .Updateable(updateStoragelocationList) .IgnoreColumns(it => new { it.IsLight, it.IsLackAlarm, it.AlarmNum, it.MaxCapacity }) .ExecuteCommandAsync(); // 发送库存变更Socket通知 string changeMessage = "库存变动"; await notificationHubContext.Clients.All.SendAsync( "PBL_storagelocation_change", changeMessage ); } // 插入库存变更日志 if (inventoryLogs.Any()) { await Context.Insertable(inventoryLogs).ExecuteCommandAsync(); } // 添加延迟以避免频繁查询(暂定3秒防误触) await Task.Delay(2000, stoppingToken); } } catch (OperationCanceledException) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 任务已取消"); } catch (Exception ex) { Console.WriteLine( $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] DoanBackGround线程ExecuteAsync异常: {ex.Message}\n堆栈跟踪: {ex.StackTrace}" ); } } /// /// 刷新料架层和点位表数据 /// /// private async Task RefreshData(object state) { try { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 开始刷新料架点位和按钮数据"); // 检查PLC连接状态,如果断开则尝试重连 if (!pLCTool.IsConnected()) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 检测到PLC连接断开,尝试重连..."); // 由于PLCTool中的ReconnectPLC是无限重连的,这里不需要额外的处理 if (!pLCTool.ReconnectPLC()) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PLC重连失败,但继续执行刷新操作"); } else { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] PLC重连成功"); } } var newStoragelocationList = await Context.Queryable().ToListAsync(); var newPointPositionList = await Context.Queryable().ToListAsync(); var newButtonPositionList = await Context.Queryable().ToListAsync(); // 原子性地更新数据 storagelocationList = newStoragelocationList; pointPositionList = newPointPositionList; buttonPositionList = newButtonPositionList; Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 刷新料架点位和按钮完成: 料架{storagelocationList.Count}个, 点位{pointPositionList.Count}个, 按钮{buttonPositionList.Count}个"); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 刷新数据异常: {ex.Message}\n堆栈跟踪: {ex.StackTrace}"); // 如果刷新失败,保持现有数据不变 } } /// /// 用于Timer回调的适配器方法 /// /// private void RefreshDataTimerCallback(object state) { _ = RefreshData(state); // 不等待异步操作完成,避免阻塞Timer } /// /// 更新料架位置的箱子数量并记录库存日志 /// /// 料架位置对象 /// 新的箱子数量 /// 操作类型 (1-出库, 2-入库) /// 操作员名称 /// 库存日志列表 private void UpdateAndLog( Storagelocation storageLocation, int newPackageCount, int operation, string operatorName, List inventoryLogs ) { storageLocation.PackageNum = newPackageCount; storageLocation.UpdatedBy = operatorName; storageLocation.UpdatedTime = DateTime.Now; var inventoryLog = new Inventorylog { Id = SnowFlakeSingle.Instance.NextId().ToString(), RackCode = storageLocation.RackCode, Partnumber = storageLocation.Partnumber, Operation = operation, PackageNum = Math.Abs( newPackageCount - storageLocation.PackageNum.GetValueOrDefault() ), CreatedBy = operatorName, CreatedTime = DateTime.Now.ToLocalTime() }; inventoryLogs.Add(inventoryLog); } public void Dispose() { try { // 取消令牌源 _cancellationTokenSource.Cancel(); // 等待任务完成,但设置超时以避免死锁 if (_executingTask != null) { Task.WaitAny(_executingTask, Task.Delay(5000)); // 最多等待5秒 } // 释放资源 _cancellationTokenSource.Dispose(); refreshTimer?.Change(Timeout.Infinite, Timeout.Infinite); refreshTimer?.Dispose(); pLCTool?.ConnectClose(); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] DoanBackGround线程Dispose异常: {ex.Message}"); } } } }