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
{
///
/// OP点散热胶GF1500
///
//[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 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(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(plcSetting.intoStation.IntoStationAsk);
if (intoStationAsk > 0)
{
_logger.Info("Processing into station request...");
// 3. 优化:消除 Task.Result 阻塞,改为异步 await,且并行执行两个 PLC 读操作
Task productModelTask = ReadPlcStringAsync(_plcService._plc, plcSetting.intoStation.ProductModel, 48);
Task productSNTask = ReadPlcStringAsync(_plcService._plc, plcSetting.intoStation.ProductSN, 48);
await Task.WhenAll(productModelTask, productSNTask);
string productModel = productModelTask.Result;
string productSN = productSNTask.Result;
string Routingcode = "";
List processOperations = new List();
bool isSkipPermissionCheck = WorkstationCode == "OP051扫码" || WorkstationCode == "OP115";
if (!isSkipPermissionCheck)
{
// 5. 优化:数据库查询传入取消令牌,支持服务快速停止
processOperations = await Context.Queryable()
.LeftJoin((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();
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");
}
}
///
/// 按业务规则解析PLC字符串(支持中文GBK编码)
///
private async Task 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;
}
}
///
/// 校验是否可以进站
///
/// 产品型号
/// 产品SN
///
protected virtual async Task checkEntryPermission(string productModel, string productSN, string workstationCode, List 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()
.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()
.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()
// .Where(it => it.ProductSN == productSN).AnyAsync();
//bool isExistproductSNProducting = await Context.Queryable()
// .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()
.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()
.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", ""); // 替换回车符
}
}
}