Compare commits

..

6 Commits

Author SHA1 Message Date
hsc
2820b2f300 合并冲突 2026-07-03 17:40:34 +08:00
hsc
cb13d3c8b5 台架移动界面 2026-07-03 14:10:52 +08:00
hsc
60dc155ccf CAN设置添加 2026-07-03 11:57:46 +08:00
hsc
63f43853fa CAN卡添加 2026-07-02 15:06:20 +08:00
hsc
f27045153c 上报数据方式优化 2026-06-30 15:19:07 +08:00
hsc
8f124fb08d 监控数据添加 2026-06-29 09:39:11 +08:00
42 changed files with 4195 additions and 1047 deletions

19
ADP.sln
View File

@@ -35,10 +35,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateInfoMoudle", "UpdateI
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonitorModule", "MonitorModule\MonitorModule.csproj", "{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CAN驱动", "CAN驱动\CAN驱动.csproj", "{D1868672-0132-4B1A-A393-C2CF49A25E74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceEditModule", "DeviceEditModule\DeviceEditModule.csproj", "{170AD4C1-189D-4FBE-B10D-2A4304527834}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZLGUSBCANFD", "ZLGUSBCANFD\ZLGUSBCANFD.csproj", "{542FE380-2344-4343-9AC6-F0C587C13B88}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchMovementModule", "BenchMovementModule\BenchMovementModule.csproj", "{31F84D0E-3634-4C70-A830-21AF412E3BBF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -105,14 +107,18 @@ Global
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Debug|Any CPU.Build.0 = Debug|Any CPU
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Release|Any CPU.ActiveCfg = Release|Any CPU
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Release|Any CPU.Build.0 = Release|Any CPU
{D1868672-0132-4B1A-A393-C2CF49A25E74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D1868672-0132-4B1A-A393-C2CF49A25E74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D1868672-0132-4B1A-A393-C2CF49A25E74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D1868672-0132-4B1A-A393-C2CF49A25E74}.Release|Any CPU.Build.0 = Release|Any CPU
{170AD4C1-189D-4FBE-B10D-2A4304527834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{170AD4C1-189D-4FBE-B10D-2A4304527834}.Debug|Any CPU.Build.0 = Debug|Any CPU
{170AD4C1-189D-4FBE-B10D-2A4304527834}.Release|Any CPU.ActiveCfg = Release|Any CPU
{170AD4C1-189D-4FBE-B10D-2A4304527834}.Release|Any CPU.Build.0 = Release|Any CPU
{542FE380-2344-4343-9AC6-F0C587C13B88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{542FE380-2344-4343-9AC6-F0C587C13B88}.Debug|Any CPU.Build.0 = Debug|Any CPU
{542FE380-2344-4343-9AC6-F0C587C13B88}.Release|Any CPU.ActiveCfg = Release|Any CPU
{542FE380-2344-4343-9AC6-F0C587C13B88}.Release|Any CPU.Build.0 = Release|Any CPU
{31F84D0E-3634-4C70-A830-21AF412E3BBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{31F84D0E-3634-4C70-A830-21AF412E3BBF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31F84D0E-3634-4C70-A830-21AF412E3BBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31F84D0E-3634-4C70-A830-21AF412E3BBF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -125,5 +131,6 @@ Global
{3456C3AC-E7FF-45F8-A68F-BEED9969812C} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{170AD4C1-189D-4FBE-B10D-2A4304527834} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{31F84D0E-3634-4C70-A830-21AF412E3BBF} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
EndGlobalSection
EndGlobal

View File

@@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CAN驱动\CAN驱动.csproj" />
<ProjectReference Include="..\BenchMovementModule\BenchMovementModule.csproj" />
<ProjectReference Include="..\Command\Command.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\DeviceEditModule\DeviceEditModule.csproj" />
@@ -31,6 +31,7 @@
<ProjectReference Include="..\TestingModule\TestingModule.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" />
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -64,10 +64,10 @@ namespace ADP
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
//初始化数据库
//DatabaseConfig.SetTenant(10001);
//DatabaseConfig.InitSqlite("127.0.0.1", 3306, "ADP", "root", "123456");
//DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
//SqlSugarContext.InitDatabase();
DatabaseConfig.SetTenant(10001);
DatabaseConfig.InitSqlite();
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
SqlSugarContext.InitDatabase();
//显示登录窗口
var login = Container.Resolve<LoginModuleView>();
var re = Container.Resolve<IRegionManager>();
@@ -84,6 +84,7 @@ namespace ADP
containerRegistry.RegisterScoped<ScopedContext>();
containerRegistry.RegisterScoped<DeviceManager>();
containerRegistry.RegisterSingleton<GlobalInfo>();
containerRegistry.RegisterSingleton<HardwareDataBroadcaster>();
//注册AutoMapper
var config = new MapperConfiguration(
cfg => cfg.AddProfile<AutoMapperProfile>(),
@@ -100,6 +101,8 @@ namespace ADP
containerRegistry.RegisterInstance<INotificationManager>(NotificationManager);
// 注册仓储
containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>));
//注册服务
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
}
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
protected override IModuleCatalog CreateModuleCatalog()

View File

@@ -762,8 +762,9 @@ namespace ADP.ViewModels
_regionManager.RequestNavigate(settingRegion, "SettingView", settingParameters);
IsLeftDrawerOpen = false;
break;
case "更新界面":
_regionManager.RequestNavigate("ShellViewManager", "UpdateInfoView");
case "台架移动界面":
if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "BenchMovementView");
break;
}
}

View File

@@ -61,11 +61,11 @@
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />
<!--<Button Content="更新界面"
<Button Content="台架移动界面"
Command="{Binding NavigateCommand}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />-->
Margin="8" />
</StackPanel>
</materialDesign:DrawerHost.LeftDrawerContent>
<Grid>

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using BenchMovementModule.Views;
namespace BenchMovementModule
{
public class BenchMovementModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
regionManager.RegisterViewWithRegion("ShellViewManager", typeof(BenchMovementView));
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<BenchMovementView>("BenchMovementView");
}
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\UIShare\UIShare.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,413 @@
using System;
using System.Windows.Input;
using UIShare.ViewModelBase;
namespace BenchMovementModule.ViewModels
{
public class BenchMovementViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
public bool KeepAlive => true;
// --- 1. 台架位置区 ---
private double _posX;
public double PosX
{
get => _posX;
set => SetProperty(ref _posX, value);
}
private double _posY;
public double PosY
{
get => _posY;
set => SetProperty(ref _posY, value);
}
private double _posZ;
public double PosZ
{
get => _posZ;
set => SetProperty(ref _posZ, value);
}
// --- 2. 信号回读区 ---
private double _pos1X;
public double Pos1X
{
get => _pos1X;
set => SetProperty(ref _pos1X, value);
}
private int _pos1Quality;
public int Pos1Quality
{
get => _pos1Quality;
set => SetProperty(ref _pos1Quality, value);
}
private double _pos2X;
public double Pos2X
{
get => _pos2X;
set => SetProperty(ref _pos2X, value);
}
private int _pos2Quality;
public int Pos2Quality
{
get => _pos2Quality;
set => SetProperty(ref _pos2Quality, value);
}
private double _pos1Y;
public double Pos1Y
{
get => _pos1Y;
set => SetProperty(ref _pos1Y, value);
}
private double _pos2Y;
public double Pos2Y
{
get => _pos2Y;
set => SetProperty(ref _pos2Y, value);
}
// --- 3. 扫描参数区 ---
private double _scanStepX = 10;
public double ScanStepX
{
get => _scanStepX;
set => SetProperty(ref _scanStepX, value);
}
private double _scanStartX = -120;
public double ScanStartX
{
get => _scanStartX;
set => SetProperty(ref _scanStartX, value);
}
private double _scanEndX = 120;
public double ScanEndX
{
get => _scanEndX;
set => SetProperty(ref _scanEndX, value);
}
private double _scanStepY = 10;
public double ScanStepY
{
get => _scanStepY;
set => SetProperty(ref _scanStepY, value);
}
private double _scanStartY = -120;
public double ScanStartY
{
get => _scanStartY;
set => SetProperty(ref _scanStartY, value);
}
private double _scanEndY = 120;
public double ScanEndY
{
get => _scanEndY;
set => SetProperty(ref _scanEndY, value);
}
private double _scanStepZ = 1;
public double ScanStepZ
{
get => _scanStepZ;
set => SetProperty(ref _scanStepZ, value);
}
private double _scanStartZ = 0;
public double ScanStartZ
{
get => _scanStartZ;
set => SetProperty(ref _scanStartZ, value);
}
private double _scanEndZ = 0;
public double ScanEndZ
{
get => _scanEndZ;
set => SetProperty(ref _scanEndZ, value);
}
// --- 4. 寸动参数区 ---
private double _jogX;
public double JogX
{
get => _jogX;
set => SetProperty(ref _jogX, value);
}
private double _jogY;
public double JogY
{
get => _jogY;
set => SetProperty(ref _jogY, value);
}
private double _jogZ;
public double JogZ
{
get => _jogZ;
set => SetProperty(ref _jogZ, value);
}
// --- 5. 设置页网络及PLC参数 ---
private string _plcIp = "192.168.0.30";
public string PlcIp
{
get => _plcIp;
set => SetProperty(ref _plcIp, value);
}
private int _plcPort = 502;
public int PlcPort
{
get => _plcPort;
set => SetProperty(ref _plcPort, value);
}
// --- 6. 设置页:速度与零点设定 ---
private double _speedX;
public double SpeedX
{
get => _speedX;
set => SetProperty(ref _speedX, value);
}
private double _speedY;
public double SpeedY
{
get => _speedY;
set => SetProperty(ref _speedY, value);
}
private double _speedZ;
public double SpeedZ
{
get => _speedZ;
set => SetProperty(ref _speedZ, value);
}
private double _zeroX;
public double ZeroX
{
get => _zeroX;
set => SetProperty(ref _zeroX, value);
}
private double _zeroY;
public double ZeroY
{
get => _zeroY;
set => SetProperty(ref _zeroY, value);
}
private double _zeroZ;
public double ZeroZ
{
get => _zeroZ;
set => SetProperty(ref _zeroZ, value);
}
// --- 7. 设置页CAN设定信号映射 ---
private int _canChannel = 0;
public int CanChannel
{
get => _canChannel;
set => SetProperty(ref _canChannel, value);
}
private string _canMsgId = "0X0";
public string CanMsgId
{
get => _canMsgId;
set => SetProperty(ref _canMsgId, value);
}
private string _canMsgName = string.Empty;
public string CanMsgName
{
get => _canMsgName;
set => SetProperty(ref _canMsgName, value);
}
private string _sigPos1X = string.Empty;
public string SigPos1X
{
get => _sigPos1X;
set => SetProperty(ref _sigPos1X, value);
}
private string _sigPos2X = string.Empty;
public string SigPos2X
{
get => _sigPos2X;
set => SetProperty(ref _sigPos2X, value);
}
private string _sigPos1Y = string.Empty;
public string SigPos1Y
{
get => _sigPos1Y;
set => SetProperty(ref _sigPos1Y, value);
}
private string _sigPos2Y = string.Empty;
public string SigPos2Y
{
get => _sigPos2Y;
set => SetProperty(ref _sigPos2Y, value);
}
private string _sigPos1Quality = string.Empty;
public string SigPos1Quality
{
get => _sigPos1Quality;
set => SetProperty(ref _sigPos1Quality, value);
}
private string _sigPos2Quality = string.Empty;
public string SigPos2Quality
{
get => _sigPos2Quality;
set => SetProperty(ref _sigPos2Quality, value);
}
#endregion
#region
// 扫描控制面板命令
public ICommand StartScanCommand { get; }
public ICommand StopScanCommand { get; }
public ICommand GoHomeCommand { get; }
// 寸动执行单轴移动
public ICommand MoveXCommand { get; }
public ICommand MoveYCommand { get; }
public ICommand MoveZCommand { get; }
// 全局控制
public ICommand EmergencyStopCommand { get; }
// 参数设置应用命令
public ICommand ApplyNetworkCommand { get; }
public ICommand SetSpeedCommand { get; }
public ICommand SetZeroCommand { get; }
#endregion
#region
private readonly IContainerProvider _containerProvider;
private bool _isInitiated = false;
private string _testStatus = string.Empty;
#endregion
public BenchMovementViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_containerProvider = containerProvider;
// 初始化扫描控制命令绑定
StartScanCommand = new DelegateCommand(OnStartScan);
StopScanCommand = new DelegateCommand(OnStopScan);
GoHomeCommand = new DelegateCommand(OnGoHome);
// 初始化寸动命令绑定
MoveXCommand = new DelegateCommand(OnMoveX);
MoveYCommand = new DelegateCommand(OnMoveY);
MoveZCommand = new DelegateCommand(OnMoveZ);
// 急停与设置保存
EmergencyStopCommand = new DelegateCommand(OnEmergencyStop);
ApplyNetworkCommand = new DelegateCommand(OnApplyNetwork);
SetSpeedCommand = new DelegateCommand(OnSetSpeed);
SetZeroCommand = new DelegateCommand(OnSetZero);
}
#region
private void OnStartScan()
{
// TODO: 等协议到了处理开始扫描
}
private void OnStopScan()
{
// TODO: 等协议到了处理停止扫描
}
private void OnGoHome()
{
// TODO: 等协议到了处理台架回原点逻辑
}
private void OnMoveX()
{
// TODO: 执行单轴X向绝对位置寸动
}
private void OnMoveY()
{
// TODO: 执行单轴Y向绝对位置寸动
}
private void OnMoveZ()
{
// TODO: 执行单轴Z向绝对位置寸动
}
private void OnEmergencyStop()
{
// TODO: 全局急停高优先级报文/IO发送
}
private void OnApplyNetwork()
{
// TODO: 应用PLC的IP和端口网络连接配置
}
private void OnSetSpeed()
{
// TODO: 发送三轴移动速度设定参数
}
private void OnSetZero()
{
// TODO: 发送台架绝对位置零点校准设定
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!_isInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
_testStatus = navigationContext.Parameters.GetValue<string>("Name");
// TODO: 若后续需要从全局环境 Scope 字典中加载参数配置,可以在此处对齐获取:
// var scope = _globalInfo.ScopeDic[_testStatus];
_isInitiated = true;
}
}
public void Dispose()
{
// 用于界面关闭或Region注销时的资源释放例如解除事件订阅、断开临时Socket通道、清空通信缓冲
}
#endregion
}
}

View File

@@ -0,0 +1,533 @@
<UserControl x:Class="BenchMovementModule.Views.BenchMovementView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1080" d:DesignWidth="1920">
<UserControl.Resources>
<Style TargetType="TextBlock" x:Key="HeaderStyle">
<Setter Property="Foreground" Value="#333333"/>
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Margin" Value="2,2,2,5"/>
</Style>
<Style TargetType="TextBlock" x:Key="LabelStyle">
<Setter Property="Foreground" Value="#555555"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="5"/>
</Style>
<Style TargetType="TextBlock" x:Key="ValueStyle">
<Setter Property="Foreground" Value="#0056B3"/>
<Setter Property="FontSize" Value="26"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="#333333"/>
<Setter Property="BorderBrush" Value="#CCCCCC"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="Height" Value="28"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<!-- 极具机械/按钮交互感的现代工业 Button 样式 -->
<Style TargetType="Button">
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#333333"/>
<Setter Property="BorderBrush" Value="#B8B8B8"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="12,5"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Height" Value="34"/>
<Setter Property="Margin" Value="0,0,0,8"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="btnBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<!-- 鼠标悬停:变亮并加深边框 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#EAEAEA"/>
<Setter TargetName="btnBorder" Property="BorderBrush" Value="#4A90E2"/>
</Trigger>
<!-- 鼠标按下:明显的下凹和变暗交互感 -->
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#DCDCDC"/>
<Setter TargetName="btnBorder" Property="BorderBrush" Value="#2A6099"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 针对特殊控制按钮单独定制的样式 -->
<Style TargetType="Button" x:Key="StopButtonStyle" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#FFF0F0"/>
<Setter Property="Foreground" Value="#C00000"/>
<Setter Property="BorderBrush" Value="#E0A0A0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="btnBorder" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="3">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#FFE0E0"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#F0C0C0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Button" x:Key="EmergencyButtonStyle" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#D9534F"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#D43F3A"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="btnBorder" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#C9302C"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="btnBorder" Property="Background" Value="#AC2925"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="GroupBox">
<Setter Property="BorderBrush" Value="#D0D0D0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="6"/>
<Setter Property="Padding" Value="10"/>
</Style>
</UserControl.Resources>
<!-- 浅灰、偏白色工业背景 -->
<Grid Background="#F0F2F5">
<TabControl Background="Transparent" BorderThickness="0">
<TabControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Background" Value="#E0E4E8"/>
<Setter Property="Foreground" Value="#333333"/>
<Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="25,6"/>
<Setter Property="Margin" Value="2,0,2,0"/>
</Style>
</TabControl.Resources>
<!-- ======================= 1. 主页标签页 ======================= -->
<TabItem Header="标签">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="3*"/>
<RowDefinition Height="2*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 左上:台架位置 & 信号回读 -->
<Grid Grid.Row="0" Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1.5*"/>
</Grid.ColumnDefinitions>
<!-- 台架位置 -->
<GroupBox Grid.Column="0">
<GroupBox.Header>
<TextBlock Text="台架位置" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="X (mm)" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding PosX}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y (mm)" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding PosY}" Style="{StaticResource ValueStyle}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Z (mm)" Style="{StaticResource LabelStyle}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding PosZ}" Style="{StaticResource ValueStyle}"/>
</Grid>
</GroupBox>
<!-- 信号回读 -->
<GroupBox Grid.Column="1">
<GroupBox.Header>
<TextBlock Text="信号回读" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<UniformGrid Columns="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS1_X(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos1X}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS1_Quality: " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos1Quality}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS2_X(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos2X}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS2_Quality: " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos2Quality}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS1_Y(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos1Y}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="POS2_Y(mm): " Style="{StaticResource LabelStyle}"/>
<TextBlock Text="{Binding Pos2Y}" Foreground="#0056B3" FontSize="18" FontWeight="Bold" VerticalAlignment="Center"/>
</StackPanel>
</UniformGrid>
</GroupBox>
</Grid>
<!-- 左中:扫描参数 -->
<GroupBox Grid.Row="1" Grid.Column="0">
<GroupBox.Header>
<TextBlock Text="扫描参数" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- X轴 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="X移动步长(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding ScanStepX}" Margin="5"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="X起始位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding ScanStartX}" Margin="5"/>
<TextBlock Grid.Row="0" Grid.Column="4" Text="X终止位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="5" Text="{Binding ScanEndX}" Margin="5"/>
<!-- Y轴 -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y移动步长(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding ScanStepY}" Margin="5"/>
<TextBlock Grid.Row="1" Grid.Column="2" Text="Y起始位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="3" Text="{Binding ScanStartY}" Margin="5"/>
<TextBlock Grid.Row="1" Grid.Column="4" Text="Y终止位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="5" Text="{Binding ScanEndY}" Margin="5"/>
<!-- Z轴 -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="Z移动步长(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding ScanStepZ}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="2" Text="Z起始位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="3" Text="{Binding ScanStartZ}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="4" Text="Z终止位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="5" Text="{Binding ScanEndZ}" Margin="5"/>
</Grid>
</GroupBox>
<!-- 左下:寸动参数与雷达位置图 -->
<Grid Grid.Row="2" Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 寸动参数 -->
<GroupBox Grid.Column="0">
<GroupBox.Header>
<TextBlock Text="寸动参数" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="X移动位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding JogX}" Margin="5"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y移动位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding JogY}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Z移动位置(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding JogZ}" Margin="5"/>
</Grid>
</GroupBox>
<!-- 坐标示意图 -->
<GroupBox Grid.Column="1">
<GroupBox.Header>
<TextBlock Text="台架现在位置" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid Margin="5" Background="White">
<!-- 十字坐标轴 -->
<Line X1="0" Y1="50" X2="200" Y2="50" Stroke="#B0B0B0" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Height="100"/>
<Line X1="100" Y1="0" X2="100" Y2="100" Stroke="#B0B0B0" StrokeThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Height="100"/>
<!-- 轴向文本标签 -->
<TextBlock Text="X+" HorizontalAlignment="Center" VerticalAlignment="Top" Foreground="#666666" Margin="0,2,0,0"/>
<TextBlock Text="X-" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="#666666" Margin="0,0,0,2"/>
<TextBlock Text="Y+" HorizontalAlignment="Left" VerticalAlignment="Center" Foreground="#666666" Margin="2,0,0,0"/>
<TextBlock Text="Y-" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="#666666" Margin="0,0,2,0"/>
<!-- 台架模拟中心点 -->
<Border Width="65" Height="35" Background="#E9ECEF" BorderBrush="#999999" BorderThickness="1" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="2">
<TextBlock Text="台架位置" Foreground="#333333" FontSize="11" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</GroupBox>
</Grid>
<!-- 右侧控制面板除去Spacing增加Margin防粘连 -->
<Grid Grid.Row="0" Grid.RowSpan="3" Grid.Column="1" Margin="6,6,0,6">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 扫描路径操作 -->
<StackPanel Grid.Row="0">
<Button Content="▶ 开始扫描" Command="{Binding StartScanCommand}"/>
<Button Content="■ 停止扫描" Style="{StaticResource StopButtonStyle}" Command="{Binding StopScanCommand}"/>
<Button Content="↩ 回原点" Command="{Binding GoHomeCommand}"/>
<TextBlock Text="扫描路径" Style="{StaticResource LabelStyle}" Margin="0,10,0,4"/>
<ComboBox Height="30" Background="White" Foreground="#333333" SelectedIndex="0" Margin="0,0,0,8" materialDesign:HintAssist.Hint="">
<ComboBoxItem Content="配置文件1.xml"/>
</ComboBox>
</StackPanel>
<!-- 寸动单轴移动按钮组 -->
<Grid Grid.Row="1">
<StackPanel VerticalAlignment="Center">
<Button Content="↕ X 移动" Command="{Binding MoveXCommand}"/>
<Button Content="↔ Y 移动" Command="{Binding MoveYCommand}"/>
<Button Content="↕ Z 移动" Command="{Binding MoveZCommand}"/>
</StackPanel>
</Grid>
<!-- 急停 -->
<Button Grid.Row="2" Content="❌ 急 停" Style="{StaticResource EmergencyButtonStyle}" FontWeight="Bold" FontSize="18" Height="55" Margin="0" Command="{Binding EmergencyStopCommand}"/>
</Grid>
</Grid>
</TabItem>
<!-- ======================= 2. 设置标签页 ======================= -->
<TabItem Header="设置">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="10">
<!-- 台架接口配置 -->
<GroupBox>
<GroupBox.Header>
<TextBlock Text="台架接口" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<StackPanel Orientation="Horizontal" Margin="5">
<StackPanel Orientation="Horizontal" Margin="0,0,15,0">
<TextBlock Text="IP: " Style="{StaticResource LabelStyle}"/>
<TextBox Width="150" Text="{Binding PlcIp}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,15,0">
<TextBlock Text="端口: " Style="{StaticResource LabelStyle}"/>
<TextBox Width="80" Text="{Binding PlcPort}"/>
</StackPanel>
<Button Content="应 用" Width="90" Margin="0" Command="{Binding ApplyNetworkCommand}"/>
</StackPanel>
</GroupBox>
<!-- 移动速度与零点设定 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 移动速度设定 -->
<GroupBox Grid.Column="0">
<GroupBox.Header>
<TextBlock Text="移动速度设定" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<StackPanel Margin="5">
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="X移动速度(mm/s)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedX}"/>
</Grid>
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y移动速度(mm/s)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedY}"/>
</Grid>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Z移动速度(mm/s)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SpeedZ}"/>
</Grid>
<Button Content="速度设定" Width="110" HorizontalAlignment="Left" Margin="0" Command="{Binding SetSpeedCommand}"/>
</StackPanel>
</GroupBox>
<!-- 零点设定 -->
<GroupBox Grid.Column="1">
<GroupBox.Header>
<TextBlock Text="零点设定" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<StackPanel Margin="5">
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="X零点(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ZeroX}"/>
</Grid>
<Grid Margin="0,0,0,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y零点(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ZeroY}"/>
</Grid>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Z零点(mm)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ZeroZ}"/>
</Grid>
<Button Content="零点设定" Width="110" HorizontalAlignment="Left" Margin="0" Command="{Binding SetZeroCommand}"/>
</StackPanel>
</GroupBox>
</Grid>
<!-- CAN设定 -->
<GroupBox>
<GroupBox.Header>
<TextBlock Text="CAN设定" Style="{StaticResource HeaderStyle}"/>
</GroupBox.Header>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="170"/>
<ColumnDefinition Width="280"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<!-- 通道与模式 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="应用程序CAN通道" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding CanChannel}" Margin="3"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="模式" Style="{StaticResource LabelStyle}" HorizontalAlignment="Center"/>
<ComboBox Grid.Row="0" Grid.Column="3" Height="28" Background="White" Foreground="#333333" SelectedIndex="0" Margin="3" materialDesign:HintAssist.Hint="">
<ComboBoxItem Content="正常模式"/>
</ComboBox>
<!-- 报文ID -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="报文ID(16进制)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding CanMsgId}" Margin="3"/>
<!-- 信号名称列表 -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="报文名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding CanMsgName}" Margin="3"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="POS1_X信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos1X}" Margin="3"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="POS2_X信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos2X}" Margin="3"/>
<TextBlock Grid.Row="5" Grid.Column="0" Text="POS1_Y信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="5" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos1Y}" Margin="3"/>
<TextBlock Grid.Row="6" Grid.Column="0" Text="POS2_Y信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos2Y}" Margin="3"/>
<TextBlock Grid.Row="7" Grid.Column="0" Text="POS1_Quality信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="7" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos1Quality}" Margin="3"/>
<TextBlock Grid.Row="8" Grid.Column="0" Text="POS2_Quality信号名称" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Row="8" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding SigPos2Quality}" Margin="3"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace BenchMovementModule.Views
{
/// <summary>
/// BenchMovementView.xaml 的交互逻辑
/// </summary>
public partial class BenchMovementView : UserControl
{
public BenchMovementView()
{
InitializeComponent();
}
}
}

View File

@@ -1,417 +0,0 @@
using Common.Attributes;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.Json;
using TSMaster;
namespace CAN驱动
{
[ADPCommand]
public class CAN
{
public static event Action ConnectEvent;
public static event Action DisConnectEvent;
public static bool ConnectFlag { get; set; } = false;
/// <summary>
/// 初始化CAN驱动
/// </summary>
/// <param name="ProjectName"></param>
/// <param name="filePath"></param>
/// <returns></returns>
[Browsable(false)]
public static int Init(string ProjectName, string? filePath = null)
{
if (string.IsNullOrEmpty(filePath))
{
return TsMasterApi.initialize_lib_tsmaster(ProjectName);
}
else
{
return TsMasterApi.initialize_lib_tsmaster_with_project(ProjectName, Path.GetFullPath(filePath));
}
}
/// <summary>
/// 释放CAN驱动
/// </summary>
[Browsable(false)]
public static void Release()
{
TsMasterApi.finalize_lib_tsmaster();
}
public static int obj = 0;
public static TCANFDQueueEvent_Win32 listener;
[Browsable(false)]
public static int RegisterListener(TCANFDQueueEvent_Win32 listenEvent)
{
listener = listenEvent;
var re = TsMasterApi.tsapp_register_event_canfd(ref obj, listener);
Debug.Assert(re == 0);
return re;
}
[Browsable(false)]
public static int UnRegisterListener(TCANFDQueueEvent_Win32 listenEvent = null)
{
if (listenEvent is not null)
{
listener = listenEvent;
}
var re = TsMasterApi.tsapp_unregister_event_canfd(ref obj, listener);
Debug.Assert(re == 0);
return re;
}
/// <summary>
/// 连接
/// </summary>
/// <returns></returns>
public static int Connect()
{
var re = TsMasterApi.tsapp_connect();
if (re == 0)
{
re = TsMasterApi.tscom_can_rbs_start();
if (re == 0)
{
ConnectFlag = true;
Task.Run(() => ConnectEvent?.Invoke());
}
else
{
Debug.WriteLine($"CAN_RBS启动失败,错误代码:{re}");
}
}
else
{
Debug.WriteLine($"CAN连接失败,错误代码:{re}");
return re;
}
return re;
}
/// <summary>
/// 断开
/// </summary>
/// <returns></returns>
public static int DisConnect()
{
var re = TsMasterApi.tsapp_disconnect();
//Debug.Assert(re == 0);
if (re == 0)
{
ConnectFlag = false;
Task.Run(() => DisConnectEvent?.Invoke());
}
else
{
Debug.WriteLine($"断开CAN连接失败,错误代码:{re}");
}
return re;
}
/// <summary>
/// 加载DBC
/// </summary>
/// <param name="filePath"></param>
/// <param name="channel"></param>
/// <param name="databaseID"></param>
/// <returns></returns>
[Browsable(false)]
public static int LoadDBC(string filePath, int[] channel, out uint databaseID)
{
databaseID = 0;
var re = TsMasterApi.tsdb_load_can_db(Path.GetFullPath(filePath), string.Join(",", channel), ref databaseID);
foreach (var item in channel)
{
DBCParse.parse(databaseID, item);
DBCParse.rbs_parse(databaseID, item);
}
return re;
}
/// <summary>
/// 卸载DBC
/// </summary>
/// <param name="databaseID"></param>
/// <returns></returns>
[Browsable(false)]
public static int UnLoadDBC(uint? databaseID = null)
{
if (databaseID == null)
{
return TsMasterApi.tsdb_unload_can_dbs();
}
else
{
return TsMasterApi.tsdb_unload_can_db(databaseID.Value);
}
}
/// <summary>
/// 开始记录日志
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
public static int StartLogging(string filePath)
{
return TsMasterApi.tsapp_start_logging(Path.GetFullPath(filePath));
}
/// <summary>
/// 结束记录日志
/// </summary>
/// <returns></returns>
public static int StopLogging()
{
return TsMasterApi.tsapp_stop_logging();
}
/// <summary>
/// 弹出通道映射窗口
/// </summary>
/// <param name="isWait"></param>
/// <returns></returns>
[Browsable(false)]
public static int ShowChannelMappingWindow(bool isWait = false)
{
return TsMasterApi.tsapp_show_tsmaster_window("Hardware", isWait);
}
/// <summary>
/// 获取信号值
/// </summary>
/// <param name="channel"></param>
/// <param name="AMsgName"></param>
/// <param name="ASgnName"></param>
/// <returns></returns>
public static double GetSignalValue(byte channel, string AMsgName, string ASgnName)
{
double value = double.NaN;
var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
var re = TsMasterApi.tsdb_get_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, ref value);
return value;
}
/// <summary>
/// 获取信号值
/// </summary>
/// <param name="channel"></param>
/// <param name="AMsgName"></param>
/// <param name="ASgnName"></param>
/// <param name="value"></param>
/// <returns></returns>
[Browsable(false)]
public static int GetSignalValue(byte channel, string AMsgName, string ASgnName, ref double value)
{
var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
return TsMasterApi.tsdb_get_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, ref value);
}
/// <summary>
/// 获取信号值
/// </summary>
/// <param name="ACANFD"></param>
/// <param name="AMsgName"></param>
/// <param name="ASgnName"></param>
/// <param name="value"></param>
/// <returns></returns>
[Browsable(false)]
public static int GetSignalValue(ref TLIBCANFD ACANFD, string AMsgName, string ASgnName, ref double value)
{
return TsMasterApi.tsdb_get_signal_value_canfd(ref ACANFD, AMsgName, ASgnName, ref value);
}
/// <summary>
/// 设置信号值
/// </summary>
/// <param name="channel"></param>
/// <param name="AMsgName"></param>
/// <param name="ASgnName"></param>
/// <param name="AValue"></param>
/// <param name="isSend"></param>
/// <param name="sendPeriod"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static int SetSignalValue(byte channel, string AMsgName, string ASgnName, double AValue, bool isSend = false, float sendPeriod = 0)
{
var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
find.ACANFD.FIdxChn = channel;
var re = TsMasterApi.tsdb_set_signal_value_canfd(ref find.ACANFD, AMsgName, ASgnName, AValue);
Debug.Assert(re == 0);
if (re != 0) throw new Exception($"设置报文失败!返回代码{re}");
if (re != 0) return re;
if (isSend)
{
if (sendPeriod == 0)
{
return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
}
else
{
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
}
return 0;
}
/// <summary>
/// 设置信号值
/// </summary>
/// <param name="channel"></param>
/// <param name="AMsgName"></param>
/// <param name="sendPeriod"></param>
/// <returns></returns>
public static int SetSignalValue(APP_CHANNEL channel, string AMsgName, float sendPeriod = 0)
{
var find = DBCParse.MsgDatabase[(byte)channel].First(s => s.msg_name == AMsgName);
find.ACANFD.FIdxChn = (byte)channel;
if (sendPeriod == 0)
{
return TsMasterApi.tsapp_transmit_canfd_async(ref find.ACANFD);
}
else
{
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
}
/// <summary>
/// 发送自定义报文
/// </summary>
/// <returns></returns>
[Browsable(false)]
public static int SetMsg(TLIBCANFD msg)
{
return TsMasterApi.tsapp_transmit_canfd_async(ref msg);
}
/// <summary>
/// 设置报文
/// </summary>
/// <param name="channel">通道</param>
/// <param name="ID">DBC数据库ID</param>
/// <param name="bytes">报文数组</param>
public static void SetMsg(APP_CHANNEL channel, int ID, byte[] bytes)
{
var find = DBCParse.MsgDatabase[(byte)channel].FirstOrDefault(s => s.ACANFD.FIdentifier == ID);
if (find != null)
{
Array.Copy(bytes, 0, find.ACANFD.FData, 0, Math.Min(bytes.Length, find.ACANFD.FData.Length));
}
}
/// <summary>
/// 发送自定义报文
/// </summary>
/// <param name="AIdxChn"></param>
/// <param name="AID"></param>
/// <param name="AIsTx"></param>
/// <param name="AIsExt"></param>
/// <param name="AIsRemote"></param>
/// <param name="ADLC"></param>
/// <param name="ADataArray"></param>
/// <param name="AIsFD"></param>
/// <param name="AIsBRS"></param>
/// <param name="isUpdateDBCDatabase"></param>
/// <param name="sendPeriod"></param>
/// <returns></returns>
public static int SendMsg(APP_CHANNEL AIdxChn, int AID, bool AIsTx, bool AIsExt,
bool AIsRemote, byte ADLC, byte[] ADataArray, bool AIsFD = true, bool AIsBRS = false,
bool isUpdateDBCDatabase = false, float sendPeriod = 0)
{
var send = new TLIBCANFD
{
FIdxChn = (byte)AIdxChn,
FProperties = 0,
FIdentifier = AID,
FDLC = ADLC,
FTimeUS = 0uL,
FData = new byte[64],
FFDProperties = 0,
FIsTx = AIsTx,
FIsError = false,
FIsExt = AIsExt,
FIsRemote = AIsRemote,
FIsFD = AIsFD,
FIsBRS = AIsBRS
};
int length = Math.Min(ADataArray.Length, 64);
Array.Copy(ADataArray, 0, send.FData, 0, length);
int re;
if (sendPeriod == 0)
{
re = TsMasterApi.tsapp_transmit_canfd_async(ref send);
}
else
{
re = TsMasterApi.tsapp_add_cyclic_msg_canfd(ref send, sendPeriod);
}
if (isUpdateDBCDatabase)
{
var find = DBCParse.MsgDatabase[(byte)AIdxChn].FirstOrDefault(s => s.ACANFD.FIdentifier == send.FIdentifier);
if (find != null)
{
Array.Copy(send.FData, 0, find.ACANFD.FData, 0, Math.Min(ADataArray.Length, find.ACANFD.FData.Length));
}
}
return re;
}
/// <summary>
/// 添加循环发送报文
/// </summary>
/// <param name="channel"></param>
/// <param name="AMsgName"></param>
/// <param name="sendPeriod"></param>
/// <returns></returns>
public static int AddCyclicMsg(byte channel, string AMsgName, float sendPeriod)
{
var find = DBCParse.MsgDatabase[channel].First(s => s.msg_name == AMsgName);
find.ACANFD.FIdxChn = channel;
return TsMasterApi.tsapp_add_cyclic_msg_canfd(ref find.ACANFD, sendPeriod);
}
/// <summary>
/// 清除循环发送的报文
/// </summary>
/// <returns></returns>
public static int DeleteCyclicMsgs()
{
return TsMasterApi.tsapp_delete_cyclic_msgs();
}
/// <summary>
/// 获取错误提示
/// </summary>
/// <param name="errorCode">错误代码</param>
/// <returns></returns>
public static string GetErrorDescription(int errorCode)
{
IntPtr ADesc = IntPtr.Zero;
TsMasterApi.tsapp_get_error_description(errorCode, ref ADesc);
if (ADesc == IntPtr.Zero) return $"未知错误代码: {errorCode}";
// 假设返回的是 ANSI 字符串(如 C 的 char*
string? description = Marshal.PtrToStringAnsi(ADesc);
return description ?? $"未知错误代码: {errorCode}";
}
}
}

View File

@@ -1,269 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using TSMaster;
namespace CAN驱动
{
public struct can_network
{
[JsonInclude]
public string network_name;
[JsonInclude]
public can_node[] can_nodes;
}
public struct can_node
{
[JsonInclude]
public string node_name;
[JsonInclude]
public can_message[] tx_can_Messages;
[JsonInclude]
public can_message[] rx_can_Messages;
}
public struct can_message
{
[JsonInclude]
public string message_name;
[JsonInclude]
public string[] signals;
}
public class _Msg_
{
public int msg_type { get; set; }
public int msg_id { get; set; }
public int msg_cyclic { get; set; }
public byte msg_dlc { get; set; }
public string msg_name { get; set; }
public string[] signal_Name { get; set; }
public int msg_brs { get; set; }
public TLIBCANFD ACANFD;
}
public enum Msg_Type
{
CAN = 0,
CANFD = 1,
}
/// <summary>
/// DBC 文件信息类型枚举,用于指定 tsdb_get_can_db_info 查询的内容类型。
/// </summary>
public enum DBCINFO
{
/// <summary>网络名称</summary>
NETWORK_Name = 0,
/// <summary>CAN 报文总数</summary>
CAN_MSG_COUNT = 11,
/// <summary>CAN FD 报文总数</summary>
CANFD_MSG_COUNT = 12,
/// <summary>节点总数</summary>
NODE_COUNT = 14,
/// <summary>信号名称</summary>
SIGNAL_NAME = 33,
/// <summary>CAN 报文类型</summary>
CAN_MSG_TYPE = 40,
/// <summary>CAN 报文 DLC数据长度码</summary>
CAN_MSG_DLC = 41,
/// <summary>CAN 报文 ID</summary>
CAN_MSG_ID = 42,
/// <summary>CAN 报文周期</summary>
CAN_MSG_CYCLIC = 43,
/// <summary>CAN 报文名称</summary>
CAN_MSG_NAME = 44,
/// <summary>CAN 报文信号数量</summary>
CAN_MSG_SIGNAL_COUNT = 47,
/// <summary>CAN 报文信号名称索引</summary>
CAN_MSG_SIGNAL_NAME_INDEX = 48,
/// <summary>CAN FD 报文类型</summary>
CANFD_MSG_TYPE = 60,
/// <summary>CAN FD 报文 DLC数据长度码</summary>
CANFD_MSG_DLC = 61,
/// <summary>CAN FD 报文 ID</summary>
CANFD_MSG_ID = 62,
/// <summary>CAN FD 报文周期</summary>
CANFD_MSG_CYCLIC = 63,
/// <summary>CAN FD 报文名称</summary>
CANFD_MSG_NAME = 64,
/// <summary>CAN FD 报文信号数量</summary>
CANFD_MSG_SIGNAL_COUNT = 67,
/// <summary>CAN FD 报文信号名称索引</summary>
CANFD_MSG_SIGNAL_NAME_INDEX = 68,
/// <summary>节点名称</summary>
NODE_Name = 101,
/// <summary>节点发送 CAN 报文数量</summary>
NODE_TX_CAN_COUNT = 103,
/// <summary>节点发送 CAN 报文索引</summary>
NODE_TX_CAN_MESSAGE_INDEX = 104,
/// <summary>节点接收 CAN 报文数量</summary>
NODE_RX_CAN_COUNT = 105,
/// <summary>节点接收 CAN 报文索引</summary>
NODE_RX_CAN_MESSAGE_INDEX = 106,
/// <summary>节点发送 CAN FD 报文数量</summary>
NODE_TX_CANFD_COUNT = 107,
/// <summary>节点发送 CAN FD 报文索引</summary>
NODE_TX_CANFD_MESSAGE_INDEX = 108,
/// <summary>节点接收 CAN FD 报文数量</summary>
NODE_RX_CANFD_COUNT = 109,
/// <summary>节点接收 CAN FD 报文索引</summary>
NODE_RX_CANFD_MESSAGE_INDEX = 110,
}
public class DBCParse
{
public static List<List<_Msg_>> MsgDatabase { get; set; } =
[.. Enumerable.Range(0, 12).Select(_ => new List<_Msg_>())];
public static can_network can_Network;
public static string tsdb_get_can_db_info(uint ADatabaseId, int AType, int AIndex, int ASubIndex)
{
nint tmpInt = new nint();
int ret = TsMasterApi.tsdb_get_can_db_info(ADatabaseId, AType, AIndex, ASubIndex, ref tmpInt);
if (ret == 0)
{
return Marshal.PtrToStringAnsi(tmpInt) ?? "";
}
else
return "";
}
public static void parse(uint Aid, int channel)
{
int CANcount = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_COUNT, 0, 0));
for (int i = 0; i < CANcount; i++)
{
_Msg_ temp = new _Msg_();
temp.msg_type = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_TYPE, i, 0));
temp.msg_dlc = byte.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_DLC, i, 0));
temp.msg_id = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_ID, i, 0));
temp.msg_name = tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_NAME, i, 0);
temp.msg_cyclic = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_CYCLIC, i, 0));
temp.signal_Name = new string[int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_SIGNAL_COUNT, i, 0))];
for (int signalcount = 0; signalcount < temp.signal_Name.Length; signalcount++)
{
temp.signal_Name[signalcount] = tsdb_get_can_db_info(Aid, (int)DBCINFO.SIGNAL_NAME, int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CAN_MSG_SIGNAL_NAME_INDEX, i, signalcount)), 0);
}
temp.msg_brs = 0;
temp.ACANFD = new TLIBCANFD(APP_CHANNEL.CHN1,
temp.msg_id,
true,
temp.msg_type == 1 || temp.msg_type == 4 ? true : false,
false, temp.msg_dlc,
temp.msg_type < 2 ? false : true,
false);
MsgDatabase[channel].Add(temp);
}
CANcount = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_COUNT, 0, 0));
for (int i = 0; i < CANcount; i++)
{
_Msg_ temp = new _Msg_();
temp.msg_type = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_TYPE, i, 0));
temp.msg_dlc = byte.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_DLC, i, 0));
temp.msg_id = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_ID, i, 0));
temp.msg_name = tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_NAME, i, 0);
temp.msg_cyclic = int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_CYCLIC, i, 0));
temp.signal_Name = new string[int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_SIGNAL_COUNT, i, 0))];
for (int signalcount = 0; signalcount < temp.signal_Name.Length; signalcount++)
{
temp.signal_Name[signalcount] = tsdb_get_can_db_info(Aid, (int)DBCINFO.SIGNAL_NAME, int.Parse(tsdb_get_can_db_info(Aid, (int)DBCINFO.CANFD_MSG_SIGNAL_NAME_INDEX, i, signalcount)), 0);
}
temp.msg_brs = int.Parse(tsdb_get_can_db_info(Aid, 69, i, 0));
temp.ACANFD = new TLIBCANFD(APP_CHANNEL.CHN1, temp.msg_id, true, temp.msg_type == 1 || temp.msg_type == 4 ? true : false, false, temp.msg_dlc, temp.msg_type < 2 ? false : true, temp.msg_brs == 1 ? true : false);
MsgDatabase[channel].Add(temp);
}
MsgDatabase[channel].Sort((a, b) => a.msg_id.CompareTo(b.msg_id));
}
public static void rbs_parse(uint AID, int channel)
{
can_Network = new can_network();
can_Network.network_name = tsdb_get_can_db_info(AID, (int)DBCINFO.NETWORK_Name, 0, 0);
int Node_count = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_COUNT, 0, 0));
can_Network.can_nodes = new can_node[Node_count];
for (int i = 0; i < Node_count; i++)
{
can_Network.can_nodes[i].node_name = tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_Name, i, 0);
int tx_can_count = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_TX_CAN_COUNT, i, 0));
int tx_canfd_count = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_TX_CANFD_COUNT, i, 0));
int rx_can_count = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_RX_CAN_COUNT, i, 0));
int rx_canfd_count = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_RX_CANFD_COUNT, i, 0));
//获取tx_can结构体
can_Network.can_nodes[i].tx_can_Messages = new can_message[tx_can_count + tx_canfd_count];
can_Network.can_nodes[i].rx_can_Messages = new can_message[rx_can_count + rx_canfd_count];
//获取报文名以及信号名
//1 get can_message idnex
//2 get can message name
//3 get signal count
//4 get signal name
int tx_index = -1;
for (int tx_can = 0; tx_can < tx_can_count; tx_can++)
{
int can_message_idnex = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_TX_CAN_MESSAGE_INDEX, i, tx_can));
can_Network.can_nodes[i].tx_can_Messages[tx_can].message_name = tsdb_get_can_db_info(AID, (int)DBCINFO.CAN_MSG_NAME, can_message_idnex, 0);
can_Network.can_nodes[i].tx_can_Messages[tx_can].signals = new string[int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CAN_MSG_SIGNAL_COUNT, can_message_idnex, 0))];
for (int signalcount = 0; signalcount < can_Network.can_nodes[i].tx_can_Messages[tx_can].signals.Length; signalcount++)
{
can_Network.can_nodes[i].tx_can_Messages[tx_can].signals[signalcount] = tsdb_get_can_db_info(AID, (int)DBCINFO.SIGNAL_NAME, int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CAN_MSG_SIGNAL_NAME_INDEX, can_message_idnex, signalcount)), 0);
}
tx_index = tx_can;
}
for (int tx_can = 0; tx_can < tx_canfd_count; tx_can++, tx_index++)
{
int can_message_idnex = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_TX_CANFD_MESSAGE_INDEX, i, tx_can));
can_Network.can_nodes[i].tx_can_Messages[tx_index + 1].message_name = tsdb_get_can_db_info(AID, (int)DBCINFO.CANFD_MSG_NAME, can_message_idnex, 0);
can_Network.can_nodes[i].tx_can_Messages[tx_index + 1].signals = new string[int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CANFD_MSG_SIGNAL_COUNT, can_message_idnex, 0))];
for (int signalcount = 0; signalcount < can_Network.can_nodes[i].tx_can_Messages[tx_index + 1].signals.Length; signalcount++)
{
can_Network.can_nodes[i].tx_can_Messages[tx_index + 1].signals[signalcount] = tsdb_get_can_db_info(AID, (int)DBCINFO.SIGNAL_NAME, int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CANFD_MSG_SIGNAL_NAME_INDEX, can_message_idnex, signalcount)), 0);
}
}
int rx_index = -1;
for (int tx_can = 0; tx_can < rx_can_count; tx_can++)
{
int can_message_idnex = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_RX_CAN_MESSAGE_INDEX, i, tx_can));
can_Network.can_nodes[i].rx_can_Messages[tx_can].message_name = tsdb_get_can_db_info(AID, (int)DBCINFO.CAN_MSG_NAME, can_message_idnex, 0);
can_Network.can_nodes[i].rx_can_Messages[tx_can].signals = new string[int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CAN_MSG_SIGNAL_COUNT, can_message_idnex, 0))];
for (int signalcount = 0; signalcount < can_Network.can_nodes[i].rx_can_Messages[tx_can].signals.Length; signalcount++)
{
can_Network.can_nodes[i].rx_can_Messages[tx_can].signals[signalcount] = tsdb_get_can_db_info(AID, (int)DBCINFO.SIGNAL_NAME, int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CAN_MSG_SIGNAL_NAME_INDEX, can_message_idnex, signalcount)), 0);
}
rx_index = tx_can;
}
for (int tx_can = 0; tx_can < rx_canfd_count; tx_can++, rx_index++)
{
int can_message_idnex = int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.NODE_RX_CANFD_MESSAGE_INDEX, i, tx_can));
can_Network.can_nodes[i].rx_can_Messages[rx_index + 1].message_name = tsdb_get_can_db_info(AID, (int)DBCINFO.CANFD_MSG_NAME, can_message_idnex, 0);
can_Network.can_nodes[i].rx_can_Messages[rx_index + 1].signals = new string[int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CANFD_MSG_SIGNAL_COUNT, can_message_idnex, 0))];
for (int signalcount = 0; signalcount < can_Network.can_nodes[i].rx_can_Messages[rx_index + 1].signals.Length; signalcount++)
{
can_Network.can_nodes[i].rx_can_Messages[rx_index + 1].signals[signalcount] = tsdb_get_can_db_info(AID, (int)DBCINFO.SIGNAL_NAME, int.Parse(tsdb_get_can_db_info(AID, (int)DBCINFO.CANFD_MSG_SIGNAL_NAME_INDEX, can_message_idnex, signalcount)), 0);
}
}
}
}
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -172,7 +172,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":APPLY {0:F3},{1:F3}{2}", , , SCPIDelimiter);
// 修正:去除前缀冒号,规范应用 APPL 短格式
string cmd = string.Format(CultureInfo.InvariantCulture, "APPL {0:F3},{1:F3}{2}", , , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -181,7 +182,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":APPLY?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"APPL?{SCPIDelimiter}", SCPIDelimiter, ct);
}
#endregion
@@ -194,7 +196,8 @@ namespace DeviceCommand.Device
[Monitorable("低压直流电源电流")]
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:CURRent?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号,改用标准 MEAS:CURR?
return await WriteReadAsync($"MEAS:CURR?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -203,16 +206,18 @@ namespace DeviceCommand.Device
[Monitorable("低压直流电源功率")]
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:POWer?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号,改用标准 MEAS:POW?
return await WriteReadAsync($"MEAS:POW?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
/// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
/// </summary
/// </summary>
[Monitorable("低压直流电源电压")]
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:VOLTage?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号,改用标准 MEAS:VOLT?
return await WriteReadAsync($"MEAS:VOLT?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -220,7 +225,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -228,7 +234,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:CAPAcity?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"MEAS:CAP?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -236,7 +243,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:TIMEr:CC?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"MEAS:TIM:CC?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -244,7 +252,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":MEASure:TIME:OUTPut?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"MEAS:TIME:OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
}
#endregion
@@ -256,8 +265,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task DC输出(bool , CancellationToken ct = default)
{
// 修正:去除前缀冒号,改用 OUTP 短格式
string = ? "ON" : "OFF";
await SendAsync($":OUTPut {状态}{SCPIDelimiter}", ct);
await SendAsync($"OUTP {状态}{SCPIDelimiter}", ct);
}
/// <summary>
@@ -265,7 +275,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> DC输出状态(CancellationToken ct = default)
{
return await WriteReadAsync($":OUTPut?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -273,8 +284,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (bool , CancellationToken ct = default)
{
// 修正:去除前缀冒号
string = ? "ON" : "OFF";
await SendAsync($":OUTPut:TIMEr {状态}{SCPIDelimiter}", ct);
await SendAsync($"OUTP:TIM {状态}{SCPIDelimiter}", ct);
}
/// <summary>
@@ -282,7 +294,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":OUTPut:TIMEr:DATA {0:F1}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号
string cmd = string.Format(CultureInfo.InvariantCulture, "OUTP:TIM:DATA {0:F1}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -291,8 +304,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (bool CV优先, CancellationToken ct = default)
{
// 修正:去除前缀冒号
string = CV优先 ? "CV" : "CC";
await SendAsync($":OUTPut:MODE {模式}{SCPIDelimiter}", ct);
await SendAsync($"OUTP:MODE {模式}{SCPIDelimiter}", ct);
}
#endregion
@@ -304,7 +318,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":CURRent:PROTection:CLEar{SCPIDelimiter}", ct);
// 修正:去除前缀冒号
await SendAsync($"CURR:PROT:CLE{SCPIDelimiter}", ct);
}
/// <summary>
@@ -312,8 +327,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task 使(bool , CancellationToken ct = default)
{
// 修正:去除前缀冒号,将 STATE 改为更常用的 STAT 简写
string = ? "ON" : "OFF";
await SendAsync($":CURRent:PROTection:STATE {状态}{SCPIDelimiter}", ct);
await SendAsync($"CURR:PROT:STAT {状态}{SCPIDelimiter}", ct);
}
/// <summary>
@@ -321,7 +337,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":CURRent:PROTection {0:F3}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号。按 NGI 通用指令树,加上层级限制或者直接作用于 CURR:PROT
string cmd = string.Format(CultureInfo.InvariantCulture, "CURR:PROT {0:F3}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -330,7 +347,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":CURRent {0:F3}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号,改用标准 CURR 短格式
string cmd = string.Format(CultureInfo.InvariantCulture, "CURR {0:F3}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -339,7 +357,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":VOLTage:PROTection:CLEar{SCPIDelimiter}", ct);
// 修正:去除前缀冒号
await SendAsync($"VOLT:PROT:CLE{SCPIDelimiter}", ct);
}
/// <summary>
@@ -347,8 +366,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task 使(bool , CancellationToken ct = default)
{
// 修正:去除前缀冒号
string = ? "ON" : "OFF";
await SendAsync($":VOLTage:PROTection:STATE {状态}{SCPIDelimiter}", ct);
await SendAsync($"VOLT:PROT:STAT {状态}{SCPIDelimiter}", ct);
}
/// <summary>
@@ -356,7 +376,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":VOLTage:PROTection {0:F3}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:PROT {0:F3}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -365,7 +386,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":VOLTage {0:F3}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号,改用标准 VOLT 短格式
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT {0:F3}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -374,7 +396,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":VOLTage:LIMIT {0:F3}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:LIM {0:F3}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -383,7 +406,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (double , CancellationToken ct = default)
{
string cmd = string.Format(CultureInfo.InvariantCulture, ":POWer {0:F3}{1}", , SCPIDelimiter);
// 修正:去除前缀冒号,改用标准 POW 短格式
string cmd = string.Format(CultureInfo.InvariantCulture, "POW {0:F3}{1}", , SCPIDelimiter);
await SendAsync(cmd, ct);
}
@@ -392,8 +416,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task 使(bool , CancellationToken ct = default)
{
// 修正:去除前缀冒号
string = ? "ON" : "OFF";
await SendAsync($":POWer:STATE {状态}{SCPIDelimiter}", ct);
await SendAsync($"POW:STAT {状态}{SCPIDelimiter}", ct);
}
#endregion
@@ -405,8 +430,9 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (bool , CancellationToken ct = default)
{
// 修正:去除前缀冒号
string = ? "ON" : "OFF";
await SendAsync($":SYSTem:BEEPer:STATE {状态}{SCPIDelimiter}", ct);
await SendAsync($"SYST:BEEP:STAT {状态}{SCPIDelimiter}", ct);
}
/// <summary>
@@ -414,7 +440,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":SYSTem:BEEPer{SCPIDelimiter}", ct);
// 修正:去除前缀冒号
await SendAsync($"SYST:BEEP{SCPIDelimiter}", ct);
}
/// <summary>
@@ -422,7 +449,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":SYSTem:ERRor:COUNT?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"SYST:ERR:COUN?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -430,7 +458,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task<string> (CancellationToken ct = default)
{
return await WriteReadAsync($":SYSTem:ERRor?{SCPIDelimiter}", SCPIDelimiter, ct);
// 修正:去除前缀冒号
return await WriteReadAsync($"SYST:ERR?{SCPIDelimiter}", SCPIDelimiter, ct);
}
/// <summary>
@@ -438,7 +467,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":SYSTem:LOCal{SCPIDelimiter}", ct);
// 修正:去除前缀冒号
await SendAsync($"SYST:LOC{SCPIDelimiter}", ct);
}
/// <summary>
@@ -446,7 +476,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":SYSTem:REMote{SCPIDelimiter}", ct);
// 修正:去除前缀冒号
await SendAsync($"SYST:REM{SCPIDelimiter}", ct);
}
/// <summary>
@@ -454,7 +485,8 @@ namespace DeviceCommand.Device
/// </summary>
public virtual async Task (CancellationToken ct = default)
{
await SendAsync($":SYSTem:RWLock{SCPIDelimiter}", ct);
// 修正:去除前缀冒号
await SendAsync($"SYST:RWL{SCPIDelimiter}", ct);
}
#endregion

View File

@@ -1,4 +1,5 @@
using System;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@@ -8,7 +9,16 @@ namespace Model.Entity
{
public class MonitorValueEntity:BaseEntity
{
public string MonitorName;
public double MonitorValue;
/// <summary>监测项名称</summary>
[SugarColumn(ColumnName = "MonitorName")]
public string MonitorName { get; set; } = string.Empty;
/// <summary>监测值</summary>
[SugarColumn(ColumnName = "MonitorValue")]
public double MonitorValue { get; set; }
/// <summary>作用域标识(台架名称)</summary>
[SugarColumn(ColumnName = "Scope")]
public string Scope { get; set; } = string.Empty;
}
}

View File

@@ -13,6 +13,8 @@ namespace Model.Models
public class AvailableMethodItem
{
public string DeviceName { get; set; } = string.Empty;
/// <summary>硬件指纹(物理设备唯一标识)</summary>
public string Fingerprint { get; set; } = string.Empty;
public string MethodName { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public MethodInfo MethodInfo { get; set; } = null!;

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Model.Models
{
public class HardwareReportArgs
{
/// <summary>作用域标识(台架名称)</summary>
public string Scope { get; set; } = string.Empty;
/// <summary>硬件指纹(物理设备唯一标识,如 "Tcp:192.168.1.1:5000"</summary>
public string HardwareFingerprint { get; set; } = string.Empty;
/// <summary>方法名</summary>
public string MethodName { get; set; } = string.Empty;
/// <summary>采样值</summary>
public double Value { get; set; }
/// <summary>采样时间</summary>
public DateTime Time { get; set; } = DateTime.Now;
}
}

View File

@@ -16,6 +16,8 @@ namespace MonitorModule.ViewModels
{
// ===== 标识 =====
public string DeviceName { get; init; } = string.Empty;
/// <summary>硬件指纹(物理设备唯一标识,用于匹配广播事件)</summary>
public string Fingerprint { get; init; } = string.Empty;
public string MethodName { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;

View File

@@ -5,9 +5,12 @@ using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Service.Interface;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -24,10 +27,12 @@ namespace MonitorModule.ViewModels
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region
private IMonitorValueService _monitorValueService;
public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; } = null!;
public GlobalInfo _globalInfo { get; set; }
private string _testStatus = string.Empty;
public string TestStatus
{
get => _testStatus;
@@ -56,6 +61,7 @@ namespace MonitorModule.ViewModels
set => SetProperty(ref _selectedAvailableMethod, value);
}
private string _statusMessage = "图表已就绪,暂无监测项";
public string StatusMessage
{
get => _statusMessage;
@@ -77,6 +83,7 @@ namespace MonitorModule.ViewModels
private DeviceManager? _deviceManager;
private CancellationTokenSource? _monitorCTS = new();
// 颜色调色盘
private static readonly OxyColor[] _palette =
{
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
@@ -85,50 +92,216 @@ namespace MonitorModule.ViewModels
};
private int _colorIndex;
private readonly DispatcherTimer _sampleTimer;
private double _sampleTime;
private const double _sampleInterval = 0.1; // 100ms
private string _testStatus = string.Empty;
private string _statusMessage = "图表已就绪,暂无监测项";
/// <summary>广播器(由 UIShare 层注入,每 Scope 一个实例)</summary>
private HardwareDataBroadcaster? _broadcaster;
/// <summary>反射方法缓存Channel → (MethodInfo, DeviceInstance)</summary>
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
/// <summary>用于 OxyPlot X 轴相对秒数</summary>
private readonly Stopwatch _stopwatch = new();
/// <summary>OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot</summary>
private readonly DispatcherTimer _plotRefreshTimer;
/// <summary>事件订阅令牌,用于 Dispose 时取消订阅</summary>
private SubscriptionToken? _subscriptionToken;
// ==========================================
// 批量入库核心结构
// ==========================================
private readonly ConcurrentQueue<MonitorValueEntity> _insertQueue = new();
private Task? _dbFlushTask;
private const int BulkInsertThreshold = 50;
private int _isFlushing = 0;
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
_monitorValueService = container.Resolve<IMonitorValueService>();
Plot = BuildEmptyPlot();
AddChannelCommand = new DelegateCommand(OnAddChannel);
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
ResetViewCommand = new DelegateCommand(OnResetView);
RefreshDataCommand = new DelegateCommand(OnRefreshData);
RefreshCommand = new DelegateCommand(OnExpand);
_sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) };
_sampleTimer.Tick += OnSampleTick;
// OxyPlot 刷新定时器1000ms只负责视觉更新
_plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background)
{
Interval = TimeSpan.FromMilliseconds(1000)
};
_plotRefreshTimer.Tick += OnPlotRefreshTick;
}
public void Dispose()
{
// 1. 率先掐断后台正在并行的所有硬件请求
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
// 2. 停掉定时器,停止触发 UI 刷新
_sampleTimer?.Stop();
_plotRefreshTimer.Stop();
_plotRefreshTimer.Tick -= OnPlotRefreshTick;
_stopwatch.Stop();
// 取消 EventAggregator 订阅
if (_subscriptionToken != null)
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Unsubscribe(_subscriptionToken);
if (_dbFlushTask != null)
{
try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { }
}
_sampleTimer.Tick -= OnSampleTick;
// 3. 释放容器作用域
_scope?.Dispose();
// 4. 清理集合
Channels.Clear();
AvailableMethods.Clear();
_methodCache.Clear();
}
#region 广
/// <summary>
/// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。
/// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。
/// 通过 HardwareFingerprint + MethodName 匹配监测通道。
/// </summary>
private void OnHardwareDataReceived(HardwareReportArgs args)
{
// 仅处理属于当前 Scope 的事件
if (string.IsNullOrEmpty(TestStatus) || args.Scope != TestStatus) return;
// 以硬件指纹 + 方法名唯一匹配通道(与逻辑名解耦)
var channel = Channels.FirstOrDefault(c =>
c.Fingerprint == args.HardwareFingerprint &&
c.MethodName == args.MethodName &&
c.IsMonitored);
if (channel == null) return;
double time = _stopwatch.Elapsed.TotalSeconds;
// 记录数据点线程安全Record 内部 ConcurrentQueue + Series 操作)
channel.Record(time, args.Value);
// 入队数据库批量写入
// CreateTime 使用 args.Time采样触发时刻而不是 DateTime.Now设备响应到达时刻
// 这样即使设备响应有延迟,数据库里的时间戳仍然按设定采样间隔分布。
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = args.Value,
Scope = args.Scope,
CreateTime = args.Time,
IsDel = 0
};
_insertQueue.Enqueue(entity);
if (_insertQueue.Count >= BulkInsertThreshold)
{
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
}
}
#endregion
#region OxyPlot
private void OnPlotRefreshTick(object? sender, EventArgs e)
{
if (!_stopwatch.IsRunning) return;
double elapsed = _stopwatch.Elapsed.TotalSeconds;
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
double window = 10000 * 0.1; // 20s 视窗
xAxis.Minimum = Math.Max(0, elapsed - window);
xAxis.Maximum = elapsed + 0.5;
}
Plot.InvalidatePlot(true);
}
#endregion
#region
private void StartDbFlushWorker(CancellationToken token)
{
_dbFlushTask = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
await DoFlushWorkAsync().ConfigureAwait(false);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DB Worker Error] {ex.Message}");
}
}
await DoFlushWorkAsync().ConfigureAwait(false);
}, token);
}
private async Task DoFlushWorkAsync()
{
if (_insertQueue.IsEmpty) return;
if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return;
try
{
var listToInsert = new List<MonitorValueEntity>();
while (_insertQueue.TryDequeue(out var entity)) listToInsert.Add(entity);
if (listToInsert.Count > 0)
{
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
}
}
finally { Interlocked.Exchange(ref _isFlushing, 0); }
}
#endregion
#region Navigation
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
Plot.Title = $"监控 - {TestStatus}";
Plot.InvalidatePlot(false);
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve<ScopedContext>();
_deviceManager = _scope.Resolve<DeviceManager>();
_broadcaster = _scope.Resolve<HardwareDataBroadcaster>();
IsInitiated = true;
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 1. 启动后台数据库消费
StartDbFlushWorker(_monitorCTS.Token);
// 2. 扫描可监测方法(供 UI 选择)
DiscoverAvailableMethods();
// 3. 启动广播器Discover + Start
_broadcaster.Discover();
_broadcaster.Start();
// 4. 订阅 HardwareDataReportedEventUI 线程回调)
_subscriptionToken = _eventAggregator.GetEvent<HardwareDataReportedEvent>()
.Subscribe(OnHardwareDataReceived, ThreadOption.UIThread);
// 5. 启动 OxyPlot 视觉刷新定时器
_stopwatch.Start();
_plotRefreshTimer.Start();
}
}
#endregion
#region PlotModel
private static PlotModel BuildEmptyPlot()
{
@@ -162,7 +335,22 @@ namespace MonitorModule.ViewModels
}
#endregion
#region
#region UI
/// <summary>
/// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。
/// </summary>
private Dictionary<object, string> BuildInstanceToFingerprint()
{
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
foreach (var poolEntry in _globalInfo.HardwarePool)
{
var lazy = poolEntry.Value;
if (lazy.IsValueCreated && lazy.Value != null)
map[lazy.Value] = poolEntry.Key;
}
return map;
}
private void DiscoverAvailableMethods()
{
AvailableMethods.Clear();
@@ -172,26 +360,26 @@ namespace MonitorModule.ViewModels
return;
}
var instanceToFp = BuildInstanceToFingerprint();
foreach (var kvp in _deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
// 改用特性过滤:仅扫描带有 [Monitorable] 特性且签名正确的方法
// 反查该实例的硬件指纹
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
continue;
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
var attribute = m.GetCustomAttribute<MonitorableAttribute>();
if (attribute == null) return false;
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
bool validParams = parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
return validParams;
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
@@ -201,15 +389,15 @@ namespace MonitorModule.ViewModels
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
var item = new AvailableMethodItem
AvailableMethods.Add(new AvailableMethodItem
{
DeviceName = deviceName,
Fingerprint = fingerprint,
MethodName = method.Name,
DisplayName = displayName,
MethodInfo = method,
Device = device
};
AvailableMethods.Add(item);
});
}
}
@@ -229,7 +417,8 @@ namespace MonitorModule.ViewModels
return;
}
if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName))
// 以指纹+方法名去重,防止同一物理设备重复添加
if (Channels.Any(c => c.Fingerprint == method.Fingerprint && c.MethodName == method.MethodName))
{
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
return;
@@ -241,6 +430,7 @@ namespace MonitorModule.ViewModels
var channel = new MonitorChannel
{
DeviceName = method.DeviceName,
Fingerprint = method.Fingerprint,
MethodName = method.MethodName,
DisplayName = method.DisplayName,
Color = color,
@@ -256,8 +446,6 @@ namespace MonitorModule.ViewModels
};
Plot.Series.Add(channel.Series);
_methodCache[channel] = (method.MethodInfo, method.Device);
channel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
@@ -280,11 +468,7 @@ namespace MonitorModule.ViewModels
return;
}
if (target.Series != null)
{
Plot.Series.Remove(target.Series);
}
_methodCache.Remove(target);
if (target.Series != null) Plot.Series.Remove(target.Series);
Channels.Remove(target);
SelectedChannel = Channels.LastOrDefault();
@@ -328,11 +512,9 @@ namespace MonitorModule.ViewModels
};
var recent = channel.DataPoints.ToArray();
var startIdx = Math.Max(0, recent.Length - 200);
var startIdx = Math.Max(0, recent.Length - 10000);
for (int i = startIdx; i < recent.Length; i++)
{
channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue));
}
Plot.Series.Add(channel.Series);
Plot.InvalidatePlot(true);
@@ -352,121 +534,13 @@ namespace MonitorModule.ViewModels
public void OnChannelMathChanged(MonitorChannel channel)
{
if (channel.Series == null) return;
var points = channel.DataPoints.ToArray();
channel.Series.Points.Clear();
var startIdx = Math.Max(0, points.Length - 200);
var startIdx = Math.Max(0, points.Length - 10000);
for (int i = startIdx; i < points.Length; i++)
{
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
}
Plot.InvalidatePlot(true);
}
#endregion
#region
private async void OnSampleTick(object? sender, EventArgs e)
{
_sampleTime += _sampleInterval;
if (Channels.Count == 0 || _methodCache.Count == 0) return;
var sampleTasks = new List<Task>();
var token = _monitorCTS?.Token ?? CancellationToken.None;
// 1. 并行发起所有通道的数据采集,防止单一通道硬件断连卡死全局采样
foreach (var channel in Channels)
{
if (!channel.IsMonitored) continue;
if (!_methodCache.TryGetValue(channel, out var entry)) continue;
sampleTasks.Add(Task.Run(async () =>
{
try
{
var (method, device) = entry;
// 动态分析方法签名准备反射参数
var parmsInfo = method.GetParameters();
object?[]? invokeArgs = parmsInfo.Length == 1 && parmsInfo[0].ParameterType == typeof(CancellationToken)
? new object[] { token }
: null;
if (method.Invoke(device, invokeArgs) is Task<string> task)
{
string raw = await task.ConfigureAwait(false);
if (double.TryParse(raw, out double value))
{
channel.Record(_sampleTime, value);
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = value,
CreateTime = DateTime.Now,
IsDel = 0
};
}
}
}
catch
{
// 某个硬件通道故障时不干扰其他通道
}
}, token));
}
// 2. 批量等待并发采样,加入 2 秒强制超时保护兜底
if (sampleTasks.Count > 0)
{
try
{
var delayTask = Task.Delay(TimeSpan.FromSeconds(2), token);
await Task.WhenAny(Task.WhenAll(sampleTasks), delayTask).ConfigureAwait(true);
}
catch (OperationCanceledException)
{
return;
}
}
// 3. 【回归UI线程】由于前面 ConfigureAwait(true),这里直接执行 OxyPlot 坐标滚动与刷新
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
double window = 200 * _sampleInterval;
xAxis.Minimum = Math.Max(0, _sampleTime - window);
xAxis.Maximum = _sampleTime + 0.5;
}
Plot.InvalidatePlot(true);
}
#endregion
#region
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
Plot.Title = $"监控 - {TestStatus}";
Plot.InvalidatePlot(false);
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve<ScopedContext>();
_deviceManager = _scope.Resolve<DeviceManager>();
IsInitiated = true;
// 初始化全新的取消信号源
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 扫描可用监测项
DiscoverAvailableMethods();
// 启动采样定时器
_sampleTimer.Start();
}
}
#endregion
}
}
}

View File

@@ -1,8 +1,13 @@
using Logger;
using Common.Attributes;
using DeviceCommand.Base;
using Logger;
using Model.Entity;
using ORM;
using SqlSugar;
using System.Collections.ObjectModel;
using System.Data;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Input;
using UIShare.GlobalVariable;
@@ -12,9 +17,9 @@ using UIShare.ViewModelBase;
namespace MonitorModule.ViewModels
{
/// <summary>
/// 记录界面:用于查看运行目录下 SQL/ADP.db 中的数据。
/// 功能:表选择 / 关键字 WHERE 查询 / 分页 / 导出 CSV。
/// 仍延续 ScopedContext 隔离 + 双击展开 的范式,每个工位独立一份查询状态
/// 记录界面:查询 MonitorValueEntity 表中的历史监测数据。
/// 功能:监测项筛选 / 台架筛选 / 日期筛选 / 分页 / 导出 CSV。
/// 使用 SqlSugarContext.DbContext 全局单例,与写入端共用同一数据库连接
/// </summary>
public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
@@ -30,30 +35,39 @@ namespace MonitorModule.ViewModels
set => SetProperty(ref _testStatus, value);
}
public ObservableCollection<string> TableNames
/// <summary>监测项名称下拉列表</summary>
public ObservableCollection<string> MonitorNames
{
get => _tableNames;
set => SetProperty(ref _tableNames, value);
get => _monitorNames;
set => SetProperty(ref _monitorNames, value);
}
public string? SelectedTable
/// <summary>选中的监测项null = 全部)</summary>
public string? SelectedMonitorName
{
get => _selectedTable;
set
{
if (SetProperty(ref _selectedTable, value))
{
PageIndex = 1;
Query();
}
}
get => _selectedMonitorName;
set => SetProperty(ref _selectedMonitorName, value);
}
// 用户填写的 WHERE 子句(不带 WHERE 关键字例如Status='OK' AND Id>10
public string WhereClause
/// <summary>台架(作用域)下拉列表</summary>
public ObservableCollection<string> ScopeNames
{
get => _whereClause;
set => SetProperty(ref _whereClause, value);
get => _scopeNames;
set => SetProperty(ref _scopeNames, value);
}
/// <summary>选中的台架null = 全部)</summary>
public string? SelectedScope
{
get => _selectedScope;
set => SetProperty(ref _selectedScope, value);
}
/// <summary>查询日期null = 全部日期)</summary>
public DateTime? SelectedDate
{
get => _selectedDate;
set => SetProperty(ref _selectedDate, value);
}
public DataTable ResultTable
@@ -111,40 +125,38 @@ namespace MonitorModule.ViewModels
#region
public ICommand LoadedCommand { get; }
public ICommand RefreshTablesCommand { get; }
public ICommand QueryCommand { get; }
public ICommand ClearFilterCommand { get; }
public ICommand FirstPageCommand { get; }
public ICommand PrevPageCommand { get; }
public ICommand NextPageCommand { get; }
public ICommand LastPageCommand { get; }
public ICommand ExportCsvCommand { get; }
// 双击展开/折叠:与 MonitorView/AutomatedTestingView 共用
public ICommand RefreshCommand { get; }
#endregion
#region
// 数据库相对路径:运行目录\SQL\ADP.db数据库未就绪时容错处理不抛异常
private static readonly string DbFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SQL");
private static readonly string DbPath = Path.Combine(DbFolder, "ADP.db");
private static readonly string ConnStr = $"Data Source={DbPath};Version=3;";
private bool IsInitiated = false;
private IScopedProvider _scope;
private string _testStatus = string.Empty;
private ObservableCollection<string> _tableNames = new();
private string? _selectedTable;
private string _whereClause = string.Empty;
private ObservableCollection<string> _monitorNames = new();
private string? _selectedMonitorName;
private ObservableCollection<string> _scopeNames = new(new[] { "全部", "TestCell1", "TestCell2", "TestCell3", "TestCell4", "TestCell5", "TestCell6", "TestCell7", "TestCell8" });
private string? _selectedScope = "全部";
private DateTime? _selectedDate;
private DataTable _resultTable = new();
private string _statusMessage = "未连接";
private int _pageIndex = 1;
private int _pageSize = 50;
private long _totalCount;
private bool IsInitiated = false;
#endregion
public RecordViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
LoadedCommand = new DelegateCommand(LoadTables);
RefreshTablesCommand = new DelegateCommand(LoadTables);
LoadedCommand = new DelegateCommand(LoadFilterOptions);
QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); });
ClearFilterCommand = new DelegateCommand(ClearFilter);
FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } });
PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } });
NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } });
@@ -158,105 +170,136 @@ namespace MonitorModule.ViewModels
_scope?.Dispose();
}
#region
private SqlSugarClient CreateClient()
#region
/// <summary>
/// 加载筛选条件:监测项通过反射发现(与 MonitorViewModel 一致),台架从数据库查询。
/// </summary>
private void LoadFilterOptions()
{
return new SqlSugarClient(new ConnectionConfig
{
DbType = SqlSugar.DbType.Sqlite,
ConnectionString = ConnStr,
IsAutoCloseConnection = true,
InitKeyType = InitKeyType.Attribute
});
// 监测项:反射发现当前作用域所有带 [Monitorable] 的设备方法
DiscoverMonitorNames();
Query();
}
/// <summary>
/// 读取数据库中的所有用户表。数据库不存在时不报错,仅在状态栏提示。
/// 反射扫描当前作用域 DeviceManager 中所有带 [Monitorable] 特性的设备方法,
/// 生成与 MonitorViewModel 完全一致的 DisplayName 列表,填充到下拉框。
/// 这样即使数据库还没数据,用户也能看到所有可筛选的监测项。
/// </summary>
private void LoadTables()
private void DiscoverMonitorNames()
{
MonitorNames = new ObservableCollection<string>();
if (_scope == null) return;
try
{
if (!Directory.Exists(DbFolder)) Directory.CreateDirectory(DbFolder);
if (!File.Exists(DbPath))
var deviceManager = _scope.Resolve<DeviceManager>();
if (deviceManager?.DeviceMap == null || deviceManager.DeviceMap.Count == 0)
{
TableNames = new ObservableCollection<string>();
SelectedTable = null;
ResultTable = new DataTable();
TotalCount = 0;
StatusMessage = $"数据库尚未创建:{DbPath}";
StatusMessage = "当前工位无可用设备";
return;
}
using var db = CreateClient();
var tables = db.DbMaintenance.GetTableInfoList(false)
.Select(t => t.Name)
.OrderBy(n => n)
.ToList();
// 构建 设备实例 → 硬件指纹 的反向查找表
var instanceToFp = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
foreach (var poolEntry in _globalInfo.HardwarePool)
{
var lazy = poolEntry.Value;
if (lazy.IsValueCreated && lazy.Value != null)
instanceToFp[lazy.Value] = poolEntry.Key;
}
TableNames = new ObservableCollection<string>(tables);
if (tables.Count == 0)
var names = new List<string>();
foreach (var kvp in deviceManager.DeviceMap)
{
SelectedTable = null;
ResultTable = new DataTable();
TotalCount = 0;
StatusMessage = "数据库已连接,但暂无数据表";
}
else
{
StatusMessage = $"已连接:{DbPath}{tables.Count} 张表)";
if (string.IsNullOrEmpty(SelectedTable) || !tables.Contains(SelectedTable))
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
// 只列出在硬件指纹池中注册的设备
if (!instanceToFp.ContainsKey(device))
continue;
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
{
SelectedTable = tables[0]; // setter 会触发 Query
}
else
{
Query();
var attr = method.GetCustomAttribute<MonitorableAttribute>();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
names.Add(displayName);
}
}
names.Sort();
MonitorNames = new ObservableCollection<string>(names);
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope,$"加载数据表失败:{ex.Message}");
StatusMessage = $"加载失败:{ex.Message}";
LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"反射发现监测项失败:{ex.Message}");
}
}
/// <summary>清除所有筛选条件</summary>
private void ClearFilter()
{
SelectedMonitorName = null;
SelectedScope = "全部";
SelectedDate = null;
PageIndex = 1;
Query();
}
#endregion
#region
/// <summary>
/// 执行分页查询。WHERE 为空则查询全部
/// 按当前筛选条件分页查询 MonitorValueEntity
/// </summary>
private void Query()
{
if (string.IsNullOrEmpty(SelectedTable) || !File.Exists(DbPath))
{
ResultTable = new DataTable();
TotalCount = 0;
return;
}
try
{
using var db = CreateClient();
string table = $"[{SelectedTable}]";
string where = string.IsNullOrWhiteSpace(WhereClause) ? "1=1" : WhereClause;
var db = SqlSugarContext.DbContext;
// COUNT 总数
var countSql = $"SELECT COUNT(*) FROM {table} WHERE {where}";
TotalCount = db.Ado.GetLong(countSql);
// 预计算日期范围,避免在表达式树中访问可空属性
DateTime? dateStart = SelectedDate?.Date;
DateTime? dateEnd = dateStart?.AddDays(1);
var query = db.Queryable<MonitorValueEntity>()
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
.WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope)
.WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc);
TotalCount = query.Count();
// 修正越界
if (PageIndex > TotalPages) PageIndex = TotalPages;
if (PageIndex < 1) PageIndex = 1;
int offset = (PageIndex - 1) * PageSize;
var dataSql = $"SELECT * FROM {table} WHERE {where} LIMIT {PageSize} OFFSET {offset}";
ResultTable = db.Ado.GetDataTable(dataSql);
int skip = (PageIndex - 1) * PageSize;
ResultTable = query
.Skip(skip)
.Take(PageSize)
.ToDataTable();
StatusMessage = $"表 [{SelectedTable}] 共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
StatusMessage = $"共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(TestStatus,$"查询失败:{ex.Message}");
LoggerHelper.ErrorWithNotify(TestStatus, $"查询失败:{ex.Message}");
StatusMessage = $"查询失败:{ex.Message}";
ResultTable = new DataTable();
TotalCount = 0;
@@ -266,36 +309,42 @@ namespace MonitorModule.ViewModels
#region CSV
/// <summary>
/// 按当前 WHERE 条件导出全部结果到 CSV不分页
/// 按当前筛选条件导出全部结果到 CSV不分页
/// </summary>
private void ExportCsv()
{
if (string.IsNullOrEmpty(SelectedTable) || !File.Exists(DbPath))
{
StatusMessage = "无可导出数据";
return;
}
var dlg = new Microsoft.Win32.SaveFileDialog
{
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
FileName = $"{SelectedTable}_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
Title = $"导出 [{SelectedTable}] 为 CSV"
};
if (dlg.ShowDialog() != true) return;
try
{
using var db = CreateClient();
string table = $"[{SelectedTable}]";
string where = string.IsNullOrWhiteSpace(WhereClause) ? "1=1" : WhereClause;
var sql = $"SELECT * FROM {table} WHERE {where}";
var dt = db.Ado.GetDataTable(sql);
var db = SqlSugarContext.DbContext;
DateTime? dateStart = SelectedDate?.Date;
DateTime? dateEnd = dateStart?.AddDays(1);
var query = db.Queryable<MonitorValueEntity>()
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
.WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope)
.WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc);
var dt = query.ToDataTable();
if (dt.Rows.Count == 0)
{
StatusMessage = "无可导出数据";
return;
}
var dlg = new Microsoft.Win32.SaveFileDialog
{
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
FileName = $"MonitorData_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
Title = "导出监测数据为 CSV"
};
if (dlg.ShowDialog() != true) return;
WriteCsv(dlg.FileName, dt);
StatusMessage = $"导出完成:{dlg.FileName}{dt.Rows.Count} 行)";
LoggerHelper.InfoWithNotify(TestStatus, $"工位 [{TestStatus}] 导出 [{SelectedTable}] 至 {dlg.FileName},共 {dt.Rows.Count} 行");
LoggerHelper.InfoWithNotify(TestStatus, $"工位 [{TestStatus}] 导出监测数据至 {dlg.FileName},共 {dt.Rows.Count} 行");
}
catch (Exception ex)
{
@@ -333,7 +382,6 @@ namespace MonitorModule.ViewModels
if (string.IsNullOrEmpty(TestStatus)) return;
_globalInfo.CurrentScope = TestStatus;
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
}
#endregion
@@ -341,14 +389,14 @@ namespace MonitorModule.ViewModels
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!IsInitiated&&navigationContext.Parameters.ContainsKey("Name"))
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve<ScopedContext>();
IsInitiated = true;
}
LoadTables();
LoadFilterOptions();
}
#endregion
}

View File

@@ -39,7 +39,7 @@
FontSize="20" FontWeight="Bold"
Margin="4,0,0,8"/>
<!-- 工具栏:表选择 / WHERE 条件 / 查询 / 刷新表 / 导出 -->
<!-- 工具栏:监测项筛选 / 台架筛选 / 日期筛选 / 查询 / 清除 / 导出 -->
<Border Grid.Row="1"
Background="White"
BorderBrush="#DDD" BorderThickness="1"
@@ -47,57 +47,71 @@
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- 监测项 -->
<TextBlock Grid.Column="0"
Text=""
Text="监测项"
VerticalAlignment="Center"
Margin="0,0,4,0"/>
<ComboBox Grid.Column="1"
materialDesign:HintAssist.Hint="数据表"
ItemsSource="{Binding TableNames}"
SelectedItem="{Binding SelectedTable}"
materialDesign:HintAssist.Hint="全部"
ItemsSource="{Binding MonitorNames}"
SelectedItem="{Binding SelectedMonitorName}"
IsEditable="True"
VerticalAlignment="Center"/>
<!-- 台架 -->
<TextBlock Grid.Column="2"
Text="WHERE"
Text="台架"
VerticalAlignment="Center"
Margin="12,0,4,0"/>
<TextBox Grid.Column="3"
Text="{Binding WhereClause, UpdateSourceTrigger=PropertyChanged}"
VerticalAlignment="Center"
materialDesign:HintAssist.Hint="条件筛选"
ToolTip="例如Status='OK' AND Id>10。留空表示查询全部">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding QueryCommand}"/>
</TextBox.InputBindings>
</TextBox>
<ComboBox Grid.Column="3"
materialDesign:HintAssist.Hint="全部"
ItemsSource="{Binding ScopeNames}"
SelectedItem="{Binding SelectedScope}"
IsEditable="True"
VerticalAlignment="Center"/>
<Button Grid.Column="4"
<!-- 日期 -->
<TextBlock Grid.Column="4"
Text="日期:"
VerticalAlignment="Center"
Margin="12,0,4,0"/>
<DatePicker Grid.Column="5"
SelectedDate="{Binding SelectedDate}"
materialDesign:HintAssist.Hint="全部"
VerticalAlignment="Center"/>
<!-- 按钮 -->
<Button Grid.Column="6"
Content="查询"
Command="{Binding QueryCommand}"
Padding="12,4" Margin="8,0,0,0"/>
<Button Grid.Column="5"
Content="刷新表"
Command="{Binding RefreshTablesCommand}"
Padding="12,4" Margin="12,0,0,0"/>
<Button Grid.Column="7"
Content="清除"
Command="{Binding ClearFilterCommand}"
Padding="12,4" Margin="6,0,0,0"/>
<Button Grid.Column="6"
<Button Grid.Column="8"
Content="导出 CSV"
Command="{Binding ExportCsvCommand}"
Padding="12,4" Margin="6,0,0,0"/>
</Grid>
</Border>
<!-- DataGrid自动生成列,绑 DataTable.DefaultView 直显示 -->
<!-- DataGrid显式列定义,隐藏 Id / IsDel -->
<DataGrid Grid.Row="2"
ItemsSource="{Binding ResultTable}"
AutoGenerateColumns="True"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
@@ -106,7 +120,14 @@
AlternatingRowBackground="#F9FAFB"
RowBackground="White"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"/>
VerticalScrollBarVisibility="Auto">
<DataGrid.Columns>
<DataGridTextColumn Header="监测项" Binding="{Binding MonitorName}" Width="*"/>
<DataGridTextColumn Header="监测值" Binding="{Binding MonitorValue}" Width="120"/>
<DataGridTextColumn Header="台架" Binding="{Binding Scope}" Width="120"/>
<DataGridTextColumn Header="记录时间" Binding="{Binding CreateTime, StringFormat=yyyy-MM-dd HH:mm:ss}" Width="180"/>
</DataGrid.Columns>
</DataGrid>
<!-- 分页控件 -->
<Border Grid.Row="3"

View File

@@ -46,7 +46,7 @@ namespace ORM
// 拼接数据库文件路径
string DBPath = Path.Combine(folder, "SQL.db");
DbConnectionString = $"Data Source={DBPath};Version=3;";
DbConnectionString = $"Data Source={DBPath};";
}
public static void InitMySql(string Server, int Port, string Database, string Uid,string Pwd)
{

View File

@@ -20,6 +20,7 @@ namespace SettingModule
// 设备连接配置弹窗
containerRegistry.RegisterDialog<TCPConfigView>("TCPConfig");
containerRegistry.RegisterDialog<SerialPortConfigView>("SerialPortConfig");
containerRegistry.RegisterDialog<CANConfigView>("CANConfig");
}
}
}

View File

@@ -0,0 +1,134 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using UIShare.PubEvent;
using UIShare.UIViewModel;
using UIShare.ViewModelBase;
namespace SettingModule.ViewModels.Dialogs
{
/// <summary>
/// CAN 连接配置对话框 VM。
/// 通过 DialogParameters 接收宿主 DeviceInfoVM保存时把副本写回宿主。
/// 参数对应 ZLGCANFD 构造函数 + 初始化并启动通道 方法。
/// </summary>
public class CANConfigViewModel : DialogViewModelBase
{
#region
private string _title = "CAN 连接配置";
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
/// <summary>编辑用的副本,取消时不会污染宿主对象。</summary>
private CANConfigVM _config = new();
public CANConfigVM Config
{
get => _config;
set => SetProperty(ref _config, value);
}
/// <summary>常用仲裁域波特率。</summary>
public ObservableCollection<string> CommonABitBauds { get; } = new()
{
"250000", "500000", "1000000"
};
/// <summary>常用数据域波特率。</summary>
public ObservableCollection<string> CommonDBitBauds { get; } = new()
{
"1000000", "2000000", "4000000", "5000000", "8000000"
};
private string _errorMessage = string.Empty;
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
#endregion
#region
public ICommand SaveCommand { get; }
public ICommand CancelCommand { get; }
#endregion
// 用于保存时把副本回写到原对象
private DeviceInfoVM? _hostDevice;
public CANConfigViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
SaveCommand = new DelegateCommand(OnSave);
CancelCommand = new DelegateCommand(OnCancel);
}
private bool Validate(out string error)
{
error = string.Empty;
if (Config.DeviceType == 0)
{
error = "设备类型号不能为 0";
return false;
}
if (string.IsNullOrWhiteSpace(Config.ABitBaud))
{
error = "仲裁域波特率不能为空";
return false;
}
if (string.IsNullOrWhiteSpace(Config.DBitBaud))
{
error = "数据域波特率不能为空";
return false;
}
return true;
}
private void OnSave()
{
if (!Validate(out var error))
{
ErrorMessage = error;
return;
}
ErrorMessage = string.Empty;
// 把副本写回宿主
if (_hostDevice != null)
{
_hostDevice.CANConfig ??= new CANConfigVM();
Config.CopyTo(_hostDevice.CANConfig);
_hostDevice.ConnectionType = "CAN";
}
RequestClose.Invoke(ButtonResult.OK);
}
private void OnCancel() => RequestClose.Invoke(ButtonResult.Cancel);
#region Prism Dialog
public override void OnDialogOpened(IDialogParameters parameters)
{
_eventAggregator.GetEvent<OverlayEvent>().Publish(true);
if (parameters.ContainsKey("Device"))
{
_hostDevice = parameters.GetValue<DeviceInfoVM>("Device");
Title = $"CAN 连接配置 - {_hostDevice?.DeviceName}";
Config = new CANConfigVM(_hostDevice?.CANConfig);
}
else if (parameters.ContainsKey("Config"))
{
Config = new CANConfigVM(parameters.GetValue<CANConfigVM>("Config"));
}
}
public override void OnDialogClosed()
{
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
}
#endregion
}
}

View File

@@ -59,10 +59,9 @@ namespace SettingModule.ViewModels
public ObservableCollection<string> ConnectionTypes { get; } = new()
{
"None", "Tcp", "Serial"
"None", "Tcp", "Serial", "CAN"
};
#endregion
#region
public ICommand RefreshCommand { get; }
public ICommand SaveCommand { get; }
@@ -165,6 +164,7 @@ namespace SettingModule.ViewModels
{
"Tcp" => "TCPConfig",
"Serial" => "SerialPortConfig",
"CAN" => "CANConfig",
_ => string.Empty
};
@@ -200,6 +200,7 @@ namespace SettingModule.ViewModels
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _globalInfo.ContextDic[TestStatus];
SystemConfig = _scope.Resolve<SystemConfig>();
ConfigService.EnsureDefaultCanDevice(SystemConfig);
if (DeviceList != null && DeviceList.Count > 0)
{
SelectedDevice = DeviceList[0];

View File

@@ -0,0 +1,131 @@
<UserControl x:Class="SettingModule.Views.Dialogs.CANConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SettingModule.Views.Dialogs"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"
Background="White"
prism:ViewModelLocator.AutoWireViewModel="True"
Width="440"
Height="360">
<prism:Dialog.WindowStyle>
<Style BasedOn="{StaticResource DialogUserManageStyle}"
TargetType="Window" />
</prism:Dialog.WindowStyle>
<UserControl.Resources>
<converters:StringToVisibilityConverter x:Key="StringToVisibility"/>
</UserControl.Resources>
<GroupBox Padding="12,8,12,8"
helpers:WindowDragHelper.EnableWindowDrag="True">
<GroupBox.Header>
<Grid Margin="0">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Title}"
Foreground="White"
VerticalAlignment="Center"
Margin="5,0,10,0" />
</Grid>
</GroupBox.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 表单 -->
<Grid Grid.Row="0" Margin="0,4,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 设备类型号 -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="设备类型号:"
VerticalAlignment="Center" Margin="0,6"/>
<TextBox Grid.Row="0" Grid.Column="1" Margin="0,6"
materialDesign:HintAssist.Hint="43 = USBCANFD-400U"
Text="{Binding Config.DeviceType, UpdateSourceTrigger=PropertyChanged}"/>
<!-- 设备索引 -->
<TextBlock Grid.Row="1" Grid.Column="0"
Text="设备索引:"
VerticalAlignment="Center" Margin="0,6"/>
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,6"
materialDesign:HintAssist.Hint=""
Text="{Binding Config.DeviceIndex, UpdateSourceTrigger=PropertyChanged}"/>
<!-- 仲裁域波特率 -->
<TextBlock Grid.Row="2" Grid.Column="0"
Text="仲裁域波特率:"
VerticalAlignment="Center" Margin="0,6"/>
<ComboBox Grid.Row="2" Grid.Column="1" Margin="0,6"
IsEditable="True"
materialDesign:HintAssist.Hint=""
ItemsSource="{Binding CommonABitBauds}"
Text="{Binding Config.ABitBaud, UpdateSourceTrigger=PropertyChanged}"/>
<!-- 数据域波特率 -->
<TextBlock Grid.Row="3" Grid.Column="0"
Text="数据域波特率:"
VerticalAlignment="Center" Margin="0,6"/>
<ComboBox Grid.Row="3" Grid.Column="1" Margin="0,6"
IsEditable="True"
materialDesign:HintAssist.Hint=""
ItemsSource="{Binding CommonDBitBauds}"
Text="{Binding Config.DBitBaud, UpdateSourceTrigger=PropertyChanged}"/>
<!-- 终端电阻 -->
<TextBlock Grid.Row="4" Grid.Column="0"
Text="终端电阻:"
VerticalAlignment="Center" Margin="0,6"/>
<ToggleButton Grid.Row="4" Grid.Column="1" Margin="0,6"
IsChecked="{Binding Config.EnableTerminalResistance}"
Style="{StaticResource MaterialDesignSwitchToggleButton}"/>
</Grid>
<!-- 错误提示 -->
<TextBlock Grid.Row="1"
Margin="0,8,0,0"
Foreground="#D32F2F"
TextWrapping="Wrap"
Text="{Binding ErrorMessage}"
Visibility="{Binding ErrorMessage, Converter={StaticResource StringToVisibility}}"/>
<!-- 按钮 -->
<StackPanel Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0,12,0,0">
<Button Content="取消"
Width="80" Padding="0,4"
Command="{Binding CancelCommand}"/>
<Button Content="保存"
Width="80" Padding="0,4"
Margin="10,0,0,0"
IsDefault="True"
Command="{Binding SaveCommand}"/>
</StackPanel>
</Grid>
</GroupBox>
</UserControl>

View File

@@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace SettingModule.Views.Dialogs
{
/// <summary>
/// CANConfigView.xaml 的交互逻辑
/// </summary>
public partial class CANConfigView : UserControl
{
public CANConfigView()
{
InitializeComponent();
}
}
}

View File

@@ -368,6 +368,33 @@
<Run Text="{Binding SelectedDevice.SerialPortConfig.Parity}"/>
</TextBlock>
</StackPanel>
<!-- CAN 参数预览 -->
<StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,4,0,0" Orientation="Horizontal">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedDevice.ConnectionType}" Value="CAN">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Foreground="#666" FontSize="12">
<Run Text="设备类型:"/>
<Run Text="{Binding SelectedDevice.CANConfig.DeviceType}"/>
<Run Text=" 设备索引:"/>
<Run Text="{Binding SelectedDevice.CANConfig.DeviceIndex}"/>
<Run Text=" 仲裁/数据波特率:"/>
<Run Text="{Binding SelectedDevice.CANConfig.ABitBaud}"/>
<Run Text="/"/>
<Run Text="{Binding SelectedDevice.CANConfig.DBitBaud}"/>
<Run Text=" 终端电阻:"/>
<Run Text="{Binding SelectedDevice.CANConfig.EnableTerminalResistance}"/>
</TextBlock>
</StackPanel>
</Grid>
</StackPanel>
</Border>

View File

@@ -1,7 +1,10 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Logger;
using UIShare.UIViewModel;
namespace UIShare.GlobalVariable
{
@@ -44,6 +47,7 @@ namespace UIShare.GlobalVariable
{
// 如果不存在,创建一个带 Title 的默认配置并保存
var defaultConfig = new SystemConfig { Title = title };
EnsureDefaultCanDevice(defaultConfig);
Save(defaultConfig);
return defaultConfig;
}
@@ -58,12 +62,16 @@ namespace UIShare.GlobalVariable
TypeNameHandling = TypeNameHandling.All
});
return config ?? new SystemConfig { Title = title };
config ??= new SystemConfig { Title = title };
EnsureDefaultCanDevice(config);
return config;
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(title, $"格子 [{title}] 配置加载失败: {ex.Message}");
return new SystemConfig { Title = title };
var fallback = new SystemConfig { Title = title };
EnsureDefaultCanDevice(fallback);
return fallback;
}
}
}
@@ -98,5 +106,34 @@ namespace UIShare.GlobalVariable
}
}
}
/// <summary>
/// 确保配置中至少包含一个 CAN 设备(对应 SystemConfig.CANFD
/// 旧配置或空配置会自动升级,使用户在设置界面能看到 CAN 设备。
/// </summary>
public static void EnsureDefaultCanDevice(SystemConfig config)
{
if (config.DeviceList == null)
{
config.DeviceList = new ObservableCollection<DeviceInfoVM>();
}
bool hasCan = config.DeviceList.Any(d =>
string.Equals(d?.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase) ||
string.Equals(d?.DeviceType, "ZLGCANFD", StringComparison.OrdinalIgnoreCase));
if (!hasCan)
{
config.DeviceList.Add(new DeviceInfoVM
{
DeviceName = "CAN",
DeviceType = "ZLGCANFD",
Remark = "周立功 CANFD 接口卡",
ConnectionType = "CAN",
IsEnabled = true,
IsConnected = false
});
}
}
}
}

View File

@@ -8,6 +8,7 @@ using System.IO.Ports;
using System.Linq;
using System.Reflection;
using UIShare.UIViewModel;
using ZLGUSBCANFD;
namespace UIShare.GlobalVariable
{
@@ -28,6 +29,7 @@ namespace UIShare.GlobalVariable
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
public ZLGCANFD CANFD { get; set; }
public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo)
{
@@ -37,6 +39,7 @@ namespace UIShare.GlobalVariable
_scopeName = _systemConfig.Title;
InitDevices();
}
/// <summary>
/// 根据设备配置提取唯一的硬件指纹字符串。
/// <para>Tcp → "Tcp:IP:Port"Serial → "Serial:PortName";无法识别则返回空字符串。</para>
@@ -55,9 +58,14 @@ namespace UIShare.GlobalVariable
return $"Serial:{config.SerialPortConfig.PortName}";
}
if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase)
&& config.CANConfig != null)
{
return $"CAN:{config.CANConfig.DeviceIndex}";
}
return string.Empty;
}
private void InitDevices()
{
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
@@ -68,6 +76,53 @@ namespace UIShare.GlobalVariable
{
if (config == null || !config.IsEnabled) continue;
// CAN 设备ZLGCANFD 不实现 IBaseInterface通过 SystemConfig.CANFD 单独管理,
// 按指纹从全局 CanPool 创建/复用实例,并注册作用域引用计数。
if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase))
{
var fingerprint = ExtractHardwareFingerprint(config);
if (string.IsNullOrEmpty(fingerprint))
{
LoggerHelper.Warn($"设备 [{config.DeviceName}] 无法提取硬件指纹(连接方式={config.ConnectionType}),已跳过。");
continue;
}
if (config.CANConfig == null)
{
LoggerHelper.Warn($"设备 [{config.DeviceName}] 缺少 CAN 连接参数,已跳过。");
continue;
}
try
{
// 按指纹全局唯一创建 ZLGCANFD 实例maxChannels 默认 4对应 USBCANFD-400U
var canLazy = _globalInfo.CanPool.GetOrAdd(fingerprint, key => new Lazy<ZLGCANFD>(() =>
new ZLGCANFD(config.CANConfig.DeviceType, config.CANConfig.DeviceIndex, 4)));
_systemConfig.CANFD = canLazy.Value;
CANFD = canLazy.Value;
// 注册作用域引用计数
if (!string.IsNullOrEmpty(_scopeName))
{
var scopeList = _globalInfo.DeviceAndScopeDic.GetOrAdd(fingerprint,
_ => new Lazy<List<string>>(() => new List<string>())).Value;
lock (scopeList)
{
if (!scopeList.Contains(_scopeName))
scopeList.Add(_scopeName);
}
}
LoggerHelper.Info($"已加载 CAN 设备 [{config.DeviceName}] 指纹={fingerprint}(通过 SystemConfig.CANFD 管理)");
}
catch (Exception ex)
{
LoggerHelper.ErrorWithNotify(_scopeName, $"CAN 设备 [{config.DeviceName}] 实例化失败:{ex.Message}");
}
continue;
}
if (string.IsNullOrWhiteSpace(config.DeviceType) ||
!_deviceTypeMap.TryGetValue(config.DeviceType, out var deviceType))
{
@@ -391,6 +446,17 @@ namespace UIShare.GlobalVariable
}
}
// 尝试从 CanPool 取出并销毁
if (_globalInfo.CanPool.TryRemove(fingerprint, out var canLazy))
{
if (canLazy.IsValueCreated)
{
try { canLazy.Value.Dispose(); }
catch { /* 销毁时忽略异常 */ }
LoggerHelper.Info($"指纹 [{fingerprint}] 无作用域引用,已销毁 CAN 设备实例。");
}
}
// 同步清除 DeviceAndScopeDic 中的空条目
_globalInfo.DeviceAndScopeDic.TryRemove(fingerprint, out _);
}

View File

@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ZLGUSBCANFD;
namespace UIShare.GlobalVariable
{
@@ -19,6 +20,9 @@ namespace UIShare.GlobalVariable
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
/// <summary>CAN 硬件指纹 → ZLGCANFD 实例的并发池,确保同一 CAN 卡全局只创建一个驱动实例。</summary>
public ConcurrentDictionary<string, Lazy<ZLGCANFD>> CanPool { get; set; }
/// <summary>硬件指纹 → 正在使用该设备的作用域名称列表,用于引用计数与安全销毁。</summary>
public ConcurrentDictionary<string, Lazy<List<string>>> DeviceAndScopeDic { get; set; }
@@ -45,6 +49,7 @@ namespace UIShare.GlobalVariable
ConfigDic = new();
ScopeDic = new();
HardwarePool = new ConcurrentDictionary<string, Lazy<IBaseInterface>>(StringComparer.OrdinalIgnoreCase);
CanPool = new ConcurrentDictionary<string, Lazy<ZLGCANFD>>(StringComparer.OrdinalIgnoreCase);
DeviceAndScopeDic = new ConcurrentDictionary<string, Lazy<List<string>>>(StringComparer.OrdinalIgnoreCase);
CurrentScope = "default";
}

View File

@@ -0,0 +1,207 @@
using Common.Attributes;
using DeviceCommand.Base;
using Model.Models;
using Prism.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using UIShare.PubEvent;
namespace UIShare.GlobalVariable
{
/// <summary>
/// 硬件数据广播器(中间层,全局单例):
/// 直接基于 GlobalInfo.HardwarePool 中的设备指纹字典进行统一采样,
/// 每个物理设备只采样一次,然后向所有引用该设备的作用域分别广播 HardwareDataReportedEvent。
/// 解决多作用域共享同一物理设备时的重复采样问题。
/// DeviceCommand 层无需引用 Prism完全解耦。
/// </summary>
public class HardwareDataBroadcaster : IDisposable
{
private readonly IEventAggregator _eventAggregator;
private readonly GlobalInfo _globalInfo;
/// <summary>采样定时器</summary>
private readonly DispatcherTimer _sampleTimer;
/// <summary>
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
/// 委托签名统一为 Func&lt;object, CancellationToken, Task&lt;string&gt;&gt;
/// - 第一个参数 = 设备实例open delegate 风格,编译时已做类型转换)
/// - 第二个参数 = CancellationToken无参方法会忽略它
/// - 返回值 = Task&lt;string&gt;
/// </summary>
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<string>> Invoker, object Device)> _registeredMethods = new();
private CancellationTokenSource? _cts;
private bool _disposed;
/// <summary>采样间隔(默认 1000ms</summary>
public TimeSpan SampleInterval
{
get => _sampleTimer.Interval;
set => _sampleTimer.Interval = value;
}
public HardwareDataBroadcaster(GlobalInfo globalInfo, IEventAggregator eventAggregator)
{
_globalInfo = globalInfo;
_eventAggregator = eventAggregator;
_sampleTimer = new DispatcherTimer(DispatcherPriority.Normal)
{
Interval = TimeSpan.FromMilliseconds(1000)
};
_sampleTimer.Tick += OnSampleTick;
}
/// <summary>
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
/// 统一签名为 Func&lt;object, CancellationToken, Task&lt;string&gt;&gt;
/// - 无参方法:忽略 CancellationToken
/// - 带 CancellationToken 方法:直接透传
/// </summary>
private static Func<object, CancellationToken, Task<string>> BuildInvoker(MethodInfo method)
{
var deviceParam = Expression.Parameter(typeof(object), "device");
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
// 将 object 转型为方法的声明类型
var castDevice = Expression.Convert(deviceParam, method.DeclaringType!);
// 根据方法签名决定传参
var parms = method.GetParameters();
Expression[] callArgs = parms.Length == 1
? new Expression[] { ctParam }
: Array.Empty<Expression>();
var call = Expression.Call(castDevice, method, callArgs);
return Expression.Lambda<Func<object, CancellationToken, Task<string>>>(
call, deviceParam, ctParam).Compile();
}
/// <summary>
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
/// 反查可监测方法并编译为委托。每个指纹只注册一次,反射只执行一次。
/// </summary>
public void Discover()
{
_registeredMethods.Clear();
foreach (var poolEntry in _globalInfo.HardwarePool)
{
string fingerprint = poolEntry.Key;
var lazy = poolEntry.Value;
// 只监控已经实例化的设备
if (!lazy.IsValueCreated || lazy.Value == null) continue;
var device = lazy.Value;
var deviceType = device.GetType();
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false;
var parms = m.GetParameters();
return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
});
foreach (var method in methods)
{
var invoker = BuildInvoker(method);
_registeredMethods.Add((fingerprint, method.Name, invoker, device));
}
}
}
/// <summary>获取指定硬件指纹当前被哪些作用域引用</summary>
private List<string> GetScopesForFingerprint(string fingerprint)
{
if (_globalInfo.DeviceAndScopeDic.TryGetValue(fingerprint, out var lazy))
{
var scopeList = lazy.Value;
lock (scopeList) return scopeList.ToList();
}
return new List<string>();
}
/// <summary>启动采样广播(幂等)</summary>
public void Start()
{
if (_disposed) return;
_cts ??= new CancellationTokenSource();
if (!_sampleTimer.IsEnabled) _sampleTimer.Start();
}
/// <summary>停止采样</summary>
public void Stop()
{
_sampleTimer.Stop();
}
private void OnSampleTick(object? sender, EventArgs e)
{
if (_registeredMethods.Count == 0 || _disposed) return;
var token = _cts?.Token ?? CancellationToken.None;
var now = DateTime.Now;
foreach (var entry in _registeredMethods)
{
// fire-and-forget每个通道独立采样完成后自行广播
_ = Task.Run(async () =>
{
try
{
// 直接委托调用,零反射开销
string raw = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
if (!double.TryParse(raw, out double value)) return;
// 向所有引用该物理设备的作用域分别广播
var scopes = GetScopesForFingerprint(entry.Fingerprint);
foreach (var scope in scopes)
{
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Publish(new HardwareReportArgs
{
Scope = scope,
HardwareFingerprint = entry.Fingerprint,
MethodName = entry.MethodName,
Value = value,
Time = now
});
}
}
catch
{
// 单个通道故障不干扰其他通道
}
}, token);
}
// 不在 UI 线程 await——任务在线程池上自行完成并广播事件
// DispatcherTimer 按设定间隔准时触发下一次 Tick。
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_sampleTimer.Stop();
_sampleTimer.Tick -= OnSampleTick;
try { _cts?.Cancel(); } catch { }
_cts?.Dispose();
_cts = null;
_registeredMethods.Clear();
}
}
}

View File

@@ -564,7 +564,11 @@ namespace UIShare
{
try
{
instance = _deviceManager.DeviceMap[targetType.Name];
if(targetType.Name== "ZLGCANFD")
{
instance = _deviceManager.CANFD;
}
else instance = _deviceManager.DeviceMap[targetType.Name];
}
catch (Exception ex)
{

View File

@@ -9,6 +9,7 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UIShare.UIViewModel;
using ZLGUSBCANFD;
using static UIShare.UIViewModel.ParameterVM;
namespace UIShare.GlobalVariable
@@ -29,6 +30,7 @@ namespace UIShare.GlobalVariable
public string DefaultDBCFilePath { get; set; } = "";
public ObservableCollection<DeviceInfoVM> DeviceList = new();
public ObservableCollection<SharedParameter> SharedParameterList = new();
public ZLGCANFD CANFD = new();
[JsonIgnore]
public ObservableCollection<ParameterVM> ParameterList = new()
{

View File

@@ -0,0 +1,13 @@
using Model.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace UIShare.PubEvent
{
public class HardwareDataReportedEvent : PubSubEvent<HardwareReportArgs>
{
}
}

View File

@@ -20,5 +20,6 @@
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\Logger\Logger.csproj" />
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,76 @@
using Prism.Mvvm;
namespace UIShare.UIViewModel
{
/// <summary>
/// CAN 连接配置(对应 ZLGCANFD 构造函数 + 初始化并启动通道 参数)。
/// </summary>
public class CANConfigVM : BindableBase
{
// ===== ZLGCANFD 构造函数参数 =====
private uint _deviceType = 43;
/// <summary>设备类型号43 = USBCANFD-400U</summary>
public uint DeviceType
{
get => _deviceType;
set => SetProperty(ref _deviceType, value);
}
private uint _deviceIndex = 0;
/// <summary>设备索引</summary>
public uint DeviceIndex
{
get => _deviceIndex;
set => SetProperty(ref _deviceIndex, value);
}
private string _abitBaud = "500000";
/// <summary>仲裁域波特率</summary>
public string ABitBaud
{
get => _abitBaud;
set => SetProperty(ref _abitBaud, value);
}
private string _dbitBaud = "2000000";
/// <summary>数据域波特率</summary>
public string DBitBaud
{
get => _dbitBaud;
set => SetProperty(ref _dbitBaud, value);
}
private bool _enableTerminalResistance = true;
/// <summary>是否开启终端电阻</summary>
public bool EnableTerminalResistance
{
get => _enableTerminalResistance;
set => SetProperty(ref _enableTerminalResistance, value);
}
public CANConfigVM() { }
/// <summary>拷贝构造,用于对话框编辑副本。</summary>
public CANConfigVM(CANConfigVM? src)
{
if (src == null) return;
DeviceType = src.DeviceType;
DeviceIndex = src.DeviceIndex;
ABitBaud = src.ABitBaud;
DBitBaud = src.DBitBaud;
EnableTerminalResistance = src.EnableTerminalResistance;
}
/// <summary>把字段拷回目标对象(保存时用)。</summary>
public void CopyTo(CANConfigVM? dst)
{
if (dst == null) return;
dst.DeviceType = DeviceType;
dst.DeviceIndex = DeviceIndex;
dst.ABitBaud = ABitBaud;
dst.DBitBaud = DBitBaud;
dst.EnableTerminalResistance = EnableTerminalResistance;
}
}
}

View File

@@ -65,5 +65,13 @@ namespace UIShare.UIViewModel
get => _serialPortConfig;
set => SetProperty(ref _serialPortConfig, value);
}
/// <summary>CAN 连接参数(对应 ZLGCANFD 构造 + 通道初始化参数)。</summary>
private CANConfigVM _canConfig = new();
public CANConfigVM CANConfig
{
get => _canConfig;
set => SetProperty(ref _canConfig, value);
}
}
}

729
ZLGUSBCANFD/USBCANFD.cs Normal file
View File

@@ -0,0 +1,729 @@
using Common.Attributes;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ZLGUSBCANFD
{
[ADPCommand]
public class ZLGCANFD : IDisposable
{
// 硬件设备与通道句柄
private IntPtr _deviceHandle = IntPtr.Zero;
private readonly IntPtr[] _channelHandles;
// 每个通道分配独立的 DBC 引擎句柄与锁彻底解决台架间DBC冲突与锁竞争
private readonly uint[] _dbcHandles;
private readonly bool[] _isDbcLoadedArray;
private readonly object[] _channelLocks; // 通道级细粒度锁
// 异步高性能接收线程控制
private volatile bool _isRunning = false;
private readonly List<Thread> _receiveThreads = new List<Thread>();
// DBC 循环发送任务管理Key = (通道号, 帧ID)
private readonly ConcurrentDictionary<(uint , uint ID), CancellationTokenSource> _cyclicSenders = new ConcurrentDictionary<(uint, uint), CancellationTokenSource>();
// 动态硬件参数
private readonly uint _deviceType; // 76: USBCANFD-400U
private readonly uint _deviceIndex; // 设备索引
private readonly int _maxChannels; // 动态通道数
/// <summary>
/// 事件:当接收到 CAN/CANFD 报文并且通过 DBC 成功解析后触发
/// 参数uint 通道号 (0,1,2,3), ZDBC.DBCMessage 解析后的DBC消息结构体
/// </summary>
public event Action<uint, ZDBC.DBCMessage>? OnDbcMessageDecoded;
public ZLGCANFD(uint deviceType = 76, uint deviceIndex = 0, int maxChannels = 4)
{
_deviceType = deviceType;
_deviceIndex = deviceIndex;
_maxChannels = maxChannels;
// 初始化通道相关状态数组
_channelHandles = new IntPtr[_maxChannels];
_dbcHandles = new uint[_maxChannels];
_isDbcLoadedArray = new bool[_maxChannels];
_channelLocks = new object[_maxChannels];
for (int i = 0; i < _maxChannels; i++)
{
_channelHandles[i] = IntPtr.Zero;
_dbcHandles[i] = 0;
_isDbcLoadedArray[i] = false;
_channelLocks[i] = new object();
}
}
#region 1.
/// <summary>
/// 仅仅打开设备,不做具体的通道波特率配置(留给具体的台架去分别配置)
/// </summary>
public virtual bool ()
{
if (_deviceHandle != IntPtr.Zero) return true;
_deviceHandle = ZLGCAN.ZCAN_OpenDevice(ZLGCAN.ZCAN_USBCANFD_400U, _deviceIndex, 0);
return _deviceHandle != IntPtr.Zero;
}
/// <summary>
/// 针对特定通道进行参数初始化并启动
/// </summary>
public virtual bool (uint , string abitBaud = "500000", string dbitBaud = "2000000", bool = true)
{
if (_deviceHandle == IntPtr.Zero) throw new InvalidOperationException("请先调用 '打开设备()' 才能初始化通道。");
if ( >= _maxChannels) return false;
lock (_channelLocks[])
{
// 如果已经启动过,先复位
if (_channelHandles[] != IntPtr.Zero)
{
ZLGCAN.ZCAN_ResetCAN(_channelHandles[]);
}
// 1. 设置该通道专属的仲裁域与数据域波特率
if (ZLGCAN.ZCAN_SetValue(_deviceHandle, $"{通道号}/canfd_abit_baud_rate", abitBaud) != 1) return false;
if (ZLGCAN.ZCAN_SetValue(_deviceHandle, $"{通道号}/canfd_dbit_baud_rate", dbitBaud) != 1) return false;
// 2. 设置该通道专属内部终端电阻状态
string resistanceStr = ? "1" : "0";
if (ZLGCAN.ZCAN_SetValue(_deviceHandle, $"{通道号}/initenal_resistance", resistanceStr) != 1) return false;
// 3. 规范配置通道结构体
ZLGCAN.ZCAN_CHANNEL_INIT_CONFIG config = new ZLGCAN.ZCAN_CHANNEL_INIT_CONFIG();
config.can_type = 1; // 1 代表 CANFD 模式
config.config.canfd.mode = 0; // 0 代表正常工作模式
IntPtr pConfig = Marshal.AllocHGlobal(Marshal.SizeOf(config));
Marshal.StructureToPtr(config, pConfig, true);
_channelHandles[] = ZLGCAN.ZCAN_InitCAN(_deviceHandle, , pConfig);
Marshal.FreeHGlobal(pConfig);
if (_channelHandles[] == IntPtr.Zero) return false;
// 4. 启动 CAN 通道
if (ZLGCAN.ZCAN_StartCAN(_channelHandles[]) != 1) return false;
// 5. 为该通道启动专属的独立后台高性能轮询接收线程(如果尚未启动轮询)
if (!_isRunning) _isRunning = true;
int chnIdx = (int);
Thread rxThread = new Thread(() => (_channelHandles[chnIdx], (uint)chnIdx))
{
IsBackground = true,
Name = $"ZLGCANFD_Dev{_deviceIndex}_CH{chnIdx}_RxThread"
};
_receiveThreads.Add(rxThread);
rxThread.Start();
return true;
}
}
public virtual void CAN卡设备()
{
_isRunning = false;
();
Thread.Sleep(50); // 确保轮询线程安全退出
_receiveThreads.Clear();
// 动态复位所有通道并释放DBC
for (uint i = 0; i < _maxChannels; i++)
{
lock (_channelLocks[i])
{
if (_channelHandles[i] != IntPtr.Zero)
{
ZLGCAN.ZCAN_ResetCAN(_channelHandles[i]);
_channelHandles[i] = IntPtr.Zero;
}
DBC(i);
}
}
// 关闭设备主句柄
if (_deviceHandle != IntPtr.Zero)
{
ZLGCAN.ZCAN_CloseDevice(_deviceHandle);
_deviceHandle = IntPtr.Zero;
}
}
#endregion
#region 2. DBC
/// <summary>
/// 指定通道加载特定的 DBC 文件支持不同通道加载不同的DBC矩阵
/// </summary>
public virtual bool DBC文件(uint , string dbcFilePath)
{
if ( >= _maxChannels) return false;
lock (_channelLocks[])
{
try
{
if (_isDbcLoadedArray[]) return true;
uint chDbcHandle = ZDBC.ZDBC_Init();
if (chDbcHandle == 0) return false;
IntPtr ptrPath = Marshal.StringToHGlobalAnsi(dbcFilePath);
bool success = ZDBC.ZDBC_LoadFile(chDbcHandle, ptrPath);
Marshal.FreeHGlobal(ptrPath);
if (success)
{
_dbcHandles[] = chDbcHandle;
_isDbcLoadedArray[] = true;
}
else
{
ZDBC.ZDBC_Release(chDbcHandle);
}
return success;
}
catch
{
return false;
}
}
}
public virtual void DBC(uint )
{
if ( >= _maxChannels) return;
lock (_channelLocks[])
{
if (_isDbcLoadedArray[] && _dbcHandles[] != 0)
{
ZDBC.ZDBC_Release(_dbcHandles[]);
_dbcHandles[] = 0;
_isDbcLoadedArray[] = false;
}
}
}
#endregion
#region 3. DBC
private void (IntPtr channelHandle, uint channelIndex)
{
const int bufferSize = 100;
int canStructSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_Receive_Data));
int canfdStructSize = Marshal.SizeOf(typeof(ZLGCAN.ZCAN_ReceiveFD_Data));
int dbcMsgSize = Marshal.SizeOf(typeof(ZDBC.DBCMessage));
IntPtr ptrCanBuffer = Marshal.AllocHGlobal(canStructSize * bufferSize);
IntPtr ptrCanFDBuffer = Marshal.AllocHGlobal(canfdStructSize * bufferSize);
IntPtr ptrDbcMsg = Marshal.AllocHGlobal(dbcMsgSize);
try
{
while (_isRunning)
{
bool currentTurnHasData = false;
// 1. 自动提取并解析经典 CAN 帧
uint canNum = ZLGCAN.ZCAN_GetReceiveNum(channelHandle, 0);
if (canNum > 0)
{
uint actualRecv = ZLGCAN.ZCAN_Receive(channelHandle, ptrCanBuffer, bufferSize, 5);
for (int i = 0; i < actualRecv; i++)
{
IntPtr framePtr = IntPtr.Add(ptrCanBuffer, i * canStructSize);
// 只锁定当前通道的DBC句柄进行解码高并发完全不卡顿
lock (_channelLocks[channelIndex])
{
if (_isDbcLoadedArray[channelIndex] && ZDBC.ZDBC_Decode(_dbcHandles[channelIndex], ptrDbcMsg, framePtr, 1, 0))
{
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
OnDbcMessageDecoded?.Invoke(channelIndex, msg);
}
}
}
currentTurnHasData = true;
}
// 2. 自动提取并解析高速 CANFD 帧
uint canfdNum = ZLGCAN.ZCAN_GetReceiveNum(channelHandle, 1);
if (canfdNum > 0)
{
uint actualRecvFd = ZLGCAN.ZCAN_ReceiveFD(channelHandle, ptrCanFDBuffer, bufferSize, 5);
for (int i = 0; i < actualRecvFd; i++)
{
IntPtr framePtr = IntPtr.Add(ptrCanFDBuffer, i * canfdStructSize);
lock (_channelLocks[channelIndex])
{
if (_isDbcLoadedArray[channelIndex] && ZDBC.ZDBC_Decode(_dbcHandles[channelIndex], ptrDbcMsg, framePtr, 1, 1))
{
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
OnDbcMessageDecoded?.Invoke(channelIndex, msg);
}
}
}
currentTurnHasData = true;
}
if (!currentTurnHasData)
{
Thread.Sleep(2);
}
}
}
finally
{
Marshal.FreeHGlobal(ptrCanBuffer);
Marshal.FreeHGlobal(ptrCanFDBuffer);
Marshal.FreeHGlobal(ptrDbcMsg);
}
}
#endregion
#region 4. DBC
/// <summary>
/// 发送一条 DBC 定义报文。
/// </summary>
/// <param name="通道号">通道号</param>
/// <param name="帧ID">DBC 中定义的帧 ID</param>
/// <param name="信号物理值字典">信号名称 → 物理值;只设置字典中包含的信号,其余信号保持 DBC 默认值</param>
/// <param name="循环间隔毫秒">0 = 只发送一次;>0 = 按指定间隔循环发送(毫秒)</param>
/// <param name="是否使用CANFD">1 = CANFD0 = CAN</param>
/// <returns>是否成功启动/发送</returns>
public virtual bool DBC定义报文(
uint ,
uint ID,
Dictionary<string, double> ,
int = 0,
int 使CANFD = 1)
{
if ( >= _maxChannels || _channelHandles[] == IntPtr.Zero) return false;
if (!_isDbcLoadedArray[]) throw new InvalidOperationException($"通道 {通道号} 的 DBC 协议未加载,无法执行打包发送指令。");
if ( < 0) throw new ArgumentOutOfRangeException(nameof(), "循环间隔必须大于或等于 0。");
if ( == null || .Count == 0)
throw new ArgumentException("信号物理值字典不能为空。", nameof());
if ( == 0)
{
// 单次发送:同步执行
lock (_channelLocks[])
{
return DBC定义报文单次(, ID, , 使CANFD);
}
}
// 循环发送:先停止同通道同帧 ID 的旧循环,再启动新循环
(, ID);
var cts = new CancellationTokenSource();
_cyclicSenders[(, ID)] = cts;
Task.Run(async () =>
{
while (!cts.Token.IsCancellationRequested)
{
try
{
lock (_channelLocks[])
{
if (_channelHandles[] == IntPtr.Zero) break;
DBC定义报文单次(, ID, , 使CANFD);
}
await Task.Delay(, cts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
// 循环发送出错时退出,避免刷屏
Console.WriteLine($"[ZLGCANFD] 循环发送 DBC 报文失败: {ex.Message}");
break;
}
}
_cyclicSenders.TryRemove((, ID), out var removedCts);
removedCts?.Dispose();
}, cts.Token);
return true;
}
/// <summary>
/// 停止指定通道、指定帧 ID 的循环发送。
/// </summary>
public virtual void (uint , uint ID)
{
if (_cyclicSenders.TryRemove((, ID), out var cts))
{
cts.Cancel();
cts.Dispose();
}
}
/// <summary>
/// 停止所有 DBC 循环发送任务。
/// </summary>
public virtual void ()
{
foreach (var kvp in _cyclicSenders)
{
kvp.Value.Cancel();
kvp.Value.Dispose();
}
_cyclicSenders.Clear();
}
/// <summary>
/// 单次发送 DBC 报文(调用方已持有通道锁)。
/// </summary>
private bool DBC定义报文单次(uint , uint ID, Dictionary<string, double> , int 使CANFD)
{
uint chDbcHandle = _dbcHandles[];
IntPtr ptrDbcMsg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCMessage)));
IntPtr ptrCount = Marshal.AllocHGlobal(sizeof(uint));
Marshal.WriteInt32(ptrCount, 1);
try
{
if (!ZDBC.ZDBC_GetMessageById(chDbcHandle, ID, ptrDbcMsg)) return false;
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
// 根据传入的物理值设置对应信号的原始值
for (int i = 0; i < msg.nSignalCount; i++)
{
var signal = msg.vSignals[i];
string signalName = Encoding.Default.GetString(signal.strName).TrimEnd('\0');
if (.TryGetValue(signalName, out double physicalValue))
{
signal.nRawvalue = (signal, physicalValue);
msg.vSignals[i] = signal;
}
}
Marshal.StructureToPtr(msg, ptrDbcMsg, true);
if (使CANFD == 0)
{
IntPtr ptrCanFrame = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZLGCAN.can_frame)));
try
{
if (!ZDBC.ZDBC_Encode(chDbcHandle, ptrCanFrame, ptrCount, ptrDbcMsg, 0)) return false;
ZLGCAN.can_frame canFrame = (ZLGCAN.can_frame)Marshal.PtrToStructure(ptrCanFrame, typeof(ZLGCAN.can_frame));
canFrame.__pad |= 0x20;
ZLGCAN.ZCAN_Transmit_Data txData = new ZLGCAN.ZCAN_Transmit_Data { frame = canFrame, transmit_type = 0 };
IntPtr pTx = Marshal.AllocHGlobal(Marshal.SizeOf(txData));
try
{
Marshal.StructureToPtr(txData, pTx, true);
return ZLGCAN.ZCAN_Transmit(_channelHandles[], pTx, 1) == 1;
}
finally
{
Marshal.FreeHGlobal(pTx);
}
}
finally
{
Marshal.FreeHGlobal(ptrCanFrame);
}
}
else
{
IntPtr ptrCanFDFrame = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZLGCAN.canfd_frame)));
try
{
if (!ZDBC.ZDBC_Encode(chDbcHandle, ptrCanFDFrame, ptrCount, ptrDbcMsg, 1)) return false;
ZLGCAN.canfd_frame canfdFrame = (ZLGCAN.canfd_frame)Marshal.PtrToStructure(ptrCanFDFrame, typeof(ZLGCAN.canfd_frame));
canfdFrame.flags |= 0x20;
ZLGCAN.ZCAN_TransmitFD_Data txFdData = new ZLGCAN.ZCAN_TransmitFD_Data { frame = canfdFrame, transmit_type = 0 };
IntPtr pTxFd = Marshal.AllocHGlobal(Marshal.SizeOf(txFdData));
try
{
Marshal.StructureToPtr(txFdData, pTxFd, true);
return ZLGCAN.ZCAN_TransmitFD(_channelHandles[], pTxFd, 1) == 1;
}
finally
{
Marshal.FreeHGlobal(pTxFd);
}
}
finally
{
Marshal.FreeHGlobal(ptrCanFDFrame);
}
}
}
finally
{
Marshal.FreeHGlobal(ptrDbcMsg);
Marshal.FreeHGlobal(ptrCount);
}
}
/// <summary>
/// 不依赖通道锁的物理值到原始值转换。
/// </summary>
private static ulong (ZDBC.DBCSignal , double )
{
IntPtr pSignal = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCSignal)));
IntPtr pValue = Marshal.AllocHGlobal(sizeof(double));
try
{
Marshal.StructureToPtr(, pSignal, true);
Marshal.StructureToPtr(, pValue, true);
return ZDBC.ZDBC_CalcRawValue(pSignal, pValue);
}
finally
{
Marshal.FreeHGlobal(pSignal);
Marshal.FreeHGlobal(pValue);
}
}
#endregion
#region 4.1 仿/ ()
/// <summary>
/// 设置并发送指定信号值。
/// 其余未指定信号使用 DBC 中定义的初始值填充,避免总线数据被意外清零。
/// </summary>
/// <param name="通道号">CAN/CANFD 通道索引</param>
/// <param name="帧ID">对应报文的帧 ID</param>
/// <param name="信号名称">DBC 中定义的英文信号名称(不区分大小写)</param>
/// <param name="物理值">要写入的实际物理数值</param>
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
/// <returns>操作是否成功</returns>
public virtual bool (uint , uint ID, string , double , int = 0)
{
if ( >= _maxChannels || _channelHandles[] == IntPtr.Zero) return false;
if (!_isDbcLoadedArray[]) return false;
if (string.IsNullOrWhiteSpace()) return false;
var merged = (, ID, new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase)
{
{ , }
});
return DBC定义报文(, ID, merged, );
}
/// <summary>
/// 发送报文:以 DBC 中所有信号的初始值作为默认值发送。
/// 如果之前调用 设置报文 保存过覆盖值,则会优先使用覆盖值。
/// </summary>
/// <param name="通道号">CAN/CANFD 通道索引</param>
/// <param name="帧ID">DBC 中定义的帧 ID</param>
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(毫秒)</param>
/// <param name="是否使用CANFD">1 = 以 CANFD 格式发送0 = 经典 CAN 格式</param>
public virtual bool (uint , uint ID, int = 0, int 使CANFD = 1)
{
if ( >= _maxChannels || _channelHandles[] == IntPtr.Zero) return false;
if (!_isDbcLoadedArray[]) return false;
if ( < 0) throw new ArgumentOutOfRangeException(nameof());
var merged = (, ID);
return DBC定义报文(, ID, merged, , 使CANFD);
}
/// <summary>
/// 发送自定义报文(原始字节)。
/// </summary>
/// <param name="通道号">CAN/CANFD 通道索引</param>
/// <param name="帧ID">帧 ID扩展帧会自动置位 0x80000000</param>
/// <param name="原始数据">原始帧数据</param>
/// <param name="dlcLength">数据长度CANFD 仅允许 0-8,12,16,20,24,32,48,64</param>
/// <param name="是否是扩展帧">是否为 29 位扩展帧</param>
/// <param name="是否是CANFD">true = CANFDfalse = 经典 CAN</param>
/// <param name="开启波特率加速BRS">CANFD 是否开启波特率加速</param>
public virtual bool (
uint ,
uint ID,
byte[] ,
byte dlcLength,
bool = false,
bool CANFD = true,
bool BRS = true)
{
if ( >= _maxChannels || _channelHandles[] == IntPtr.Zero) return false;
if ( == null) return false;
// 扩展帧需要把最高位标志位置起来
uint canId = ID & 0x7FFFFFFF;
if () canId |= 0x80000000;
lock (_channelLocks[])
{
if (CANFD)
{
// CANFD 有效 DLC 校验
byte validFdDlc = CANFD的Dlc(dlcLength);
ZLGCAN.canfd_frame fdFrame = new ZLGCAN.canfd_frame
{
can_id = canId,
len = validFdDlc,
flags = (byte)(( ? 0x01 : 0x00) | (BRS ? 0x02 : 0x00) | 0x20), // 0x20 本地回显
data = new byte[64]
};
Array.Copy(, 0, fdFrame.data, 0, Math.Min(.Length, 64));
ZLGCAN.ZCAN_TransmitFD_Data txFdData = new ZLGCAN.ZCAN_TransmitFD_Data { frame = fdFrame, transmit_type = 0 };
IntPtr pTxFd = Marshal.AllocHGlobal(Marshal.SizeOf(txFdData));
try
{
Marshal.StructureToPtr(txFdData, pTxFd, true);
return ZLGCAN.ZCAN_TransmitFD(_channelHandles[], pTxFd, 1) == 1;
}
finally { Marshal.FreeHGlobal(pTxFd); }
}
else
{
ZLGCAN.can_frame standardFrame = new ZLGCAN.can_frame
{
can_id = canId,
can_dlc = dlcLength > 8 ? (byte)8 : dlcLength,
__pad = 0x20, // 发送回显
data = new byte[8]
};
Array.Copy(, 0, standardFrame.data, 0, Math.Min(.Length, 8));
ZLGCAN.ZCAN_Transmit_Data txData = new ZLGCAN.ZCAN_Transmit_Data { frame = standardFrame, transmit_type = 0 };
IntPtr pTx = Marshal.AllocHGlobal(Marshal.SizeOf(txData));
try
{
Marshal.StructureToPtr(txData, pTx, true);
return ZLGCAN.ZCAN_Transmit(_channelHandles[], pTx, 1) == 1;
}
finally { Marshal.FreeHGlobal(pTx); }
}
}
}
/// <summary>
/// 构建发送用信号字典DBC 初始值 + 额外覆盖。
/// 调用方需自行保证通道已初始化且 DBC 已加载。
/// </summary>
private Dictionary<string, double> (uint , uint ID, Dictionary<string, double>? = null)
{
uint chDbcHandle = _dbcHandles[];
IntPtr ptrDbcMsg = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCMessage)));
try
{
lock (_channelLocks[])
{
if (!ZDBC.ZDBC_GetMessageById(chDbcHandle, ID, ptrDbcMsg))
return new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
var dict = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < msg.nSignalCount; i++)
{
var signal = msg.vSignals[i];
string signalName = Encoding.Default.GetString(signal.strName).TrimEnd('\0');
if (!string.IsNullOrEmpty(signalName) && signal.initialValueValid != 0)
{
// ZDBC 文档标注 initialValue 为“原始值”,但实际按 DBC 规范这里存放的是物理值
dict[signalName] = signal.initialValue;
}
}
if ( != null)
{
foreach (var kvp in )
{
dict[kvp.Key] = kvp.Value;
}
}
return dict;
}
}
finally
{
Marshal.FreeHGlobal(ptrDbcMsg);
}
}
/// <summary>
/// CANFD 有效 DLC 校验,非法值自动就近取到下一个合法 DLC。
/// </summary>
private static byte CANFD的Dlc(byte dlc)
{
if (dlc <= 8) return dlc;
if (dlc <= 12) return 12;
if (dlc <= 16) return 16;
if (dlc <= 20) return 20;
if (dlc <= 24) return 24;
if (dlc <= 32) return 32;
if (dlc <= 48) return 48;
return 64;
}
#endregion
#region 5.
/// <summary>
/// 计算物理值也需要传入对应通道使用通道专属的DBC句柄进行计算
/// </summary>
[Browsable(false)]
public ulong (uint , ZDBC.DBCSignal , double )
{
if ( >= _maxChannels) return 0;
IntPtr pSignal = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.DBCSignal)));
IntPtr pValue = Marshal.AllocHGlobal(sizeof(double));
try
{
Marshal.StructureToPtr(, pSignal, true);
Marshal.StructureToPtr(, pValue, true);
lock (_channelLocks[])
{
return ZDBC.ZDBC_CalcRawValue(pSignal, pValue);
}
}
finally
{
Marshal.FreeHGlobal(pSignal);
Marshal.FreeHGlobal(pValue);
}
}
#endregion
[Browsable(false)]
public void Dispose()
{
CAN卡设备();
}
}
}

1150
ZLGUSBCANFD/ZLGAPI.cs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>false</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Interop.TSMasterAPI">
<HintPath>..\CanDriver\bin\Debug\net8.0\Interop.TSMasterAPI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>