299 lines
12 KiB
C#
299 lines
12 KiB
C#
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<CurveSamplePoint> Buffer = new List<CurveSamplePoint>();
|
||
public decimal? StableWeight;
|
||
public int? StableStartSeq;
|
||
public int? StableEndSeq;
|
||
public DateTime LastCalibrationErrorLogTime;
|
||
}
|
||
|
||
private static readonly ConcurrentDictionary<string, CurveCollectorState> _curveCollectors
|
||
= new ConcurrentDictionary<string, CurveCollectorState>(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;
|
||
|
||
/// <summary>
|
||
/// 允许工作后启动曲线采集,每个工位同一时间只保留一个采集段。
|
||
/// </summary>
|
||
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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 采集线程:200ms读取一次实时重量,先按工具标定公式计算,再缓存到内存,保存时一次性写库。
|
||
/// </summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止采样线程,但暂不关闭曲线记录,便于保存流程先计算最终重量。
|
||
/// </summary>
|
||
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); // 保存前先把点位字符串落库,供算法读取
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存完成后关闭曲线记录,并关联称重记录ID。
|
||
/// </summary>
|
||
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 _);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 优先用已标定曲线点计算稳定重量;失败时调用方继续使用PLC锁定重量。
|
||
/// </summary>
|
||
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<decimal> 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<CurveSamplePoint> CurveCollector_GetPoints(CurveCollectorState state)
|
||
{
|
||
lock (state.BufferLock)
|
||
{
|
||
return state.Buffer.OrderBy(p => p.SeqNo).ToList();
|
||
}
|
||
}
|
||
|
||
private string CurveCollector_BuildPointValues(List<CurveSamplePoint> 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<CurveSamplePoint> 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<decimal> CurveCollector_ParsePointValues(string pointValues)
|
||
{
|
||
var result = new List<decimal>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 找连续20点中极差最小的一段,去掉一个最大值和一个最小值后取平均。
|
||
/// </summary>
|
||
private bool CurveCollector_CalculateStableWeight(List<decimal> weights, out decimal stableWeight, out int startSeq, out int endSeq)
|
||
{
|
||
stableWeight = 0;
|
||
startSeq = 0;
|
||
endSeq = 0;
|
||
if (weights == null || weights.Count < CurveStableWindowSize) return false;
|
||
|
||
int bestStart = 0; // 0-based窗口起点,保存时转成1-based序号
|
||
decimal bestRange = decimal.MaxValue;
|
||
for (int i = 0; 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<decimal> 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;
|
||
}
|
||
}
|
||
}
|