Files
sy_hx_pbl_backend/DOAN.ServiceCore/DoanBackgroundService2.cs
2025-09-11 16:22:12 +08:00

478 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using DOAN.Infrastructure.PLC;
using DOAN.Model.PBL;
using DOAN.ServiceCore.Signalr;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
namespace DOAN.ServiceCore
{
/// <summary>
/// 永驻线程
/// 功能:检测传感器信号,判断箱子数
/// </summary>
public class DoanBackgroundService2 : BaseService<Storagelocation>, IHostedService, IDisposable
{
private readonly IHubContext<PBLhub> notificationHubContext;
private readonly CancellationTokenSource _cancellationTokenSource =
new CancellationTokenSource();
private Task _executingTask;
private PLCTool pLCTool;
private List<Storagelocation> storagelocationList = new List<Storagelocation>();
private List<PlcAddressTable> pointPositionList = new List<PlcAddressTable>();
private List<PlcButton> buttonPositionList = new List<PlcButton>();
private Timer refreshTimer;
public DoanBackgroundService2(IHubContext<PBLhub> 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; // 返回取反后的值
}
/// <summary>
/// 功能:检测传感器信号,判断箱子数并更新库存及日志
/// </summary>
/// <param name="stoppingToken">取消令牌</param>
/// <returns></returns>
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<Storagelocation>()
.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<Storagelocation>();
var inventoryLogs = new List<Inventorylog>();
foreach (var layerItem in storagelocationList)
{
// 获取这个料架层的点位表
var layerPoints = await Context
.Queryable<PlcAddressTable>()
.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<AlarmLog>()
.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}"
);
}
}
/// <summary>
/// 刷新料架层和点位表数据
/// </summary>
/// <param name="state"></param>
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<Storagelocation>().ToListAsync();
var newPointPositionList = await Context.Queryable<PlcAddressTable>().ToListAsync();
var newButtonPositionList = await Context.Queryable<PlcButton>().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}");
// 如果刷新失败,保持现有数据不变
}
}
/// <summary>
/// 用于Timer回调的适配器方法
/// </summary>
/// <param name="state"></param>
private void RefreshDataTimerCallback(object state)
{
_ = RefreshData(state); // 不等待异步操作完成避免阻塞Timer
}
/// <summary>
/// 更新料架位置的箱子数量并记录库存日志
/// </summary>
/// <param name="storageLocation">料架位置对象</param>
/// <param name="newPackageCount">新的箱子数量</param>
/// <param name="operation">操作类型 (1-出库, 2-入库)</param>
/// <param name="operatorName">操作员名称</param>
/// <param name="inventoryLogs">库存日志列表</param>
private void UpdateAndLog(
Storagelocation storageLocation,
int newPackageCount,
int operation,
string operatorName,
List<Inventorylog> 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}");
}
}
}
}