508 lines
17 KiB
C#
508 lines
17 KiB
C#
using CapMachine.Model.CANLIN;
|
||
using CapMachine.Wpf.CanDrive;
|
||
using CapMachine.Wpf.CanDrive.ZlgCan;
|
||
using CapMachine.Wpf.Dtos;
|
||
using ImTools;
|
||
using Prism.Mvvm;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace CapMachine.Wpf.Services
|
||
{
|
||
/// <summary>
|
||
/// ZLG CAN/CANFD 驱动服务(共享设备句柄)。
|
||
/// </summary>
|
||
public sealed class ZlgCanDriveService : BindableBase
|
||
{
|
||
private readonly ILogService _log;
|
||
|
||
/// <summary>
|
||
/// 共享的 ZLG 驱动实例(CAN/CANFD/LIN)。
|
||
/// </summary>
|
||
public ZlgCanFd200uDriver Driver { get; }
|
||
|
||
/// <summary>
|
||
/// 当前选中的配置程序(沿用原有 FreeSql 模型)。
|
||
/// </summary>
|
||
public CanLinConfigPro? SelectedCanLinConfigPro { get; set; }
|
||
|
||
/// <summary>
|
||
/// Dbc 消息集合(用于 UI 绑定)。
|
||
/// </summary>
|
||
public ObservableCollection<CanDbcModel> ListCanDbcModel { get; private set; } = new ObservableCollection<CanDbcModel>();
|
||
|
||
private bool _dbcParserState;
|
||
/// <summary>
|
||
/// DBC 解析状态。
|
||
/// </summary>
|
||
public bool DbcParserState
|
||
{
|
||
get { return _dbcParserState; }
|
||
private set { _dbcParserState = value; RaisePropertyChanged(); }
|
||
}
|
||
|
||
private bool _openState;
|
||
/// <summary>
|
||
/// 设备打开状态。
|
||
/// </summary>
|
||
public bool OpenState
|
||
{
|
||
get { return _openState; }
|
||
private set { _openState = value; RaisePropertyChanged(); }
|
||
}
|
||
|
||
private ZlgCanMode _mode;
|
||
/// <summary>
|
||
/// CAN 模式(单选:CAN 或 CANFD)。
|
||
/// </summary>
|
||
public ZlgCanMode Mode
|
||
{
|
||
get { return _mode; }
|
||
set { _mode = value; RaisePropertyChanged(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发送使能(与 UI 的调度表使能语义对齐)。
|
||
/// </summary>
|
||
public bool SchEnable
|
||
{
|
||
get { return Driver.SchEnable; }
|
||
set { Driver.SchEnable = value; RaisePropertyChanged(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否启用事件驱动发送。
|
||
/// </summary>
|
||
public bool IsCycleSend
|
||
{
|
||
get { return Driver.IsCycleSend; }
|
||
set { Driver.IsCycleSend = value; RaisePropertyChanged(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否正在循环接收(对齐 Toomoss:IsCycleRevice)。
|
||
/// </summary>
|
||
public bool IsCycleRevice
|
||
{
|
||
get { return Driver.IsReceiving; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最近是否发送成功(用于 UI 指示)。
|
||
/// </summary>
|
||
public bool IsSendOk
|
||
{
|
||
get { return Driver.IsSendOk; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 最近是否接收成功(用于 UI 指示)。
|
||
/// </summary>
|
||
public bool IsReviceOk
|
||
{
|
||
get { return Driver.IsReviceOk; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 要发送的 CAN 指令数据。
|
||
/// </summary>
|
||
public List<CanCmdData> CmdData { get; } = new List<CanCmdData>();
|
||
|
||
private readonly object _scheduleLock = new object();
|
||
private List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> _scheduleItems = new List<(string, int, int, int)>();
|
||
private CancellationTokenSource? _scheduleCts;
|
||
private Task? _scheduleTask;
|
||
private bool _scheduleUseConfigItems;
|
||
|
||
private CanCmdData? SpeedCanCmdData { get; set; }
|
||
|
||
private uint _deviceIndex = 0;
|
||
private ZlgCanFdChannelOptions _channel0 = new ZlgCanFdChannelOptions();
|
||
|
||
/// <summary>
|
||
/// 构造。
|
||
/// </summary>
|
||
/// <param name="logService">日志服务。</param>
|
||
public ZlgCanDriveService(ILogService logService)
|
||
{
|
||
_log = logService;
|
||
Driver = new ZlgCanFd200uDriver(logService);
|
||
|
||
Driver.PropertyChanged += (_, __) =>
|
||
{
|
||
OpenState = Driver.OpenState;
|
||
RaisePropertyChanged(nameof(IsCycleRevice));
|
||
RaisePropertyChanged(nameof(IsSendOk));
|
||
RaisePropertyChanged(nameof(IsReviceOk));
|
||
};
|
||
|
||
OpenState = Driver.OpenState;
|
||
Mode = ZlgCanMode.Can;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化 CAN 配置信息,并将配置中的名称映射到 DBC 信号集合(用于 UI 显示)。
|
||
/// </summary>
|
||
/// <param name="selectedCanLinConfigPro">选中的配置。</param>
|
||
public void InitCanConfig(CanLinConfigPro selectedCanLinConfigPro)
|
||
{
|
||
SelectedCanLinConfigPro = selectedCanLinConfigPro;
|
||
if (SelectedCanLinConfigPro?.CanLinConfigContents == null) return;
|
||
|
||
foreach (var item in SelectedCanLinConfigPro.CanLinConfigContents)
|
||
{
|
||
var find = ListCanDbcModel.FindFirst(a => a.SignalName == item.SignalName);
|
||
if (find != null)
|
||
{
|
||
find.Name = item.Name;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新配置(从 DTO/DB 同步到驱动)。
|
||
/// </summary>
|
||
/// <param name="deviceIndex">设备索引。</param>
|
||
/// <param name="arbBaudRate">仲裁波特率(bps)。</param>
|
||
/// <param name="dataBaudRate">数据波特率(bps)。</param>
|
||
/// <param name="resEnable">终端电阻使能。</param>
|
||
/// <param name="mergeReceive">是否合并接收。</param>
|
||
/// <param name="mergeReceiveBufferFrames">合并接收缓冲帧数。</param>
|
||
public void UpdateConfig(uint deviceIndex, uint arbBaudRate, uint dataBaudRate, bool resEnable, bool mergeReceive = false, int mergeReceiveBufferFrames = 100)
|
||
{
|
||
_deviceIndex = deviceIndex;
|
||
_channel0.ArbitrationBaudRate = arbBaudRate;
|
||
_channel0.DataBaudRate = dataBaudRate;
|
||
_channel0.EnableInternalResistance = resEnable;
|
||
_channel0.EnableMergeReceive = mergeReceive;
|
||
_channel0.MergeReceiveBufferFrames = mergeReceiveBufferFrames;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开 CAN/CANFD(按 Mode)。
|
||
/// </summary>
|
||
public void StartCanDrive()
|
||
{
|
||
if (OpenState)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Driver.OpenAndInitCan(_deviceIndex, _channel0);
|
||
Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
|
||
OpenState = Driver.OpenState;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使能/停止循环接收。
|
||
/// </summary>
|
||
/// <param name="enable">true=启动接收线程,false=停止接收线程。</param>
|
||
public void SetReceiveEnabled(bool enable)
|
||
{
|
||
if (!OpenState)
|
||
{
|
||
throw new InvalidOperationException("设备未连接,无法切换循环接收。");
|
||
}
|
||
|
||
if (enable)
|
||
{
|
||
if (!Driver.IsReceiving)
|
||
{
|
||
Driver.StartReceiveLoop(_channel0.EnableMergeReceive, _channel0.MergeReceiveBufferFrames);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (Driver.IsReceiving)
|
||
{
|
||
Driver.StopReceiveLoop();
|
||
}
|
||
}
|
||
|
||
RaisePropertyChanged(nameof(IsCycleRevice));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭设备(会同时关闭共享的 LIN 通道)。
|
||
/// </summary>
|
||
public void CloseDevice()
|
||
{
|
||
try
|
||
{
|
||
// Close 语义:关闭时必须停止循环发送与循环接收。
|
||
// - 循环发送:停止软件调度,并关闭事件驱动发送标志。
|
||
// - 循环接收:Driver.Close 内部会 StopReceiveLoop。
|
||
StopSchedule();
|
||
IsCycleSend = false;
|
||
|
||
Driver.Close();
|
||
}
|
||
finally
|
||
{
|
||
OpenState = Driver.OpenState;
|
||
DbcParserState = false;
|
||
}
|
||
}
|
||
|
||
public void SetScheduleConfigs(IEnumerable<CANScheduleConfigDto> configs)
|
||
{
|
||
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANScheduleConfigDto>();
|
||
lock (_scheduleLock)
|
||
{
|
||
_scheduleItems = list
|
||
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
|
||
.ToList();
|
||
}
|
||
}
|
||
|
||
public void SetScheduleConfigs(IEnumerable<CANFdScheduleConfigDto> configs)
|
||
{
|
||
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANFdScheduleConfigDto>();
|
||
lock (_scheduleLock)
|
||
{
|
||
_scheduleItems = list
|
||
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
|
||
.ToList();
|
||
}
|
||
}
|
||
|
||
public void StartSchedule()
|
||
{
|
||
if (!OpenState)
|
||
{
|
||
throw new InvalidOperationException("设备未连接,无法启动调度表。");
|
||
}
|
||
|
||
List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items;
|
||
lock (_scheduleLock)
|
||
{
|
||
items = _scheduleItems.ToList();
|
||
}
|
||
|
||
if (items.Count == 0)
|
||
{
|
||
throw new InvalidOperationException("调度表为空,无法启动调度表。");
|
||
}
|
||
|
||
_scheduleUseConfigItems = true;
|
||
StartSoftwareScheduler(items);
|
||
}
|
||
|
||
public void StartPrecisionCycleSend(int cycleMs)
|
||
{
|
||
if (!OpenState)
|
||
{
|
||
throw new InvalidOperationException("设备未连接,无法启动循环发送。");
|
||
}
|
||
|
||
var ms = Math.Max(1, cycleMs);
|
||
var msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList();
|
||
if (msgNames.Count == 0)
|
||
{
|
||
throw new InvalidOperationException("CmdData 为空,无法启动循环发送。");
|
||
}
|
||
|
||
var items = msgNames.Select(n => (n, ms, 1, 0)).ToList();
|
||
_scheduleUseConfigItems = false;
|
||
StartSoftwareScheduler(items);
|
||
}
|
||
|
||
public void StopSchedule()
|
||
{
|
||
CancellationTokenSource? cts;
|
||
Task? task;
|
||
lock (_scheduleLock)
|
||
{
|
||
cts = _scheduleCts;
|
||
task = _scheduleTask;
|
||
_scheduleCts = null;
|
||
_scheduleTask = null;
|
||
}
|
||
|
||
try
|
||
{
|
||
cts?.Cancel();
|
||
if (task != null)
|
||
{
|
||
task.Wait(TimeSpan.FromSeconds(2));
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
finally
|
||
{
|
||
cts?.Dispose();
|
||
}
|
||
}
|
||
|
||
private void StartSoftwareScheduler(List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items)
|
||
{
|
||
StopSchedule();
|
||
|
||
var cts = new CancellationTokenSource();
|
||
lock (_scheduleLock)
|
||
{
|
||
_scheduleCts = cts;
|
||
}
|
||
|
||
// 统一:软件调度开启后,等同“循环发送开启”
|
||
IsCycleSend = true;
|
||
|
||
_scheduleTask = Task.Run(async () =>
|
||
{
|
||
var token = cts.Token;
|
||
|
||
// next due time for each msg
|
||
var now = DateTime.UtcNow;
|
||
var due = new Dictionary<string, DateTime>(StringComparer.Ordinal);
|
||
var cycle = new Dictionary<string, int>(StringComparer.Ordinal);
|
||
var order = new Dictionary<string, int>(StringComparer.Ordinal);
|
||
foreach (var it in items)
|
||
{
|
||
if (!due.ContainsKey(it.MsgName))
|
||
{
|
||
due[it.MsgName] = now;
|
||
cycle[it.MsgName] = Math.Max(1, it.CycleMs);
|
||
order[it.MsgName] = it.OrderSend;
|
||
}
|
||
}
|
||
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
if (!OpenState)
|
||
{
|
||
await Task.Delay(50, token).ConfigureAwait(false);
|
||
continue;
|
||
}
|
||
|
||
var utcNow = DateTime.UtcNow;
|
||
var minDue = due.Values.Min();
|
||
var delay = minDue - utcNow;
|
||
if (delay > TimeSpan.Zero)
|
||
{
|
||
var ms = (int)Math.Min(delay.TotalMilliseconds, 200);
|
||
await Task.Delay(ms, token).ConfigureAwait(false);
|
||
continue;
|
||
}
|
||
|
||
// due messages
|
||
var ready = due.Where(kv => kv.Value <= utcNow).Select(kv => kv.Key).ToList();
|
||
if (ready.Count == 0)
|
||
{
|
||
await Task.Delay(1, token).ConfigureAwait(false);
|
||
continue;
|
||
}
|
||
|
||
// 顺序/并行:这里只决定同一 tick 内的发送顺序(并行模式仍按字典序依次发)
|
||
ready.Sort(StringComparer.Ordinal);
|
||
foreach (var msg in ready)
|
||
{
|
||
if (token.IsCancellationRequested) break;
|
||
try
|
||
{
|
||
Driver.SendOneMsgByCmdData(msg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Warn($"调度表发送失败:{msg},{ex.Message}");
|
||
}
|
||
finally
|
||
{
|
||
due[msg] = DateTime.UtcNow.AddMilliseconds(cycle[msg]);
|
||
}
|
||
}
|
||
}
|
||
}, cts.Token);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载 DBC。
|
||
/// </summary>
|
||
/// <param name="path">DBC 路径。</param>
|
||
public ObservableCollection<CanDbcModel> StartDbc(string path)
|
||
{
|
||
if (!OpenState)
|
||
{
|
||
throw new InvalidOperationException("请先打开设备后再加载 DBC。");
|
||
}
|
||
|
||
ListCanDbcModel = Driver.StartDbc(path);
|
||
DbcParserState = ListCanDbcModel != null && ListCanDbcModel.Count > 0;
|
||
return ListCanDbcModel;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 加载要发送的数据(订阅数据变化事件)。
|
||
/// </summary>
|
||
/// <param name="cmdData">指令数据集合。</param>
|
||
public void LoadCmdDataToDrive(List<CanCmdData> cmdData)
|
||
{
|
||
var list = cmdData ?? new List<CanCmdData>();
|
||
|
||
CmdData.Clear();
|
||
CmdData.AddRange(list);
|
||
|
||
foreach (var item in CmdData)
|
||
{
|
||
if (string.Equals(item.ConfigName, "转速", StringComparison.Ordinal))
|
||
{
|
||
SpeedCanCmdData = item;
|
||
}
|
||
}
|
||
|
||
Driver.LoadCmdDataToDrive(CmdData, channelIndex: 0, frameType: Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手动发送(目前对齐原服务:仅发送转速)。
|
||
/// </summary>
|
||
/// <param name="speedData">转速。</param>
|
||
public void SendMsgToCanDrive(double speedData)
|
||
{
|
||
if (!OpenState)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (SpeedCanCmdData != null)
|
||
{
|
||
SpeedCanCmdData.SignalCmdValue = speedData;
|
||
}
|
||
else
|
||
{
|
||
_log.Warn("未配置转速指令项(ConfigName=转速),忽略手动发送。");
|
||
}
|
||
|
||
// 若未启用事件驱动发送,则这里主动发送一次(与旧行为兼容)
|
||
if (!IsCycleSend || !SchEnable)
|
||
{
|
||
var firstMsg = CmdData.FirstOrDefault()?.MsgName;
|
||
if (!string.IsNullOrWhiteSpace(firstMsg))
|
||
{
|
||
Driver.SendOneMsgByCmdData(firstMsg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// ZLG CAN 工作模式。
|
||
/// </summary>
|
||
public enum ZlgCanMode
|
||
{
|
||
/// <summary>
|
||
/// CAN 经典帧。
|
||
/// </summary>
|
||
Can = 0,
|
||
/// <summary>
|
||
/// CAN FD。
|
||
/// </summary>
|
||
CanFd = 1
|
||
}
|
||
}
|