Files
WC-CZGKJ/SCADA/Funtion/MIS_CurveCollector.cs
2026-05-12 11:14:28 +08:00

282 lines
11 KiB
C#
Raw 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 System;
using System.Collections.Concurrent;
using System.Collections.Generic;
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;
}
private static readonly ConcurrentDictionary<string, CurveCollectorState> _curveCollectors
= new ConcurrentDictionary<string, CurveCollectorState>(StringComparer.OrdinalIgnoreCase);
private const int CurveCollectorSampleIntervalMs = 200;
private const int CurveStableWindowSize = 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 weight = Convert.ToDecimal(ReadPLC(100220, state.OpName)); // 实时重量点位
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;
}
}
}