前言
工厂里的风机转了好几年没出过毛病,结果某个周五下午轴承突然异响,一停就是大半天。事后大家都在想同一个问题:要是能提前知道就好了。
这就是振动监控的意义。最近用 C# + WinForms + ScottPlot 5.x 写了一套多设备振动监控的小系统,从数据采集、实时绘图到报警和历史回放都走了一遍。过程中踩了一些坑,也摸到了一些门道,记录一下。
运行效果
实时监控界面展示多通道振动曲线:
报警日志和历史回放界面:
多设备数据叠加对比:
为什么选生产者-消费者模式
最开始的想法很简单:定时器读数据,画图,循环。结果压力测试下 UI 卡得没法看。问题在于数据是源源不断进来的,而界面刷新是有上限的,混在一起肯定出问题。
后来改成这套结构:
传感器数据 → 生产者线程 → Channel 缓冲区 → 消费者线程 → 内存缓存
↓
UI 定时器刷新图表
用 System.Threading.Channels 里的 Channel<T> 做缓冲,比 ConcurrentQueue 顺手得多,支持异步等待,不用空转 CPU。
_dataChannel = Channel.CreateBounded<VibrationPacket>(new BoundedChannelOptions(10000)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleWriter = false,
SingleReader = true
});
DropOldest 这个设置是故意选的。工业场景下数据的新鲜度比完整性更重要,缓冲区满了就丢掉最旧的数据,不能让内存撑爆。
数据包结构
public class VibrationPacket
{
public DateTime Timestamp { get; set; }
public string DeviceId { get; set; }
public string Channel { get; set; }
public double Value { get; set; }
}
看着简单,但时间戳有个坑。演示程序用 DateTime.Now 没问题,真要接硬件传感器,必须用设备自带的硬件时间戳。否则多通道数据对齐会出现毫秒级偏差,高频场景下频谱图直接废掉。
信号仿真要真实一点
很多例子的信号仿真就是一个正弦波,太假了。真实旋转机械的振动是基频加多倍频 harmonics 再加随机噪声,偶尔还有冲击。
double fundamental = 5.0 * Math.Sin(2 * Math.PI * baseFreq * t);
double harmonic2x = 2.0 * Math.Sin(2 * Math.PI * baseFreq * 2 * t);
double harmonic3x = 0.8 * Math.Sin(2 * Math.PI * baseFreq * 3 * t);
double noise = (rng.NextDouble() - 0.5) * 1.5;
double impulse = rng.NextDouble() < 0.005
? (rng.NextDouble() > 0.5 ? 1 : -1) * (8 + rng.NextDouble() * 4)
: 0;
0.5% 概率触发冲击,模拟轴承剥落那种突发信号。这样跑起来报警偶尔会触发,演示效果比一条平滑曲线真实得多。
ScottPlot 5.x 的更新方式
ScottPlot 从 4.x 升到 5.x,API 变化不小。最大的坑是 SignalXY 的更新方式——不能像 4.x 那样直接改数组内容了,必须移除旧的 plot 再添加新的:
fpRealtime.Plot.Remove(signalPlot);
var newPlot = fpRealtime.Plot.Add.SignalXY(xs, ys);
newPlot.Color = signalPlot.Color;
newPlot.LineWidth = 1.5f;
newPlot.LegendText = signalPlot.LegendText;
_signalPlots[key] = newPlot;
实测 20Hz 刷新率、3 个通道、3000 个点以内没压力。
滑动窗口用队列实现:
buffer.Enqueue((relTime, packet.Value));
double cutoff = relTime - DISPLAY_TIME_WINDOW;
while (buffer.Count > 0 && buffer.Peek().time < cutoff)
buffer.Dequeue();
30 秒窗口,最多 3000 个点,内存占用可控。
报警用边沿检测
值超阈值就弹窗是最蠢的做法。超标持续一分钟,能弹出一千个窗口。正确做法是只在状态切换的时候触发:
bool wasAlarm = _alarmState.ContainsKey(key) && _alarmState[key];
bool isAlarm = Math.Abs(packet.Value) >= ALARM_THRESHOLD;
_alarmState[key] = isAlarm;
if (isAlarm && !wasAlarm) // 上升沿触发
{
string level = Math.Abs(packet.Value) >= ALARM_THRESHOLD * 1.5
? "紧急" : "报警";
// 记录日志
}
写到 UI 控件时用 BeginInvoke 而不是 Invoke,前者不阻塞调用线程,高频场景下吞吐量差别明显。
BeginInvoke((Action)(() =>
{
AppendAlarmRow(packet, level);
}));
历史回放
实时监控是看当下,回放是复盘。异常发生后,工程师需要倒回去看事发前后的曲线变化。
回放定时器每 50ms 推进一次,支持 0.5x 到 8x 倍速:
double stepPerTick = 0.5 * speedMultiplier;
_playbackTimer.Interval = 50;
_playbackTimer.Tick += (s, e) =>
{
_playbackCursor += stepPerTick;
// 从历史数据中截取窗口段并绘制
};
切换倍速时停掉旧定时器用新步长重启,比改 Interval 更干净。
回放期间实时图不刷新,用 _isPlayback 标志控制,避免两张图同时更新造成混乱。
暗色主题
工厂控制室光线通常偏暗,暗色主题不是花活,是刚需。切换逻辑很简单:
bool isDark = fpRealtime.Plot.FigureBackground.Color.R < 100;
var bg = isDark ? new ScottPlot.Color(245, 245, 245) : new ScottPlot.Color(30, 30, 30);
fpRealtime.Plot.FigureBackground.Color = bg;
fpRealtime.Plot.DataBackground.Color = isDark ? new ScottPlot.Color(255,255,255) : new ScottPlot.Color(45,45,48);
判断当前背景色亮度,切到另一套配色。
CSV 导出
导出功能看似简单,但有个编码问题:
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
如果设备名或通道名里有中文,不指定 UTF-8 的话 Excel 打开大概率乱码。
还可以往哪延伸
目前这套是单机仿真,真要上产线还有几块要补:
数据源对接:Modbus TCP、OPC-UA 或串口协议,替换掉仿真生成器
频域分析:加 FFT 频谱图,通过频率成分诊断不平衡、不对中、轴承磨损等具体故障
报警持久化:报警记录写 SQLite,程序重启不丢
总结
做这套东西最大的收获不是写了多少行代码,而是想明白了数据从哪来、以什么节奏来、UI 能扛多大压力、报警怎么不把人逼疯。这几个问题想清楚了,代码实现反而不是最难的部分。
生产者-消费者解耦、边沿检测、滑动窗口、回放隔离——每个设计背后都有具体的工程场景在驱动,不是为了用某个技术而用。
如果大家也在做类似的数据采集和监控项目,希望这些细节能帮你少踩几个坑。
关键词
振动监控、C#、WinForms、ScottPlot、生产者消费者、实时数据、报警检测、历史回放、工业传感器、Channel、边沿检测、数据可视化、多设备监控
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!
作者:技术老小子
出处:mp.weixin.qq.com/s/8S6eYw06z7xJo_ESRyUymg
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!