2026-02-03 17:28:39 +08:00

384 lines
18 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Infrastructure.Attribute;
using Infrastructure.Extensions;
using Infrastructure.Model;
using MDM.Model.Plant;
using MDM.Model.Process;
using Microsoft.AspNetCore.JsonPatch.Operations;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NLog;
using RIZO.Model.Mes;
using RIZO.Model.MES.product_trace;
using RIZO.Repository;
using RIZO.Service.MES.product.IService;
using RIZO.Service.PLC;
using S7.Net;
using SqlSugar.IOC;
using System;
using System.Runtime.Intrinsics.X86;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RIZO.Service.PLCBackground.Stations.Into
{
/// <summary>
/// OP点散热胶GF1500
/// </summary>
//[AppService(ServiceType = typeof(PlcIntoStationService_Common), ServiceLifetime = LifeTime.Singleton)]
public class PlcIntoStationService_Common : BackgroundService
{
protected static Logger _logger = LogManager.GetCurrentClassLogger();
protected PlcConntectHepler _plcService;
protected readonly TimeSpan _pollingInterval = TimeSpan.FromSeconds(5);
// private readonly string _ipAddress = "192.168.10.222";
// private readonly CpuType _cpuType = CpuType.S71500;
protected string WorkstationCode;
protected readonly SqlSugarClient Context;
protected readonly OptionsSetting _optionsSetting;
protected PlcSettings plcSetting;
public PlcIntoStationService_Common(IOptions<OptionsSetting> options)
{
Context = DbScoped.SugarScope.CopyNew();
_optionsSetting= options.Value;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.Info("PLC Polling Service started");
try
{
// 1. 优化:配置查询增加空值判断,避免未找到配置抛出异常
plcSetting = _optionsSetting.PlcSettings
.FirstOrDefault(it => it.WorkStationCode == WorkstationCode);
if (plcSetting == null)
{
_logger.Error($"未找到工站 {WorkstationCode} 对应的 PLC 配置,服务退出");
return;
}
// 2. 优化:判断 Enum 解析结果,避免无效 PLC 连接
if (!Enum.TryParse<CpuType>(plcSetting.PlcType, out CpuType cpuType))
{
_logger.Error($"工站 {WorkstationCode} 的 PLC 类型 {plcSetting.PlcType} 解析失败,服务退出");
return;
}
// 配置未启用,直接退出
if (!plcSetting.isEnble)
{
_logger.Info($"工站 {WorkstationCode} 的 PLC 配置未启用,服务退出");
return;
}
// 保持原有 using 语法,确保 PLC 连接资源自动释放
using (_plcService = new PlcConntectHepler(plcSetting.IpAddress, cpuType))
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 心跳检测
await _plcService.WriteAsync2(plcSetting.intoStation.Heartbeat, 1);
// 轮询进站请求信号
short intoStationAsk = await _plcService.ReadAsync<short>(plcSetting.intoStation.IntoStationAsk);
if (intoStationAsk > 0)
{
_logger.Info("Processing into station request...");
// 3. 优化:消除 Task.Result 阻塞,改为异步 await且并行执行两个 PLC 读操作
Task<string> productModelTask = ReadPlcStringAsync(_plcService._plc, plcSetting.intoStation.ProductModel, 48);
Task<string> productSNTask = ReadPlcStringAsync(_plcService._plc, plcSetting.intoStation.ProductSN, 48);
await Task.WhenAll(productModelTask, productSNTask);
string productModel = productModelTask.Result;
string productSN = productSNTask.Result;
string Routingcode = "";
List<ProcessOperation> processOperations = new List<ProcessOperation>();
bool isSkipPermissionCheck = WorkstationCode == "OP051扫码" || WorkstationCode == "OP115";
if (!isSkipPermissionCheck)
{
// 5. 优化:数据库查询传入取消令牌,支持服务快速停止
processOperations = await Context.Queryable<ProcessRouting>()
.LeftJoin<ProcessOperation>((r, o) => r.RoutingCode == o.FkRoutingCode)
.Where((r, o) => r.FkProductMaterialCode == productModel && r.Status == 1)
.Select((r, o) => o)
.ToListAsync(stoppingToken);
Routingcode = processOperations?.FirstOrDefault()?.FkRoutingCode ?? "";
}
// 插入入站请求ASK过站记录保留原有业务逻辑
ProductPassStationRecord ASKintoStation = new ProductPassStationRecord
{
ProductSN = productSN,
WorkstationCode = WorkstationCode,
Routingcode = Routingcode,
OperationCode = WorkstationCode,
OperationName = plcSetting.WorkStationName,
ProductionLifeStage = 1, // 1表示生产中
PasstationType = 0, // 0表示入站请求
PasstationDescription = "入站请求ASK",
EffectTime = DateTime.Now,
CreatedTime = DateTime.Now,
};
await Context.Insertable(ASKintoStation).ExecuteCommandAsync(stoppingToken);
if (!isSkipPermissionCheck)
{
// 判断该产品是否允许进站
EntryPermissionResult result = await checkEntryPermission(productModel, productSN, WorkstationCode, processOperations);
// 6. 优化OP050 的多个写操作并行执行,减少 PLC 通信耗时
var plcWriteTasks = new List<Task>();
plcWriteTasks.Add(_plcService.WriteAsync2(plcSetting.intoStation.IntoStationResp, (int)result));
if (WorkstationCode == "OP050")
{
plcWriteTasks.Add(_plcService.WriteAsync2(plcSetting.intoStation.IntoStationResp2, (int)result));
plcWriteTasks.Add(_plcService.WriteAsync2(plcSetting.intoStation.IntoStationResp3, (int)result));
plcWriteTasks.Add(_plcService.WriteAsync2(plcSetting.intoStation.IntoStationResp4, (int)result));
}
// 并行执行所有写操作,提升 PLC 反馈效率
await Task.WhenAll(plcWriteTasks);
}
else
{
// 直接允许进站
await _plcService.WriteAsync2(plcSetting.intoStation.IntoStationResp, 1);
}
}
// 轮询延迟,保留取消令牌,服务停止时无需等待延迟完成
await Task.Delay(_pollingInterval, stoppingToken);
}
catch (OperationCanceledException)
{
_logger.Info("PLC Polling Service is stopping");
break;
}
catch (Exception ex)
{
_logger.Error(ex, "PLC polling error");
await Task.Delay(10, stoppingToken);
}
}
}
}
catch (Exception ex)
{
_logger.Error(ex, "PLC Polling Service encountered fatal error during initialization");
}
finally
{
_logger.Info("PLC Polling Service stopped");
}
}
/// <summary>
/// 按业务规则解析PLC字符串支持中文GBK编码
/// </summary>
private async Task<string> ReadPlcStringAsync(Plc plc, string addr, int maxLen)
{
try
{
// 解析地址DB1010.DBB50 → DB编号1010 + 起始字节50
var parts = addr.Split('.', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
return string.Empty;
if (!int.TryParse(parts[0].Replace("DB", ""), out int dbNum) || dbNum <= 0)
return string.Empty;
if (!int.TryParse(parts[1].Replace("DBB", ""), out int startByte) || startByte < 0)
return string.Empty;
// 读取字节数组
var bytes = await Task.Run(() => plc.ReadBytes(DataType.DataBlock, dbNum, startByte, maxLen));
if (bytes == null || bytes.Length < 3)
return string.Empty;
// 严格按规则解析bytes[0]=总长度(备用)、bytes[1]=有效长度、bytes[2+]=内容
int validLen = Math.Clamp(bytes[1], 0, bytes.Length - 2);
if (validLen <= 0)
return string.Empty;
// 提取有效内容并转字符串GBK编码支持中文
var contentBytes = new byte[validLen];
Array.Copy(bytes, 2, contentBytes, 0, validLen);
return Encoding.GetEncoding("GBK").GetString(contentBytes).Trim('\0', ' ', '\t');
}
catch
{
return string.Empty;
}
}
/// <summary>
/// 校验是否可以进站
/// </summary>
/// <param name="productModel">产品型号</param>
/// <param name="productSN">产品SN</param>
/// <returns></returns>
protected virtual async Task<EntryPermissionResult> checkEntryPermission(string productModel, string productSN, string workstationCode, List<ProcessOperation> processOperations)
{
workstationCode = workstationCode.Trim();
EntryPermissionResult result = EntryPermissionResult.UnkownException;
//2上工位无记录
var LastOperationSeqObject = processOperations?.Where(operation =>
string.Equals(CleanString(operation.OperationCode), CleanString(workstationCode), StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
if (LastOperationSeqObject.LastOperationSeq != 0)
{
ProcessOperation LastOperation = processOperations.Where(it => it.OperationSeq == LastOperationSeqObject.LastOperationSeq).First();
// 2. 清理 OperationCode去除首尾空格 + 移除 \r 等不可见控制字符(兼容你之前的问题)
string cleanOperationCode = LastOperation.OperationCode
.Replace("\r", "")
.Replace("\n", "")
.Trim();
productSN = productSN.Replace("\r", "").Replace("\n", "").Trim();
// 3. 构建并执行真正的异步查询(补充执行方法,使 await 生效)
var ExistLastOperationRecord = await DbScoped.SugarScope.CopyNew()
.Queryable<ProductPassStationRecord>()
.Where(it => CleanString(it.ProductSN) == productSN)
.Where(it => CleanString(it.OperationCode) == cleanOperationCode) // 使用清理后的字符串匹配
// 补充:根据需求选择合适的执行方法(二选一或其他)
.ToListAsync(); // 推荐:返回符合条件的所有数据(列表)
// .FirstOrDefaultAsync(); // 若只需返回第一条数据,用这个(无匹配返回 null
// bool isExistLastOperationRecord = false;
if (ExistLastOperationRecord != null && ExistLastOperationRecord.Count() == 0)
{
return EntryPermissionResult.NoRecordAtPreviousStation;
}
// 3 上工位NG 入站或者出站结果 NG
var ExistLastOperationNG = Context.Queryable<ProductPassStationRecord>()
.Where(it => CleanString(it.ProductSN) == productSN)
.Where(it => CleanString(it.OperationCode) == cleanOperationCode)
.Where(it => it.PasstationType == 3)
.Where(it => it.ResultCode == 1).ToList();
if (ExistLastOperationNG.Count() == 0)
{
result = EntryPermissionResult.PreviousStationNG;
goto InsertPassrecord;
}
// 4 扫码产品型号错误
//bool isExistproductSN = await Context.Queryable<ProductLifecycle>()
// .Where(it => it.ProductSN == productSN).AnyAsync();
//bool isExistproductSNProducting = await Context.Queryable<ProductLifecycle>()
// .Where(it => it.ProductSN == productSN && (it.ProductCurrentStatus != 1 && it.ProductCurrentStatus != 3)).AnyAsync();
//if (!isExistproductSN || !isExistproductSNProducting)
//{
// result = EntryPermissionResult.ProductModelError;
// goto InsertPassrecord;
//}
//6时间超出规定无法生产
//7固化时间未到规定时间
int LastOperationStandardTime = processOperations.Where(operation => operation.OperationCode == LastOperation.OperationCode)
.Select(operation => operation.StandardTime ?? 0).First();
if (LastOperationStandardTime > 0)
{
// 上一站的出站时间 和本站的请求时间差值
DateTime LastInStationTime = await Context.Queryable<ProductPassStationRecord>()
.Where(it => it.ProductSN == productSN)
.Where(it => CleanString(it.OperationCode) == cleanOperationCode)
.Where(it => it.PasstationType == 3)
.MaxAsync(it => it.OutStationTime ?? DateTime.MinValue);
TimeSpan timeDiff = DateTime.Now - LastInStationTime;
double totalSeconds = timeDiff.TotalSeconds;
if (totalSeconds < LastOperationStandardTime)
{
result = EntryPermissionResult.CuringTimeNotReached;
goto InsertPassrecord;
}
}
}
//12 最大重复入站次数
int MaxRepeatEntries = processOperations.Where(operation => operation.OperationCode == workstationCode).Select(operation => operation.MaxStationCount??1).First();
if(MaxRepeatEntries>1)
{
int currentStationEntryCount = await Context.Queryable<ProductPassStationRecord>()
.Where(it => it.ProductSN == productSN)
.Where(it => it.OperationCode == workstationCode)
.Where(it => it.PasstationType == 1)
.CountAsync();
//if(currentStationEntryCount >= MaxRepeatEntries)
//{
// result = EntryPermissionResult.MaximumRepeatedEntries;
// goto InsertPassrecord;
//}
}
//OK 准许进站
result = EntryPermissionResult.AllowIntoStation;
goto InsertPassrecord;
InsertPassrecord:
//插入入站请求Resp过站记录
ProductPassStationRecord RespintoStation = new ProductPassStationRecord
{
ProductSN = productSN,
WorkstationCode = WorkstationCode,
Routingcode = processOperations?.First()?.FkRoutingCode??"",
OperationCode = WorkstationCode,
OperationName = plcSetting.WorkStationName,
ProductionLifeStage = 1, // 1表示生产中
PasstationType = 1, // 入站完毕
PasstationDescription = "入站完毕Resp",
EffectTime = DateTime.Now,
InStationTime = DateTime.Now,
ResultCode = (int)result,
ResultDescription = result.ToString(),
CreatedTime = DateTime.Now,
};
await Context.Insertable(RespintoStation).ExecuteCommandAsync();
return result;
}
private string CleanString(string input)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
// 清洗逻辑:
// 1. Trim():去除前后普通半角空格
// 2. 替换全角空格、制表符、换行符、回车符(常见隐藏空白字符)
return input.Trim()
.Replace(" ", "") // 替换全角空格(中文输入法空格)
.Replace("\t", "") // 替换制表符
.Replace("\n", "") // 替换换行符
.Replace("\r", ""); // 替换回车符
}
}
}