using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Configuration; using System.Globalization; using System.Linq; using System.Threading; namespace MesWork { public partial class MesWorkForm { private class CurveCollectorState { public string OpName; public long CurveId; public string ProductNo; public Thread WorkerThread; public volatile bool IsRunning; public bool IsSegmentEnded; public bool IsPointValuesSaved; public int SeqNo; public readonly object BufferLock = new object(); public readonly List Buffer = new List(); public decimal? StableWeight; public int? StableStartSeq; public int? StableEndSeq; public DateTime LastCalibrationErrorLogTime; } private static readonly ConcurrentDictionary _curveCollectors = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private static readonly int CurveCollectorSampleIntervalMs = int.TryParse( ConfigurationManager.AppSettings["CurveCollectorSampleIntervalMs"], out int _sampleIntervalMs) ? _sampleIntervalMs : 200; private static readonly int CurveStableWindowSize = int.TryParse( ConfigurationManager.AppSettings["CurveStableWindowSize"], out int _stableWindowSize) ? _stableWindowSize : 20; private static readonly int CurveStableSkipStartPointCount = int.TryParse( ConfigurationManager.AppSettings["CurveStableSkipStartPointCount"], out int _skipStartPointCount) ? _skipStartPointCount : 0; /// /// 允许工作后启动曲线采集,每个工位同一时间只保留一个采集段。 /// private void CurveCollector_Start(string opName, string productNo, string modelNo) { try { if (_curveCollectors.TryGetValue(opName, out CurveCollectorState oldState) && !oldState.IsSegmentEnded) { if (oldState.IsRunning) return; CurveCollector_EndSegment(opName, 0); // 清理上一次未正常结束的曲线 } long curveId = B_DB_Opera.CurveRecord_Start(opName, productNo, modelNo, CurveCollectorSampleIntervalMs); // 先建曲线主记录 if (curveId <= 0) { WeighingPage?.AppendLog($"[{opName}] 曲线记录创建失败,未启动采集"); return; } var state = new CurveCollectorState { OpName = opName, CurveId = curveId, ProductNo = productNo, IsRunning = true }; state.WorkerThread = new Thread(() => CurveCollector_Work(state)) { IsBackground = true, Name = $"CurveCollector_{opName}" }; _curveCollectors[opName] = state; state.WorkerThread.Start(); WeighingPage?.AppendLog($"[{opName}] 实时重量曲线采集启动,记录ID={curveId}"); } catch (Exception err) { WeighingPage?.AppendLog($"[{opName}] 曲线采集启动异常:{err.Message}"); } } /// /// 采集线程:200ms读取一次实时重量,先按工具标定公式计算,再缓存到内存,保存时一次性写库。 /// private void CurveCollector_Work(CurveCollectorState state) { while (state.IsRunning) { try { decimal rawWeight = Convert.ToDecimal(ReadPLC(100220, state.OpName)); // 实时重量点位 if (!WeightCalibrationHelper.Apply(state.OpName, rawWeight, out decimal weight, out _, out string msg)) { if ((DateTime.Now - state.LastCalibrationErrorLogTime).TotalSeconds >= 5) { state.LastCalibrationErrorLogTime = DateTime.Now; WeighingPage?.AppendLog($"[{state.OpName}] 曲线采样标定失败:{msg}"); } Thread.Sleep(CurveCollectorSampleIntervalMs); continue; } lock (state.BufferLock) { state.Buffer.Add(new CurveSamplePoint { SeqNo = ++state.SeqNo, // 序号用于追溯最终取点区间 SampleTime = DateTime.Now, Weight = weight }); } } catch (Exception err) { WeighingPage?.AppendLog($"[{state.OpName}] 曲线采样异常:{err.Message}"); } Thread.Sleep(CurveCollectorSampleIntervalMs); } } /// /// 停止采样线程,但暂不关闭曲线记录,便于保存流程先计算最终重量。 /// private void CurveCollector_StopSampling(string opName, bool waitingSave = false) { if (!_curveCollectors.TryGetValue(opName, out CurveCollectorState state)) return; state.IsRunning = false; if (state.WorkerThread != null && state.WorkerThread.IsAlive) { state.WorkerThread.Join(2000); } if (waitingSave) { CurveCollector_SavePointValues(state, 0, null); // 保存前先把点位字符串落库,供算法读取 } } /// /// 保存完成后关闭曲线记录,并关联称重记录ID。 /// private void CurveCollector_EndSegment(string opName, long weighingRecordId) { if (!_curveCollectors.TryGetValue(opName, out CurveCollectorState state)) return; CurveCollector_StopSampling(opName); if (!state.IsSegmentEnded) { CurveCollector_SavePointValues(state, weighingRecordId, state.StableWeight, state.StableStartSeq, state.StableEndSeq); // 补写称重记录ID和算法依据 state.IsSegmentEnded = true; WeighingPage?.AppendLog($"[{opName}] 实时重量曲线采集结束,记录ID={state.CurveId},点数={state.SeqNo}"); } _curveCollectors.TryRemove(opName, out _); } /// /// 优先用已标定曲线点计算稳定重量;失败时调用方继续使用PLC锁定重量。 /// private bool CurveCollector_TryGetStableWeight(string opName, out decimal weight, out int pointCount, out string msg) { weight = 0; pointCount = 0; msg = "未启动曲线采集"; if (!_curveCollectors.TryGetValue(opName, out CurveCollectorState state)) { return false; } CurveCollector_SavePointValues(state, 0, null); // 确保数据库已有当前点位字符串 if (!B_DB_Opera.CurveRecord_QueryLatest(opName, state.ProductNo, out string pointValues, out _)) { msg = "未查询到当前产品曲线记录"; return false; } List weights = CurveCollector_ParsePointValues(pointValues); // 从数据库点位字符串还原曲线 pointCount = weights.Count; if (CurveCollector_CalculateStableWeight(weights, out weight, out int startSeq, out int endSeq)) { state.StableWeight = weight; state.StableStartSeq = startSeq; // 记录最终采用窗口起点 state.StableEndSeq = endSeq; // 记录最终采用窗口终点 msg = ""; return true; } msg = $"曲线点数不足或无法计算,当前点数={pointCount}"; return false; } private List CurveCollector_GetPoints(CurveCollectorState state) { lock (state.BufferLock) { return state.Buffer.OrderBy(p => p.SeqNo).ToList(); } } private string CurveCollector_BuildPointValues(List points) { return string.Join("|", points.Select(p => p.Weight.ToString("F3", CultureInfo.InvariantCulture))); } private void CurveCollector_SavePointValues(CurveCollectorState state, long weighingRecordId, decimal? finalWeight) { CurveCollector_SavePointValues(state, weighingRecordId, finalWeight, null, null); } private void CurveCollector_SavePointValues(CurveCollectorState state, long weighingRecordId, decimal? finalWeight, int? finalStartSeq, int? finalEndSeq) { if (state.IsPointValuesSaved && weighingRecordId <= 0 && !finalWeight.HasValue) return; List points = CurveCollector_GetPoints(state); string pointValues = CurveCollector_BuildPointValues(points); // 例:50.120|50.115|50.118 B_DB_Opera.CurveRecord_Save(state.CurveId, weighingRecordId, points.Count, pointValues, finalWeight, finalStartSeq, finalEndSeq); state.IsPointValuesSaved = true; } private List CurveCollector_ParsePointValues(string pointValues) { var result = new List(); if (string.IsNullOrWhiteSpace(pointValues)) return result; foreach (string item in pointValues.Split('|')) { if (decimal.TryParse(item, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value)) { result.Add(value); } } return result; } /// /// 跳过前置配置点数后,找连续稳定窗口中极差最小的一段,去掉一个最大值和一个最小值后取平均。 /// private bool CurveCollector_CalculateStableWeight(List weights, out decimal stableWeight, out int startSeq, out int endSeq) { stableWeight = 0; startSeq = 0; endSeq = 0; int skipStartPointCount = Math.Max(0, CurveStableSkipStartPointCount); if (weights == null || weights.Count - skipStartPointCount < CurveStableWindowSize) return false; int bestStart = 0; // 0-based窗口起点,保存时转成1-based序号 decimal bestRange = decimal.MaxValue; for (int i = skipStartPointCount; i <= weights.Count - CurveStableWindowSize; i++) { decimal min = weights[i]; decimal max = weights[i]; for (int j = i + 1; j < i + CurveStableWindowSize; j++) { if (weights[j] < min) min = weights[j]; if (weights[j] > max) max = weights[j]; } decimal range = max - min; if (range <= bestRange) { bestRange = range; bestStart = i; // 极差相同时取靠后窗口,更接近保存时刻 } } List window = weights.Skip(bestStart).Take(CurveStableWindowSize).ToList(); decimal windowMin = window.Min(); decimal windowMax = window.Max(); bool removedMin = false; bool removedMax = false; decimal sum = 0; int count = 0; foreach (decimal value in window) { if (!removedMin && value == windowMin) { removedMin = true; continue; } if (!removedMax && value == windowMax) { removedMax = true; // 只剔除一个最大值,避免过度过滤 continue; } sum += value; count++; } if (count <= 0) return false; stableWeight = Math.Round(sum / count, 3, MidpointRounding.AwayFromZero); // 剩余18点平均值 startSeq = bestStart + 1; // 转为数据库可读的1-based序号 endSeq = bestStart + CurveStableWindowSize; return true; } } }