Compare commits
6 Commits
571bdbf258
...
2820b2f300
| Author | SHA1 | Date | |
|---|---|---|---|
| 2820b2f300 | |||
| cb13d3c8b5 | |||
| 60dc155ccf | |||
| 63f43853fa | |||
| f27045153c | |||
| 8f124fb08d |
19
ADP.sln
19
ADP.sln
@@ -35,10 +35,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateInfoMoudle", "UpdateI
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonitorModule", "MonitorModule\MonitorModule.csproj", "{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonitorModule", "MonitorModule\MonitorModule.csproj", "{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}"
|
||||||
EndProject
|
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}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceEditModule", "DeviceEditModule\DeviceEditModule.csproj", "{170AD4C1-189D-4FBE-B10D-2A4304527834}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{170AD4C1-189D-4FBE-B10D-2A4304527834}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{170AD4C1-189D-4FBE-B10D-2A4304527834}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -125,5 +131,6 @@ Global
|
|||||||
{3456C3AC-E7FF-45F8-A68F-BEED9969812C} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
{3456C3AC-E7FF-45F8-A68F-BEED9969812C} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
{850A5BE8-0BC1-4D6A-8351-DD812C9C4427} = {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}
|
{170AD4C1-189D-4FBE-B10D-2A4304527834} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
|
{31F84D0E-3634-4C70-A830-21AF412E3BBF} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CAN驱动\CAN驱动.csproj" />
|
<ProjectReference Include="..\BenchMovementModule\BenchMovementModule.csproj" />
|
||||||
<ProjectReference Include="..\Command\Command.csproj" />
|
<ProjectReference Include="..\Command\Command.csproj" />
|
||||||
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
||||||
<ProjectReference Include="..\DeviceEditModule\DeviceEditModule.csproj" />
|
<ProjectReference Include="..\DeviceEditModule\DeviceEditModule.csproj" />
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
<ProjectReference Include="..\TestingModule\TestingModule.csproj" />
|
<ProjectReference Include="..\TestingModule\TestingModule.csproj" />
|
||||||
<ProjectReference Include="..\UIShare\UIShare.csproj" />
|
<ProjectReference Include="..\UIShare\UIShare.csproj" />
|
||||||
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
|
<ProjectReference Include="..\UpdateInfoMoudle\UpdateInfoMoudle.csproj" />
|
||||||
|
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ namespace ADP
|
|||||||
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
||||||
|
|
||||||
//初始化数据库
|
//初始化数据库
|
||||||
//DatabaseConfig.SetTenant(10001);
|
DatabaseConfig.SetTenant(10001);
|
||||||
//DatabaseConfig.InitSqlite("127.0.0.1", 3306, "ADP", "root", "123456");
|
DatabaseConfig.InitSqlite();
|
||||||
//DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
||||||
//SqlSugarContext.InitDatabase();
|
SqlSugarContext.InitDatabase();
|
||||||
//显示登录窗口
|
//显示登录窗口
|
||||||
var login = Container.Resolve<LoginModuleView>();
|
var login = Container.Resolve<LoginModuleView>();
|
||||||
var re = Container.Resolve<IRegionManager>();
|
var re = Container.Resolve<IRegionManager>();
|
||||||
@@ -84,6 +84,7 @@ namespace ADP
|
|||||||
containerRegistry.RegisterScoped<ScopedContext>();
|
containerRegistry.RegisterScoped<ScopedContext>();
|
||||||
containerRegistry.RegisterScoped<DeviceManager>();
|
containerRegistry.RegisterScoped<DeviceManager>();
|
||||||
containerRegistry.RegisterSingleton<GlobalInfo>();
|
containerRegistry.RegisterSingleton<GlobalInfo>();
|
||||||
|
containerRegistry.RegisterSingleton<HardwareDataBroadcaster>();
|
||||||
//注册AutoMapper
|
//注册AutoMapper
|
||||||
var config = new MapperConfiguration(
|
var config = new MapperConfiguration(
|
||||||
cfg => cfg.AddProfile<AutoMapperProfile>(),
|
cfg => cfg.AddProfile<AutoMapperProfile>(),
|
||||||
@@ -100,6 +101,8 @@ namespace ADP
|
|||||||
containerRegistry.RegisterInstance<INotificationManager>(NotificationManager);
|
containerRegistry.RegisterInstance<INotificationManager>(NotificationManager);
|
||||||
// 注册仓储
|
// 注册仓储
|
||||||
containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>));
|
containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>));
|
||||||
|
//注册服务
|
||||||
|
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
|
||||||
}
|
}
|
||||||
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
||||||
protected override IModuleCatalog CreateModuleCatalog()
|
protected override IModuleCatalog CreateModuleCatalog()
|
||||||
|
|||||||
@@ -762,8 +762,9 @@ namespace ADP.ViewModels
|
|||||||
_regionManager.RequestNavigate(settingRegion, "SettingView", settingParameters);
|
_regionManager.RequestNavigate(settingRegion, "SettingView", settingParameters);
|
||||||
IsLeftDrawerOpen = false;
|
IsLeftDrawerOpen = false;
|
||||||
break;
|
break;
|
||||||
case "更新界面":
|
case "台架移动界面":
|
||||||
_regionManager.RequestNavigate("ShellViewManager", "UpdateInfoView");
|
if (_globalInfo.CurrentScope != "default") return;
|
||||||
|
_regionManager.RequestNavigate("ShellViewManager", "BenchMovementView");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,11 +61,11 @@
|
|||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
Margin="8" />
|
Margin="8" />
|
||||||
|
|
||||||
<!--<Button Content="更新界面"
|
<Button Content="台架移动界面"
|
||||||
Command="{Binding NavigateCommand}"
|
Command="{Binding NavigateCommand}"
|
||||||
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
Margin="8" />-->
|
Margin="8" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</materialDesign:DrawerHost.LeftDrawerContent>
|
</materialDesign:DrawerHost.LeftDrawerContent>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|||||||
19
BenchMovementModule/BenchMovementModule.cs
Normal file
19
BenchMovementModule/BenchMovementModule.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
BenchMovementModule/BenchMovementModule.csproj
Normal file
14
BenchMovementModule/BenchMovementModule.csproj
Normal 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>
|
||||||
413
BenchMovementModule/ViewModels/BenchMovementViewModel.cs
Normal file
413
BenchMovementModule/ViewModels/BenchMovementViewModel.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
533
BenchMovementModule/Views/BenchMovementView.xaml
Normal file
533
BenchMovementModule/Views/BenchMovementView.xaml
Normal 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>
|
||||||
28
BenchMovementModule/Views/BenchMovementView.xaml.cs
Normal file
28
BenchMovementModule/Views/BenchMovementView.xaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
417
CAN驱动/CAN.cs
417
CAN驱动/CAN.cs
@@ -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}";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
@@ -172,7 +172,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 快捷设置电压电流(double 电压, double 电流, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +182,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询快捷电压电流设定(CancellationToken ct = default)
|
public virtual async Task<string> 查询快捷电压电流设定(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":APPLY?{SCPIDelimiter}", SCPIDelimiter, ct);
|
// 修正:去除前缀冒号
|
||||||
|
return await WriteReadAsync($"APPL?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -194,7 +196,8 @@ namespace DeviceCommand.Device
|
|||||||
[Monitorable("低压直流电源电流")]
|
[Monitorable("低压直流电源电流")]
|
||||||
public virtual async Task<string> 查询实际电流(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -203,16 +206,18 @@ namespace DeviceCommand.Device
|
|||||||
[Monitorable("低压直流电源功率")]
|
[Monitorable("低压直流电源功率")]
|
||||||
public virtual async Task<string> 查询实际功率(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
/// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
|
/// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
|
||||||
/// </summary
|
/// </summary>
|
||||||
[Monitorable("低压直流电源电压")]
|
[Monitorable("低压直流电源电压")]
|
||||||
public virtual async Task<string> 查询实际电压(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -220,7 +225,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询电压电流功率数组(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -228,7 +234,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询测得电池容量(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -236,7 +243,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询恒流输出时间长度(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -244,7 +252,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询总输出时间长度(CancellationToken ct = default)
|
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
|
#endregion
|
||||||
@@ -256,8 +265,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号,改用 OUTP 短格式
|
||||||
string 状态 = 开启 ? "ON" : "OFF";
|
string 状态 = 开启 ? "ON" : "OFF";
|
||||||
await SendAsync($":OUTPut {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"OUTP {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -265,7 +275,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询DC输出状态(CancellationToken ct = default)
|
public virtual async Task<string> 查询DC输出状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":OUTPut?{SCPIDelimiter}", SCPIDelimiter, ct);
|
// 修正:去除前缀冒号
|
||||||
|
return await WriteReadAsync($"OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -273,8 +284,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置定时器状态(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置定时器状态(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号
|
||||||
string 状态 = 开启 ? "ON" : "OFF";
|
string 状态 = 开启 ? "ON" : "OFF";
|
||||||
await SendAsync($":OUTPut:TIMEr {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"OUTP:TIM {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -282,7 +294,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置定时器时间(double 秒数, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,8 +304,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置输出模式优先(bool 是CV优先, CancellationToken ct = default)
|
public virtual async Task 设置输出模式优先(bool 是CV优先, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号
|
||||||
string 模式 = 是CV优先 ? "CV" : "CC";
|
string 模式 = 是CV优先 ? "CV" : "CC";
|
||||||
await SendAsync($":OUTPut:MODE {模式}{SCPIDelimiter}", ct);
|
await SendAsync($"OUTP:MODE {模式}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -304,7 +318,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 清除电流保护状态(CancellationToken ct = default)
|
public virtual async Task 清除电流保护状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":CURRent:PROTection:CLEar{SCPIDelimiter}", ct);
|
// 修正:去除前缀冒号
|
||||||
|
await SendAsync($"CURR:PROT:CLE{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -312,8 +327,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电流保护使能(bool 启用, CancellationToken ct = default)
|
public virtual async Task 设置电流保护使能(bool 启用, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号,将 STATE 改为更常用的 STAT 简写
|
||||||
string 状态 = 启用 ? "ON" : "OFF";
|
string 状态 = 启用 ? "ON" : "OFF";
|
||||||
await SendAsync($":CURRent:PROTection:STATE {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"CURR:PROT:STAT {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -321,7 +337,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置过流保护值(double 电流, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +347,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +357,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 清除电压保护状态(CancellationToken ct = default)
|
public virtual async Task 清除电压保护状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":VOLTage:PROTection:CLEar{SCPIDelimiter}", ct);
|
// 修正:去除前缀冒号
|
||||||
|
await SendAsync($"VOLT:PROT:CLE{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -347,8 +366,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电压保护使能(bool 启用, CancellationToken ct = default)
|
public virtual async Task 设置电压保护使能(bool 启用, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号
|
||||||
string 状态 = 启用 ? "ON" : "OFF";
|
string 状态 = 启用 ? "ON" : "OFF";
|
||||||
await SendAsync($":VOLTage:PROTection:STATE {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"VOLT:PROT:STAT {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -356,7 +376,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置过压保护值(double 电压, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +386,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +396,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置电压上限限制(double 电压, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,7 +406,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置功率(double 功率, CancellationToken ct = default)
|
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);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,8 +416,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置恒功率输出使能(bool 启用, CancellationToken ct = default)
|
public virtual async Task 设置恒功率输出使能(bool 启用, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号
|
||||||
string 状态 = 启用 ? "ON" : "OFF";
|
string 状态 = 启用 ? "ON" : "OFF";
|
||||||
await SendAsync($":POWer:STATE {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"POW:STAT {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -405,8 +430,9 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 设置蜂鸣器状态(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置蜂鸣器状态(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
// 修正:去除前缀冒号
|
||||||
string 状态 = 开启 ? "ON" : "OFF";
|
string 状态 = 开启 ? "ON" : "OFF";
|
||||||
await SendAsync($":SYSTem:BEEPer:STATE {状态}{SCPIDelimiter}", ct);
|
await SendAsync($"SYST:BEEP:STAT {状态}{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -414,7 +440,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 蜂鸣器鸣叫(CancellationToken ct = default)
|
public virtual async Task 蜂鸣器鸣叫(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:BEEPer{SCPIDelimiter}", ct);
|
// 修正:去除前缀冒号
|
||||||
|
await SendAsync($"SYST:BEEP{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -422,7 +449,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询错误记录数量(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -430,7 +458,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
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>
|
/// <summary>
|
||||||
@@ -438,7 +467,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:LOCal{SCPIDelimiter}", ct);
|
// 修正:去除前缀冒号
|
||||||
|
await SendAsync($"SYST:LOC{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -446,7 +476,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:REMote{SCPIDelimiter}", ct);
|
// 修正:去除前缀冒号
|
||||||
|
await SendAsync($"SYST:REM{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -454,7 +485,8 @@ namespace DeviceCommand.Device
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task 远程控制模式锁定(CancellationToken ct = default)
|
public virtual async Task 远程控制模式锁定(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:RWLock{SCPIDelimiter}", ct);
|
// 修正:去除前缀冒号
|
||||||
|
await SendAsync($"SYST:RWL{SCPIDelimiter}", ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -8,7 +9,16 @@ namespace Model.Entity
|
|||||||
{
|
{
|
||||||
public class MonitorValueEntity:BaseEntity
|
public class MonitorValueEntity:BaseEntity
|
||||||
{
|
{
|
||||||
public string MonitorName;
|
/// <summary>监测项名称</summary>
|
||||||
public double MonitorValue;
|
[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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace Model.Models
|
|||||||
public class AvailableMethodItem
|
public class AvailableMethodItem
|
||||||
{
|
{
|
||||||
public string DeviceName { get; set; } = string.Empty;
|
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 MethodName { get; set; } = string.Empty;
|
||||||
public string DisplayName { get; set; } = string.Empty;
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
public MethodInfo MethodInfo { get; set; } = null!;
|
public MethodInfo MethodInfo { get; set; } = null!;
|
||||||
|
|||||||
26
Model/Models/HardwareReportArgs.cs
Normal file
26
Model/Models/HardwareReportArgs.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ namespace MonitorModule.ViewModels
|
|||||||
{
|
{
|
||||||
// ===== 标识 =====
|
// ===== 标识 =====
|
||||||
public string DeviceName { get; init; } = string.Empty;
|
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 MethodName { get; init; } = string.Empty;
|
||||||
public string DisplayName { get; init; } = string.Empty;
|
public string DisplayName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ using OxyPlot;
|
|||||||
using OxyPlot.Axes;
|
using OxyPlot.Axes;
|
||||||
using OxyPlot.Legends;
|
using OxyPlot.Legends;
|
||||||
using OxyPlot.Series;
|
using OxyPlot.Series;
|
||||||
|
using Service.Interface;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -24,10 +27,12 @@ namespace MonitorModule.ViewModels
|
|||||||
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||||
{
|
{
|
||||||
#region 属性
|
#region 属性
|
||||||
|
private IMonitorValueService _monitorValueService;
|
||||||
public bool KeepAlive => true;
|
public bool KeepAlive => true;
|
||||||
public ScopedContext _scopedContext { get; set; } = null!;
|
public ScopedContext _scopedContext { get; set; } = null!;
|
||||||
public GlobalInfo _globalInfo { get; set; }
|
public GlobalInfo _globalInfo { get; set; }
|
||||||
|
|
||||||
|
private string _testStatus = string.Empty;
|
||||||
public string TestStatus
|
public string TestStatus
|
||||||
{
|
{
|
||||||
get => _testStatus;
|
get => _testStatus;
|
||||||
@@ -56,6 +61,7 @@ namespace MonitorModule.ViewModels
|
|||||||
set => SetProperty(ref _selectedAvailableMethod, value);
|
set => SetProperty(ref _selectedAvailableMethod, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string _statusMessage = "图表已就绪,暂无监测项";
|
||||||
public string StatusMessage
|
public string StatusMessage
|
||||||
{
|
{
|
||||||
get => _statusMessage;
|
get => _statusMessage;
|
||||||
@@ -77,6 +83,7 @@ namespace MonitorModule.ViewModels
|
|||||||
private DeviceManager? _deviceManager;
|
private DeviceManager? _deviceManager;
|
||||||
private CancellationTokenSource? _monitorCTS = new();
|
private CancellationTokenSource? _monitorCTS = new();
|
||||||
|
|
||||||
|
// 颜色调色盘
|
||||||
private static readonly OxyColor[] _palette =
|
private static readonly OxyColor[] _palette =
|
||||||
{
|
{
|
||||||
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
|
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
|
||||||
@@ -85,50 +92,216 @@ namespace MonitorModule.ViewModels
|
|||||||
};
|
};
|
||||||
private int _colorIndex;
|
private int _colorIndex;
|
||||||
|
|
||||||
private readonly DispatcherTimer _sampleTimer;
|
/// <summary>广播器(由 UIShare 层注入,每 Scope 一个实例)</summary>
|
||||||
private double _sampleTime;
|
private HardwareDataBroadcaster? _broadcaster;
|
||||||
private const double _sampleInterval = 0.1; // 100ms
|
|
||||||
private string _testStatus = string.Empty;
|
|
||||||
private string _statusMessage = "图表已就绪,暂无监测项";
|
|
||||||
|
|
||||||
/// <summary>反射方法缓存:Channel → (MethodInfo, DeviceInstance)</summary>
|
/// <summary>用于 OxyPlot X 轴相对秒数</summary>
|
||||||
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
|
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
|
#endregion
|
||||||
|
|
||||||
public MonitorViewModel(IContainerExtension container) : base(container)
|
public MonitorViewModel(IContainerExtension container) : base(container)
|
||||||
{
|
{
|
||||||
_globalInfo = container.Resolve<GlobalInfo>();
|
_globalInfo = container.Resolve<GlobalInfo>();
|
||||||
|
_monitorValueService = container.Resolve<IMonitorValueService>();
|
||||||
|
|
||||||
Plot = BuildEmptyPlot();
|
Plot = BuildEmptyPlot();
|
||||||
|
|
||||||
AddChannelCommand = new DelegateCommand(OnAddChannel);
|
AddChannelCommand = new DelegateCommand(OnAddChannel);
|
||||||
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
|
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
|
||||||
ResetViewCommand = new DelegateCommand(OnResetView);
|
ResetViewCommand = new DelegateCommand(OnResetView);
|
||||||
RefreshDataCommand = new DelegateCommand(OnRefreshData);
|
RefreshDataCommand = new DelegateCommand(OnRefreshData);
|
||||||
RefreshCommand = new DelegateCommand(OnExpand);
|
RefreshCommand = new DelegateCommand(OnExpand);
|
||||||
|
|
||||||
_sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) };
|
// OxyPlot 刷新定时器(1000ms,只负责视觉更新)
|
||||||
_sampleTimer.Tick += OnSampleTick;
|
_plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background)
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromMilliseconds(1000)
|
||||||
|
};
|
||||||
|
_plotRefreshTimer.Tick += OnPlotRefreshTick;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// 1. 率先掐断后台正在并行的所有硬件请求
|
|
||||||
_monitorCTS?.Cancel();
|
_monitorCTS?.Cancel();
|
||||||
_monitorCTS?.Dispose();
|
_monitorCTS?.Dispose();
|
||||||
_monitorCTS = null;
|
_monitorCTS = null;
|
||||||
|
|
||||||
// 2. 停掉定时器,停止触发 UI 刷新
|
_plotRefreshTimer.Stop();
|
||||||
_sampleTimer?.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;
|
_sampleTimer.Tick -= OnSampleTick;
|
||||||
|
|
||||||
// 3. 释放容器作用域
|
// 3. 释放容器作用域
|
||||||
_scope?.Dispose();
|
_scope?.Dispose();
|
||||||
|
|
||||||
// 4. 清理集合
|
|
||||||
Channels.Clear();
|
Channels.Clear();
|
||||||
AvailableMethods.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. 订阅 HardwareDataReportedEvent(UI 线程回调)
|
||||||
|
_subscriptionToken = _eventAggregator.GetEvent<HardwareDataReportedEvent>()
|
||||||
|
.Subscribe(OnHardwareDataReceived, ThreadOption.UIThread);
|
||||||
|
|
||||||
|
// 5. 启动 OxyPlot 视觉刷新定时器
|
||||||
|
_stopwatch.Start();
|
||||||
|
_plotRefreshTimer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region PlotModel 构建
|
#region PlotModel 构建
|
||||||
private static PlotModel BuildEmptyPlot()
|
private static PlotModel BuildEmptyPlot()
|
||||||
{
|
{
|
||||||
@@ -162,7 +335,22 @@ namespace MonitorModule.ViewModels
|
|||||||
}
|
}
|
||||||
#endregion
|
#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()
|
private void DiscoverAvailableMethods()
|
||||||
{
|
{
|
||||||
AvailableMethods.Clear();
|
AvailableMethods.Clear();
|
||||||
@@ -172,26 +360,26 @@ namespace MonitorModule.ViewModels
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var instanceToFp = BuildInstanceToFingerprint();
|
||||||
|
|
||||||
foreach (var kvp in _deviceManager.DeviceMap)
|
foreach (var kvp in _deviceManager.DeviceMap)
|
||||||
{
|
{
|
||||||
string deviceName = kvp.Key;
|
string deviceName = kvp.Key;
|
||||||
var device = kvp.Value;
|
var device = kvp.Value;
|
||||||
var deviceType = device.GetType();
|
var deviceType = device.GetType();
|
||||||
|
|
||||||
// 改用特性过滤:仅扫描带有 [Monitorable] 特性且签名正确的方法
|
// 反查该实例的硬件指纹
|
||||||
|
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
|
||||||
|
continue;
|
||||||
|
|
||||||
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||||
.Where(m =>
|
.Where(m =>
|
||||||
{
|
{
|
||||||
var attribute = m.GetCustomAttribute<MonitorableAttribute>();
|
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||||
if (attribute == null) return false;
|
|
||||||
|
|
||||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||||
|
|
||||||
var parms = m.GetParameters();
|
var parms = m.GetParameters();
|
||||||
bool validParams = parms.Length == 0 ||
|
return parms.Length == 0 ||
|
||||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||||
|
|
||||||
return validParams;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var method in methods)
|
foreach (var method in methods)
|
||||||
@@ -201,15 +389,15 @@ namespace MonitorModule.ViewModels
|
|||||||
? $"{deviceName}.{attr.Description}"
|
? $"{deviceName}.{attr.Description}"
|
||||||
: $"{deviceName}.{method.Name}";
|
: $"{deviceName}.{method.Name}";
|
||||||
|
|
||||||
var item = new AvailableMethodItem
|
AvailableMethods.Add(new AvailableMethodItem
|
||||||
{
|
{
|
||||||
DeviceName = deviceName,
|
DeviceName = deviceName,
|
||||||
|
Fingerprint = fingerprint,
|
||||||
MethodName = method.Name,
|
MethodName = method.Name,
|
||||||
DisplayName = displayName,
|
DisplayName = displayName,
|
||||||
MethodInfo = method,
|
MethodInfo = method,
|
||||||
Device = device
|
Device = device
|
||||||
};
|
});
|
||||||
AvailableMethods.Add(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +417,8 @@ namespace MonitorModule.ViewModels
|
|||||||
return;
|
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}] 已存在";
|
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
|
||||||
return;
|
return;
|
||||||
@@ -241,6 +430,7 @@ namespace MonitorModule.ViewModels
|
|||||||
var channel = new MonitorChannel
|
var channel = new MonitorChannel
|
||||||
{
|
{
|
||||||
DeviceName = method.DeviceName,
|
DeviceName = method.DeviceName,
|
||||||
|
Fingerprint = method.Fingerprint,
|
||||||
MethodName = method.MethodName,
|
MethodName = method.MethodName,
|
||||||
DisplayName = method.DisplayName,
|
DisplayName = method.DisplayName,
|
||||||
Color = color,
|
Color = color,
|
||||||
@@ -256,8 +446,6 @@ namespace MonitorModule.ViewModels
|
|||||||
};
|
};
|
||||||
Plot.Series.Add(channel.Series);
|
Plot.Series.Add(channel.Series);
|
||||||
|
|
||||||
_methodCache[channel] = (method.MethodInfo, method.Device);
|
|
||||||
|
|
||||||
channel.PropertyChanged += (s, e) =>
|
channel.PropertyChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
|
if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
|
||||||
@@ -280,11 +468,7 @@ namespace MonitorModule.ViewModels
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target.Series != null)
|
if (target.Series != null) Plot.Series.Remove(target.Series);
|
||||||
{
|
|
||||||
Plot.Series.Remove(target.Series);
|
|
||||||
}
|
|
||||||
_methodCache.Remove(target);
|
|
||||||
Channels.Remove(target);
|
Channels.Remove(target);
|
||||||
SelectedChannel = Channels.LastOrDefault();
|
SelectedChannel = Channels.LastOrDefault();
|
||||||
|
|
||||||
@@ -328,11 +512,9 @@ namespace MonitorModule.ViewModels
|
|||||||
};
|
};
|
||||||
|
|
||||||
var recent = channel.DataPoints.ToArray();
|
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++)
|
for (int i = startIdx; i < recent.Length; i++)
|
||||||
{
|
|
||||||
channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue));
|
channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue));
|
||||||
}
|
|
||||||
|
|
||||||
Plot.Series.Add(channel.Series);
|
Plot.Series.Add(channel.Series);
|
||||||
Plot.InvalidatePlot(true);
|
Plot.InvalidatePlot(true);
|
||||||
@@ -352,121 +534,13 @@ namespace MonitorModule.ViewModels
|
|||||||
public void OnChannelMathChanged(MonitorChannel channel)
|
public void OnChannelMathChanged(MonitorChannel channel)
|
||||||
{
|
{
|
||||||
if (channel.Series == null) return;
|
if (channel.Series == null) return;
|
||||||
|
|
||||||
var points = channel.DataPoints.ToArray();
|
var points = channel.DataPoints.ToArray();
|
||||||
channel.Series.Points.Clear();
|
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++)
|
for (int i = startIdx; i < points.Length; i++)
|
||||||
{
|
|
||||||
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
|
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
|
||||||
}
|
|
||||||
Plot.InvalidatePlot(true);
|
Plot.InvalidatePlot(true);
|
||||||
}
|
}
|
||||||
#endregion
|
#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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
using Logger;
|
using Common.Attributes;
|
||||||
|
using DeviceCommand.Base;
|
||||||
|
using Logger;
|
||||||
|
using Model.Entity;
|
||||||
|
using ORM;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using UIShare.GlobalVariable;
|
using UIShare.GlobalVariable;
|
||||||
@@ -12,9 +17,9 @@ using UIShare.ViewModelBase;
|
|||||||
namespace MonitorModule.ViewModels
|
namespace MonitorModule.ViewModels
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 记录界面:用于查看运行目录下 SQL/ADP.db 中的数据。
|
/// 记录界面:查询 MonitorValueEntity 表中的历史监测数据。
|
||||||
/// 功能:表选择 / 关键字 WHERE 查询 / 分页 / 导出 CSV。
|
/// 功能:监测项筛选 / 台架筛选 / 日期筛选 / 分页 / 导出 CSV。
|
||||||
/// 仍延续 ScopedContext 隔离 + 双击展开 的范式,每个工位独立一份查询状态。
|
/// 使用 SqlSugarContext.DbContext 全局单例,与写入端共用同一数据库连接。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||||
{
|
{
|
||||||
@@ -30,30 +35,39 @@ namespace MonitorModule.ViewModels
|
|||||||
set => SetProperty(ref _testStatus, value);
|
set => SetProperty(ref _testStatus, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<string> TableNames
|
/// <summary>监测项名称下拉列表</summary>
|
||||||
|
public ObservableCollection<string> MonitorNames
|
||||||
{
|
{
|
||||||
get => _tableNames;
|
get => _monitorNames;
|
||||||
set => SetProperty(ref _tableNames, value);
|
set => SetProperty(ref _monitorNames, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? SelectedTable
|
/// <summary>选中的监测项(null = 全部)</summary>
|
||||||
|
public string? SelectedMonitorName
|
||||||
{
|
{
|
||||||
get => _selectedTable;
|
get => _selectedMonitorName;
|
||||||
set
|
set => SetProperty(ref _selectedMonitorName, value);
|
||||||
{
|
|
||||||
if (SetProperty(ref _selectedTable, value))
|
|
||||||
{
|
|
||||||
PageIndex = 1;
|
|
||||||
Query();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户填写的 WHERE 子句(不带 WHERE 关键字),例如:Status='OK' AND Id>10
|
/// <summary>台架(作用域)下拉列表</summary>
|
||||||
public string WhereClause
|
public ObservableCollection<string> ScopeNames
|
||||||
{
|
{
|
||||||
get => _whereClause;
|
get => _scopeNames;
|
||||||
set => SetProperty(ref _whereClause, value);
|
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
|
public DataTable ResultTable
|
||||||
@@ -111,40 +125,38 @@ namespace MonitorModule.ViewModels
|
|||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
public ICommand LoadedCommand { get; }
|
public ICommand LoadedCommand { get; }
|
||||||
public ICommand RefreshTablesCommand { get; }
|
|
||||||
public ICommand QueryCommand { get; }
|
public ICommand QueryCommand { get; }
|
||||||
|
public ICommand ClearFilterCommand { get; }
|
||||||
public ICommand FirstPageCommand { get; }
|
public ICommand FirstPageCommand { get; }
|
||||||
public ICommand PrevPageCommand { get; }
|
public ICommand PrevPageCommand { get; }
|
||||||
public ICommand NextPageCommand { get; }
|
public ICommand NextPageCommand { get; }
|
||||||
public ICommand LastPageCommand { get; }
|
public ICommand LastPageCommand { get; }
|
||||||
public ICommand ExportCsvCommand { get; }
|
public ICommand ExportCsvCommand { get; }
|
||||||
// 双击展开/折叠:与 MonitorView/AutomatedTestingView 共用
|
|
||||||
public ICommand RefreshCommand { get; }
|
public ICommand RefreshCommand { get; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 私有字段
|
#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 IScopedProvider _scope;
|
||||||
private string _testStatus = string.Empty;
|
private string _testStatus = string.Empty;
|
||||||
private ObservableCollection<string> _tableNames = new();
|
private ObservableCollection<string> _monitorNames = new();
|
||||||
private string? _selectedTable;
|
private string? _selectedMonitorName;
|
||||||
private string _whereClause = string.Empty;
|
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 DataTable _resultTable = new();
|
||||||
private string _statusMessage = "未连接";
|
private string _statusMessage = "未连接";
|
||||||
private int _pageIndex = 1;
|
private int _pageIndex = 1;
|
||||||
private int _pageSize = 50;
|
private int _pageSize = 50;
|
||||||
private long _totalCount;
|
private long _totalCount;
|
||||||
|
private bool IsInitiated = false;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public RecordViewModel(IContainerExtension container) : base(container)
|
public RecordViewModel(IContainerExtension container) : base(container)
|
||||||
{
|
{
|
||||||
_globalInfo = container.Resolve<GlobalInfo>();
|
_globalInfo = container.Resolve<GlobalInfo>();
|
||||||
LoadedCommand = new DelegateCommand(LoadTables);
|
LoadedCommand = new DelegateCommand(LoadFilterOptions);
|
||||||
RefreshTablesCommand = new DelegateCommand(LoadTables);
|
|
||||||
QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); });
|
QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); });
|
||||||
|
ClearFilterCommand = new DelegateCommand(ClearFilter);
|
||||||
FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } });
|
FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } });
|
||||||
PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } });
|
PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } });
|
||||||
NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } });
|
NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } });
|
||||||
@@ -158,105 +170,136 @@ namespace MonitorModule.ViewModels
|
|||||||
_scope?.Dispose();
|
_scope?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 数据库操作
|
#region 筛选条件加载
|
||||||
private SqlSugarClient CreateClient()
|
/// <summary>
|
||||||
|
/// 加载筛选条件:监测项通过反射发现(与 MonitorViewModel 一致),台架从数据库查询。
|
||||||
|
/// </summary>
|
||||||
|
private void LoadFilterOptions()
|
||||||
{
|
{
|
||||||
return new SqlSugarClient(new ConnectionConfig
|
// 监测项:反射发现当前作用域所有带 [Monitorable] 的设备方法
|
||||||
{
|
DiscoverMonitorNames();
|
||||||
DbType = SqlSugar.DbType.Sqlite,
|
|
||||||
ConnectionString = ConnStr,
|
Query();
|
||||||
IsAutoCloseConnection = true,
|
|
||||||
InitKeyType = InitKeyType.Attribute
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取数据库中的所有用户表。数据库不存在时不报错,仅在状态栏提示。
|
/// 反射扫描当前作用域 DeviceManager 中所有带 [Monitorable] 特性的设备方法,
|
||||||
|
/// 生成与 MonitorViewModel 完全一致的 DisplayName 列表,填充到下拉框。
|
||||||
|
/// 这样即使数据库还没数据,用户也能看到所有可筛选的监测项。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void LoadTables()
|
private void DiscoverMonitorNames()
|
||||||
{
|
{
|
||||||
|
MonitorNames = new ObservableCollection<string>();
|
||||||
|
|
||||||
|
if (_scope == null) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(DbFolder)) Directory.CreateDirectory(DbFolder);
|
var deviceManager = _scope.Resolve<DeviceManager>();
|
||||||
if (!File.Exists(DbPath))
|
if (deviceManager?.DeviceMap == null || deviceManager.DeviceMap.Count == 0)
|
||||||
{
|
{
|
||||||
TableNames = new ObservableCollection<string>();
|
StatusMessage = "当前工位无可用设备";
|
||||||
SelectedTable = null;
|
|
||||||
ResultTable = new DataTable();
|
|
||||||
TotalCount = 0;
|
|
||||||
StatusMessage = $"数据库尚未创建:{DbPath}";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var db = CreateClient();
|
// 构建 设备实例 → 硬件指纹 的反向查找表
|
||||||
var tables = db.DbMaintenance.GetTableInfoList(false)
|
var instanceToFp = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||||
.Select(t => t.Name)
|
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||||
.OrderBy(n => n)
|
{
|
||||||
.ToList();
|
var lazy = poolEntry.Value;
|
||||||
|
if (lazy.IsValueCreated && lazy.Value != null)
|
||||||
|
instanceToFp[lazy.Value] = poolEntry.Key;
|
||||||
|
}
|
||||||
|
|
||||||
TableNames = new ObservableCollection<string>(tables);
|
var names = new List<string>();
|
||||||
if (tables.Count == 0)
|
|
||||||
|
foreach (var kvp in deviceManager.DeviceMap)
|
||||||
{
|
{
|
||||||
SelectedTable = null;
|
string deviceName = kvp.Key;
|
||||||
ResultTable = new DataTable();
|
var device = kvp.Value;
|
||||||
TotalCount = 0;
|
var deviceType = device.GetType();
|
||||||
StatusMessage = "数据库已连接,但暂无数据表";
|
|
||||||
}
|
// 只列出在硬件指纹池中注册的设备
|
||||||
else
|
if (!instanceToFp.ContainsKey(device))
|
||||||
{
|
continue;
|
||||||
StatusMessage = $"已连接:{DbPath}({tables.Count} 张表)";
|
|
||||||
if (string.IsNullOrEmpty(SelectedTable) || !tables.Contains(SelectedTable))
|
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
|
var attr = method.GetCustomAttribute<MonitorableAttribute>();
|
||||||
}
|
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||||
else
|
? $"{deviceName}.{attr.Description}"
|
||||||
{
|
: $"{deviceName}.{method.Name}";
|
||||||
Query();
|
names.Add(displayName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
names.Sort();
|
||||||
|
MonitorNames = new ObservableCollection<string>(names);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope,$"加载数据表失败:{ex.Message}");
|
LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"反射发现监测项失败:{ex.Message}");
|
||||||
StatusMessage = $"加载失败:{ex.Message}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>清除所有筛选条件</summary>
|
||||||
|
private void ClearFilter()
|
||||||
|
{
|
||||||
|
SelectedMonitorName = null;
|
||||||
|
SelectedScope = "全部";
|
||||||
|
SelectedDate = null;
|
||||||
|
PageIndex = 1;
|
||||||
|
Query();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 查询
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行分页查询。WHERE 为空则查询全部。
|
/// 按当前筛选条件分页查询 MonitorValueEntity。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void Query()
|
private void Query()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(SelectedTable) || !File.Exists(DbPath))
|
|
||||||
{
|
|
||||||
ResultTable = new DataTable();
|
|
||||||
TotalCount = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var db = CreateClient();
|
var db = SqlSugarContext.DbContext;
|
||||||
string table = $"[{SelectedTable}]";
|
|
||||||
string where = string.IsNullOrWhiteSpace(WhereClause) ? "1=1" : WhereClause;
|
|
||||||
|
|
||||||
// COUNT 总数
|
// 预计算日期范围,避免在表达式树中访问可空属性
|
||||||
var countSql = $"SELECT COUNT(*) FROM {table} WHERE {where}";
|
DateTime? dateStart = SelectedDate?.Date;
|
||||||
TotalCount = db.Ado.GetLong(countSql);
|
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 > TotalPages) PageIndex = TotalPages;
|
||||||
if (PageIndex < 1) PageIndex = 1;
|
if (PageIndex < 1) PageIndex = 1;
|
||||||
|
|
||||||
int offset = (PageIndex - 1) * PageSize;
|
int skip = (PageIndex - 1) * PageSize;
|
||||||
var dataSql = $"SELECT * FROM {table} WHERE {where} LIMIT {PageSize} OFFSET {offset}";
|
ResultTable = query
|
||||||
ResultTable = db.Ado.GetDataTable(dataSql);
|
.Skip(skip)
|
||||||
|
.Take(PageSize)
|
||||||
|
.ToDataTable();
|
||||||
|
|
||||||
StatusMessage = $"表 [{SelectedTable}] 共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
|
StatusMessage = $"共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.ErrorWithNotify(TestStatus,$"查询失败:{ex.Message}");
|
LoggerHelper.ErrorWithNotify(TestStatus, $"查询失败:{ex.Message}");
|
||||||
StatusMessage = $"查询失败:{ex.Message}";
|
StatusMessage = $"查询失败:{ex.Message}";
|
||||||
ResultTable = new DataTable();
|
ResultTable = new DataTable();
|
||||||
TotalCount = 0;
|
TotalCount = 0;
|
||||||
@@ -266,36 +309,42 @@ namespace MonitorModule.ViewModels
|
|||||||
|
|
||||||
#region 导出 CSV
|
#region 导出 CSV
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按当前 WHERE 条件导出全部结果到 CSV(不分页)。
|
/// 按当前筛选条件导出全部结果到 CSV(不分页)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ExportCsv()
|
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
|
try
|
||||||
{
|
{
|
||||||
using var db = CreateClient();
|
var db = SqlSugarContext.DbContext;
|
||||||
string table = $"[{SelectedTable}]";
|
|
||||||
string where = string.IsNullOrWhiteSpace(WhereClause) ? "1=1" : WhereClause;
|
DateTime? dateStart = SelectedDate?.Date;
|
||||||
var sql = $"SELECT * FROM {table} WHERE {where}";
|
DateTime? dateEnd = dateStart?.AddDays(1);
|
||||||
var dt = db.Ado.GetDataTable(sql);
|
|
||||||
|
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);
|
WriteCsv(dlg.FileName, dt);
|
||||||
|
|
||||||
StatusMessage = $"导出完成:{dlg.FileName}({dt.Rows.Count} 行)";
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -333,7 +382,6 @@ namespace MonitorModule.ViewModels
|
|||||||
if (string.IsNullOrEmpty(TestStatus)) return;
|
if (string.IsNullOrEmpty(TestStatus)) return;
|
||||||
_globalInfo.CurrentScope = TestStatus;
|
_globalInfo.CurrentScope = TestStatus;
|
||||||
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
|
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
|
||||||
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -341,14 +389,14 @@ namespace MonitorModule.ViewModels
|
|||||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||||
{
|
{
|
||||||
base.OnNavigatedTo(navigationContext);
|
base.OnNavigatedTo(navigationContext);
|
||||||
if (!IsInitiated&&navigationContext.Parameters.ContainsKey("Name"))
|
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
|
||||||
{
|
{
|
||||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||||
_scope = _globalInfo.ScopeDic[TestStatus];
|
_scope = _globalInfo.ScopeDic[TestStatus];
|
||||||
_scopedContext = _scope.Resolve<ScopedContext>();
|
_scopedContext = _scope.Resolve<ScopedContext>();
|
||||||
IsInitiated = true;
|
IsInitiated = true;
|
||||||
}
|
}
|
||||||
LoadTables();
|
LoadFilterOptions();
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
FontSize="20" FontWeight="Bold"
|
FontSize="20" FontWeight="Bold"
|
||||||
Margin="4,0,0,8"/>
|
Margin="4,0,0,8"/>
|
||||||
|
|
||||||
<!-- 工具栏:表选择 / WHERE 条件 / 查询 / 刷新表 / 导出 -->
|
<!-- 工具栏:监测项筛选 / 台架筛选 / 日期筛选 / 查询 / 清除 / 导出 -->
|
||||||
<Border Grid.Row="1"
|
<Border Grid.Row="1"
|
||||||
Background="White"
|
Background="White"
|
||||||
BorderBrush="#DDD" BorderThickness="1"
|
BorderBrush="#DDD" BorderThickness="1"
|
||||||
@@ -47,57 +47,71 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<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="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
<ColumnDefinition Width="Auto"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 监测项 -->
|
||||||
<TextBlock Grid.Column="0"
|
<TextBlock Grid.Column="0"
|
||||||
Text="表:"
|
Text="监测项:"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="0,0,4,0"/>
|
Margin="0,0,4,0"/>
|
||||||
<ComboBox Grid.Column="1"
|
<ComboBox Grid.Column="1"
|
||||||
materialDesign:HintAssist.Hint="数据表"
|
materialDesign:HintAssist.Hint="全部"
|
||||||
ItemsSource="{Binding TableNames}"
|
ItemsSource="{Binding MonitorNames}"
|
||||||
SelectedItem="{Binding SelectedTable}"
|
SelectedItem="{Binding SelectedMonitorName}"
|
||||||
|
IsEditable="True"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<!-- 台架 -->
|
||||||
<TextBlock Grid.Column="2"
|
<TextBlock Grid.Column="2"
|
||||||
Text="WHERE:"
|
Text="台架:"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Margin="12,0,4,0"/>
|
Margin="12,0,4,0"/>
|
||||||
<TextBox Grid.Column="3"
|
<ComboBox Grid.Column="3"
|
||||||
Text="{Binding WhereClause, UpdateSourceTrigger=PropertyChanged}"
|
materialDesign:HintAssist.Hint="全部"
|
||||||
VerticalAlignment="Center"
|
ItemsSource="{Binding ScopeNames}"
|
||||||
materialDesign:HintAssist.Hint="条件筛选"
|
SelectedItem="{Binding SelectedScope}"
|
||||||
ToolTip="例如:Status='OK' AND Id>10。留空表示查询全部">
|
IsEditable="True"
|
||||||
<TextBox.InputBindings>
|
VerticalAlignment="Center"/>
|
||||||
<KeyBinding Key="Enter" Command="{Binding QueryCommand}"/>
|
|
||||||
</TextBox.InputBindings>
|
|
||||||
</TextBox>
|
|
||||||
|
|
||||||
<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="查询"
|
Content="查询"
|
||||||
Command="{Binding QueryCommand}"
|
Command="{Binding QueryCommand}"
|
||||||
Padding="12,4" Margin="8,0,0,0"/>
|
Padding="12,4" Margin="12,0,0,0"/>
|
||||||
<Button Grid.Column="5"
|
<Button Grid.Column="7"
|
||||||
Content="刷新表"
|
Content="清除"
|
||||||
Command="{Binding RefreshTablesCommand}"
|
Command="{Binding ClearFilterCommand}"
|
||||||
Padding="12,4" Margin="6,0,0,0"/>
|
Padding="12,4" Margin="6,0,0,0"/>
|
||||||
<Button Grid.Column="6"
|
<Button Grid.Column="8"
|
||||||
Content="导出 CSV"
|
Content="导出 CSV"
|
||||||
Command="{Binding ExportCsvCommand}"
|
Command="{Binding ExportCsvCommand}"
|
||||||
Padding="12,4" Margin="6,0,0,0"/>
|
Padding="12,4" Margin="6,0,0,0"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- DataGrid:自动生成列,绑 DataTable.DefaultView 直显示 -->
|
<!-- DataGrid:显式列定义,隐藏 Id / IsDel -->
|
||||||
<DataGrid Grid.Row="2"
|
<DataGrid Grid.Row="2"
|
||||||
ItemsSource="{Binding ResultTable}"
|
ItemsSource="{Binding ResultTable}"
|
||||||
AutoGenerateColumns="True"
|
AutoGenerateColumns="False"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
CanUserAddRows="False"
|
CanUserAddRows="False"
|
||||||
CanUserDeleteRows="False"
|
CanUserDeleteRows="False"
|
||||||
@@ -106,7 +120,14 @@
|
|||||||
AlternatingRowBackground="#F9FAFB"
|
AlternatingRowBackground="#F9FAFB"
|
||||||
RowBackground="White"
|
RowBackground="White"
|
||||||
HorizontalScrollBarVisibility="Auto"
|
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"
|
<Border Grid.Row="3"
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace ORM
|
|||||||
|
|
||||||
// 拼接数据库文件路径
|
// 拼接数据库文件路径
|
||||||
string DBPath = Path.Combine(folder, "SQL.db");
|
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)
|
public static void InitMySql(string Server, int Port, string Database, string Uid,string Pwd)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace SettingModule
|
|||||||
// 设备连接配置弹窗
|
// 设备连接配置弹窗
|
||||||
containerRegistry.RegisterDialog<TCPConfigView>("TCPConfig");
|
containerRegistry.RegisterDialog<TCPConfigView>("TCPConfig");
|
||||||
containerRegistry.RegisterDialog<SerialPortConfigView>("SerialPortConfig");
|
containerRegistry.RegisterDialog<SerialPortConfigView>("SerialPortConfig");
|
||||||
|
containerRegistry.RegisterDialog<CANConfigView>("CANConfig");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
SettingModule/ViewModels/Dialogs/CANConfigViewModel.cs
Normal file
134
SettingModule/ViewModels/Dialogs/CANConfigViewModel.cs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,10 +59,9 @@ namespace SettingModule.ViewModels
|
|||||||
|
|
||||||
public ObservableCollection<string> ConnectionTypes { get; } = new()
|
public ObservableCollection<string> ConnectionTypes { get; } = new()
|
||||||
{
|
{
|
||||||
"None", "Tcp", "Serial"
|
"None", "Tcp", "Serial", "CAN"
|
||||||
};
|
};
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 命令
|
#region 命令
|
||||||
public ICommand RefreshCommand { get; }
|
public ICommand RefreshCommand { get; }
|
||||||
public ICommand SaveCommand { get; }
|
public ICommand SaveCommand { get; }
|
||||||
@@ -165,6 +164,7 @@ namespace SettingModule.ViewModels
|
|||||||
{
|
{
|
||||||
"Tcp" => "TCPConfig",
|
"Tcp" => "TCPConfig",
|
||||||
"Serial" => "SerialPortConfig",
|
"Serial" => "SerialPortConfig",
|
||||||
|
"CAN" => "CANConfig",
|
||||||
_ => string.Empty
|
_ => string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,6 +200,7 @@ namespace SettingModule.ViewModels
|
|||||||
_scope = _globalInfo.ScopeDic[TestStatus];
|
_scope = _globalInfo.ScopeDic[TestStatus];
|
||||||
_scopedContext = _globalInfo.ContextDic[TestStatus];
|
_scopedContext = _globalInfo.ContextDic[TestStatus];
|
||||||
SystemConfig = _scope.Resolve<SystemConfig>();
|
SystemConfig = _scope.Resolve<SystemConfig>();
|
||||||
|
ConfigService.EnsureDefaultCanDevice(SystemConfig);
|
||||||
if (DeviceList != null && DeviceList.Count > 0)
|
if (DeviceList != null && DeviceList.Count > 0)
|
||||||
{
|
{
|
||||||
SelectedDevice = DeviceList[0];
|
SelectedDevice = DeviceList[0];
|
||||||
|
|||||||
131
SettingModule/Views/Dialogs/CANConfigView.xaml
Normal file
131
SettingModule/Views/Dialogs/CANConfigView.xaml
Normal 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>
|
||||||
15
SettingModule/Views/Dialogs/CANConfigView.xaml.cs
Normal file
15
SettingModule/Views/Dialogs/CANConfigView.xaml.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -368,6 +368,33 @@
|
|||||||
<Run Text="{Binding SelectedDevice.SerialPortConfig.Parity}"/>
|
<Run Text="{Binding SelectedDevice.SerialPortConfig.Parity}"/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</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>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Logger;
|
using Logger;
|
||||||
|
using UIShare.UIViewModel;
|
||||||
|
|
||||||
namespace UIShare.GlobalVariable
|
namespace UIShare.GlobalVariable
|
||||||
{
|
{
|
||||||
@@ -44,6 +47,7 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
// 如果不存在,创建一个带 Title 的默认配置并保存
|
// 如果不存在,创建一个带 Title 的默认配置并保存
|
||||||
var defaultConfig = new SystemConfig { Title = title };
|
var defaultConfig = new SystemConfig { Title = title };
|
||||||
|
EnsureDefaultCanDevice(defaultConfig);
|
||||||
Save(defaultConfig);
|
Save(defaultConfig);
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
@@ -58,12 +62,16 @@ namespace UIShare.GlobalVariable
|
|||||||
TypeNameHandling = TypeNameHandling.All
|
TypeNameHandling = TypeNameHandling.All
|
||||||
});
|
});
|
||||||
|
|
||||||
return config ?? new SystemConfig { Title = title };
|
config ??= new SystemConfig { Title = title };
|
||||||
|
EnsureDefaultCanDevice(config);
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.ErrorWithNotify(title, $"格子 [{title}] 配置加载失败: {ex.Message}");
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ using System.IO.Ports;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using UIShare.UIViewModel;
|
using UIShare.UIViewModel;
|
||||||
|
using ZLGUSBCANFD;
|
||||||
|
|
||||||
namespace UIShare.GlobalVariable
|
namespace UIShare.GlobalVariable
|
||||||
{
|
{
|
||||||
@@ -28,6 +29,7 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
|
/// <summary>类名 → Type 的反射缓存(仅扫描一次)。</summary>
|
||||||
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
|
private static readonly IReadOnlyDictionary<string, Type> _deviceTypeMap = BuildDeviceTypeMap();
|
||||||
|
public ZLGCANFD CANFD { get; set; }
|
||||||
|
|
||||||
public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo)
|
public DeviceManager(SystemConfig systemConfig, GlobalInfo globalInfo)
|
||||||
{
|
{
|
||||||
@@ -37,6 +39,7 @@ namespace UIShare.GlobalVariable
|
|||||||
_scopeName = _systemConfig.Title;
|
_scopeName = _systemConfig.Title;
|
||||||
InitDevices();
|
InitDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 根据设备配置提取唯一的硬件指纹字符串。
|
/// 根据设备配置提取唯一的硬件指纹字符串。
|
||||||
/// <para>Tcp → "Tcp:IP:Port";Serial → "Serial:PortName";无法识别则返回空字符串。</para>
|
/// <para>Tcp → "Tcp:IP:Port";Serial → "Serial:PortName";无法识别则返回空字符串。</para>
|
||||||
@@ -55,9 +58,14 @@ namespace UIShare.GlobalVariable
|
|||||||
return $"Serial:{config.SerialPortConfig.PortName}";
|
return $"Serial:{config.SerialPortConfig.PortName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(config.ConnectionType, "CAN", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& config.CANConfig != null)
|
||||||
|
{
|
||||||
|
return $"CAN:{config.CANConfig.DeviceIndex}";
|
||||||
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitDevices()
|
private void InitDevices()
|
||||||
{
|
{
|
||||||
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
DeviceMap = new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -68,6 +76,53 @@ namespace UIShare.GlobalVariable
|
|||||||
{
|
{
|
||||||
if (config == null || !config.IsEnabled) continue;
|
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) ||
|
if (string.IsNullOrWhiteSpace(config.DeviceType) ||
|
||||||
!_deviceTypeMap.TryGetValue(config.DeviceType, out var 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 中的空条目
|
// 同步清除 DeviceAndScopeDic 中的空条目
|
||||||
_globalInfo.DeviceAndScopeDic.TryRemove(fingerprint, out _);
|
_globalInfo.DeviceAndScopeDic.TryRemove(fingerprint, out _);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using ZLGUSBCANFD;
|
||||||
|
|
||||||
namespace UIShare.GlobalVariable
|
namespace UIShare.GlobalVariable
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,9 @@ namespace UIShare.GlobalVariable
|
|||||||
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
|
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
|
||||||
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
|
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
|
||||||
|
|
||||||
|
/// <summary>CAN 硬件指纹 → ZLGCANFD 实例的并发池,确保同一 CAN 卡全局只创建一个驱动实例。</summary>
|
||||||
|
public ConcurrentDictionary<string, Lazy<ZLGCANFD>> CanPool { get; set; }
|
||||||
|
|
||||||
/// <summary>硬件指纹 → 正在使用该设备的作用域名称列表,用于引用计数与安全销毁。</summary>
|
/// <summary>硬件指纹 → 正在使用该设备的作用域名称列表,用于引用计数与安全销毁。</summary>
|
||||||
public ConcurrentDictionary<string, Lazy<List<string>>> DeviceAndScopeDic { get; set; }
|
public ConcurrentDictionary<string, Lazy<List<string>>> DeviceAndScopeDic { get; set; }
|
||||||
|
|
||||||
@@ -45,6 +49,7 @@ namespace UIShare.GlobalVariable
|
|||||||
ConfigDic = new();
|
ConfigDic = new();
|
||||||
ScopeDic = new();
|
ScopeDic = new();
|
||||||
HardwarePool = new ConcurrentDictionary<string, Lazy<IBaseInterface>>(StringComparer.OrdinalIgnoreCase);
|
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);
|
DeviceAndScopeDic = new ConcurrentDictionary<string, Lazy<List<string>>>(StringComparer.OrdinalIgnoreCase);
|
||||||
CurrentScope = "default";
|
CurrentScope = "default";
|
||||||
}
|
}
|
||||||
|
|||||||
207
UIShare/GlobalVariable/HardwareDataBroadcaster.cs
Normal file
207
UIShare/GlobalVariable/HardwareDataBroadcaster.cs
Normal 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<object, CancellationToken, Task<string>>
|
||||||
|
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
|
||||||
|
/// - 第二个参数 = CancellationToken(无参方法会忽略它)
|
||||||
|
/// - 返回值 = Task<string>
|
||||||
|
/// </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<object, CancellationToken, Task<string>>:
|
||||||
|
/// - 无参方法:忽略 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -564,7 +564,11 @@ namespace UIShare
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
instance = _deviceManager.DeviceMap[targetType.Name];
|
if(targetType.Name== "ZLGCANFD")
|
||||||
|
{
|
||||||
|
instance = _deviceManager.CANFD;
|
||||||
|
}
|
||||||
|
else instance = _deviceManager.DeviceMap[targetType.Name];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Reflection;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UIShare.UIViewModel;
|
using UIShare.UIViewModel;
|
||||||
|
using ZLGUSBCANFD;
|
||||||
using static UIShare.UIViewModel.ParameterVM;
|
using static UIShare.UIViewModel.ParameterVM;
|
||||||
|
|
||||||
namespace UIShare.GlobalVariable
|
namespace UIShare.GlobalVariable
|
||||||
@@ -29,6 +30,7 @@ namespace UIShare.GlobalVariable
|
|||||||
public string DefaultDBCFilePath { get; set; } = "";
|
public string DefaultDBCFilePath { get; set; } = "";
|
||||||
public ObservableCollection<DeviceInfoVM> DeviceList = new();
|
public ObservableCollection<DeviceInfoVM> DeviceList = new();
|
||||||
public ObservableCollection<SharedParameter> SharedParameterList = new();
|
public ObservableCollection<SharedParameter> SharedParameterList = new();
|
||||||
|
public ZLGCANFD CANFD = new();
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ObservableCollection<ParameterVM> ParameterList = new()
|
public ObservableCollection<ParameterVM> ParameterList = new()
|
||||||
{
|
{
|
||||||
|
|||||||
13
UIShare/PubEvent/HardwareDataReportedEvent.cs
Normal file
13
UIShare/PubEvent/HardwareDataReportedEvent.cs
Normal 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>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,5 +20,6 @@
|
|||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
||||||
<ProjectReference Include="..\Logger\Logger.csproj" />
|
<ProjectReference Include="..\Logger\Logger.csproj" />
|
||||||
|
<ProjectReference Include="..\ZLGUSBCANFD\ZLGUSBCANFD.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
76
UIShare/UIViewModel/CANConfigVM.cs
Normal file
76
UIShare/UIViewModel/CANConfigVM.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,5 +65,13 @@ namespace UIShare.UIViewModel
|
|||||||
get => _serialPortConfig;
|
get => _serialPortConfig;
|
||||||
set => SetProperty(ref _serialPortConfig, value);
|
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
729
ZLGUSBCANFD/USBCANFD.cs
Normal 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 = CANFD,0 = 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 = CANFD;false = 经典 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
1150
ZLGUSBCANFD/ZLGAPI.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UseWPF>false</UseWPF>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Interop.TSMasterAPI">
|
|
||||||
<HintPath>..\CanDriver\bin\Debug\net8.0\Interop.TSMasterAPI.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
Reference in New Issue
Block a user