Files
ACP/UIShare/GlobalVariable/CANSignalBroadcaster.cs
T

244 lines
10 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 Model.Models;
using Prism.Events;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UIShare.PubEvent;
using UIShare.UIViewModel;
namespace UIShare.GlobalVariable
{
/// <summary>
/// CAN 信号广播器(全局单例,支持多作用域):
/// 从所有已注册的 <see cref="CANMonitoringService"/> 的 RealTimeSignals 字典中轮询读取信号值,
/// 按每个已注册作用域的 ConfigurationList 分别映射,向对应作用域广播 HardwareDataReportedEvent。
/// <para>
/// 一拖三场景下 CAN 硬件共享,但各工位的 DBC/ConfigurationList 不同,
/// 因此广播器必须遍历所有已注册作用域的配置分别广播。
/// </para>
/// </summary>
public class CANSignalBroadcaster
{
private readonly IEventAggregator _eventAggregator;
/// <summary>
/// 作用域注册表:scopeName → (SystemConfig, 预构建的 ConfigMap)。
/// 每个工位的 MonitorViewModel 在初始化时调用 <see cref="RegisterScope"/> 注册自己的配置。
/// </summary>
private readonly ConcurrentDictionary<string, (SystemConfig Config, Dictionary<string, CANSignalConfig> ConfigMap)> _scopeRegistry = new();
/// <summary>全局已注册的 CANMonitoringService 实例列表(静态,跨作用域共享)</summary>
private static readonly ConcurrentDictionary<CANMonitoringService, byte> _registeredServices = new();
private CancellationTokenSource? _cts;
private Task? _broadcastTask;
/// <summary>广播轮询间隔(毫秒)</summary>
public int PollingIntervalMs { get; set; } = 100;
public CANSignalBroadcaster(SystemConfig systemConfig, IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
// 构造时自动注册第一个作用域(向后兼容)
RegisterScope(systemConfig.Title, systemConfig);
}
#region 服务注册(供 CANMonitoringService 调用)
/// <summary>注册一个 CANMonitoringService,使其信号纳入广播</summary>
public static void RegisterService(CANMonitoringService service)
{
_registeredServices.TryAdd(service, 0);
}
/// <summary>注销一个 CANMonitoringService</summary>
public static void UnregisterService(CANMonitoringService service)
{
_registeredServices.TryRemove(service, out _);
}
#endregion
#region 启动 / 停止
/// <summary>启动广播(幂等:多次调用只启动一次)</summary>
public void Start()
{
if (_broadcastTask != null && !_broadcastTask.IsCompleted) return;
_cts = new CancellationTokenSource();
_broadcastTask = Task.Run(() => BroadcastLoop(_cts.Token));
}
/// <summary>停止广播</summary>
public void Stop()
{
_cts?.Cancel();
_broadcastTask = null;
}
/// <summary>
/// 注册一个作用域的 CAN 配置。
/// 每个工位在 MonitorViewModel 初始化时调用此方法,将自己的 SystemConfig 注册进来,
/// 广播器会在每轮广播中为该作用域独立映射信号并广播。
/// </summary>
/// <param name="scopeName">作用域名称(SystemConfig.Title</param>
/// <param name="config">该作用域的 SystemConfig</param>
public void RegisterScope(string scopeName, SystemConfig config)
{
if (string.IsNullOrEmpty(scopeName) || config == null) return;
var configMap = BuildConfigMap(config);
_scopeRegistry.AddOrUpdate(scopeName, (config, configMap), (_, _) => (config, configMap));
}
/// <summary>注销一个作用域(工位销毁时调用)</summary>
public void UnregisterScope(string scopeName)
{
if (!string.IsNullOrEmpty(scopeName))
_scopeRegistry.TryRemove(scopeName, out _);
}
/// <summary>兼容旧接口:Discover 已无需执行任何操作</summary>
public void Discover() { }
#endregion
#region 广播核心逻辑
/// <summary>
/// 轮询所有已注册的 CANMonitoringService 的 RealTimeSignals
/// 遍历所有已注册作用域的配置分别映射并广播 HardwareDataReportedEvent。
/// </summary>
private async Task BroadcastLoop(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
// 快照当前作用域注册表,避免枚举期间被修改
var scopeSnapshot = _scopeRegistry.ToArray();
foreach (var scopeEntry in scopeSnapshot)
{
if (ct.IsCancellationRequested) return;
string scopeName = scopeEntry.Key;
var scopeConfig = scopeEntry.Value.Config;
var configMap = scopeEntry.Value.ConfigMap;
foreach (var service in _registeredServices.Keys)
{
if (service.IsStopped) continue;
foreach (var kvp in service.RealTimeSignals)
{
if (ct.IsCancellationRequested) return;
// 信号 Key 格式: "{channel}/{MessageName}/{SignalName}"
if (!TryParseSignalKey(kvp.Key, out int channel, out string? messageName, out string? signalName))
continue;
// 从该作用域的配置映射中查找对应的 MessageID
if (!configMap.TryGetValue($"{channel}/{messageName}/{signalName}", out var cfg))
continue;
string canFingerprint = $"CAN:{channel}";
string fingerprint = BuildFingerprint(canFingerprint, (uint)channel);
string methodName = BuildMethodName((uint)cfg.MessageID, signalName);
// 广播信号值到对应作用域
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
{
Scope = scopeName,
HardwareFingerprint = fingerprint,
MethodName = methodName,
Value = kvp.Value,
Time = DateTime.Now
});
// 报警检查(使用该作用域自己的配置)
string alarmStatus = ValueLimitAlarmHelper.CheckAlarm(fingerprint, methodName, kvp.Value, scopeConfig);
if (!string.IsNullOrEmpty(alarmStatus) && alarmStatus != "未报警")
{
_eventAggregator.GetEvent<AlarmEvent>().Publish((scopeName, canFingerprint, alarmStatus));
}
}
}
}
await Task.Delay(PollingIntervalMs, ct);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Logger.LoggerHelper.Error($"CANSignalBroadcaster 广播异常: {ex.Message}");
await Task.Delay(1000, ct);
}
}
}
/// <summary>
/// 从 SystemConfig.ConfigurationList 构建 "channel/MessageName/SignalName" → CANSignalConfig 的映射
/// </summary>
private static Dictionary<string, CANSignalConfig> BuildConfigMap(SystemConfig systemConfig)
{
var map = new Dictionary<string, CANSignalConfig>(StringComparer.OrdinalIgnoreCase);
if (systemConfig?.ConfigurationList == null) return map;
foreach (var cfg in systemConfig.ConfigurationList)
{
if (string.IsNullOrEmpty(cfg.SignalName) || string.IsNullOrEmpty(cfg.MessageName)) continue;
string key = $"{cfg.Channel}/{cfg.MessageName}/{cfg.SignalName}";
map.TryAdd(key, cfg); // 第一个匹配的优先
}
return map;
}
/// <summary>解析信号 Key: "{channel}/{MessageName}/{SignalName}"</summary>
private static bool TryParseSignalKey(string key, out int channel, out string? messageName, out string? signalName)
{
channel = 0;
messageName = null;
signalName = null;
var parts = key.Split('/');
if (parts.Length < 3) return false;
if (!int.TryParse(parts[0], out channel)) return false;
messageName = parts[1];
signalName = parts[2];
return !string.IsNullOrEmpty(messageName) && !string.IsNullOrEmpty(signalName);
}
#endregion
#region 静态工具方法(保持向后兼容)
/// <summary>生成 CAN 信号的 DisplayName 格式:"{MessageName}.{SignalName}"</summary>
public static string BuildDisplayName(string messageName, string signalName)
{
return $"{messageName}.{signalName}";
}
/// <summary>生成 CAN 信号的 MethodName 格式:"{MessageId:X}.{SignalName}"</summary>
public static string BuildMethodName(uint messageId, string signalName)
{
return $"{messageId:X}.{signalName}";
}
/// <summary>
/// 生成 CAN 信号的 Fingerprint 格式:"{canDeviceFingerprint}:{channel}"
/// canDeviceFingerprint 来自 DeviceManager.ExtractHardwareFingerprint,如 "CAN:0"
/// </summary>
public static string BuildFingerprint(string canDeviceFingerprint, uint channel)
{
return $"{canDeviceFingerprint}:{channel}";
}
#endregion
}
}