diff --git a/A1Lib/A1Lib.csproj b/A1Lib/A1Lib.csproj
new file mode 100644
index 0000000..9485136
--- /dev/null
+++ b/A1Lib/A1Lib.csproj
@@ -0,0 +1,76 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <RootNamespace>ALib</RootNamespace>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>disable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <OutputPath>..\Bin\A1Lib</OutputPath>
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+ <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+ </PropertyGroup>
+ <ItemGroup>
+ <!-- 相机采集引擎(替换 OpenCV VideoCapture,DirectShow) -->
+ <PackageReference Include="AForge.NetCore" Version="1.0.0" />
+ <PackageReference Include="AForgeCore.Video.DirectShow" Version="1.0.0" />
+ <PackageReference Include="AForgeCore.Video" Version="1.0.0" />
+ <PackageReference Include="HidSharp" Version="2.6.4" />
+ <PackageReference Include="LomontSharp" Version="0.6.6" />
+ <PackageReference Include="LomontWin" Version="0.6.6" />
+ <PackageReference Include="MathNet.Numerics" Version="5.0.0" />
+ <PackageReference Include="NAudio" Version="2.3.0" />
+ <PackageReference Include="NewLife.Core" Version="11.17.2026.701" />
+ <PackageReference Include="System.IO.Ports" Version="10.0.9" />
+ <PackageReference Include="System.Management" Version="10.0.9" />
+ <PackageReference Include="NewLife.ModbusRTU" Version="2.0.2026.610" />
+ </ItemGroup>
+ <!-- 原生库嵌入方式:zip 压缩优先,裸文件兜底。
+ 原生 DLL 多为未压缩 PE,zip 压缩率可达 3~8 倍。
+ 用 A1Lib\pack_native_res.ps1 生成 .zip 后,构建时优先嵌入 zip,
+ 运行时由 ZipResourceExtractor 解压还原,可显著减小程序集体积。
+ 若某个 zip 尚未生成(或想临时改用裸文件排查),会自动退回嵌入原文件,
+ 两种形态的代码路径都保留,互不影响。
+ 单文件约定资源名:{前缀}.{文件名去扩展名}.zip
+ 目录 约定资源名:{前缀}.{目录名}.zip -->
+
+ <!-- 1) 单文件:zip 优先 -->
+ <ItemGroup Condition="Exists('CAN\CX_CAN\ControlCAN.zip')">
+ <EmbeddedResource Include="CAN\CX_CAN\ControlCAN.zip" LogicalName="ALib.CX_CAN.ControlCAN.zip" />
+ </ItemGroup>
+ <ItemGroup Condition="!Exists('CAN\CX_CAN\ControlCAN.zip')">
+ <EmbeddedResource Include="CAN\CX_CAN\ControlCAN.dll" LogicalName="ALib.CX_CAN.ControlCAN.dll" />
+ </ItemGroup>
+
+ <ItemGroup Condition="Exists('CAN\ZLG_CAN\zlgcan.zip')">
+ <EmbeddedResource Include="CAN\ZLG_CAN\zlgcan.zip" LogicalName="ALib.ZLG_CAN.zlgcan.zip" />
+ </ItemGroup>
+ <ItemGroup Condition="!Exists('CAN\ZLG_CAN\zlgcan.zip')">
+ <EmbeddedResource Include="CAN\ZLG_CAN\zlgcan.dll" LogicalName="ALib.ZLG_CAN.zlgcan.dll" />
+ </ItemGroup>
+
+ <ItemGroup Condition="Exists('CAN\PCAN\PCANBasic.zip')">
+ <EmbeddedResource Include="CAN\PCAN\PCANBasic.zip" LogicalName="ALib.PCAN.PCANBasic.zip" />
+ </ItemGroup>
+ <ItemGroup Condition="!Exists('CAN\PCAN\PCANBasic.zip')">
+ <EmbeddedResource Include="CAN\PCAN\PCANBasic.dll" LogicalName="ALib.PCAN.PCANBasic.dll" />
+ </ItemGroup>
+
+ <!-- 2) kerneldlls 目录:zip 优先
+ zlgcan.dll 运行时按「自身同级目录\kerneldlls\」加载各设备内核驱动(USBCAN.dll 等)
+ 并读取 kerneldlls\dll_cfg.ini 决定设备类型到驱动的映射;
+ 缺少该目录时 ZCAN_OpenDevice 一律返回 0,表现为「枚举不到任何设备」。
+ 运行时由 KernelDllExtractor 还原目录结构到程序目录,
+ 因此发布时不依赖外部物理目录,引用 A1Lib 的项目也无需各自配置复制。
+ 未打包时的裸文件资源名示例(%(RecursiveDir) 的反斜杠会被 MSBuild 原样保留):
+ ALib.ZLG_CAN.kerneldlls.dll_cfg.ini
+ ALib.ZLG_CAN.kerneldlls.devices_property\default\usbcan2.xml -->
+ <ItemGroup Condition="Exists('CAN\ZLG_CAN\kerneldlls.zip')">
+ <EmbeddedResource Include="CAN\ZLG_CAN\kerneldlls.zip" LogicalName="ALib.ZLG_CAN.kerneldlls.zip" />
+ </ItemGroup>
+ <ItemGroup Condition="!Exists('CAN\ZLG_CAN\kerneldlls.zip')">
+ <EmbeddedResource Include="CAN\ZLG_CAN\kerneldlls\**\*.*"
+ LogicalName="ALib.ZLG_CAN.kerneldlls.%(RecursiveDir)%(Filename)%(Extension)"
+ Exclude="CAN\ZLG_CAN\kerneldlls\**\*.tmp;CAN\ZLG_CAN\kerneldlls\**\*.zip" />
+ </ItemGroup>
+</Project>
diff --git a/XCoder/Windows/AudioHelper.cs b/A1Lib/Audio/AudioDevice.cs
similarity index 65%
rename from XCoder/Windows/AudioHelper.cs
rename to A1Lib/Audio/AudioDevice.cs
index 12ccc24..3342ead 100644
--- a/XCoder/Windows/AudioHelper.cs
+++ b/A1Lib/Audio/AudioDevice.cs
@@ -1,22 +1,16 @@
-
-using Microsoft.Win32;
-using NAudio.CoreAudioApi;
-using NewLife;
+using NAudio.CoreAudioApi;
using NewLife.Collections;
using System;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Configuration;
using System.Linq;
-using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
-namespace XCoder
+namespace ALib.Audio
{
public class AudioDevice
{
- /// <summary>设备名称</summary>
+ /// <summary>设备名称 ContainerId</summary>
public string Name { get; set; }
/// <summary>ContainerID</summary>
@@ -28,6 +22,21 @@ namespace XCoder
/// <summary>录音设备</summary>
public MMDevice RecordingDevice;
+
+ #region
+
+ /// <summary>创建录音器,wav</summary>
+ /// <returns></returns>
+ public AudioRecord CreateWavRecord()
+ {
+ if (RecordingDevice == null) return null;
+ if (RecordingDevice.AudioClient.MixFormat.Channels < 1) return null;
+
+ return new AudioRecord(this);
+ }
+
+ #endregion
+
public override string ToString()
{
var sb = Pool.StringBuilder.Get();
@@ -96,92 +105,5 @@ namespace XCoder
}
return null;
}
-
- }
-
- public static class AudioHelper
- {
- /// <summary>从注册表获取音频设备ContainerID</summary>
- /// <param name="deviceId"></param>
- /// <param name="basepath"></param>
- /// <returns></returns>
- public static string GetAudioContainerID(string deviceId, string basepath = "SYSTEM\\CurrentControlSet\\Enum\\SWD\\MMDEVAPI\\")
- {
- string registryPath = basepath + deviceId;
-
- using (RegistryKey deviceKey = Registry.LocalMachine.OpenSubKey(registryPath))
- {
- if (deviceKey == null) return null;
-
- string containerIdStr = deviceKey.GetValue("ContainerID") as string;
- if (!string.IsNullOrEmpty(containerIdStr))
- {
- return containerIdStr;
- // Guid.TryParse(containerIdStr, out Guid containerId);
- // return containerId;
- }
- }
-
- return null;
- }
-
- /// <summary>获取所有声卡</summary>
- /// <returns></returns>
- public static Dictionary<string, AudioDevice> GetAudioDevices()
- {
- var dic = new Dictionary<string, AudioDevice>();
- var enumerator = new MMDeviceEnumerator();
- var playbackDevices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);
- var recordingDevices = enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active);
-
- foreach (var dev in playbackDevices)
- {
- var containerId = GetAudioContainerID(dev.ID);
- if (containerId != null)
- {
- if (!dic.TryGetValue(containerId, out var audioDevice))
- {
- audioDevice = new AudioDevice { ContainerId = containerId };
- dic[containerId] = audioDevice;
- }
- audioDevice.PlaybackDevice = dev;
- }
- }
- foreach (var dev in recordingDevices)
- {
- var containerId = GetAudioContainerID(dev.ID);
- if (containerId != null)
- {
- if (!dic.TryGetValue(containerId, out var audioDevice))
- {
- audioDevice = new AudioDevice { ContainerId = containerId };
- dic[containerId] = audioDevice;
- }
- audioDevice.RecordingDevice = dev;
- }
- }
-
- foreach (var item in dic)
- {
- var dev = item.Value;
- // 尝试从ContainerID获取设备名称
- dev.Name = dev.ContainerId;
- var name = dev.GetCardName();
- if (!name.IsNullOrEmpty()) dev.Name = name;
- }
-
- return dic;
- }
-
- /// <summary>从USB设备获取声卡</summary>
- /// <param name="dev"></param>
- /// <returns></returns>
- public static AudioDevice GetAudioDevice(this UsbDevice dev)
- {
- var devs = AudioHelper.GetAudioDevices();
- if (devs.ContainsKey(dev.ContainerId)) { return devs[dev.ContainerId]; }
-
- return null;
- }
}
}
diff --git a/A1Lib/Audio/AudioDeviceHelper.cs b/A1Lib/Audio/AudioDeviceHelper.cs
new file mode 100644
index 0000000..377489c
--- /dev/null
+++ b/A1Lib/Audio/AudioDeviceHelper.cs
@@ -0,0 +1,111 @@
+using ALib.UsbHub;
+using Microsoft.Win32;
+using NAudio.CoreAudioApi;
+using NewLife;
+using NewLife.Collections;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Configuration;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Audio
+{
+ public static class AudioDeviceHelper
+ {
+ /// <summary>从注册表获取音频设备ContainerID</summary>
+ /// <param name="deviceId"></param>
+ /// <param name="basepath"></param>
+ /// <returns></returns>
+ public static string GetAudioContainerID(string deviceId, string basepath = "SYSTEM\\CurrentControlSet\\Enum\\SWD\\MMDEVAPI\\")
+ {
+ string registryPath = basepath + deviceId;
+
+ using (RegistryKey deviceKey = Registry.LocalMachine.OpenSubKey(registryPath))
+ {
+ if (deviceKey == null) return null;
+
+ string containerIdStr = deviceKey.GetValue("ContainerID") as string;
+ if (!string.IsNullOrEmpty(containerIdStr))
+ {
+ return containerIdStr;
+ // Guid.TryParse(containerIdStr, out Guid containerId);
+ // return containerId;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>获取所有声卡</summary>
+ /// <returns></returns>
+ public static Dictionary<string, AudioDevice> GetAudioDevices()
+ {
+ var dic = new Dictionary<string, AudioDevice>();
+ var enumerator = new MMDeviceEnumerator();
+ var playbackDevices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);
+ var recordingDevices = enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active);
+
+ foreach (var dev in playbackDevices)
+ {
+ var containerId = GetAudioContainerID(dev.ID);
+ if (containerId != null)
+ {
+ if (!dic.TryGetValue(containerId, out var audioDevice))
+ {
+ audioDevice = new AudioDevice { ContainerId = containerId };
+ dic[containerId] = audioDevice;
+ }
+ audioDevice.PlaybackDevice = dev;
+ }
+ }
+ foreach (var dev in recordingDevices)
+ {
+ var containerId = GetAudioContainerID(dev.ID);
+ if (containerId != null)
+ {
+ if (!dic.TryGetValue(containerId, out var audioDevice))
+ {
+ audioDevice = new AudioDevice { ContainerId = containerId };
+ dic[containerId] = audioDevice;
+ }
+ audioDevice.RecordingDevice = dev;
+ }
+ }
+
+ foreach (var item in dic)
+ {
+ var dev = item.Value;
+ // 尝试从ContainerID获取设备名称
+ dev.Name = dev.ContainerId;
+ var name = dev.GetCardName();
+ if (!name.IsNullOrEmpty()) dev.Name = name;
+ }
+
+ return dic;
+ }
+
+ /// <summary>从USB设备获取声卡</summary>
+ /// <param name="dev"></param>
+ /// <returns></returns>
+ public static AudioDevice GetAudioDevice(this UsbDevice dev)
+ {
+ var devs = AudioDeviceHelper.GetAudioDevices();
+ if (devs.ContainsKey(dev.ContainerId)) { return devs[dev.ContainerId]; }
+
+ return null;
+ }
+
+ /// <summary>从HUB端口获取声卡</summary>
+ /// <param name="dev"></param>
+ /// <returns></returns>
+ public static AudioDevice GetAudioDevice(this UsbHubPort dev)
+ {
+ if (!dev.Exists()) return null;
+ return GetAudioDevice(dev.GetUsbDevice());
+ }
+ }
+}
diff --git a/A1Lib/Audio/AudioFeatureExtractor.cs b/A1Lib/Audio/AudioFeatureExtractor.cs
new file mode 100644
index 0000000..989cc3f
--- /dev/null
+++ b/A1Lib/Audio/AudioFeatureExtractor.cs
@@ -0,0 +1,599 @@
+using MathNet.Numerics;
+using MathNet.Numerics.IntegralTransforms;
+using NAudio.Wave;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Numerics;
+using static System.Net.Mime.MediaTypeNames;
+
+namespace ALib.Audio
+{
+ /// <summary>
+ /// 音频特征提取器,处理单声道音频数据
+ /// </summary>
+ public class AudioFeatureExtractor : IDisposable
+ {
+ #region 参数 修改后调用一下 Initialize,确保参数正确
+
+ /// <summary>采样率 (默认48000Hz)</summary>
+ public int SampleRate { get; set; } = 48000;
+
+ /// <summary>FFT窗口大小 (默认2048,必须为2的幂)</summary>
+ public int FftSize { get; set; } = 2048;
+
+ /// <summary>每秒输出的Mel点数 (默认30)</summary>
+ public int MelPointsPerSecond { get; set; } = 30;
+
+ /// <summary>梅尔带数量 (默认80)</summary>
+ public int MelBands { get; set; } = 80;
+
+ /// <summary>最小频率Hz (默认100)</summary>
+ public float MinFrequency { get; set; } = 200f;
+
+ /// <summary>最大频率Hz (默认8000)</summary>
+ public float MaxFrequency { get; set; } = 13500f;
+
+ /// <summary>帧移 (根据采样率和MelPointsPerSecond自动计算)</summary>
+ public int HopSize => SampleRate / MelPointsPerSecond;
+
+ #endregion
+
+ #region 输出结果
+
+ /// <summary>Mel频谱 [帧数, MelBands]</summary>
+ public double[][] MelSpectrum { get; private set; }
+
+ /// <summary>音量曲线(dB) [帧数]</summary>
+ public double[] VolumeCurveDb { get; private set; }
+
+ #endregion
+
+ #region 私有字段
+
+
+ /// <summary>预计算的Mel滤波器组</summary>
+ private double[][] _melFilterBank;
+
+ /// <summary>_melFilterBank对应的频点mel数</summary>
+ private double[] _melPoints;
+
+ /// <summary>_melFilterBank对应的频点hz数</summary>
+ private double[] _hzPoints;
+
+ /// <summary>预计算的汉宁窗</summary>
+ private double[] _window;
+
+ /// <summary>音频数据缓冲区(固定大小 = FftSize)</summary>
+ private float[] _buffer;
+
+ /// <summary>缓冲区中的有效数据长度(0 到 FftSize-1)</summary>
+ private int _bufferLength = 0;
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>
+ /// 创建音频特征提取器实例
+ /// </summary>
+ public AudioFeatureExtractor()
+ {
+ Initialize();
+ }
+
+ /// <summary>
+ /// 创建音频特征提取器实例,指定采样率
+ /// </summary>
+ /// <param name="sampleRate">音频采样率</param>
+ /// <param name="melPoint">每秒Mel点数</param>
+ public AudioFeatureExtractor(int sampleRate, int melPoint = 30)
+ {
+ SampleRate = sampleRate;
+ MelPointsPerSecond = melPoint;
+ Initialize();
+ }
+
+ /// <summary>
+ /// 创建音频特征提取器实例,指定音频格式
+ /// </summary>
+ public AudioFeatureExtractor(WaveFormat format, int melPoint = 30)
+ {
+ SampleRate = format.SampleRate;
+ MelPointsPerSecond = melPoint;
+ Initialize();
+ }
+
+ public void Dispose() { }
+
+ #endregion
+
+ #region 私有方法
+
+ public void Initialize()
+ {
+ // 初始化固定大小缓冲区
+ _buffer = new float[FftSize];
+ _bufferLength = 0;
+
+ // 预计算汉宁窗
+ _window = Window.Hann(FftSize);
+
+ // 预计算Mel滤波器组
+ _melFilterBank = CreateMelFilterBank();
+ }
+
+ #endregion
+
+ #region 核心处理
+
+ /// <summary>
+ /// 处理单声道音频数据,生成Mel频谱(使用缓冲区+内存池,支持持续输入)
+ /// </summary>
+ /// <param name="audioData">单声道音频数据 float[],取值范围[-1, 1]</param>
+ /// <returns>Mel频谱 [帧数, MelBands]</returns>
+ private double[][] Process(float[] audioData)
+ {
+ int newDataLength = audioData?.Length ?? 0;
+ int totalLength = _bufferLength + newDataLength;
+
+ // 数据不足,追加到缓冲区并返回空结果
+ if (totalLength < FftSize)
+ {
+ if (newDataLength > 0)
+ {
+ Array.Copy(audioData, 0, _buffer, _bufferLength, newDataLength);
+ _bufferLength += newDataLength;
+ }
+ MelSpectrum = new double[0][];
+ VolumeCurveDb = new double[0];
+ return MelSpectrum;
+ }
+
+ // 缓冲区没有数据
+ if (_bufferLength == 0)
+ {
+ // 执行处理
+ ProcessInternal(audioData, totalLength);
+ return MelSpectrum;
+ }
+
+ // 需要结合缓冲区和新数据
+ // 从内存池租借临时缓冲区
+ float[] pooledBuffer = ArrayPool<float>.Shared.Rent(totalLength);
+ try
+ {
+ // 复制缓冲区数据
+ if (_bufferLength > 0)
+ {
+ Array.Copy(_buffer, 0, pooledBuffer, 0, _bufferLength);
+ }
+ // 追加新数据
+ if (newDataLength > 0)
+ {
+ Array.Copy(audioData, 0, pooledBuffer, _bufferLength, newDataLength);
+ }
+
+ // 执行处理
+ ProcessInternal(pooledBuffer, totalLength);
+ return MelSpectrum;
+ }
+ finally
+ {
+ // 归还内存池
+ ArrayPool<float>.Shared.Return(pooledBuffer);
+ }
+ }
+
+ /// <summary>
+ /// 内部处理方法
+ /// </summary>
+ private void ProcessInternal(float[] data, int dataLength)
+ {
+ // 1. 计算帧数
+ int frames = (dataLength - FftSize) / HopSize + 1;
+
+ // 计算已处理的数据长度
+ int processedLength = (frames - 1) * HopSize + FftSize;
+
+ // 2. 预处理音频(分帧、加窗)
+ Complex[][] framesComplex = new Complex[frames][];
+ for (int i = 0; i < frames; i++)
+ {
+ // 应用汉宁窗并转换为复数
+ framesComplex[i] = new Complex[FftSize];
+ for (int j = 0; j < FftSize; j++)
+ {
+ framesComplex[i][j] = new Complex(data[i * HopSize + j] * _window[j], 0);
+ }
+ }
+
+ // 3. 计算FFT和功率谱
+ int spectrumSize = FftSize / 2 + 1;
+ double[][] powerSpectrum = new double[frames][];
+ for (int i = 0; i < frames; i++)
+ {
+ Fourier.Forward(framesComplex[i], FourierOptions.NoScaling);
+
+ powerSpectrum[i] = new double[spectrumSize];
+ for (int j = 0; j < spectrumSize; j++)
+ {
+ powerSpectrum[i][j] = framesComplex[i][j].MagnitudeSquared();
+ }
+ }
+
+ // 4. 应用Mel滤波器组
+ MelSpectrum = new double[frames][];
+ for (int i = 0; i < frames; i++)
+ {
+ MelSpectrum[i] = new double[MelBands];
+ for (int j = 0; j < MelBands; j++)
+ {
+ double sum = 0;
+ for (int k = 0; k < spectrumSize; k++)
+ {
+ sum += powerSpectrum[i][k] * _melFilterBank[j][k];
+ }
+ MelSpectrum[i][j] = sum;
+ }
+ }
+
+ // 5. 计算音量曲线(dB)
+ VolumeCurveDb = new double[frames];
+ for (int i = 0; i < frames; i++)
+ {
+ double rms = Math.Sqrt(MelSpectrum[i].Sum(x => x * x) / MelSpectrum[i].Length);
+ if (rms > 0)
+ {
+ VolumeCurveDb[i] = 10 * Math.Log10(rms + double.Epsilon);
+ VolumeCurveDb[i] = Math.Max(VolumeCurveDb[i], -100.0);
+ VolumeCurveDb[i] = Math.Min(VolumeCurveDb[i], 120.0);
+ }
+ else
+ {
+ VolumeCurveDb[i] = -100.0;
+ }
+ }
+
+ // 6. 更新缓冲区:保留未处理的数据
+ int remainingLength = dataLength - processedLength;
+ if (remainingLength > 0)
+ {
+ Array.Copy(data, processedLength, _buffer, 0, remainingLength);
+ }
+ _bufferLength = remainingLength;
+ }
+
+ /// <summary>通过mel获取音量(RMS)</summary>
+ /// <returns>音量数组</returns>
+ public double[] Volumes()
+ {
+ var mel = MelSpectrum;
+ if (mel == null || mel.Length == 0) return null;
+
+ var vol = new double[mel.Length];
+ for (int i = 0; i < vol.Length; i++)
+ {
+ vol[i] = Math.Sqrt(mel[i].Sum(x => x * x) / mel[i].Length);
+ }
+ return vol;
+ }
+
+ /// <summary>
+ /// 处理单声道音频数据,返回Mel频谱(dB单位)
+ /// </summary>
+ /// <param name="audioData">单声道音频数据 float[]</param>
+ /// <returns>Mel频谱(dB) [帧数, MelBands]</returns>
+ public double[][] ProcessAsDecibels(float[] audioData)
+ {
+ Process(audioData);
+ if (MelSpectrum == null || MelSpectrum.Length == 0) return MelSpectrum;
+
+ var dbs = new double[MelSpectrum.Length][];
+
+ // 转换为分贝
+ for (int i = 0; i < MelSpectrum.Length; i++)
+ {
+ dbs[i] = new double[MelSpectrum[i].Length];
+ for (int j = 0; j < MelSpectrum[i].Length; j++)
+ {
+ double db = 10 * Math.Log10(MelSpectrum[i][j] + double.Epsilon);
+ dbs[i][j] = Math.Max(db, -100.0);
+ // dbs[i][j] = Math.Min(db, 120.0);
+ }
+ }
+
+ return dbs;
+ }
+
+ #endregion
+
+ #region 缓冲区管理
+
+ /// <summary>
+ /// 清空缓冲区
+ /// </summary>
+ public void ClearBuffer()
+ {
+ _bufferLength = 0;
+ }
+
+ /// <summary>
+ /// 获取缓冲区中当前的数据长度
+ /// </summary>
+ public int BufferedLength => _bufferLength;
+
+ #endregion
+
+ #region Mel滤波器组
+
+ private double[][] CreateMelFilterBank()
+ {
+ double[][] filterBank = new double[MelBands][];
+ int spectrumSize = FftSize / 2 + 1;
+
+ // 计算Mel频率范围
+ double minMel = HzToMel(MinFrequency);
+ double maxMel = HzToMel(MaxFrequency);
+
+ // 创建Mel点
+ double[] melPoints = new double[MelBands + 2];
+ for (int i = 0; i < MelBands + 2; i++)
+ {
+ melPoints[i] = minMel + i * (maxMel - minMel) / (MelBands + 1);
+ }
+ _melPoints = melPoints;
+
+ // 将Mel点转换回Hz
+ double[] hzPoints = melPoints.Select(MelToHz).ToArray();
+ _hzPoints = hzPoints;
+
+ // 将Hz点转换为FFT bin索引
+ int[] binIndices = hzPoints.Select(hz => (int)Math.Floor((FftSize + 1) * hz / SampleRate)).ToArray();
+
+ // 创建三角形滤波器
+ for (int i = 0; i < MelBands; i++)
+ {
+ filterBank[i] = new double[spectrumSize];
+ int start = binIndices[i];
+ int peak = binIndices[i + 1];
+ int end = binIndices[i + 2];
+
+ // 防止除零
+ if (peak == start) peak = start + 1;
+ if (end == peak) end = peak + 1;
+
+ // 上升斜坡
+ for (int j = start; j <= peak && j < spectrumSize; j++)
+ {
+ if (j >= 0)
+ {
+ filterBank[i][j] = (double)(j - start) / (peak - start);
+ }
+ }
+
+ // 下降斜坡
+ for (int j = peak; j <= end && j < spectrumSize; j++)
+ {
+ if (j >= 0)
+ {
+ filterBank[i][j] = 1.0 - (double)(j - peak) / (end - peak);
+ }
+ }
+ }
+
+ return filterBank;
+ }
+
+ private static double HzToMel(double hz)
+ {
+ return 2595 * Math.Log10(1 + hz / 700.0);
+ }
+
+ private static double MelToHz(double mel)
+ {
+ return 700 * (Math.Pow(10, mel / 2595.0) - 1);
+ }
+
+ #endregion
+
+ #region 参数更新
+
+ /// <summary>
+ /// 更新采样率,需要重新初始化滤波器组
+ /// </summary>
+ /// <param name="sampleRate">新的采样率</param>
+ public void UpdateSampleRate(int sampleRate)
+ {
+ if (SampleRate != sampleRate)
+ {
+ SampleRate = sampleRate;
+ _melFilterBank = CreateMelFilterBank();
+ }
+ }
+
+ /// <summary>
+ /// 更新FFT大小,需要重新初始化窗函数和滤波器组
+ /// </summary>
+ /// <param name="fftSize">新的FFT大小(必须为2的幂)</param>
+ public void UpdateFftSize(int fftSize)
+ {
+ if (FftSize != fftSize)
+ {
+ FftSize = fftSize;
+ _buffer = new float[FftSize];
+ _bufferLength = 0;
+ _window = Window.Hann(FftSize);
+ _melFilterBank = CreateMelFilterBank();
+ }
+ }
+
+ /// <summary>
+ /// 更新Mel滤波器参数,需要重新计算滤波器组
+ /// </summary>
+ /// <param name="melBands">Mel带数量</param>
+ /// <param name="minFreq">最小频率</param>
+ /// <param name="maxFreq">最大频率</param>
+ public void UpdateMelParameters(int melBands, float minFreq, float maxFreq)
+ {
+ MelBands = melBands;
+ MinFrequency = minFreq;
+ MaxFrequency = maxFreq;
+ _melFilterBank = CreateMelFilterBank();
+ }
+
+ #endregion
+
+ #region 辅助
+
+ /// <summary>根据Mel带索引获取对应的中心频率</summary>
+ /// <param name="melIdx">Mel带索引 (0 ~ MelBands-1)</param>
+ /// <returns>对应的中心频率 (Hz)</returns>
+ public double GetHzFromMelIdx(int melIdx)
+ {
+ if (melIdx < 0 || melIdx >= MelBands)
+ throw new ArgumentOutOfRangeException(nameof(melIdx), $"Mel索引必须在0到{MelBands - 1}之间");
+
+ return _hzPoints[melIdx];
+ }
+
+ /// <summary>根据Mel带索引获取对应的MEL值</summary>
+ /// <param name="melIdx">Mel带索引 (0 ~ MelBands-1)</param>
+ /// <returns>对应的中心频率 (Hz)</returns>
+ public double GetMelFromMelIdx(int melIdx)
+ {
+ if (melIdx < 0 || melIdx >= MelBands)
+ throw new ArgumentOutOfRangeException(nameof(melIdx), $"Mel索引必须在0到{MelBands - 1}之间");
+
+ return _melPoints[melIdx];
+ }
+
+ #endregion
+
+ #region 单一频率(扫频)处理
+
+ /// <summary>从mel点拿到中心频点</summary>
+ /// <remarks>针对单一频率音频,需要中心频率的音量大于其他频率 20db</remarks>
+ /// <param name="dbs">mel点,db数值。 ProcessAsDecibels返回值逐个分析</param>
+ /// <param name="diff">db差值。数学 10xlog(10)</param>
+ /// <param name="maxInp">外部限定的max值,多点时统一标准</param>
+ /// <returns>default没有中心点,MaxValue不是单一频点</returns>
+ public double GetHzFromMel(double[] dbs, double diff = 4.0, double maxInp = -1000.0)
+ {
+ if (dbs == null) return default;
+ if (dbs.Length < 8) return default;
+
+ // 找到最大值位置, 中心频点位置
+ int maxidx = 0;
+ double max = -1000.0;
+ for (int i = 0; i < dbs.Length; i++)
+ {
+ if (dbs[i] > max)
+ {
+ max = dbs[i];
+ maxidx = i;
+ }
+ }
+
+ max = Math.Max(max, maxInp);
+
+ // 阈值
+ var thr = max - diff;
+ // 上升沿个数(小于thr到大于thr 的上升沿)
+ int cnt = 0;
+ // 大于thr的个数,用于判断是不是单一频率。
+ int upCnt = 0;
+ // 一开始就大
+ if (dbs[0] > thr) { cnt++; upCnt++; }
+ for (int i = 1; i < dbs.Length; i++)
+ {
+ if (dbs[i] > thr) upCnt++;
+ if ((dbs[i - 1] < thr) && (dbs[i] > thr))
+ {
+ cnt++;
+ }
+ }
+
+ if (cnt == 0) return default;
+ // 一个上升沿 的情况下
+ // 大于判定阈值的频点超过x个,应该是有问题的。
+ // 正常来说,3个是正常的。
+ if (upCnt > 6) return double.MaxValue;
+ if (cnt > 1) return double.MaxValue;
+
+ return _hzPoints[maxidx];
+ }
+
+ /// <summary>从mels获取hzs</summary>
+ /// <remarks>针对单一频率音频,需要中心频率的音量大于其他频率 diff db</remarks>
+ /// <param name="mels">mel图,db数值。ProcessAsDecibels返回值逐个分析</param>
+ /// <param name="diff">db差值。数学 10xlog(10)</param>
+ /// <param name="maxInp">外部限定的max值,多点时统一标准</param>
+ /// <returns></returns>
+ public double[] GetHzsFromMels(double[][] dbss, double diff = 4.0)
+ {
+ if (dbss == null) return null;
+
+ // 10 x log(10)
+ // 4db 大概是 2.512 倍
+ var max = dbss.Max(fram => fram.Max());
+ var hzs = new double[dbss.Length];
+ for (int i = 0; i < hzs.Length; i++) hzs[i] = GetHzFromMel(dbss[i], diff, max);
+
+ return hzs;
+ }
+
+ /// <summary>单频点图</summary>
+ /// <param name="hzs">GetHzsFromMels</param>
+ /// <returns></returns>
+ public Bitmap GetSingleHzImage(double[] hzs)
+ {
+ var image = new Bitmap(hzs.Length, MelBands);
+ using (Graphics g = Graphics.FromImage(image))
+ {
+ // 绘制黑色背景
+ g.Clear(Color.Black);
+ }
+
+ var div = (MaxFrequency - MinFrequency) / MelBands;
+ var clr = Color.White;
+
+ for (int i = 0; i < hzs.Length; i++)
+ {
+ if (hzs[i] < MinFrequency) continue;
+ if (hzs[i] > MaxFrequency) continue;
+
+ int x = i;
+ var dy = (hzs[i] - MinFrequency) / div;
+ int y = MelBands - (int)dy;
+ if (y < 0) continue;
+ if (y >= MelBands) continue;
+
+ image.SetPixel(x, y, clr);
+ }
+
+ for (int i = 0; i < hzs.Length; i++)
+ {
+ // if (hzs[i] == default) image.SetPixel(i, 0, Color.Green);
+ if (hzs[i] == double.MaxValue) image.SetPixel(i, 0, Color.Red);
+ }
+
+ return image;
+ }
+
+ /// <summary>从mel 获取单频点图</summary>
+ /// <param name="diff">db差值。数学 10xlog(10)</param>
+ /// <returns></returns>
+ public Bitmap GetSingleHzImageFromMels(double[][] dbmels, double diff = 4.0)
+ {
+ // 10 x log(10)
+ // 4db 大概是 2.512 倍
+ var hzs = GetHzsFromMels(dbmels, diff);
+ return GetSingleHzImage(hzs);
+ }
+
+ #endregion
+ }
+}
diff --git a/A1Lib/Audio/AudioPreciseFreqDetector.cs b/A1Lib/Audio/AudioPreciseFreqDetector.cs
new file mode 100644
index 0000000..fcd1757
--- /dev/null
+++ b/A1Lib/Audio/AudioPreciseFreqDetector.cs
@@ -0,0 +1,561 @@
+using MathNet.Numerics;
+using MathNet.Numerics.IntegralTransforms;
+using NAudio.Wave;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+namespace ALib.Audio
+{
+ /// <summary>
+ /// 精确频率检测器
+ /// 对输入音频做FFT,每秒输出多个点;
+ /// 每个点在有效频率范围(MinFrequency~MaxFrequency)内扫描所有bin找到功率峰值,
+ /// 再用二次插值精确定位频率(精度50Hz),输出频率(Hz)和音量(dB)
+ /// </summary>
+ public class AudioPreciseFreqDetector : IDisposable
+ {
+ #region 参数 修改后调用一下 Initialize,确保参数正确
+
+ /// <summary>采样率 (默认48000Hz)</summary>
+ public int SampleRate { get; set; } = 48000;
+
+ /// <summary>FFT窗口大小 (默认4096,必须为2的幂)</summary>
+ public int FftSize { get; set; } = 4096;
+
+ /// <summary>每秒输出的点数 (默认30)</summary>
+ public int PointsPerSecond { get; set; } = 30;
+
+ /// <summary>粗略筛选的bin搜索起始索引(由MinFrequency自动计算)</summary>
+ private int _minSearchBin;
+
+ /// <summary>粗略筛选的bin搜索结束索引(由MaxFrequency自动计算)</summary>
+ private int _maxSearchBin;
+
+ /// <summary>精确查找时的频率精度Hz (默认50)</summary>
+ public float FinePrecisionHz { get; set; } = 50f;
+
+ /// <summary>最小频率Hz</summary>
+ public float MinFrequency { get; set; } = 200f;
+
+ /// <summary>最大频率Hz</summary>
+ public float MaxFrequency { get; set; } = 13500f;
+
+ /// <summary>帧移 (根据采样率和PointsPerSecond自动计算)</summary>
+ public int HopSize => SampleRate / PointsPerSecond;
+
+ #endregion
+
+ #region 输出结果
+
+ /// <summary>每帧检测到的精确频率Hz [帧数]</summary>
+ public double[] DetectedFrequencies { get; private set; }
+
+ /// <summary>每帧检测到的音量dB [帧数]</summary>
+ public double[] DetectedDecibels { get; private set; }
+
+ /// <summary>每帧粗略筛选的频率段起始Hz [帧数]</summary>
+ public double[] CoarseFrequencies { get; private set; }
+
+ /// <summary>每帧的功率谱(dB) [帧数, spectrumSize],用于画频谱图</summary>
+ public double[][] FrameSpectrumDb { get; private set; }
+
+ #endregion
+
+ #region 私有字段
+
+ /// <summary>预计算的汉宁窗</summary>
+ private double[] _window;
+
+ /// <summary>音频数据缓冲区</summary>
+ private float[] _buffer;
+
+ /// <summary>缓冲区中的有效数据长度</summary>
+ private int _bufferLength = 0;
+
+ /// <summary>频谱分辨率 (Hz/bin),用于粗略筛选</summary>
+ private double _binResolution;
+
+ #endregion
+
+ #region 构造
+
+ public AudioPreciseFreqDetector()
+ {
+ Initialize();
+ }
+
+ public AudioPreciseFreqDetector(int sampleRate, int pointsPerSecond = 30)
+ {
+ SampleRate = sampleRate;
+ PointsPerSecond = pointsPerSecond;
+ Initialize();
+ }
+
+ public AudioPreciseFreqDetector(WaveFormat format, int pointsPerSecond = 30)
+ {
+ SampleRate = format.SampleRate;
+ PointsPerSecond = pointsPerSecond;
+ Initialize();
+ }
+
+ public void Dispose() { }
+
+ #endregion
+
+ #region 初始化
+
+ public void Initialize()
+ {
+ _buffer = new float[FftSize];
+ _bufferLength = 0;
+
+ // 预计算汉宁窗
+ _window = Window.Hann(FftSize);
+
+ // 计算频谱分辨率
+ _binResolution = (double)SampleRate / FftSize;
+
+ // 预计算有效频率范围对应的bin搜索区间
+ RecalcSearchRange();
+ }
+
+ /// <summary>
+ /// 根据 MinFrequency / MaxFrequency 预计算有效bin搜索区间
+ /// </summary>
+ private void RecalcSearchRange()
+ {
+ int minBin = Math.Max(1, (int)Math.Round(MinFrequency / _binResolution));
+ int maxBin = Math.Min(FftSize / 2 - 1, (int)Math.Round(MaxFrequency / _binResolution));
+ if (minBin >= maxBin)
+ {
+ minBin = 1;
+ maxBin = FftSize / 2 - 1;
+ }
+ _minSearchBin = minBin;
+ _maxSearchBin = maxBin;
+ }
+
+ #endregion
+
+ #region 核心处理
+
+ /// <summary>
+ /// 处理单声道音频数据,输出每帧的精确频率和音量
+ /// </summary>
+ /// <param name="audioData">单声道音频数据 float[],取值范围[-1, 1]</param>
+ /// <returns>检测到的频率Hz数组</returns>
+ public double[] Process(float[] audioData)
+ {
+ int newDataLength = audioData?.Length ?? 0;
+ int totalLength = _bufferLength + newDataLength;
+
+ // 数据不足,追加到缓冲区
+ if (totalLength < FftSize)
+ {
+ if (newDataLength > 0)
+ {
+ Array.Copy(audioData, 0, _buffer, _bufferLength, newDataLength);
+ _bufferLength += newDataLength;
+ }
+ DetectedFrequencies = Array.Empty<double>();
+ DetectedDecibels = Array.Empty<double>();
+ CoarseFrequencies = Array.Empty<double>();
+ return DetectedFrequencies;
+ }
+
+ if (_bufferLength == 0)
+ {
+ ProcessInternal(audioData, totalLength);
+ return DetectedFrequencies;
+ }
+
+ // 需要结合缓冲区和新数据
+ float[] pooledBuffer = ArrayPool<float>.Shared.Rent(totalLength);
+ try
+ {
+ if (_bufferLength > 0)
+ Array.Copy(_buffer, 0, pooledBuffer, 0, _bufferLength);
+ if (newDataLength > 0)
+ Array.Copy(audioData, 0, pooledBuffer, _bufferLength, newDataLength);
+
+ ProcessInternal(pooledBuffer, totalLength);
+ return DetectedFrequencies;
+ }
+ finally
+ {
+ ArrayPool<float>.Shared.Return(pooledBuffer);
+ }
+ }
+
+ private void ProcessInternal(float[] data, int dataLength)
+ {
+ int spectrumSize = FftSize / 2 + 1;
+ int frames = (dataLength - FftSize) / HopSize + 1;
+ int processedLength = (frames - 1) * HopSize + FftSize;
+
+ DetectedFrequencies = new double[frames];
+ DetectedDecibels = new double[frames];
+ CoarseFrequencies = new double[frames];
+ FrameSpectrumDb = new double[frames][];
+
+ // 预分配复数数组,复用减少GC
+ Complex[] fftBuffer = new Complex[FftSize];
+
+ for (int frame = 0; frame < frames; frame++)
+ {
+ // 1. 分帧 + 加窗
+ int offset = frame * HopSize;
+ for (int j = 0; j < FftSize; j++)
+ {
+ fftBuffer[j] = new Complex(data[offset + j] * _window[j], 0);
+ }
+
+ // 2. FFT
+ Fourier.Forward(fftBuffer, FourierOptions.NoScaling);
+
+ // 3. 计算功率谱 (平方幅度)
+ double[] powerSpectrum = new double[spectrumSize];
+ for (int j = 0; j < spectrumSize; j++)
+ {
+ powerSpectrum[j] = fftBuffer[j].MagnitudeSquared();
+ }
+
+ // 4. 在有效频率范围内扫描所有bin,找到功率最大的bin(峰值)
+ int bestCoarseBin = FindBestCoarseBin(powerSpectrum);
+ double coarseHz = bestCoarseBin * _binResolution;
+ CoarseFrequencies[frame] = coarseHz;
+
+ // 5. 精确查找:在粗略频率附近,以50Hz精度精确定位
+ var (preciseHz, preciseDb) = FindPreciseFrequency(fftBuffer, powerSpectrum, bestCoarseBin);
+ DetectedFrequencies[frame] = preciseHz;
+ DetectedDecibels[frame] = preciseDb;
+
+ // 6. 保存该帧的dB频谱(用于画图)
+ FrameSpectrumDb[frame] = new double[spectrumSize];
+ for (int j = 0; j < spectrumSize; j++)
+ {
+ double db = 10 * Math.Log10(powerSpectrum[j] + double.Epsilon);
+ FrameSpectrumDb[frame][j] = Math.Max(db, -100.0);
+ }
+ }
+
+ // 7. 更新缓冲区
+ int remainingLength = dataLength - processedLength;
+ if (remainingLength > 0)
+ Array.Copy(data, processedLength, _buffer, 0, remainingLength);
+ _bufferLength = remainingLength;
+ }
+
+ /// <summary>
+ /// 在有效频率范围内扫描所有bin,找到功率最大的bin(峰值)
+ /// </summary>
+ private int FindBestCoarseBin(double[] powerSpectrum)
+ {
+ int bestBin = _minSearchBin;
+ double bestPower = powerSpectrum[bestBin];
+
+ for (int bin = _minSearchBin + 1; bin <= _maxSearchBin && bin < powerSpectrum.Length; bin++)
+ {
+ if (powerSpectrum[bin] > bestPower)
+ {
+ bestPower = powerSpectrum[bin];
+ bestBin = bin;
+ }
+ }
+
+ return bestBin;
+ }
+
+ /// <summary>
+ /// 在粗略筛选出的频率附近,以FinePrecisionHz精度进行精确频率查找
+ /// 使用二次插值法提高频率分辨率
+ /// </summary>
+ /// <param name="fftBuffer">FFT结果(复数)</param>
+ /// <param name="powerSpectrum">功率谱</param>
+ /// <param name="coarseBin">粗略筛选出的bin</param>
+ /// <returns>(精确频率Hz, 音量dB)</returns>
+ private (double freqHz, double db) FindPreciseFrequency(Complex[] fftBuffer, double[] powerSpectrum, int coarseBin)
+ {
+ int spectrumSize = powerSpectrum.Length;
+
+ // 使用二次抛物线插值提高频率精度
+ // 在coarseBin附近取3个点做二次拟合,找到真正的峰值位置
+ int leftBin = Math.Max(1, coarseBin - 1);
+ int rightBin = Math.Min(spectrumSize - 1, coarseBin + 1);
+
+ double yL = powerSpectrum[leftBin];
+ double yC = powerSpectrum[coarseBin];
+ double yR = powerSpectrum[rightBin];
+
+ double preciseBin = coarseBin;
+
+ // 二次插值: 峰值偏移 = 0.5 * (yL - yR) / (yL - 2*yC + yR)
+ double denominator = yL - 2 * yC + yR;
+ if (Math.Abs(denominator) > 1e-15)
+ {
+ double delta = 0.5 * (yL - yR) / denominator;
+ // 限制偏移在合理范围内 [-1, 1]
+ delta = Math.Max(-1.0, Math.Min(1.0, delta));
+ preciseBin = coarseBin + delta;
+ }
+
+ // 将精确bin转换为频率
+ double preciseHz = preciseBin * _binResolution;
+
+ // 取附近多个bin的功率平均值作为该频率的音量
+ // 精细精度50Hz对应的大致bin数
+ int fineBinWidth = Math.Max(1, (int)Math.Round(FinePrecisionHz / _binResolution));
+ double powerSum = 0;
+ int powerCount = 0;
+
+ int searchStart = Math.Max(0, (int)Math.Floor(preciseBin) - fineBinWidth);
+ int searchEnd = Math.Min(spectrumSize - 1, (int)Math.Ceiling(preciseBin) + fineBinWidth);
+
+ for (int j = searchStart; j <= searchEnd; j++)
+ {
+ powerSum += powerSpectrum[j];
+ powerCount++;
+ }
+
+ double avgPower = powerSum / powerCount;
+ double db = -100.0;
+ if (avgPower > 0)
+ {
+ // 功率谱是MagnitudeSquared,转dB用 10*log10
+ db = 10 * Math.Log10(avgPower + double.Epsilon);
+ db = Math.Max(db, -100.0);
+ db = Math.Min(db, 120.0);
+ }
+
+ return (preciseHz, db);
+ }
+
+ #endregion
+
+ #region 缓冲区管理
+
+ /// <summary>清空缓冲区</summary>
+ public void ClearBuffer()
+ {
+ _bufferLength = 0;
+ }
+
+ /// <summary>缓冲区中当前的数据长度</summary>
+ public int BufferedLength => _bufferLength;
+
+ #endregion
+
+ #region 参数更新
+
+ /// <summary>更新采样率,重新初始化</summary>
+ public void UpdateSampleRate(int sampleRate)
+ {
+ if (SampleRate != sampleRate)
+ {
+ SampleRate = sampleRate;
+ _binResolution = (double)SampleRate / FftSize;
+ RecalcSearchRange();
+ }
+ }
+
+ /// <summary>更新FFT大小,重新初始化</summary>
+ public void UpdateFftSize(int fftSize)
+ {
+ if (FftSize != fftSize)
+ {
+ FftSize = fftSize;
+ _buffer = new float[FftSize];
+ _bufferLength = 0;
+ _window = Window.Hann(FftSize);
+ _binResolution = (double)SampleRate / FftSize;
+ RecalcSearchRange();
+ }
+ }
+
+ #endregion
+
+ #region 便捷方法
+
+ /// <summary>
+ /// 获取检测结果的简明摘要
+ /// </summary>
+ public string GetSummary()
+ {
+ if (DetectedFrequencies == null || DetectedFrequencies.Length == 0)
+ return "No data";
+
+ var validFreqs = DetectedFrequencies.Where(f => f > 0).ToArray();
+ if (validFreqs.Length == 0)
+ return "No valid frequencies detected";
+
+ return $"Frames:{DetectedFrequencies.Length}, " +
+ $"Freq: {validFreqs.Average():F1}Hz, " +
+ $"dB: {DetectedDecibels.Where(d => d > -100).Average():F1}dB";
+ }
+
+ #endregion
+
+ #region 频谱图生成
+
+ /// <summary>
+ /// 生成精确频率检测的频谱图(无坐标轴标签)
+ /// X轴=时间(帧),Y轴=频率(Hz),颜色=dB值
+ /// 并在图上用亮色十字标记每帧检测到的精确频率位置
+ /// </summary>
+ /// <param name="channelName">未使用(保留兼容)</param>
+ /// <param name="freqMarkerColor">频率标记颜色,null则使用亮绿</param>
+ /// <param name="heatmap">true=热力图配色(默认);false=灰度图(亮=能量强)</param>
+ /// <returns>频谱图 Bitmap</returns>
+ public Bitmap GetSpectrumImage(string channelName = "", Color? freqMarkerColor = null, bool heatmap = true)
+ {
+ if (FrameSpectrumDb == null || FrameSpectrumDb.Length == 0)
+ return null;
+
+ int frames = FrameSpectrumDb.Length;
+ int spectrumSize = FrameSpectrumDb[0].Length;
+ var markerColor = freqMarkerColor ?? Color.Lime;
+
+ // 只绘制有效频率范围 [MinFrequency, MaxFrequency] 对应的 bin 范围
+ int minBin = Math.Max(0, (int)(MinFrequency / _binResolution));
+ int maxBin = Math.Min(spectrumSize - 1, (int)Math.Ceiling(MaxFrequency / _binResolution));
+ int dataHeight = maxBin - minBin + 1;
+ if (dataHeight <= 0)
+ return null;
+
+ // 每帧2像素宽;超长音频会生成数万像素宽的超大图(内存爆炸 + 超出 GDI+ 尺寸限制),
+ // 这里封顶最大宽度并按比例降采样,避免绘制/内存开销失控。
+ const int MaxWidth = 4000;
+ int imgWidth = Math.Max(200, frames * 2);
+ double frameToX = 2.0;
+ if (imgWidth > MaxWidth)
+ {
+ imgWidth = MaxWidth;
+ frameToX = (double)imgWidth / frames;
+ }
+ int imgHeight = dataHeight;
+
+ // 预计算 dB 归一化范围
+ double minDb = double.MaxValue, maxDb = double.MinValue;
+ for (int f = 0; f < frames; f++)
+ {
+ var row = FrameSpectrumDb[f];
+ for (int b = minBin; b <= maxBin; b++)
+ {
+ double db = row[b];
+ if (db < minDb) minDb = db;
+ if (db > maxDb) maxDb = db;
+ }
+ }
+ if (maxDb <= minDb) maxDb = minDb + 1;
+ double invRange = 1.0 / (maxDb - minDb);
+
+ // 用 LockBits 直接写像素:避免逐像素 new SolidBrush + FillRectangle
+ // (在长音频下会产生数千万次 GDI 调用与海量临时对象,是单文件最耗时的步骤)。
+ Bitmap image = new Bitmap(imgWidth, imgHeight, PixelFormat.Format32bppArgb);
+ BitmapData bd = null;
+ try
+ {
+ bd = image.LockBits(new Rectangle(0, 0, imgWidth, imgHeight),
+ ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
+ int stride = bd.Stride;
+ int[] line = new int[imgWidth];
+ for (int y = 0; y < imgHeight; y++)
+ {
+ // Y轴:顶部=高频,底部=低频
+ int b = minBin + (imgHeight - 1 - y);
+ for (int x = 0; x < imgWidth; x++)
+ {
+ int f = (int)(x / frameToX);
+ if (f >= frames) f = frames - 1;
+ double db = FrameSpectrumDb[f][b];
+ double normalized = (db - minDb) * invRange;
+ normalized = normalized < 0 ? 0 : (normalized > 1 ? 1 : normalized);
+ Color color = heatmap
+ ? HeatMapColor(normalized)
+ : GrayscaleColor(normalized);
+ line[x] = (255 << 24) | (color.R << 16) | (color.G << 8) | color.B;
+ }
+ Marshal.Copy(line, 0, (IntPtr)((long)bd.Scan0 + (long)y * stride), imgWidth);
+ }
+ }
+ finally
+ {
+ if (bd != null) image.UnlockBits(bd);
+ }
+
+ /*
+ // 频率十字标记(数量=帧数,相对整图绘制量很小)
+ using (Graphics g = Graphics.FromImage(image))
+ using (Pen pen = new Pen(markerColor))
+ {
+ for (int f = 0; f < frames; f++)
+ {
+ double freqHz = DetectedFrequencies[f];
+ if (freqHz < MinFrequency || freqHz > MaxFrequency)
+ continue;
+ int freqBin = (int)Math.Round(freqHz / _binResolution);
+ freqBin = Math.Max(minBin, Math.Min(maxBin, freqBin));
+ int markerY = dataHeight - 1 - (freqBin - minBin);
+ int markerX = (int)(f * frameToX) + 1;
+ g.DrawLine(pen, markerX - 2, markerY, markerX + 2, markerY);
+ g.DrawLine(pen, markerX, markerY - 1, markerX, markerY + 1);
+ }
+ }
+ */
+ return image;
+ }
+
+ /// <summary>
+ /// 热力图颜色映射
+ /// </summary>
+ private static Color HeatMapColor(double value)
+ {
+ int r, g, b;
+
+ if (value < 0.25)
+ {
+ r = 0;
+ g = (int)(4 * 255 * value);
+ b = 255;
+ }
+ else if (value < 0.5)
+ {
+ r = 0;
+ g = 255;
+ b = (int)(255 - 4 * 255 * (value - 0.25));
+ }
+ else if (value < 0.75)
+ {
+ r = (int)(4 * 255 * (value - 0.5));
+ g = 255;
+ b = 0;
+ }
+ else
+ {
+ r = 255;
+ g = (int)(255 - 4 * 255 * (value - 0.75));
+ b = 0;
+ }
+
+ return Color.FromArgb(r, g, b);
+ }
+
+ /// <summary>
+ /// 灰度图颜色映射:归一化值直接映射为灰阶,亮(白)=能量强,暗(黑)=能量弱
+ /// </summary>
+ private static Color GrayscaleColor(double value)
+ {
+ int v = (int)Math.Round(value * 255.0);
+ v = v < 0 ? 0 : (v > 255 ? 255 : v);
+ return Color.FromArgb(v, v, v);
+ }
+
+ #endregion
+ }
+}
diff --git a/A1Lib/Audio/AudioRecord.cs b/A1Lib/Audio/AudioRecord.cs
new file mode 100644
index 0000000..3331796
--- /dev/null
+++ b/A1Lib/Audio/AudioRecord.cs
@@ -0,0 +1,661 @@
+using NAudio.CoreAudioApi;
+using NAudio.Dmo;
+using NAudio.Wave;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Audio
+{
+ /// <summary>
+ /// 录音,只录wav格式
+ /// 数据默认存内存,不存磁盘,提供 SaveTo 转存功能
+ /// </summary>
+ public class AudioRecord : IDisposable
+ {
+ #region 属性
+
+ /// <summary>存文件的wav格式,如果与设备录音格式不同,会自动转换为此格式</summary>
+ public WaveFormat Format { get; set; }
+
+ /// <summary>设备实际录音格式</summary>
+ public WaveFormat RecordFormat => _waveIn?.WaveFormat;
+
+ /// <summary>是否正在录音</summary>
+ public bool IsRecording { get; private set; }
+
+ /// <summary>录音数据(内存)</summary>
+ public MemoryStream AudioData => _memoryStream;
+
+ /// <summary>开始录音时,会有一段空白数据,此长度为忽略的帧数</summary>
+ public Int32 IgnoreLength { get; set; } = 16000;
+
+ /// <summary>还需要忽略的剩余长度,启动录音时重置</summary>
+ private Int32 _IgnoreRemain;
+
+ #endregion
+
+ #region 字段
+
+ /// <summary>最大内存容量 (512MB)</summary>
+ public const long MaxMemorySize = 512L * 1024 * 1024;
+
+ /// <summary>录音采集器</summary>
+ private WasapiCapture _waveIn;
+
+ /// <summary>内存流,用于存储录音原始数据</summary>
+ private MemoryStream _memoryStream;
+
+ /// <summary>定时停止用的 CancellationTokenSource</summary>
+ private System.Threading.CancellationTokenSource _cts;
+
+ /// <summary>是否因内存满而停止</summary>
+ private bool _stoppedByMemoryLimit;
+
+ #endregion
+
+ #region 构造
+
+ /// <summary>
+ /// 创建录音实例
+ /// </summary>
+ /// <param name="dev">录音设备</param>
+ public AudioRecord(AudioDevice dev)
+ {
+ if (dev == null) throw new ArgumentNullException(nameof(dev));
+ if (dev.RecordingDevice == null) throw new ArgumentException("录音设备的 RecordingDevice 不能为 null", nameof(dev));
+ if (dev.RecordingDevice.AudioClient.MixFormat.Channels < 1)
+ throw new ArgumentException("录音设备声道数无效", nameof(dev));
+
+ // 使用 WasapiCapture 直接通过 MMDevice 录音
+ _waveIn = new WasapiCapture(dev.RecordingDevice) { ShareMode = AudioClientShareMode.Shared };
+
+ // 初始化内存流
+ _memoryStream = new MemoryStream();
+
+ // 注册事件
+ _waveIn.DataAvailable += OnDataAvailable;
+ _waveIn.RecordingStopped += OnRecordingStopped;
+
+ Format = new WaveFormat(16000, 16, RecordFormat.Channels);
+ }
+
+ #endregion
+
+ #region 异步
+
+ /// <summary>启动录音</summary>
+ /// <param name="ms">录音时长(毫秒),-1:一直录直到手动停止</param>
+ /// <returns></returns>
+ public bool Start(int ms = -1)
+ {
+ if (IsRecording) return false;
+
+ try
+ {
+ // 重置内存流,默认32M容量
+ _memoryStream?.Dispose();
+ _memoryStream = new MemoryStream(32 * 1024 * 1024);
+
+ // 重置内存满标志
+ _stoppedByMemoryLimit = false;
+ _IgnoreRemain = IgnoreLength;
+
+ _waveIn.StartRecording();
+ IsRecording = true;
+
+ // 如果指定了时长,设置定时停止
+ if (ms > 0)
+ {
+ _cts = new System.Threading.CancellationTokenSource();
+ Task.Delay(ms, _cts.Token).ContinueWith(_ =>
+ {
+ if (!_cts.Token.IsCancellationRequested)
+ Stop();
+ });
+ }
+
+ return true;
+ }
+ catch (Exception)
+ {
+ IsRecording = false;
+ return false;
+ }
+ }
+
+ /// <summary>停止录音</summary>
+ public void Stop()
+ {
+ if (!IsRecording) return;
+
+ // 取消定时器
+ _cts?.Cancel();
+ _cts?.Dispose();
+ _cts = null;
+
+ try
+ {
+ _waveIn.StopRecording();
+ }
+ catch (Exception) { }
+ }
+
+ /// <summary>录音完成事件</summary>
+ public event EventHandler Stoped;
+
+ #endregion
+
+ #region 同步
+
+ /// <summary>录音,录完返回</summary>
+ /// <param name="ms">录音时长(毫秒)</param>
+ /// <returns>是否成功</returns>
+ public async Task<bool> Record(int ms)
+ {
+ var tcs = new TaskCompletionSource<bool>();
+
+ EventHandler handler = null;
+ handler = (s, e) =>
+ {
+ Stoped -= handler;
+ tcs.TrySetResult(true);
+ };
+ Stoped += handler;
+
+ if (!Start(ms)) return false;
+
+ return await tcs.Task;
+ }
+
+ #endregion
+
+ #region 数据输出
+
+ /// <summary>
+ /// 将内存中的录音数据转存为 WAV 文件
+ /// </summary>
+ /// <param name="path">保存路径(如 "C:/output.wav")</param>
+ /// <returns>是否成功</returns>
+ public bool SaveTo(string path)
+ {
+ if (_memoryStream == null || _memoryStream.Length == 0)
+ return false;
+
+ try
+ {
+ // 确定使用的格式:优先使用用户设定的 Format,否则使用实际录音格式
+ var targetFormat = Format ?? _waveIn.WaveFormat;
+
+ string dir = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ Directory.CreateDirectory(dir);
+
+ using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
+ using var writer = new WaveFileWriter(fs, targetFormat);
+
+ // 将内存数据写入文件
+ var buffer = _memoryStream.ToArray();
+
+ // 如果目标格式与录音格式一致,直接写入
+ if (targetFormat.Encoding == _waveIn.WaveFormat.Encoding &&
+ targetFormat.SampleRate == _waveIn.WaveFormat.SampleRate &&
+ targetFormat.Channels == _waveIn.WaveFormat.Channels &&
+ targetFormat.BitsPerSample == _waveIn.WaveFormat.BitsPerSample)
+ {
+ writer.Write(buffer, 0, buffer.Length);
+ }
+ else
+ {
+ // 需要转换格式:从内存流读取原始数据并重采样
+ using var msCopy = new MemoryStream(buffer);
+ using var reader = new RawWaveStream(msCopy, _waveIn.WaveFormat);
+ using var resampler = new MediaFoundationResampler(reader, targetFormat);
+
+ byte[] resampleBuffer = new byte[4096];
+ int read;
+ while ((read = resampler.Read(resampleBuffer, 0, resampleBuffer.Length)) > 0)
+ {
+ writer.Write(resampleBuffer, 0, read);
+ }
+ }
+
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ /// <summary>获取数据</summary>
+ /// <remarks>长度不足返回静音内容,在后面填充静音</remarks>
+ /// <param name="length">数据个数,每声道都有。-1表示取所有数据</param>
+ /// <returns>按照声道获取的音频数据,格式为 AudioFileReader 按照声道整理</returns>
+ public float[][] ReadAudioData(int length = -1)
+ {
+ int channelCount = _waveIn?.WaveFormat?.Channels ?? 1;
+
+ // 无数据时返回静音
+ if (_memoryStream == null || _memoryStream.Length == 0)
+ {
+ // -1 时返回空数组
+ if (length < 0) return new float[0][];
+
+ float[][] silence = new float[channelCount][];
+ for (int i = 0; i < channelCount; i++)
+ {
+ silence[i] = new float[length];
+ }
+ return silence;
+ }
+
+ var format = _waveIn.WaveFormat;
+ int bytesPerSample = format.BitsPerSample / 8;
+
+ // 获取原始数据
+ byte[] rawData = _memoryStream.ToArray();
+ int totalSamples = rawData.Length / (bytesPerSample * channelCount);
+
+ // -1 时取所有数据
+ if (length < 0) length = totalSamples;
+ if (length <= 0) return new float[0][];
+
+ // 计算实际读取的样本数
+ int samplesToRead = Math.Min(length, totalSamples);
+
+ // 为每个声道创建缓冲区
+ float[][] channelBuffers = new float[channelCount][];
+ for (int i = 0; i < channelCount; i++)
+ {
+ channelBuffers[i] = new float[length];
+ }
+
+ // 解交错音频数据并转换为float
+ for (int i = 0; i < samplesToRead; i++)
+ {
+ for (int ch = 0; ch < channelCount; ch++)
+ {
+ int byteIndex = i * bytesPerSample * channelCount + ch * bytesPerSample;
+ if (format.Encoding == WaveFormatEncoding.IeeeFloat)
+ {
+ channelBuffers[ch][i] = BitConverter.ToSingle(rawData, byteIndex);
+ }
+ else if (format.BitsPerSample == 16)
+ {
+ channelBuffers[ch][i] = BitConverter.ToInt16(rawData, byteIndex) / 32768f;
+ }
+ else if (format.BitsPerSample == 32)
+ {
+ channelBuffers[ch][i] = BitConverter.ToInt32(rawData, byteIndex) / (float)int.MaxValue;
+ }
+ }
+ }
+
+ // 长度不足时,后面已经是静音(默认为0),无需额外处理
+ return channelBuffers;
+ }
+
+ /// <summary>获取数据</summary>
+ /// <remarks>长度不足返回静音内容,在前面填充静音</remarks>
+ /// <param name="length">数据个数,每声道都有。-1表示取所有数据</param>
+ /// <returns>按照声道获取的音频数据,格式为 AudioFileReader 按照声道整理</returns>
+ public float[][] ReadAudioLastData(int length = -1)
+ {
+ int channelCount = _waveIn?.WaveFormat?.Channels ?? 1;
+
+ // 无数据时返回静音
+ if (_memoryStream == null || _memoryStream.Length == 0)
+ {
+ // -1 时返回空数组
+ if (length < 0) return new float[0][];
+
+ float[][] silence = new float[channelCount][];
+ for (int i = 0; i < channelCount; i++)
+ {
+ silence[i] = new float[length];
+ }
+ return silence;
+ }
+
+ var format = _waveIn.WaveFormat;
+ int bytesPerSample = format.BitsPerSample / 8;
+
+ // 获取原始数据
+ byte[] rawData = _memoryStream.ToArray();
+ int totalSamples = rawData.Length / (bytesPerSample * channelCount);
+
+ // -1 时取所有数据
+ if (length < 0) length = totalSamples;
+ if (length <= 0) return new float[0][];
+
+ // 为每个声道创建缓冲区
+ float[][] channelBuffers = new float[channelCount][];
+ for (int i = 0; i < channelCount; i++)
+ {
+ channelBuffers[i] = new float[length];
+ }
+
+ // 计算需要跳过的样本数(从末尾取 length 个,不足则前面填充静音)
+ int skipSamples = Math.Max(0, totalSamples - length);
+ int samplesToRead = Math.Min(length, totalSamples);
+ int destOffset = length - samplesToRead; // 目标偏移,用于前面填充静音
+
+ // 解交错音频数据并转换为float
+ for (int i = 0; i < samplesToRead; i++)
+ {
+ for (int ch = 0; ch < channelCount; ch++)
+ {
+ int srcByteIndex = (skipSamples + i) * bytesPerSample * channelCount + ch * bytesPerSample;
+ if (format.Encoding == WaveFormatEncoding.IeeeFloat)
+ {
+ channelBuffers[ch][destOffset + i] = BitConverter.ToSingle(rawData, srcByteIndex);
+ }
+ else if (format.BitsPerSample == 16)
+ {
+ channelBuffers[ch][destOffset + i] = BitConverter.ToInt16(rawData, srcByteIndex) / 32768f;
+ }
+ else if (format.BitsPerSample == 32)
+ {
+ channelBuffers[ch][destOffset + i] = BitConverter.ToInt32(rawData, srcByteIndex) / (float)int.MaxValue;
+ }
+ }
+ }
+
+ // 前面已经是静音(默认为0),无需额外处理
+ return channelBuffers;
+ }
+
+ /// <summary>获取最后录音的平均音量</summary>
+ /// <returns>各声道音量数组(0~100),长度等于声道数</returns>
+ public byte[] ReadAudioLastDataVolumes()
+ {
+ var datas = ReadAudioLastData();
+ var vols = new byte[datas.Length];
+ for (int i = 0; i < datas.Length; i++)
+ {
+ var sum = 0f;
+ for (int j = 0; j < datas[i].Length; j++)
+ {
+ sum += Math.Abs(datas[i][j]);
+ }
+ vols[i] = (byte)(sum / datas[i].Length * 100);
+ }
+
+ return vols;
+ }
+
+ /// <summary>给定时间算帧数</summary>
+ /// <param name="ms">毫秒</param>
+ /// <returns>音频帧数</returns>
+ public int FramsFormTime(int ms) { return (int)(ms * _waveIn.WaveFormat.SampleRate / 1000); }
+
+ #endregion
+
+ #region 音量
+
+ /// <summary>音量变化回调,参数为各声道音量字节数组 (0~100),数组长度等于声道数</summary>
+ // public delegate void VolumeUpdate(byte[] vols);
+
+ /// <summary>音量变化事件</summary>
+ public event EventHandler<byte[]> VolumeUpdated;
+
+ /// <summary>内存满事件</summary>
+ public event EventHandler MemoryLimitReached;
+
+ /// <summary>是否因内存满而停止</summary>
+ public bool StoppedByMemoryLimit => _stoppedByMemoryLimit;
+
+ #endregion
+
+ #region 音频数据
+
+ /// <summary>音频数据回调,参数为各声道音频数据 float[][],第一维为声道索引</summary>
+ // public delegate void AudioDataReceived(float[][] audioData);
+
+ /// <summary>音频数据接收事件,实时返回按声道组织的音频数据。可以用于AudioFeatureExtractor->MelStreamImage生成实时mel图</summary>
+ public event EventHandler<float[][]> DataUpdated;
+
+ #endregion
+
+ #region 内部事件处理
+
+ private void OnDataAvailable(object sender, WaveInEventArgs e)
+ {
+ if (e.BytesRecorded > 0)
+ {
+ if (_IgnoreRemain > 0)
+ {
+ var blen = e.BytesRecorded;
+ var format = RecordFormat;
+ var fmlen = format.Channels;
+ if (format.Encoding == WaveFormatEncoding.IeeeFloat) { fmlen *= 4; }
+ else if (format.BitsPerSample == 16) { fmlen *= 2; }
+ else if (format.BitsPerSample == 32) { fmlen *= 2; }
+ // 本次帧个数
+ var fmsize = blen / fmlen;
+ // 忽略掉的,可以多忽略一些,音频,无所谓。
+ _IgnoreRemain -= fmsize;
+ return;
+ }
+
+ // 检查内存限制
+ if (_memoryStream.Length + e.BytesRecorded > MaxMemorySize)
+ {
+ // 标记为内存满停止
+ _stoppedByMemoryLimit = true;
+
+ // 触发内存满事件
+ MemoryLimitReached?.Invoke(this, EventArgs.Empty);
+
+ // 自动停止录音
+ Stop();
+ return;
+ }
+
+ // 写入内存
+ _memoryStream?.Write(e.Buffer, 0, e.BytesRecorded);
+
+ // 计算并报告音量
+ if (VolumeUpdated != null)
+ {
+ var vols = CalculateVolume(e.Buffer, e.BytesRecorded, _waveIn.WaveFormat);
+ VolumeUpdated?.Invoke(this, vols);
+ }
+
+ // 触发音频数据事件
+ if (DataUpdated != null)
+ {
+ var audioData = ConvertToAudioData(e.Buffer, e.BytesRecorded, _waveIn.WaveFormat);
+ DataUpdated?.Invoke(this, audioData);
+ }
+ }
+ }
+
+ private void OnRecordingStopped(object sender, StoppedEventArgs e)
+ {
+ IsRecording = false;
+
+ // 触发完成事件
+ Stoped?.Invoke(this, EventArgs.Empty);
+ }
+
+ /// <summary>
+ /// 将原始音频缓冲区转换为按声道组织的 float[][] 格式
+ /// </summary>
+ /// <param name="buffer">原始音频数据</param>
+ /// <param name="bytesRecorded">有效数据长度</param>
+ /// <param name="format">音频格式</param>
+ /// <returns>按声道组织的音频数据,第一维为声道索引</returns>
+ private static float[][] ConvertToAudioData(byte[] buffer, int bytesRecorded, WaveFormat format)
+ {
+ if (format.Channels < 1 || buffer == null || bytesRecorded <= 0)
+ {
+ return new float[0][];
+ }
+
+ int channels = format.Channels;
+ int bytesPerSample = format.BitsPerSample / 8;
+ int frameSize = bytesPerSample * channels;
+ int sampleCount = bytesRecorded / frameSize;
+
+ // 为每个声道创建缓冲区
+ float[][] channelBuffers = new float[channels][];
+ for (int i = 0; i < channels; i++)
+ {
+ channelBuffers[i] = new float[sampleCount];
+ }
+
+ // 解交错音频数据并转换为float
+ for (int i = 0; i < sampleCount; i++)
+ {
+ for (int ch = 0; ch < channels; ch++)
+ {
+ int byteIndex = i * frameSize + ch * bytesPerSample;
+ if (format.Encoding == WaveFormatEncoding.IeeeFloat)
+ {
+ channelBuffers[ch][i] = BitConverter.ToSingle(buffer, byteIndex);
+ }
+ else if (format.BitsPerSample == 16)
+ {
+ channelBuffers[ch][i] = BitConverter.ToInt16(buffer, byteIndex) / 32768f;
+ }
+ else if (format.BitsPerSample == 32)
+ {
+ channelBuffers[ch][i] = BitConverter.ToInt32(buffer, byteIndex) / (float)int.MaxValue;
+ }
+ }
+ }
+
+ return channelBuffers;
+ }
+
+ /// <summary>
+ /// 计算各声道音量
+ /// </summary>
+ /// <returns>各声道音量数组(0~100),长度等于声道数</returns>
+ private static byte[] CalculateVolume(byte[] buffer, int bytesRecorded, WaveFormat format)
+ {
+ if (format.Channels < 1 || buffer == null || bytesRecorded <= 0)
+ {
+ return format.Channels > 0 ? new byte[format.Channels] : new byte[0];
+ }
+
+ int channels = format.Channels;
+ var bytesPerSample = format.BitsPerSample / 8;
+ int frameSize = bytesPerSample * channels;
+
+ // 各声道累加和
+ float[] sums = new float[channels];
+ int count = 0;
+
+ for (int i = 0; i + frameSize <= bytesRecorded; i += frameSize)
+ {
+ for (int ch = 0; ch < channels; ch++)
+ {
+ int byteIndex = i + ch * bytesPerSample;
+ float sampleValue = 0;
+
+ if (format.Encoding == WaveFormatEncoding.IeeeFloat)
+ {
+ sampleValue = Math.Abs(BitConverter.ToSingle(buffer, byteIndex));
+ }
+ else if (format.BitsPerSample == 16)
+ {
+ short sample = BitConverter.ToInt16(buffer, byteIndex);
+ sampleValue = sample == short.MinValue ? short.MaxValue : Math.Abs(sample);
+ sampleValue /= short.MaxValue;
+ }
+ else if (format.BitsPerSample == 32)
+ {
+ int sample = BitConverter.ToInt32(buffer, byteIndex);
+ sampleValue = Math.Abs((float)sample / int.MaxValue);
+ }
+
+ sums[ch] += sampleValue;
+ }
+
+ count++;
+ }
+
+ if (count == 0) return new byte[channels];
+
+ byte[] volumes = new byte[channels];
+ for (int ch = 0; ch < channels; ch++)
+ {
+ // 对于 float 格式,sums[ch] 已经是 0~1 范围
+ // 对于 16/32 位整型,已在上面归一化到 0~1 范围
+ float vol = (sums[ch] / count) * 100;
+ volumes[ch] = (byte)Math.Min((int)vol, 100);
+ }
+
+ return volumes;
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Stop();
+ _waveIn.DataAvailable -= OnDataAvailable;
+ _waveIn.RecordingStopped -= OnRecordingStopped;
+ _waveIn?.Dispose();
+ _waveIn = null;
+ _memoryStream?.Dispose();
+ _memoryStream = null;
+ _cts?.Dispose();
+ _cts = null;
+ }
+
+ #endregion
+ }
+
+ #region 辅助类 - RawWaveStream
+
+ /// <summary>
+ /// 将原始 PCM 数据包装为 IWaveProvider,用于格式转换时的输入源
+ /// </summary>
+ internal class RawWaveStream : Stream, IWaveProvider
+ {
+ private readonly Stream _source;
+ private readonly WaveFormat _waveFormat;
+
+ public RawWaveStream(Stream source, WaveFormat format)
+ {
+ _source = source;
+ _waveFormat = format;
+ }
+
+ public WaveFormat WaveFormat => _waveFormat;
+
+ public override bool CanRead => true;
+ public override bool CanSeek => _source.CanSeek;
+ public override bool CanWrite => false;
+ public override long Length => _source.Length;
+ public override long Position
+ {
+ get => _source.Position;
+ set => _source.Position = value;
+ }
+
+ public override void Flush() => _source.Flush();
+ public override long Seek(long offset, SeekOrigin origin) => _source.Seek(offset, origin);
+ public override void SetLength(long value) => _source.SetLength(value);
+
+ public override int Read(byte[] buffer, int offset, int count) => _source.Read(buffer, offset, count);
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing) _source?.Dispose();
+ base.Dispose(disposing);
+ }
+ }
+
+ #endregion
+}
diff --git a/A1Lib/Audio/MelStreamImage.cs b/A1Lib/Audio/MelStreamImage.cs
new file mode 100644
index 0000000..a88596a
--- /dev/null
+++ b/A1Lib/Audio/MelStreamImage.cs
@@ -0,0 +1,227 @@
+using Microsoft.VisualBasic;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Audio
+{
+ public class MelStreamImage
+ {
+ /// <summary>ProcessAsDecibels返回值</summary>
+ public CircularQueue<double[]> Queue { get; private set; } = null;
+
+ /// <summary>帧数</summary>
+ public int FrameSize { get; private set; } = 30;
+
+ private int _ColorMode = 0;
+ /// <summary>
+ /// MEL图颜色模式,0热力图,1灰度图
+ /// </summary>
+ public int ColorMode
+ {
+ get { return _ColorMode; }
+ set
+ {
+ _ColorMode = value;
+ if (value < 0) _ColorMode = 0;
+ if (value > 1) _ColorMode = 1;
+ }
+ }
+
+ /// <summary>mel流动图</summary>
+ /// <param name="frameSize"></param>
+ public MelStreamImage(int frameSize = 30)
+ {
+ this.FrameSize = frameSize;
+ Queue = new CircularQueue<double[]>(frameSize);
+ }
+
+ /// <summary>加入数据</summary>
+ /// <param name="dbs"></param>
+ public void PushDbs(double[][] dbs)
+ {
+ if (Queue == null)
+ {
+ Queue = new CircularQueue<double[]>(FrameSize);
+ }
+
+ // 一次塞满队列,则清空队列并重新写入
+ if (FrameSize <= dbs.Length)
+ {
+ dbs = dbs.TakeLast(FrameSize).ToArray();
+ Queue.Clear();
+ Queue.WriteArray(dbs);
+ return;
+ }
+
+ var remain = Queue.Remain();
+ if (remain < dbs.Length)
+ {
+ Queue.Seek(dbs.Length - remain);
+ }
+ Queue.WriteArray(dbs);
+ }
+
+ /// <summary>清理数据</summary>
+ public void Clear()
+ {
+ Queue.Clear();
+ Normalization_Min = 10000;
+ Normalization_Max = -10000;
+ }
+
+ /// <summary>获取图像</summary>
+ /// <param name="channelName"></param>
+ /// <param name="hasTitle"></param>
+ /// <returns></returns>
+ public Bitmap GetImage(string channelName, bool hasTitle = false)
+ {
+ if (Queue == null) return null;
+ if (Queue.Count() < 1) return null;
+ if (Queue.Count() < FrameSize)
+ {
+ var dbs = new double[FrameSize][];
+ var has = Queue.ToArray();
+ for (int i = 0; i < FrameSize - has.Length; i++)
+ {
+ dbs[i] = new double[has[0].Length];
+ }
+ for (int i = 0; i < has.Length; i++)
+ {
+ dbs[FrameSize - has.Length + i] = has[i];
+ }
+
+ return CreateSpectrogramImage(dbs, channelName, hasTitle);
+ }
+ else
+ {
+ var dbs = Queue.ToArray();
+ return CreateSpectrogramImage(dbs, channelName, hasTitle);
+ }
+ }
+
+ /// <summary>归一化最小</summary>
+ public double Normalization_Min = 10000;
+ /// <summary>归一化最大</summary>
+ public double Normalization_Max = -10000;
+
+ /// <summary>创建图像</summary>
+ /// <param name="melSpectrogramDb"></param>
+ /// <param name="channelName"></param>
+ /// <param name="hasTitle"></param>
+ /// <returns></returns>
+ private Bitmap CreateSpectrogramImage(double[][] melSpectrogramDb, string channelName, bool hasTitle = false)
+ {
+ int width = melSpectrogramDb.Length;
+ int height = melSpectrogramDb[0].Length;
+
+ // 增加高度以容纳标题
+ int titleHeight = 30;
+ if (!hasTitle) titleHeight = 0;
+ Bitmap image = new Bitmap(width, height + titleHeight, PixelFormat.Format32bppArgb);
+
+ using (Graphics g = Graphics.FromImage(image))
+ {
+ // 绘制黑色背景
+ g.Clear(Color.Black);
+
+ // 绘制标题
+ if (hasTitle)
+ {
+ using (Font font = new Font("Arial", 12))
+ using (Brush brush = new SolidBrush(Color.White))
+ {
+ g.DrawString(channelName, font, brush, 10, 5);
+ }
+ }
+ }
+
+ // 找到最小和最大值用于归一化
+ double min = melSpectrogramDb.Min(frame => frame.Min());
+ double max = melSpectrogramDb.Max(frame => frame.Max());
+
+ if (max != default && max > Normalization_Max) Normalization_Max = max;
+ if (min != default && min < Normalization_Min) Normalization_Min = min;
+
+ if (max < Normalization_Max) max = Normalization_Max;
+ if (min > Normalization_Min) min = Normalization_Min;
+
+ for (int x = 0; x < width; x++)
+ {
+ for (int y = 0; y < height; y++)
+ {
+ // 归一化到0-1范围
+ double normalized = (melSpectrogramDb[x][y] - min) / (max - min);
+
+ // 使用热图颜色方案
+ Color color = Color.Black;
+ if (ColorMode == 0) color = HeatMapColor(normalized);
+ if (ColorMode == 1) color = GrayScale(normalized);
+
+ // 注意: 图像坐标系y轴向下,频谱y轴向上,所以需要翻转
+ // 并且向下偏移titleHeight像素
+ image.SetPixel(x, height - 1 - y + titleHeight, color);
+ }
+ }
+
+ return image;
+ }
+
+ /// <summary>热力图颜色</summary>
+ /// <param name="value"></param>
+ /// <returns></returns>
+ private static Color HeatMapColor(double value)
+ {
+ // 简单的热图颜色方案
+ // 蓝色(冷) -> 青色 -> 绿色 -> 黄色 -> 红色(热)
+
+ int r, g, b;
+
+ if (value < 0.25)
+ {
+ // 蓝色到青色
+ r = 0;
+ g = (int)(4 * 255 * value);
+ b = 255;
+ }
+ else if (value < 0.5)
+ {
+ // 青色到绿色
+ r = 0;
+ g = 255;
+ b = (int)(255 - 4 * 255 * (value - 0.25));
+ }
+ else if (value < 0.75)
+ {
+ // 绿色到黄色
+ r = (int)(4 * 255 * (value - 0.5));
+ g = 255;
+ b = 0;
+ }
+ else
+ {
+ // 黄色到红色
+ r = 255;
+ g = (int)(255 - 4 * 255 * (value - 0.75));
+ b = 0;
+ }
+
+ return Color.FromArgb(r, g, b);
+ }
+
+ /// <summary>灰度颜色</summary>
+ /// <param name="value"></param>
+ /// <returns></returns>
+ private static Color GrayScale(double value)
+ {
+ // 灰度颜色方案:黑色(0) -> 白色(1)
+ int gray = (int)(255 * value);
+ return Color.FromArgb(gray, gray, gray);
+ }
+ }
+}
diff --git a/XCoder/Windows/MultiChannelAudioProcessor.cs b/A1Lib/Audio/MultiChannelAudioProcessor.cs
similarity index 90%
rename from XCoder/Windows/MultiChannelAudioProcessor.cs
rename to A1Lib/Audio/MultiChannelAudioProcessor.cs
index 8aa3aa0..f477445 100644
--- a/XCoder/Windows/MultiChannelAudioProcessor.cs
+++ b/A1Lib/Audio/MultiChannelAudioProcessor.cs
@@ -1,20 +1,19 @@
using System;
-using System;
-using System.Collections.Generic;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Drawing;
-using System.Drawing.Imaging;
-using System.Linq;
using System.Linq;
-using System.Numerics;
using System.Text;
using System.Threading.Tasks;
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
using MathNet.Numerics;
using MathNet.Numerics.IntegralTransforms;
using NAudio.Wave;
+using System.Numerics;
+using System.Collections.Generic;
-namespace XCoder
+namespace ALib.Audio
{
public class MultiChannelAudioProcessor
{
diff --git a/A1Lib/Camera/CameraDevice.cs b/A1Lib/Camera/CameraDevice.cs
new file mode 100644
index 0000000..c4ae85e
--- /dev/null
+++ b/A1Lib/Camera/CameraDevice.cs
@@ -0,0 +1,274 @@
+using ALib.UsbHub;
+using AForge.Video;
+using AForge.Video.DirectShow;
+using NewLife;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+
+namespace ALib.Camera
+{
+ /// <summary>AForge 相机</summary>
+ public class CameraDevice : DisposeBase
+ {
+ /// <summary>摄像头编号(与 Windows 视频设备枚举序一致,仅作兜底/日志用)</summary>
+ // public int CameraIndex = 0;
+
+ /// <summary>目标相机的 PNPDeviceID,用于与 DirectShow 设备做精确匹配,避免索引顺序不一致导致选错相机</summary>
+ public string TargetPnp { get; set; }
+
+ /// <summary>像素尺寸(Bitmap 像素尺寸)</summary>
+ public Size Size { get; set; } = new Size(640, 480);
+
+ // public CameraDevice(int idx = 0) { CameraIndex = idx; }
+ public CameraDevice() { }
+
+ // ---- AForge(DirectShow) 采集相关 ----
+ private FilterInfoCollection _Devices;
+ private VideoCaptureDevice _Source;
+ private readonly object _lockBmp = new object();
+ private System.Drawing.Bitmap _Latest;
+
+ public bool IsOpened() => _Source != null && _Source.IsRunning;
+
+ private bool _Error = false;
+ public bool Error() => _Error;
+
+
+ public void Open(bool setPix = false, bool tryMax = true)
+ {
+ _Error = false;
+ if (_Source != null && _Source.IsRunning) return;
+
+ if (_Devices == null)
+ _Devices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
+ if (_Devices.Count == 0)
+ throw new Exception("未找到任何摄像头设备");
+
+ // 优先按 PnPId 精确匹配 DirectShow 设备,避免 PnP 枚举序与 DSHOW 枚举序不一致选错相机
+ _Source = null;
+ if (!string.IsNullOrEmpty(TargetPnp))
+ {
+ string norm = NormalizePnpId(TargetPnp);
+ foreach (FilterInfo fi in _Devices)
+ {
+ if (fi.MonikerString.IndexOf(norm, StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ XTrace.WriteLine($"按 PnPId 匹配到相机: {fi.Name} (Moniker={fi.MonikerString})");
+ _Source = new VideoCaptureDevice(fi.MonikerString);
+ break;
+ }
+ }
+ }
+
+ _Source.NewFrame += OnNewFrame;
+ _Source.VideoSourceError += _Source_VideoSourceError; ;
+
+ // 选择分辨率(必须在 Start 之前设置)
+ if (_Source.VideoCapabilities != null && _Source.VideoCapabilities.Length > 0)
+ {
+ VideoCapabilities chosen = null;
+ if (tryMax)
+ chosen = _Source.VideoCapabilities
+ .OrderByDescending(c => (long)c.FrameSize.Width * c.FrameSize.Height).FirstOrDefault();
+ else if (setPix)
+ chosen = _Source.VideoCapabilities.OrderBy(
+ c => Math.Abs(c.FrameSize.Width - Size.Width) + Math.Abs(c.FrameSize.Height - Size.Height)
+ ).FirstOrDefault();
+
+ if (chosen != null)
+ {
+ _Source.VideoResolution = chosen;
+ Size = new Size(chosen.FrameSize.Width, chosen.FrameSize.Height);
+ }
+ }
+
+ _Source.Start();
+ }
+
+ private void _Source_VideoSourceError(object sender, VideoSourceErrorEventArgs eventArgs) { _Error = true; }
+
+ /// <summary>把任意 Bitmap 深层拷贝成一张完全独立的新位图(不共享底层像素)</summary>
+ private static System.Drawing.Bitmap DeepCloneBitmap(System.Drawing.Bitmap src)
+ {
+ if (src == null) return null;
+ try
+ {
+ var dst = new System.Drawing.Bitmap(src.Width, src.Height, src.PixelFormat);
+ dst.SetResolution(src.HorizontalResolution, src.VerticalResolution);
+ using (var g = System.Drawing.Graphics.FromImage(dst)) { g.DrawImageUnscaled(src, 0, 0); }
+ return dst;
+ }
+ catch (Exception ex)
+ {
+ NewLife.Log.XTrace.WriteException(ex);
+ return null;
+ }
+ }
+
+ /// <summary>新帧事件</summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ private void OnNewFrame(object sender, NewFrameEventArgs e)
+ {
+ // AForge 的 e.Frame 是它拥有的共享帧。
+ // 这里必须做"真正深层"拷贝(复制像素),不能只做共享底层内存的 Clone(),
+ // 否则异步保存/识别时可能拿到被 AForge 复用的半写帧数据。
+ var clone = DeepCloneBitmap(e.Frame);
+ lock (_lockBmp)
+ {
+ var old = _Latest;
+ _Latest = clone;
+ old?.Dispose();
+ }
+ }
+
+ /// <summary>拍一张照片,返回 Bitmap</summary>
+ public System.Drawing.Bitmap TakePhoto_AsBitmap()
+ {
+ if (!IsOpened()) return null;
+
+ lock (_lockBmp)
+ {
+ return DeepCloneBitmap(_Latest);
+ }
+ }
+
+ #region 十字准星 照片
+
+ /// <summary>获取一张照片,画上十字准星</summary>
+ /// <returns></returns>
+ public async Task<Bitmap> TakeCrosshairImage(int w = 0, int h = 0)
+ {
+ var img = TakePhoto_AsBitmap();
+ if (img == null) return null;
+
+ if (w > 0 && h > 0)
+ {
+ // 剪裁一下,太大没有用
+ if ((img.Width > w) || (img.Height > h))
+ {
+ var size = new System.Drawing.Size(w, h);
+ var point = new System.Drawing.Point((img.Width - w) / 2, (img.Height - h) / 2);
+ var croRect = new Rectangle(point, size);
+ var cropped = img.Clone(croRect, img.PixelFormat);
+ img.Dispose(); // 剪裁后释放原整图
+ img = cropped;
+ }
+ }
+
+ // 中心不划线长度
+ int crossSize = 5;
+ // 增加显示效果并显示
+ using (var g = Graphics.FromImage(img))
+ {
+ // 红色十字,线宽2
+ Pen pen = new Pen(Color.Red, 1);
+ // 绘制十字准星(不画中心点圆点)
+ int centerX = img.Width / 2;
+ int centerY = img.Height / 2;
+ // 水平线(不画中心点)
+ g.DrawLine(pen, 0, centerY, centerX - crossSize, centerY);
+ g.DrawLine(pen, centerX + crossSize, centerY, img.Width - 1, centerY);
+ // 垂直线(不画中心点)
+ g.DrawLine(pen, centerX, 0, centerX, centerY - crossSize);
+ g.DrawLine(pen, centerX, centerY + crossSize, centerX, img.Height - 1);
+ // // 图片上写RGB值
+ // var Font = new Font("Arial", centerY / 8, FontStyle.Bold);
+ // var brush = new SolidBrush(Color.Black);
+ // g.DrawString($"A{p.A} R{p.R} G{p.G} B{p.B}", Font, brush, new Point(0, 0));
+ // g.DrawString($"Bri p {pbnss:F2} agv {pAvgbnss:F2}", Font, brush,
+ // new Point(0, img.Height - (centerY / 8) - 5));
+ pen.Dispose();
+ }
+
+ return img;
+ }
+
+ /// <summary>获取一张照片,画上十字准星</summary>
+ /// <param name="size">照片大小</param>
+ /// <param name="timeout"></param>
+ /// <returns></returns>
+ public async Task<Bitmap> TakeCrosshairImage(System.Drawing.Size size) => await TakeCrosshairImage(size.Width, size.Height);
+
+ #endregion
+
+ /// <summary>获取当前摄像头参数值(仅在摄像头打开状态下有效)</summary>
+ public Dictionary<string, double> GetCameraParameters()
+ {
+ if (!IsOpened()) return null;
+
+ var parameters = new Dictionary<string, double>();
+ try
+ {
+ parameters["FrameWidth"] = Size.Width;
+ parameters["FrameHeight"] = Size.Height;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"读取摄像头参数时出错: {ex.Message}");
+ }
+ return parameters;
+ }
+
+ public void Close()
+ {
+ if (_Source != null)
+ {
+ try { _Source.SignalToStop(); } catch { }
+ try { _Source.WaitForStop(); } catch { }
+ _Source.NewFrame -= OnNewFrame;
+ _Source.VideoSourceError -= _Source_VideoSourceError;
+ _Source = null;
+ }
+
+ lock (_lockBmp)
+ {
+ _Latest?.Dispose();
+ _Latest = null;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ Close();
+ }
+
+ public static CameraDevice Create(UsbDevice dev)
+ {
+ var pnp = dev.GetCameraPnp();
+ if(pnp.IsNullOrEmpty()) return null;
+
+ var cam = new CameraDevice();
+ cam.TargetPnp = pnp;
+
+ return cam;
+ }
+
+ /// <summary>创建相机</summary>
+ /// <param name="port"></param>
+ /// <returns></returns>
+ public static CameraDevice Create(UsbHubPort port)
+ {
+ if (!port.Exists()) return null;
+ var pnp = port.GetUsbDevice().GetCameraPnp();
+ if (pnp.IsNullOrEmpty()) return null;
+
+ var cam = new CameraDevice();
+ cam.TargetPnp = pnp;
+
+ return cam;
+ }
+
+ /// <summary>将 PNPDeviceID (如 USB\VID_046D&PID_08C9\...) 规范化为可与 DirectShow MonikerString 比较的形式</summary>
+ private static string NormalizePnpId(string pnpId)
+ {
+ // MonikerString 形如 @device:pnp:\\?\usb#vid_046d&pid_08c9&mi_00#... ,其中路径片段与 PNPDeviceID 一致(\ 替换为 #)
+ return pnpId.Replace('\\', '#').ToLowerInvariant();
+ }
+ }
+}
diff --git a/A1Lib/Camera/CameraHelper.cs b/A1Lib/Camera/CameraHelper.cs
new file mode 100644
index 0000000..e6388ce
--- /dev/null
+++ b/A1Lib/Camera/CameraHelper.cs
@@ -0,0 +1,84 @@
+using ALib.UsbHub;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Management;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Camera
+{
+ public static class CameraHelper
+ {
+ /// <summary>获取所有相机PNPID,不区分是否是USB摄像头</summary>
+ /// <returns></returns>
+ public static string[] GetAllCameraPnpId()
+ {
+ List<string> cameras = new List<string>();
+ try
+ {
+ // 查询Win32_PnPEntity中所有摄像头设备
+ ManagementObjectSearcher searcher = new ManagementObjectSearcher(
+ "SELECT * FROM Win32_PnPEntity WHERE (PNPClass = 'Image' OR PNPClass = 'Camera')");
+ foreach (ManagementObject device in searcher.Get())
+ {
+ string name = device["PNPDeviceID"]?.ToString();
+ if (!string.IsNullOrEmpty(name))
+ {
+ cameras.Add(name);
+ }
+ }
+ }
+ catch { }
+
+ return cameras.ToArray();
+ }
+
+ /*
+ /// <summary>获取相机编号</summary>
+ /// <returns>不是相机返回 -1</returns>
+ public static int GetCameraIndex(this UsbDevice dev)
+ {
+ if (dev == null) return -1;
+ var cans = GetAllCameraPnpId();
+ if (cans == null || cans.Length < 1) return -1;
+
+ for (int i = 0; i < cans.Length; i++)
+ {
+ var can = cans[i];
+ var cid = UsbHelper.GetContainerId(can);
+ if (cid == null) continue;
+
+ if (cid.Equals(dev.ContainerId, StringComparison.OrdinalIgnoreCase))
+ {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+ */
+
+ public static string GetCameraPnp(this UsbDevice dev)
+ {
+ if (dev == null) return null;
+ var cans = GetAllCameraPnpId();
+ if (cans == null || cans.Length < 1) return null;
+
+ for (int i = 0; i < cans.Length; i++)
+ {
+ var can = cans[i];
+ var cid = UsbHelper.GetContainerId(can);
+ if (cid == null) continue;
+
+ if (cid.Equals(dev.ContainerId, StringComparison.OrdinalIgnoreCase))
+ {
+ return cans[i];
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/A1Lib/CAN/CanMsg.cs b/A1Lib/CAN/CanMsg.cs
new file mode 100644
index 0000000..f12eb35
--- /dev/null
+++ b/A1Lib/CAN/CanMsg.cs
@@ -0,0 +1,90 @@
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.CAN
+{
+ public class CanMsg
+ {
+ /// <summary>时间戳</summary>
+ public TimeSpan TimeStamp;
+
+ /// <summary>标准帧11bit,扩展帧29bit</summary>
+ public Int32 ID;
+
+ /// <summary>扩展帧标志</summary>
+ public bool ExternFlag;
+
+ /// <summary>RTR 远程帧标志</summary>
+ public bool RemoteFlag;
+
+ /// <summary>负载数据,最大8字节</summary>
+ public byte[] Data = new byte[8];
+
+ /// <summary>64位数据,用于一次读写 Data</summary>
+ public UInt64 Data64
+ {
+ get { return BitConverter.ToUInt64(Data); }
+ set { Data = value.GetBytes(); }
+ }
+
+ public int DataLen => Data.Length;
+
+ public CanMsg(Int32 id, byte[] data, bool rtr, bool ext)
+ {
+ ID = id;
+ Data = data;
+ RemoteFlag = rtr;
+ ExternFlag = ext;
+ }
+
+ public CanMsg(Int32 id, UInt64 data, bool rtr, bool ext)
+ {
+ ID = id;
+ Data64 = data;
+ RemoteFlag = rtr;
+ ExternFlag = ext;
+ }
+
+ /// <summary>格式化字符串(输出格式与 yCanTool 保持一致)</summary>
+ /// <returns></returns>
+ public override String ToString()
+ {
+ var sb = new StringBuilder();
+
+ sb.Append(TimeStamp.ToString("hh\\:mm\\:ss\\.fff"));
+
+ // CANID,扩展帧 8 位十六进制,标准帧低 2 字节十六进制
+ if (ExternFlag)
+ {
+ var id = ID.GetBytes(false).ReadBytes(0, 4).ToHex();
+ sb.Append(" " + id + " ");
+ }
+ else
+ {
+ var id = ID.GetBytes(false).ReadBytes(2, 2).ToHex();
+ sb.Append(" " + id + " ");
+ }
+
+ var R = RemoteFlag ? "R " : " ";
+ sb.Append(R);
+
+ var E = ExternFlag ? "E " : " ";
+ sb.Append(E);
+
+ var data = Data;
+ var len = data?.Length ?? 0;
+ if (len > 8) len = 8;
+ sb.Append("[" + len + "] ");
+ if (data != null && len > 0)
+ {
+ sb.Append(" " + data.ReadBytes(0, len).ToHex(" "));
+ }
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/A1Lib/CAN/CX_CAN/ControlCAN.zip b/A1Lib/CAN/CX_CAN/ControlCAN.zip
new file mode 100644
index 0000000..ee09893
Binary files /dev/null and b/A1Lib/CAN/CX_CAN/ControlCAN.zip differ
diff --git a/A1Lib/CAN/CX_CAN/CxCanPort.cs b/A1Lib/CAN/CX_CAN/CxCanPort.cs
new file mode 100644
index 0000000..0082fe3
--- /dev/null
+++ b/A1Lib/CAN/CX_CAN/CxCanPort.cs
@@ -0,0 +1,415 @@
+using ALib;
+using NewLife.Collections;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using static ALib.CX_CAN.CxControlCAN;
+using ALib.Can;
+using ALib.CAN;
+
+namespace ALib.CX_CAN
+{
+ public class CxCanPort : ICanPort
+ {
+ /// <summary>构造,指定枚举名称</summary>
+ /// <param name="name">枚举时候的名字</param>
+ public CxCanPort(String name) : base(name) { }
+
+ #region DLL管理
+
+ private static readonly string DllFileName = "ControlCAN.dll";
+
+ /// <summary>
+ /// 静态构造函数,确保DLL可用
+ /// </summary>
+ static CxCanPort()
+ {
+ // 使用通用DLL资源管理器确保DLL可用
+ bool success = DllResourceManager.EnsureDllAvailable("ALib.CX_CAN", DllFileName);
+ if (!success)
+ {
+ XTrace.WriteLine($"ControlCAN.dll 加载失败");
+ throw new InvalidOperationException("ControlCAN.dll 加载失败");
+ }
+ }
+
+ #endregion
+
+ #region 硬件相关
+
+ // USBCAN2
+ static UInt32 m_devtype = 4;
+
+ /// <summary>CAN 分析仪下标</summary>
+ public UInt32 DevId = 0;
+
+ /// <summary>打开的</summary>
+ public bool Opened = false;
+
+ // 收数据包缓冲
+ private VCI_CAN_OBJ[] m_recobj = new VCI_CAN_OBJ[1000];
+
+ /// <summary>打开</summary>
+ /// <returns></returns>
+ public override bool Open()
+ {
+ if (Opened) return true;
+
+ // 以下两行为多卡同机测试代码,用来获取序列号与对应的设备索引号,单卡可以不使用。
+ // VCI_BOARD_INFO[] vbi2 = new VCI_BOARD_INFO[50];
+ // uint num1 = VCI_FindUsbDevice2(ref vbi2[0]);
+
+ try
+ {
+ // 打开(引用计数:一个设备有多个CAN口,仅首个口真正打开设备)
+ if (!_OpenDevice(m_devtype, DevId))
+ {
+ return false;
+ }
+ // 配置
+ VCI_INIT_CONFIG cfg = new VCI_INIT_CONFIG();
+ cfg.AccCode = 0;
+ cfg.AccMask = UInt32.MaxValue;
+
+ switch (Baudrate)
+ {
+ case Baudrate_e.B1000K: cfg.Timing0 = 0; cfg.Timing1 = 0x14; break;
+ case Baudrate_e.B500K: cfg.Timing0 = 0; cfg.Timing1 = 0x1c; break;
+ case Baudrate_e.B250K: cfg.Timing0 = 1; cfg.Timing1 = 0x1c; break;
+ case Baudrate_e.B200K: cfg.Timing0 = 0x81; cfg.Timing1 = 0xFA; break;
+ case Baudrate_e.B125K: cfg.Timing0 = 3; cfg.Timing1 = 0x1c; break;
+ case Baudrate_e.B100K: cfg.Timing0 = 4; cfg.Timing1 = 0x1c; break;
+ default: break;
+ }
+
+ // 00 1c 500k 01 1c 250k
+ // cfg.Timing0 = 0;
+ // cfg.Timing1 = 0x1c;
+ cfg.Filter = 1;
+ cfg.Mode = 0;
+ VCI_InitCAN(m_devtype, DevId, (UInt32)PortId, ref cfg);
+ // 启动
+ CxControlCAN.VCI_StartCAN(m_devtype, DevId, (UInt32)PortId);
+ }
+ catch
+ {
+ // 初始化异常,回退设备引用计数
+ _CloseDevice(m_devtype, DevId);
+ return false;
+ }
+
+ Opened = true;
+ // 复位停止标志,恢复正常收发
+ _ResumeReceive();
+ _StartRx();
+
+ return true;
+ }
+
+ /// <summary>关闭</summary>
+ public override void Close()
+ {
+ // 先置停止标志并发出取消,接收线程不会再分发消息,也会尽快退出
+ _StopRx();
+
+ // 必须等接收线程真正退出后再释放设备,否则线程仍在 VCI_Receive 中访问已关闭的设备。
+ // 此处不会长时间阻塞:停止标志已置位,线程最多跑完当前一次 VCI_Receive 便会退出。
+ var thr = _thrRx;
+ if ((thr != null) && thr.IsAlive)
+ {
+ try { thr.Join(1000); } catch { }
+ }
+
+ if (Opened)
+ {
+ try
+ {
+ // 停止本CAN口
+ CxControlCAN.VCI_ResetCAN(m_devtype, DevId, (UInt32)PortId);
+ }
+ catch { }
+
+ // 关闭设备(引用计数:一个设备多个CAN口,需所有口都关闭后才真正关闭设备)
+ _CloseDevice(m_devtype, DevId);
+ }
+
+ Opened = false;
+ }
+
+ #region 设备引用计数(一个CAN工具有多个CAN口,需所有口都关闭才能关闭设备)
+
+ /// <summary>设备打开引用计数,key = (设备类型 << 32) | 设备索引</summary>
+ private static readonly Dictionary<UInt64, Int32> _devRefCount = new Dictionary<UInt64, Int32>();
+
+ /// <summary>引用计数锁</summary>
+ private static readonly Object _devLock = new Object();
+
+ /// <summary>设备key</summary>
+ private static UInt64 _DevKey(UInt32 devtype, UInt32 devid) => ((UInt64)devtype << 32) | devid;
+
+ /// <summary>打开设备(引用计数,仅首个CAN口真正调用 VCI_OpenDevice)</summary>
+ private static bool _OpenDevice(UInt32 devtype, UInt32 devid)
+ {
+ lock (_devLock)
+ {
+ var key = _DevKey(devtype, devid);
+ if (_devRefCount.TryGetValue(key, out var cnt) && cnt > 0)
+ {
+ // 设备已打开,仅增加引用
+ _devRefCount[key] = cnt + 1;
+ return true;
+ }
+
+ // 首次打开设备
+ if (CxControlCAN.VCI_OpenDevice(devtype, devid, 0) == 0)
+ {
+ return false;
+ }
+ _devRefCount[key] = 1;
+ return true;
+ }
+ }
+
+ /// <summary>关闭设备(引用计数,仅最后一个CAN口真正调用 VCI_CloseDevice)</summary>
+ private static void _CloseDevice(UInt32 devtype, UInt32 devid)
+ {
+ lock (_devLock)
+ {
+ var key = _DevKey(devtype, devid);
+ if (!_devRefCount.TryGetValue(key, out var cnt) || cnt <= 0)
+ {
+ return;
+ }
+
+ cnt--;
+ if (cnt > 0)
+ {
+ // 还有其它CAN口在用,仅减少引用
+ _devRefCount[key] = cnt;
+ return;
+ }
+
+ // 最后一个CAN口,真正关闭设备
+ _devRefCount.Remove(key);
+ try { CxControlCAN.VCI_CloseDevice(devtype, devid); } catch { }
+ }
+ }
+
+ #endregion
+
+
+ /// <summary>释放资源</summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(bool disposing)
+ {
+ // 仅显式释放时清理;终结器/进程退出阶段不触碰 _ctsRx(等待句柄)与原生 DLL,
+ // 否则会导致进程崩溃(0xC0000409 STATUS_STACK_BUFFER_OVERRUN)
+ if (disposing)
+ {
+ _ctsRx?.Dispose();
+ _ctsRx = null;
+ }
+
+ base.Dispose(disposing); // 会调用 Close() -> _StopRx()(仅 disposing 时真正关闭硬件)
+ }
+
+ #region 设备枚举
+
+ /// <summary>枚举当前计算机上所有可用的 CX CAN 口(未打开,仅列出)</summary>
+ /// <returns>key = "CX-{设备类型}-{DevId}-{口序号}",value = 可配置的 ICanPort 实例</returns>
+ public new static Dictionary<String, ICanPort> EnumDevices()
+ {
+ var dict = new Dictionary<String, ICanPort>();
+ try
+ {
+ // 注意:VCI_FindUsbDevice2 会按设备数量向传入缓冲区连续写入 VCI_BOARD_INFO,
+ // 必须传入足够大的数组(官方示例为 50 个元素)。
+ // 若只传单个元素的引用,多设备时驱动会越界写栈,导致进程原生崩溃
+ // (0xC0000409 STATUS_STACK_BUFFER_OVERRUN,表现为“在这行跑飞”)。
+ VCI_BOARD_INFO[] infos = new VCI_BOARD_INFO[50];
+ UInt32 count = CxControlCAN.VCI_FindUsbDevice2(ref infos[0]);
+ if (count <= 0) return dict;
+ if (count > infos.Length) count = (UInt32)infos.Length;
+
+ // 为每个 USB-CAN 适配器(DevId = 索引)枚举其所有 CAN 口
+ for (UInt32 devId = 0; devId < count; devId++)
+ {
+ byte canNum = infos[devId].can_Num;
+ if (canNum == 0) canNum = 1;
+
+ for (byte ch = 0; ch < canNum; ch++)
+ {
+ var name = $"CX-{m_devtype}-{devId}-{ch}";
+ var port = new CxCanPort(name);
+ port.DevId = devId;
+ port.PortId = ch;
+ dict[name] = port;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // 输出完整异常(含 DllNotFoundException / EntryPointNotFoundException 等类型),便于定位
+ XTrace.WriteLine("枚举CX设备失败: " + ex.ToString());
+ }
+ return dict;
+ }
+
+ #endregion
+
+ #region 接收线程
+
+ /// <summary>后台接收线程</summary>
+ private Thread _thrRx;
+
+ /// <summary>终止线程</summary>
+ private CancellationTokenSource _ctsRx = new CancellationTokenSource();
+
+ /// <summary>启动接收线程</summary>
+ private void _StartRx()
+ {
+ if ((_thrRx != null) && (_thrRx.IsAlive))
+ {
+ _StopRx();
+ // 旧线程已置停止标志,会很快退出;短暂等待避免新旧线程同时访问硬件
+ _thrRx.Join(1000);
+ _thrRx = null;
+ }
+
+ _ctsRx?.Dispose();
+ _ctsRx = new CancellationTokenSource();
+ _thrRx = new Thread(() => _RxThreadFunc());
+ _thrRx.IsBackground = true;
+ _thrRx.Start();
+ }
+
+ /// <summary>
+ /// 停止接收线程。
+ /// 只发出停止信号并置位停止标志,不在调用线程上阻塞等待。
+ /// 原因:关闭通常由 UI 线程发起,接收线程可能正阻塞在向 UI 线程 Invoke 上,
+ /// 若此处同步等待线程退出,会与 UI 线程形成双向死等,表现为主窗口关闭卡顿。
+ /// 置位停止标志后,接收线程不再分发消息,也会立即退出等待。
+ /// </summary>
+ private void _StopRx()
+ {
+ // 先置位停止标志,后续收到的帧不再分发(也不会再 Invoke 到 UI 线程)
+ _StopReceive();
+
+ if (_thrRx == null) return;
+ if (_ctsRx == null) return;
+
+ try { _ctsRx.Cancel(); } catch { }
+ }
+
+ /// <summary>接收线程函数</summary>
+ private void _RxThreadFunc()
+ {
+ var cts = _ctsRx;
+ while (true)
+ {
+ if (cts == null || cts.Token.IsCancellationRequested) break;
+ // 收到停止信号立即退出,不再处理本批剩余帧
+ if (RxStopped) break;
+
+ try
+ {
+ UInt32 res = new UInt32();
+ res = VCI_Receive(m_devtype, DevId, (UInt32)PortId, ref m_recobj[0], 1000, 100);
+ if (res == 0xFFFFFFFF) res = 0;
+
+ for (int i = 0; i < res; i++)
+ {
+ // 停止过程中丢弃剩余帧,避免继续向 UI 线程 Invoke
+ if (RxStopped) break;
+
+ RxMsgProcess(m_recobj[i]);
+ }
+ }
+ catch { }
+
+ if (RxStopped) break;
+
+ Thread.Sleep(10);
+ }
+ }
+
+ #endregion
+
+ /// <summary>发送消息</summary>
+ /// <param name="msg"></param>
+ public void Send(VCI_CAN_OBJ msg)
+ {
+ if (!Opened)
+ {
+ WriteLog("发送失败,端口未打开");
+ return;
+ }
+
+ if (VCI_Transmit(m_devtype, DevId, (UInt32)PortId, ref msg, 1) == 0) { }
+ }
+
+ #endregion
+
+ #region 发送
+
+ /// <summary>发送数据</summary>
+ /// <param name="id">CANID</param>
+ /// <param name="data">数据</param>
+ /// <param name="remote">远程帧</param>
+ /// <param name="ext">扩展帧</param>
+ public override void Send(UInt32 id, byte[] data, bool remote = false, bool ext = false)
+ {
+ if (!Opened)
+ {
+ WriteLog("发送失败,端口未打开");
+ return;
+ }
+
+ VCI_CAN_OBJ msg = new VCI_CAN_OBJ();
+ msg.ID = id;
+ msg.RemoteFlag = (byte)(remote ? 1 : 0);
+ msg.ExternFlag = (byte)(ext ? 1 : 0);
+ var len = data.Length;
+ if (len > 8) len = 8;
+ msg.DataLen = (byte)len;
+
+ unsafe
+ {
+ for (int i = 0; i < len; i++)
+ {
+ msg.Data[i] = data[i];
+ }
+ }
+
+ WriteLog(msg.ToString() + " TX");
+ Send(msg);
+ }
+
+ #endregion
+
+ #region 处理消息,软件逻辑
+
+ /// <summary>处理消息</summary>
+ /// <param name="msg"></param>
+ /// <returns></returns>
+ private bool RxMsgProcess(VCI_CAN_OBJ msg)
+ {
+ return ProcessReceived(msg.ToCanMsg());
+ }
+
+ #endregion
+
+ #region 日志
+
+ /// <summary>日志前缀</summary>
+ protected override String LogPre => "CX ";
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/A1Lib/CAN/CX_CAN/CxControlCAN.cs b/A1Lib/CAN/CX_CAN/CxControlCAN.cs
new file mode 100644
index 0000000..f1e0a6d
--- /dev/null
+++ b/A1Lib/CAN/CX_CAN/CxControlCAN.cs
@@ -0,0 +1,275 @@
+using ALib.Can;
+using ALib.CAN;
+using NewLife;
+using NewLife.Collections;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.CX_CAN
+{
+ public static class CxControlCAN
+ {
+ /*------------兼容ZLG的数据类型---------------------------------*/
+
+ //1.ZLGCAN系列接口卡信息的数据类型。
+ //public struct VCI_BOARD_INFO
+ //{
+ // public UInt16 hw_Version;
+ // public UInt16 fw_Version;
+ // public UInt16 dr_Version;
+ // public UInt16 in_Version;
+ // public UInt16 irq_Num;
+ // public byte can_Num;
+ // [MarshalAs(UnmanagedType.ByValArray, SizeConst=20)] public byte []str_Serial_Num;
+ // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)]
+ // public byte[] str_hw_Type;
+ // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ // public byte[] Reserved;
+ //}
+
+ //以下为简易定义与调用方式,在项目属性->生成->勾选使用不安全代码即可
+ unsafe public struct VCI_BOARD_INFO //使用不安全代码
+ {
+ public UInt16 hw_Version;
+ public UInt16 fw_Version;
+ public UInt16 dr_Version;
+ public UInt16 in_Version;
+ public UInt16 irq_Num;
+ public byte can_Num;
+
+ public fixed byte str_Serial_Num[20];
+ public fixed byte str_hw_Type[40];
+ public fixed byte Reserved[8];
+ }
+
+ /////////////////////////////////////////////////////
+ //2.定义CAN信息帧的数据类型。
+ unsafe public struct VCI_CAN_OBJ //使用不安全代码
+ {
+ public uint ID; //标准帧11bit 。 canopen 高4bit 消息类型,低7bit节点地址
+ public uint TimeStamp; //时间标识
+ public byte TimeFlag; //是否使用时间标识
+ public byte SendType; //发送标志。保留,未用
+ public byte RemoteFlag; //是否是远程帧。canopen 用 0
+ public byte ExternFlag; //是否是扩展帧
+ public byte DataLen; //数据长度。 DLC 4bit
+ public fixed byte Data[8]; //数据
+ public fixed byte Reserved[3];//保留位
+
+
+ /// <summary>fixed byte Data 转托管数组</summary>
+ /// <returns></returns>
+ public byte[] GetData()
+ {
+ var bs = new byte[8];
+
+ unsafe
+ {
+ for (int i = 0; i < 8; i++)
+ {
+ bs[i] = Data[i];
+ }
+ }
+
+ return bs;
+ }
+
+ public CanPacket ToCanPacket()
+ {
+ var pkt = new CanPacket();
+
+ pkt.COB_ID = (int)ID;
+ pkt.RTR = RemoteFlag > 0;
+ if (DataLen > 0)
+ {
+ pkt.Data = new byte[DataLen];
+ unsafe
+ {
+ for (int i = 0; i < DataLen; i++)
+ {
+ pkt.Data[i] = Data[i];
+ }
+ }
+ }
+
+ return pkt;
+ }
+
+ public static VCI_CAN_OBJ FromCanPacket(CanPacket pkt)
+ {
+ var obj = new VCI_CAN_OBJ();
+ obj.ID = (uint)pkt.COB_ID;
+ obj.RemoteFlag = (byte)(pkt.RTR ? 1 : 0);
+ obj.ExternFlag = 0;
+ obj.DataLen = (byte)pkt.DLC;
+ if (pkt.DLC > 0)
+ {
+ unsafe
+ {
+ for (int i = 0; i < pkt.DLC; i++)
+ {
+ obj.Data[i] = pkt.Data[i];
+ }
+ }
+ }
+ return obj;
+ }
+
+ /// <summary>VCI_CAN_OBJ -> CanMsg</summary>
+ /// <returns></returns>
+ public CanMsg ToCanMsg()
+ {
+ var len = DataLen > 8 ? 8 : DataLen;
+ var data = new byte[len];
+ unsafe
+ {
+ for (int i = 0; i < len; i++)
+ {
+ data[i] = Data[i];
+ }
+ }
+
+ var pkt = new CanMsg((Int32)ID, data, RemoteFlag > 0, ExternFlag > 0);
+ // ZLG 时间标识单位为 0.1ms,仅在使用时间标识时有效
+ if (TimeFlag > 0) pkt.TimeStamp = TimeSpan.FromMilliseconds(TimeStamp / 10.0);
+ return pkt;
+ }
+
+
+ }
+
+ /// <summary>格式化字符串</summary>
+ /// <param name="msg"></param>
+ /// <returns></returns>
+ public static String MsgToString(VCI_CAN_OBJ msg)
+ {
+ var sb = Pool.StringBuilder.Get();
+
+ var tsp = TimeSpan.FromMilliseconds(msg.TimeStamp / 10);
+ sb.Append(tsp.ToString("hh\\:mm\\:ss\\.fff"));
+
+ // var T = msg.TimeFlag > 0 ? " T " : "NT ";
+ // sb.Append(T);
+
+ if (msg.ExternFlag > 0)
+ {
+ var id = msg.ID.GetBytes(false).ReadBytes(0, 4).ToHex();
+ sb.Append(" " + id + " ");
+ }
+ else
+ {
+ var id = msg.ID.GetBytes(false).ReadBytes(2, 2).ToHex();
+ sb.Append(" " + id + " ");
+ }
+
+ var R = msg.RemoteFlag > 0 ? "R " : " ";
+ sb.Append(R);
+
+ var E = msg.ExternFlag > 0 ? "E " : " ";
+ sb.Append(E);
+
+ sb.Append("[" + msg.DataLen + "] ");
+ sb.Append(" " + msg.GetData().ReadBytes(0, msg.DataLen).ToHex(" "));
+ return sb.Return(true);
+ }
+
+
+ //3.定义初始化CAN的数据类型
+ public struct VCI_INIT_CONFIG
+ {
+ public UInt32 AccCode;
+ public UInt32 AccMask;
+ public UInt32 Reserved;
+ public byte Filter; //0或1接收所有帧。2标准帧滤波,3是扩展帧滤波。
+ public byte Timing0; //波特率参数,具体配置,请查看二次开发库函数说明书。
+ public byte Timing1;
+ public byte Mode; //模式,0表示正常模式,1表示只听模式,2自测模式
+ }
+
+ /*------------其他数据结构描述---------------------------------*/
+ //4.USB-CAN总线适配器板卡信息的数据类型1,该类型为VCI_FindUsbDevice函数的返回参数。
+ public struct VCI_BOARD_INFO1
+ {
+ public UInt16 hw_Version;
+ public UInt16 fw_Version;
+ public UInt16 dr_Version;
+ public UInt16 in_Version;
+ public UInt16 irq_Num;
+ public byte can_Num;
+ public byte Reserved;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] str_Serial_Num;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
+ public byte[] str_hw_Type;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
+ public byte[] str_Usb_Serial;
+ }
+
+ /*------------数据结构描述完成---------------------------------*/
+
+ public struct CHGDESIPANDPORT
+ {
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
+ public byte[] szpwd;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
+ public byte[] szdesip;
+ public Int32 desport;
+
+ public void Init()
+ {
+ szpwd = new byte[10];
+ szdesip = new byte[20];
+ }
+ }
+
+
+ const int DEV_USBCAN = 3;
+ const int DEV_USBCAN2 = 4;
+ /// <summary>
+ ///
+ /// </summary>
+ /// <param name="DeviceType"></param>
+ /// <param name="DeviceInd"></param>
+ /// <param name="Reserved"></param>
+ /// <returns></returns>
+ /*------------兼容ZLG的函数描述---------------------------------*/
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_OpenDevice(UInt32 DeviceType, UInt32 DeviceInd, UInt32 Reserved);
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_CloseDevice(UInt32 DeviceType, UInt32 DeviceInd);
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_InitCAN(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd, ref VCI_INIT_CONFIG pInitConfig);
+
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_ReadBoardInfo(UInt32 DeviceType, UInt32 DeviceInd, ref VCI_BOARD_INFO pInfo);
+
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_GetReceiveNum(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd);
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_ClearBuffer(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd);
+
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_StartCAN(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd);
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_ResetCAN(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd);
+
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_Transmit(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd, ref VCI_CAN_OBJ pSend, UInt32 Len);
+
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_Receive(UInt32 DeviceType, UInt32 DeviceInd, UInt32 CANInd, ref VCI_CAN_OBJ pReceive, UInt32 Len, Int32 WaitTime);
+
+ /*------------其他函数描述---------------------------------*/
+
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_ConnectDevice(UInt32 DevType, UInt32 DevIndex);
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_UsbDeviceReset(UInt32 DevType, UInt32 DevIndex, UInt32 Reserved);
+ [DllImport("controlcan.dll")]
+ public static extern UInt32 VCI_FindUsbDevice2(ref VCI_BOARD_INFO pInfo);
+ }
+}
diff --git a/A1Lib/CAN/DeviceMsgs.cs b/A1Lib/CAN/DeviceMsgs.cs
new file mode 100644
index 0000000..a71bff7
--- /dev/null
+++ b/A1Lib/CAN/DeviceMsgs.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static System.Runtime.InteropServices.JavaScript.JSType;
+
+namespace ALib.CAN
+{
+ public class MsgBitsTag
+ {
+ #region
+
+ public int CanId
+ {
+ get { return (int)(_tag >> 32); }
+ set
+ {
+ UInt64 mask = 0x0000_0000_ffff_ffff;
+ _tag &= mask;
+ _tag |= ((UInt64)value << 32);
+ }
+ }
+
+ public byte BitOffset
+ {
+ get { return (byte)(_tag >> 24); }
+ set
+ {
+ UInt64 mask = 0xffff_ffff_00ff_ffff;
+ _tag &= mask;
+ _tag |= ((UInt64)value << 24);
+ }
+ }
+
+ public byte BitLenght
+ {
+ get { return (byte)(_tag >> 16); }
+ set
+ {
+ UInt64 mask = 0xffff_ffff_ff00_ffff;
+ _tag &= mask;
+ _tag |= ((UInt64)value << 16);
+ }
+ }
+
+ public ushort Other
+ {
+ get { return (ushort)(_tag); }
+ set
+ {
+ UInt64 mask = 0xffff_ffff_ffff_0000;
+ _tag &= mask;
+ _tag |= ((UInt64)value);
+ }
+ }
+
+ private UInt64 _tag;
+
+ public UInt64 Tag { get { return _tag; } set { _tag = value; } }
+
+ #endregion
+
+ public MsgBitsTag(UInt64 tag) { Tag = tag; }
+
+ public MsgBitsTag(int canid, byte offset, byte len, ushort other = 0)
+ {
+ // Tag = 0;
+ CanId = canid;
+ BitOffset = offset;
+ BitLenght = len;
+ Other = other;
+ }
+
+ /// <summary>从消息获取数据</summary>
+ /// <param name="msg"></param>
+ /// <returns></returns>
+ public UInt64 GetDataFrom(UInt64 msg)
+ {
+ msg >>= BitOffset;
+ var mask = ~(UInt64.MaxValue << BitLenght);
+
+ return msg & mask;
+ }
+
+ /// <summary>设置数据到小消息</summary>
+ /// <param name="msg"></param>
+ /// <param name="data"></param>
+ /// <returns></returns>
+ public UInt64 SetDataTo(UInt64 msg, UInt64 data)
+ {
+ // cnt 位宽掩码(低 cnt 位为 1)
+ UInt64 bitMask = ~(UInt64.MaxValue << BitLenght);
+
+ // 清掉目标位区间,再写入新值
+ msg &= ~(bitMask << BitOffset);
+ msg |= ((UInt64)data & bitMask) << BitOffset;
+
+ return msg;
+ }
+ }
+
+ /// <summary>设备消息汇总</summary>
+ public class DeviceMsgs
+ {
+ /// <summary>Msgs对应的canid</summary>
+ public int[] CandIds { get; private set; }
+
+ /// <summary>canid,data</summary>
+ private Dictionary<Int32, UInt64> Msgs;
+
+ /// <summary>构造</summary>
+ public DeviceMsgs(int[] canIds)
+ {
+ CandIds = canIds;
+
+ Msgs = new Dictionary<Int32, UInt64>(CandIds.Length);
+ foreach (var id in CandIds) { Msgs.Add(id, 0); }
+ }
+
+ #region 整条消息读写
+
+ /// <summary></summary>
+ /// <param name="canid"></param>
+ /// <param name="msg"></param>
+ public void Update(Int32 canid, UInt64? msg)
+ {
+ if (msg == null) return;
+ if (!CandIds.Contains(canid)) return;
+ Msgs[canid] = msg.Value;
+ }
+
+ public void Update(Int32 canid, byte[] msg)
+ {
+ if (msg == null) return;
+ if (msg.Length != 8) return;
+
+ Update(canid, BitConverter.ToUInt64(msg));
+ }
+
+ public void Update(CanMsg msg) => Update(msg.ID, msg.Data);
+
+ /// <summary>索引器:以 canid 直接获取/设置对应消息内容(不存在的 canid 返回 null)</summary>
+ public UInt64? this[Int32 canid]
+ {
+ get => Msgs.TryGetValue(canid, out var data) ? data : null;
+ set => Update(canid, value);
+ }
+
+ /// <summary>清空所有消息到0</summary>
+ public void CleanAll()
+ {
+ foreach (var id in CandIds) { Update(id, 0); }
+ }
+
+ #endregion
+
+ #region 消息内读写
+
+ /// <summary>读取 tag 指定的位域数据,消息不存在返回 null</summary>
+ /// <param name="tag">位域标签</param>
+ /// <returns></returns>
+ public UInt64? GetData(MsgBitsTag tag)
+ {
+ if (tag == null) return null;
+
+ var msg = this[tag.CanId];
+ if (msg == null) return null;
+
+ return tag.GetDataFrom(msg.Value);
+ }
+
+ /// <summary>写入 tag 指定的位域数据,返回写入之前的原始值</summary>
+ /// <param name="tag">位域标签</param>
+ /// <param name="value">写入的值</param>
+ /// <returns>返回写入之前的数值</returns>
+ public UInt64 SetData(MsgBitsTag tag, UInt64 value)
+ {
+ if (tag == null) return 0;
+
+ var oldVal = GetData(tag) ?? 0;
+
+ var msg = this[tag.CanId] ?? 0;
+ this[tag.CanId] = tag.SetDataTo(msg, value);
+
+ return oldVal;
+ }
+
+ #endregion
+
+ #region 比较
+
+ /// <summary>逐条比较两个设备消息的所有 canid 数据是否完全一致,最后一字节seq不参与比较</summary>
+ public static bool operator ==(DeviceMsgs a, DeviceMsgs b)
+ {
+ if (ReferenceEquals(a, b)) return true;
+ if (a is null || b is null) return false;
+
+ if (a.Msgs.Count != b.Msgs.Count) return false;
+
+ foreach (var kvp in a.Msgs)
+ {
+ if (!b.Msgs.TryGetValue(kvp.Key, out var otherData)) return false;
+
+ var data = kvp.Value << 8;
+ var other = otherData << 8;
+
+ if (data != other) return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>逐条比较两个设备消息的所有 canid 数据是否完全一致,最后一字节seq不参与比较</summary>
+ public static bool operator !=(DeviceMsgs a, DeviceMsgs b) { return !(a == b); }
+
+ /// <summary>逐条比较两个设备消息的所有 canid 数据是否完全一致,最后一字节seq不参与比较</summary>
+ public override bool Equals(object obj) { return obj is DeviceMsgs other && this == other; }
+
+ public override int GetHashCode()
+ {
+ var hash = new HashCode();
+ foreach (var kvp in Msgs)
+ {
+ hash.Add(kvp.Key);
+ hash.Add(kvp.Value);
+ }
+ return hash.ToHashCode();
+ }
+
+ #endregion
+ }
+}
diff --git a/A1Lib/CAN/ICanPort.cs b/A1Lib/CAN/ICanPort.cs
new file mode 100644
index 0000000..29c06d3
--- /dev/null
+++ b/A1Lib/CAN/ICanPort.cs
@@ -0,0 +1,223 @@
+using ALib.Can;
+using NewLife;
+using NewLife.Log;
+using NewLife.Net;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.CAN
+{
+ public abstract class ICanRx
+ {
+ /// <summary>数据包CAN ID 通过掩码之后的值,如果匹配则代表处理器正确</summary>
+ public int CanId = 0;
+
+ /// <summary>CAN ID 掩码</summary>
+ public int CanIdMask = 0;
+
+ /// <summary>数据处理</summary>
+ /// <param name="bs">can 帧</param>
+ /// <returns></returns>
+ public virtual bool Process(CanMsg msg, ICanPort port)
+ {
+ // XTrace.WriteLine(MsgToString(msg));
+ return false;
+ }
+ }
+
+ public abstract class ICanPort : DisposeBase
+ {
+ /// <summary>由子类构造时指定名称</summary>
+ /// <param name="name">枚举时候的名字</param>
+ protected ICanPort(String name) { Name = name; }
+
+ public enum Baudrate_e
+ {
+ B1000K,
+ B500K,
+ B250K,
+ B200K,
+ B125K,
+ B100K,
+ };
+
+ /// <summary>
+ /// 枚举各 ICanPort 实现(ZLG/CX/PCAN 等)的设备并汇总输出。
+ /// 注意:这里显式调用各实现的静态 EnumDevices(),不使用反射 Invoke。
+ /// 原因:部分实现内部调用含 unsafe/fixed 结构体 ref 参数的 P/Invoke,
+ /// 反射 Invoke 会走通用封送路径,导致传给原生库的指针错误而原生崩溃(0xC0000409)。
+ /// </summary>
+ /// <returns>key = 描述性名称,value = ICanPort 实例</returns>
+ public static Dictionary<String, ICanPort> EnumDevices()
+ {
+ var dict = new Dictionary<String, ICanPort>();
+
+ // 新增 ICanPort 实现时,在此追加一行
+ _Collect(dict, () => ALib.ZLG_CAN.ZlgCanPort.EnumDevices());
+ _Collect(dict, () => ALib.CX_CAN.CxCanPort.EnumDevices());
+ _Collect(dict, () => ALib.PEAK_CAN.PeakCanPort.EnumDevices());
+
+ return dict;
+ }
+
+ /// <summary>调用某个实现的静态 EnumDevices(),异常不影响其它实现</summary>
+ /// <param name="dict">汇总字典</param>
+ /// <param name="enumFunc">实现的枚举委托</param>
+ static void _Collect(Dictionary<String, ICanPort> dict, Func<Dictionary<String, ICanPort>> enumFunc)
+ {
+ try
+ {
+ var sub = enumFunc();
+ if (sub == null) return;
+
+ foreach (var kv in sub)
+ {
+ dict[kv.Key] = kv.Value;
+ }
+ }
+ catch (Exception ex)
+ {
+ NewLife.Log.XTrace.WriteLine("枚举CAN设备失败: " + ex.ToString());
+ }
+ }
+
+ /// <summary>枚举时候的名字,由子类构造时传入,创建后不可修改</summary>
+ public String Name { get; }
+
+ /// <summary>CAN 分析仪多个CAN口,下标</summary>
+ public Int32 PortId = 0;
+
+ /// <summary>波特率,默认 250K</summary>
+ public Baudrate_e Baudrate = Baudrate_e.B250K;
+
+ /// <summary>打开</summary>
+ /// <returns></returns>
+ public abstract bool Open();
+
+ /// <summary>关闭</summary>
+ public abstract void Close();
+
+ /// <summary>释放资源(硬件设备,确保关闭)</summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(bool disposing)
+ {
+ // 仅在显式释放时关闭硬件。
+ // 终结器/进程退出阶段(disposing == false)不得触碰原生 DLL 与等待句柄,
+ // 否则在 CLR 关闭期间操作已失效的原生资源会导致进程崩溃(0xC0000409)。
+ if (disposing)
+ {
+ try { Close(); }
+ catch { }
+ }
+
+ base.Dispose(disposing);
+ }
+
+ public abstract void Send(UInt32 id, byte[] data, bool remote = false, bool ext = false);
+
+ /// <summary>发送消息</summary>
+ /// <param name="pkt"></param>
+ public void Send(CanMsg pkt) { Send((UInt32)pkt.ID, pkt.Data, pkt.RemoteFlag, pkt.ExternFlag); }
+
+ public void Send(CanPacket pkt) { Send(pkt.ToCanMsg()); }
+
+ /// <summary>
+ /// 收发停止标志。关闭/停止接收时置位,
+ /// 接收线程据此立即停止把帧分发给订阅者(<see cref="Received"/> 与 <see cref="ICanRx"/>),
+ /// 避免关闭过程中 UI 线程等待接收线程、而接收线程正阻塞在 UI 线程 Invoke 上的双向死等。
+ /// </summary>
+ private volatile Boolean _rxStopped = false;
+
+ /// <summary>是否已停止收发(接收线程与分发逻辑的短路依据)</summary>
+ public Boolean RxStopped => _rxStopped;
+
+ /// <summary>置位收发停止标志,之后 <see cref="ProcessReceived"/> 不再分发消息</summary>
+ protected void _StopReceive() => _rxStopped = true;
+
+ /// <summary>复位收发停止标志,用于重新打开端口前恢复工作</summary>
+ protected void _ResumeReceive() => _rxStopped = false;
+
+ /// <summary>数据到达事件</summary>
+ public event EventHandler<CanMsg> Received;
+
+ /// <summary>消息处理器</summary>
+ protected readonly List<ICanRx> _RxProcess = new List<ICanRx>();
+
+ /// <summary>注册数据处理器</summary>
+ /// <param name="proc"></param>
+ /// <returns></returns>
+ public virtual bool Register(ICanRx proc)
+ {
+ lock (_RxProcess)
+ {
+ // 排除重复
+ foreach (var item in _RxProcess)
+ {
+ if ((item.CanIdMask == proc.CanIdMask) && (item.CanId == proc.CanId))
+ {
+ return false;
+ }
+ }
+
+ _RxProcess.Add(proc);
+ return true;
+ }
+ }
+
+ /// <summary>反注册</summary>
+ /// <param name="proc"></param>
+ public virtual void Unregister(ICanRx proc)
+ {
+ lock (_RxProcess) _RxProcess.Remove(proc);
+ }
+
+ /// <summary>处理收到的 CAN 帧(分发到已注册的处理器,无处理器处理时写日志)</summary>
+ /// <param name="pkt">已转换的 CAN 帧</param>
+ /// <returns>是否至少有一个处理器处理了该帧</returns>
+ protected bool ProcessReceived(CanMsg pkt)
+ {
+ // 已停止接收:直接丢弃本帧。
+ // 关闭端口时 UI 线程会同步等待接收线程退出,若此处仍回调订阅者,
+ // 订阅者会 Invoke 回正在等待的 UI 线程,形成双向死等导致关闭卡顿。
+ if (_rxStopped) return false;
+
+ Received?.Invoke(this, pkt);
+
+ bool res = false;
+ lock (_RxProcess)
+ {
+ foreach (var item in _RxProcess)
+ {
+ // 用于匹配处理器的 CAN ID
+ var id = pkt.ID;
+ if ((id & (UInt32)item.CanIdMask) == (UInt32)item.CanId)
+ {
+ if (item.Process(pkt, this)) res = true;
+ }
+ }
+ }
+
+ if (!res)
+ {
+ WriteLog(pkt.ToString());
+ }
+
+ return res;
+ }
+
+ /// <summary>日志</summary>
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>写日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ protected void WriteLog(String format, params Object[] args) => Log?.Info(LogPre + format, args);
+
+ /// <summary>日志前缀,由各实现指定(如 "ZLG " / "CX " / "PCAN ")</summary>
+ protected virtual String LogPre => "CAN ";
+ }
+}
diff --git a/A1Lib/CAN/PCAN/PCANBasic.cs b/A1Lib/CAN/PCAN/PCANBasic.cs
new file mode 100644
index 0000000..26890ca
--- /dev/null
+++ b/A1Lib/CAN/PCAN/PCANBasic.cs
@@ -0,0 +1,1729 @@
+// PCANBasic.cs
+//
+// ~~~~~~~~~~~~
+//
+// PCAN-Basic API
+//
+// ~~~~~~~~~~~~
+//
+// ------------------------------------------------------------------
+// Author : Keneth Wagner
+// Last change: 2025-10-24
+//
+// Language: C# 1.0
+// ------------------------------------------------------------------
+//
+// Copyright (C) 1999-2025 PEAK-System Technik GmbH, Darmstadt
+// more Info at http://www.peak-system.com
+//
+using System;
+using System.Text;
+using System.Runtime.InteropServices;
+
+namespace Peak.Can.Basic
+{
+ using TPCANHandle = System.UInt16;
+ using TPCANBitrateCC = System.String;
+ using TPCANBitrateFD = System.String;
+ using TPCANBitrateXL = System.String;
+ using TPCANTimestampFD = System.UInt64;
+ using TPCANTimestampXL = System.UInt64;
+
+ #region Enumerations
+ /// <summary>
+ /// Represents a PCAN status/error code
+ /// </summary>
+ [Flags]
+ public enum TPCANStatus : uint
+ {
+ /// <summary>
+ /// No error
+ /// </summary>
+ PCAN_ERROR_OK = 0x00000,
+ /// <summary>
+ /// Transmit buffer in CAN controller is full
+ /// </summary>
+ PCAN_ERROR_XMTFULL = 0x00001,
+ /// <summary>
+ /// CAN controller was read too late
+ /// </summary>
+ PCAN_ERROR_OVERRUN = 0x00002,
+ /// <summary>
+ /// Bus error: an error counter reached the 'light' limit
+ /// </summary>
+ PCAN_ERROR_BUSLIGHT = 0x00004,
+ /// <summary>
+ /// Bus error: an error counter reached the 'heavy' limit
+ /// </summary>
+ PCAN_ERROR_BUSHEAVY = 0x00008,
+ /// <summary>
+ /// Bus error: an error counter reached the 'warning' limit
+ /// </summary>
+ PCAN_ERROR_BUSWARNING = PCAN_ERROR_BUSHEAVY,
+ /// <summary>
+ /// Bus error: the CAN controller is error passive
+ /// </summary>
+ PCAN_ERROR_BUSPASSIVE = 0x40000,
+ /// <summary>
+ /// Bus error: the CAN controller is in bus-off state
+ /// </summary>
+ PCAN_ERROR_BUSOFF = 0x00010,
+ /// <summary>
+ /// Mask for all bus errors
+ /// </summary>
+ PCAN_ERROR_ANYBUSERR = (PCAN_ERROR_BUSWARNING | PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY | PCAN_ERROR_BUSOFF | PCAN_ERROR_BUSPASSIVE),
+ /// <summary>
+ /// Receive queue is empty
+ /// </summary>
+ PCAN_ERROR_QRCVEMPTY = 0x00020,
+ /// <summary>
+ /// Receive queue was read too late
+ /// </summary>
+ PCAN_ERROR_QOVERRUN = 0x00040,
+ /// <summary>
+ /// Transmit queue is full
+ /// </summary>
+ PCAN_ERROR_QXMTFULL = 0x00080,
+ /// <summary>
+ /// Test of the CAN controller hardware registers failed (no hardware found)
+ /// </summary>
+ PCAN_ERROR_REGTEST = 0x00100,
+ /// <summary>
+ /// Driver not loaded
+ /// </summary>
+ PCAN_ERROR_NODRIVER = 0x00200,
+ /// <summary>
+ /// Hardware already in use by a Net
+ /// </summary>
+ PCAN_ERROR_HWINUSE = 0x00400,
+ /// <summary>
+ /// A Client is already connected to the Net
+ /// </summary>
+ PCAN_ERROR_NETINUSE = 0x00800,
+ /// <summary>
+ /// Hardware handle is invalid
+ /// </summary>
+ PCAN_ERROR_ILLHW = 0x01400,
+ /// <summary>
+ /// Net handle is invalid
+ /// </summary>
+ PCAN_ERROR_ILLNET = 0x01800,
+ /// <summary>
+ /// Client handle is invalid
+ /// </summary>
+ PCAN_ERROR_ILLCLIENT = 0x01C00,
+ /// <summary>
+ /// Mask for all handle errors
+ /// </summary>
+ PCAN_ERROR_ILLHANDLE = (PCAN_ERROR_ILLHW | PCAN_ERROR_ILLNET | PCAN_ERROR_ILLCLIENT),
+ /// <summary>
+ /// Resource (FIFO, Client, timeout) cannot be created
+ /// </summary>
+ PCAN_ERROR_RESOURCE = 0x02000,
+ /// <summary>
+ /// Invalid parameter
+ /// </summary>
+ PCAN_ERROR_ILLPARAMTYPE = 0x04000,
+ /// <summary>
+ /// Invalid parameter value
+ /// </summary>
+ PCAN_ERROR_ILLPARAMVAL = 0x08000,
+ /// <summary>
+ /// Unknown error
+ /// </summary>
+ PCAN_ERROR_UNKNOWN = 0x10000,
+ /// <summary>
+ /// Invalid data, function, or action.
+ /// </summary>
+ PCAN_ERROR_ILLDATA = 0x20000,
+ /// <summary>
+ /// Driver object state is wrong for the attempted operation
+ /// </summary>
+ PCAN_ERROR_ILLMODE = 0x80000,
+ /// <summary>
+ /// An operation was successfully carried out, however, irregularities were registered
+ /// </summary>
+ PCAN_ERROR_CAUTION = 0x2000000,
+ /// <summary>
+ /// Channel is not initialized
+ /// <remarks>Value was changed from 0x40000 to 0x4000000</remarks>
+ /// </summary>
+ PCAN_ERROR_INITIALIZE = 0x4000000,
+ /// <summary>
+ /// Invalid operation
+ /// <remarks>Value was changed from 0x80000 to 0x8000000</remarks>
+ /// </summary>
+ PCAN_ERROR_ILLOPERATION = 0x8000000,
+ }
+
+ /// <summary>
+ /// Represents a PCAN device
+ /// </summary>
+ public enum TPCANDevice : byte
+ {
+ /// <summary>
+ /// Undefined, unknown or not selected PCAN device value
+ /// </summary>
+ PCAN_NONE = 0,
+ /// <summary>
+ /// PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, PCAN-PC/104-Plus, PCAN-PCI Express, PCAN-PCIe FD, PCAN-PCI FD, PCAN-Chip PCIe
+ /// </summary>
+ PCAN_PCI = 4,
+ /// <summary>
+ /// PCAN-USB, PCAN-USB Pro, PCAN-USB FD, PCAN-USB Pro FD, PCAN-Chip USB, PCAN-USB XL
+ /// </summary>
+ PCAN_USB = 5,
+ /// <summary>
+ /// PCAN-Gateway, and PCAN-Gateway FD PCAN devices
+ /// </summary>
+ PCAN_LAN = 8
+ }
+
+ /// <summary>
+ /// Represents a PCAN parameter to be read or set
+ /// </summary>
+ public enum TPCANParameter : byte
+ {
+ /// <summary>
+ /// Device identifier parameter
+ /// </summary>
+ PCAN_DEVICE_ID = 1,
+ /// <summary>
+ /// 5-Volt power parameter
+ /// </summary>
+ PCAN_5VOLTS_POWER = 2,
+ /// <summary>
+ /// PCAN receive event handler parameter
+ /// </summary>
+ PCAN_RECEIVE_EVENT = 3,
+ /// <summary>
+ /// PCAN message filter parameter
+ /// </summary>
+ PCAN_MESSAGE_FILTER = 4,
+ /// <summary>
+ /// PCAN-Basic API version parameter
+ /// </summary>
+ PCAN_API_VERSION = 5,
+ /// <summary>
+ /// PCAN device channel version parameter
+ /// </summary>
+ PCAN_CHANNEL_VERSION = 6,
+ /// <summary>
+ /// PCAN Reset-On-Busoff parameter
+ /// </summary>
+ PCAN_BUSOFF_AUTORESET = 7,
+ /// <summary>
+ /// PCAN Listen-Only parameter
+ /// </summary>
+ PCAN_LISTEN_ONLY = 8,
+ /// <summary>
+ /// Directory path for log files
+ /// </summary>
+ PCAN_LOG_LOCATION = 9,
+ /// <summary>
+ /// Debug-Log activation status
+ /// </summary>
+ PCAN_LOG_STATUS = 10,
+ /// <summary>
+ /// Configuration of the debugged information (LOG_FUNCTION_***)
+ /// </summary>
+ PCAN_LOG_CONFIGURE = 11,
+ /// <summary>
+ /// Custom insertion of text into the log file
+ /// </summary>
+ PCAN_LOG_TEXT = 12,
+ /// <summary>
+ /// Availability status of a PCAN-Channel
+ /// </summary>
+ PCAN_CHANNEL_CONDITION = 13,
+ /// <summary>
+ /// PCAN hardware name parameter
+ /// </summary>
+ PCAN_HARDWARE_NAME = 14,
+ /// <summary>
+ /// Message reception status of a PCAN-Channel
+ /// </summary>
+ PCAN_RECEIVE_STATUS = 15,
+ /// <summary>
+ /// CAN-Controller number of a PCAN-Channel
+ /// </summary>
+ PCAN_CONTROLLER_NUMBER = 16,
+ /// <summary>
+ /// Directory path for PCAN trace files
+ /// </summary>
+ PCAN_TRACE_LOCATION = 17,
+ /// <summary>
+ /// CAN tracing activation status
+ /// </summary>
+ PCAN_TRACE_STATUS = 18,
+ /// <summary>
+ /// Configuration of the maximum file size of a CAN trace
+ /// </summary>
+ PCAN_TRACE_SIZE = 19,
+ /// <summary>
+ /// Configuration of the trace file storing mode (TRACE_FILE_***)
+ /// </summary>
+ PCAN_TRACE_CONFIGURE = 20,
+ /// <summary>
+ /// Physical identification of a USB based PCAN-Channel by blinking its associated LED
+ /// </summary>
+ PCAN_CHANNEL_IDENTIFYING = 21,
+ /// <summary>
+ /// Capabilities of a PCAN device (FEATURE_***)
+ /// </summary>
+ PCAN_CHANNEL_FEATURES = 22,
+ /// <summary>
+ /// Using of an existing bit rate (PCAN-View connected to a channel)
+ /// </summary>
+ PCAN_BITRATE_ADAPTING = 23,
+ /// <summary>
+ /// Configured bit rate as a Baud Rate Timing Register value
+ /// </summary>
+ PCAN_BITRATE_INFO_BTR = 24,
+ /// <summary>
+ /// Deprecated parameter. Use PCAN_BITRATE_INFO_BTR instead
+ /// </summary>
+ [Obsolete]
+ PCAN_BITRATE_INFO = PCAN_BITRATE_INFO_BTR,
+ /// <summary>
+ /// Configured bit rate as TPCANBitrateFD string
+ /// </summary>
+ PCAN_BITRATE_INFO_FD = 25,
+ /// <summary>
+ /// Configured nominal CAN speed as bits per seconds
+ /// </summary>
+ PCAN_BUSSPEED_NOMINAL = 26,
+ /// <summary>
+ /// Configured CAN FD speed as bits per seconds
+ /// </summary>
+ PCAN_BUSSPEED_FD = 27,
+ /// <summary>
+ /// DEPRECATED parameter. Use PCAN_BUSSPEED_FD instead
+ /// </summary>
+ [Obsolete]
+ PCAN_BUSSPEED_DATA = PCAN_BUSSPEED_FD,
+ /// <summary>
+ /// Remote address of a LAN channel as string in IPv4 format
+ /// </summary>
+ PCAN_IP_ADDRESS = 28,
+ /// <summary>
+ /// Status of the Virtual PCAN-Gateway Service
+ /// </summary>
+ PCAN_LAN_SERVICE_STATUS = 29,
+ /// <summary>
+ /// Status messages reception status within a PCAN-Channel
+ /// </summary>
+ PCAN_ALLOW_STATUS_FRAMES = 30,
+ /// <summary>
+ /// RTR messages reception status within a PCAN-Channel
+ /// </summary>
+ PCAN_ALLOW_RTR_FRAMES = 31,
+ /// <summary>
+ /// Error messages reception status within a PCAN-Channel
+ /// </summary>
+ PCAN_ALLOW_ERROR_FRAMES = 32,
+ /// <summary>
+ /// Delay, in microseconds, between sending frames
+ /// </summary>
+ PCAN_INTERFRAME_DELAY = 33,
+ /// <summary>
+ /// Filter over code and mask patterns for 11-Bit messages
+ /// </summary>
+ PCAN_ACCEPTANCE_FILTER_11BIT = 34,
+ /// <summary>
+ /// Filter over code and mask patterns for 29-Bit messages
+ /// </summary>
+ PCAN_ACCEPTANCE_FILTER_29BIT = 35,
+ /// <summary>
+ /// Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive
+ /// </summary>
+ PCAN_IO_DIGITAL_CONFIGURATION = 36,
+ /// <summary>
+ /// Value assigned to a 32 digital I/O pins of a PCAN-USB Chip
+ /// </summary>
+ PCAN_IO_DIGITAL_VALUE = 37,
+ /// <summary>
+ /// Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High
+ /// </summary>
+ PCAN_IO_DIGITAL_SET = 38,
+ /// <summary>
+ /// Clear multiple digital I/O pins to 0
+ /// </summary>
+ PCAN_IO_DIGITAL_CLEAR = 39,
+ /// <summary>
+ /// Get value of a single analog input pin
+ /// </summary>
+ PCAN_IO_ANALOG_VALUE = 40,
+ /// <summary>
+ /// Get the version of the firmware used by the device associated with a PCAN-Channel
+ /// </summary>
+ PCAN_FIRMWARE_VERSION = 41,
+ /// <summary>
+ /// Get the amount of PCAN channels attached to a system
+ /// </summary>
+ PCAN_ATTACHED_CHANNELS_COUNT = 42,
+ /// <summary>
+ /// Get information about PCAN channels attached to a system
+ /// </summary>
+ PCAN_ATTACHED_CHANNELS = 43,
+ /// <summary>
+ /// Echo messages reception status within a PCAN-Channel
+ /// </summary>
+ PCAN_ALLOW_ECHO_FRAMES = 44,
+ /// <summary>
+ /// Get the part number associated to a device
+ /// </summary>
+ PCAN_DEVICE_PART_NUMBER = 45,
+ /// <summary>
+ /// Activation status of hard reset processing via PCANBasic.Reset calls
+ /// </summary>
+ PCAN_HARD_RESET_STATUS = 46,
+ /// <summary>
+ /// Communication direction of a PCAN-Channel representing a PCAN-LAN interface
+ /// </summary>
+ PCAN_LAN_CHANNEL_DIRECTION = 47,
+ /// <summary>
+ /// Get the global unique device identifier (GUID) associated to a device
+ /// </summary>
+ PCAN_DEVICE_GUID = 48,
+ /// <summary>
+ /// Configured bit rate as TPCANBitrateCC string
+ /// </summary>
+ PCAN_BITRATE_INFO_CC = 49,
+ /// <summary>
+ /// Configured bit rate as TPCANBitrateXL string
+ /// </summary>
+ PCAN_BITRATE_INFO_XL = 50,
+ /// <summary>
+ /// Configured CAN XL speed as bits per seconds
+ /// </summary>
+ PCAN_BUSSPEED_XL = 51,
+ }
+
+ /// <summary>
+ /// Represents the type of a PCAN message
+ /// </summary>
+ [Flags]
+ public enum TPCANMessageType : byte
+ {
+ /// <summary>
+ /// The PCAN message is a CAN Standard Frame (11-bit identifier)
+ /// </summary>
+ PCAN_MESSAGE_STANDARD = 0x00,
+ /// <summary>
+ /// The PCAN message is a CAN Remote-Transfer-Request Frame
+ /// </summary>
+ PCAN_MESSAGE_RTR = 0x01,
+ /// <summary>
+ /// The PCAN message is a CAN Extended Frame (29-bit identifier)
+ /// </summary>
+ PCAN_MESSAGE_EXTENDED = 0x02,
+ /// <summary>
+ /// The PCAN message represents a FD frame in terms of CiA Specs
+ /// </summary>
+ PCAN_MESSAGE_FD = 0x04,
+ /// <summary>
+ /// The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate)
+ /// </summary>
+ PCAN_MESSAGE_BRS = 0x08,
+ /// <summary>
+ /// The PCAN message represents a FD error state indicator(CAN FD transmitter was error active)
+ /// </summary>
+ PCAN_MESSAGE_ESI = 0x10,
+ /// <summary>
+ /// The PCAN message represents an echo CAN Frame
+ /// </summary>
+ PCAN_MESSAGE_ECHO = 0x20,
+ /// <summary>
+ /// The PCAN message represents an error frame
+ /// </summary>
+ PCAN_MESSAGE_ERRFRAME = 0x40,
+ /// <summary>
+ /// The PCAN message represents a PCAN status message
+ /// </summary>
+ PCAN_MESSAGE_STATUS = 0x80,
+ }
+
+ /// <summary>
+ /// Represents the type of a PCAN message XL
+ /// </summary>
+ [Flags]
+ public enum TPCANMessageTypeXL : ushort
+ {
+ /// <summary>
+ /// The PCAN message is a CAN Standard Frame (11-bit identifier)
+ /// </summary>
+ PCAN_MESSAGE_STANDARD = 0x00,
+ /// <summary>
+ /// The PCAN message is a CAN Remote-Transfer-Request Frame
+ /// </summary>
+ PCAN_MESSAGE_RTR = 0x01,
+ /// <summary>
+ /// The PCAN message is a CAN Extended Frame (29-bit identifier)
+ /// </summary>
+ PCAN_MESSAGE_EXTENDED = 0x02,
+ /// <summary>
+ /// The PCAN message represents a FD frame in terms of CiA Specs
+ /// </summary>
+ PCAN_MESSAGE_FD = 0x04,
+ /// <summary>
+ /// The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate)
+ /// </summary>
+ PCAN_MESSAGE_BRS = 0x08,
+ /// <summary>
+ /// The PCAN message represents a FD error state indicator(CAN FD transmitter was error active)
+ /// </summary>
+ PCAN_MESSAGE_ESI = 0x10,
+ /// <summary>
+ /// The PCAN message represents an echo CAN Frame
+ /// </summary>
+ PCAN_MESSAGE_ECHO = 0x20,
+ /// <summary>
+ /// The PCAN message represents an error frame
+ /// </summary>
+ PCAN_MESSAGE_ERRFRAME = 0x40,
+ /// <summary>
+ /// The PCAN message represents a PCAN status message
+ /// </summary>
+ PCAN_MESSAGE_STATUS = 0x80,
+ /// <summary>
+ /// The PCAN message represents an XL frame in terms of CiA Specs
+ /// </summary>
+ PCAN_MESSAGE_XL = 0x100,
+ /// <summary>
+ /// The PCAN message represents a protocol exception from CAN core
+ /// </summary>
+ PCAN_MESSAGE_PROTOCOL_EXCEPTION = 0x200,
+ /// <summary>
+ /// The PCAN message represents an error notification from CAN core
+ /// </summary>
+ PCAN_MESSAGE_ERROR_NOTIFICATION = 0x400
+ }
+
+ /// <summary>
+ /// Represents a PCAN filter mode
+ /// </summary>
+ public enum TPCANMode : byte
+ {
+ /// <summary>
+ /// Mode is Standard (11-bit identifier)
+ /// </summary>
+ PCAN_MODE_STANDARD = TPCANMessageType.PCAN_MESSAGE_STANDARD,
+ /// <summary>
+ /// Mode is Extended (29-bit identifier)
+ /// </summary>
+ PCAN_MODE_EXTENDED = TPCANMessageType.PCAN_MESSAGE_EXTENDED,
+ }
+
+ /// <summary>
+ /// Represents a PCAN Baud Rate Timing Register value
+ /// </summary>
+ public enum TPCANBaudrate : ushort
+ {
+ /// <summary>
+ /// 1 MBit/s
+ /// </summary>
+ PCAN_BAUD_1M = 0x0014,
+ /// <summary>
+ /// 800 KBit/s
+ /// </summary>
+ PCAN_BAUD_800K = 0x0016,
+ /// <summary>
+ /// 500 kBit/s
+ /// </summary>
+ PCAN_BAUD_500K = 0x001C,
+ /// <summary>
+ /// 250 kBit/s
+ /// </summary>
+ PCAN_BAUD_250K = 0x011C,
+ /// <summary>
+ /// 125 kBit/s
+ /// </summary>
+ PCAN_BAUD_125K = 0x031C,
+ /// <summary>
+ /// 100 kBit/s
+ /// </summary>
+ PCAN_BAUD_100K = 0x432F,
+ /// <summary>
+ /// 95,238 KBit/s
+ /// </summary>
+ PCAN_BAUD_95K = 0xC34E,
+ /// <summary>
+ /// 83,333 KBit/s
+ /// </summary>
+ PCAN_BAUD_83K = 0x852B,
+ /// <summary>
+ /// 50 kBit/s
+ /// </summary>
+ PCAN_BAUD_50K = 0x472F,
+ /// <summary>
+ /// 47,619 KBit/s
+ /// </summary>
+ PCAN_BAUD_47K = 0x1414,
+ /// <summary>
+ /// 33,333 KBit/s
+ /// </summary>
+ PCAN_BAUD_33K = 0x8B2F,
+ /// <summary>
+ /// 20 kBit/s
+ /// </summary>
+ PCAN_BAUD_20K = 0x532F,
+ /// <summary>
+ /// 10 kBit/s
+ /// </summary>
+ PCAN_BAUD_10K = 0x672F,
+ /// <summary>
+ /// 5 kBit/s
+ /// </summary>
+ PCAN_BAUD_5K = 0x7F7F,
+ }
+ #endregion
+
+ #region Structures
+ /// <summary>
+ /// Represents a PCAN message
+ /// </summary>
+ public struct TPCANMsg
+ {
+ /// <summary>
+ /// 11/29-bit message identifier
+ /// </summary>
+ public uint ID;
+ /// <summary>
+ /// Type of the message
+ /// </summary>
+ [MarshalAs(UnmanagedType.U1)]
+ public TPCANMessageType MSGTYPE;
+ /// <summary>
+ /// Data Length Code of the message (0..8)
+ /// </summary>
+ public byte LEN;
+ /// <summary>
+ /// Data of the message (DATA[0]..DATA[7])
+ /// </summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
+ public byte[] DATA;
+ }
+
+ /// <summary>
+ /// Represents a timestamp of a received PCAN message.
+ /// Total Microseconds = micros + (1000UL * millis) + (0x100000000UL * 1000UL * millis_overflow)
+ /// </summary>
+ public struct TPCANTimestamp
+ {
+ /// <summary>
+ /// Base-value: milliseconds: 0.. 2^32-1
+ /// </summary>
+ public uint millis;
+ /// <summary>
+ /// Roll-arounds of millis
+ /// </summary>
+ public ushort millis_overflow;
+ /// <summary>
+ /// Microseconds: 0..999
+ /// </summary>
+ public ushort micros;
+ }
+
+ /// <summary>
+ /// Represents a PCAN message from a FD capable hardware
+ /// </summary>
+ public struct TPCANMsgFD
+ {
+ /// <summary>
+ /// 11/29-bit message identifier
+ /// </summary>
+ public uint ID;
+ /// <summary>
+ /// Type of the message
+ /// </summary>
+ [MarshalAs(UnmanagedType.U1)]
+ public TPCANMessageType MSGTYPE;
+ /// <summary>
+ /// Data Length Code of the message (0..15)
+ /// </summary>
+ public byte DLC;
+ /// <summary>
+ /// Data of the message (DATA[0]..DATA[63])
+ /// </summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
+ public byte[] DATA;
+ }
+
+ /// <summary>
+ /// Represents a PCAN message from an XL capable hardware
+ /// REMARKS:
+ /// CAN-CC/CAN-FD: only the fields PID, MSGTYPE, DLC,and DATA are used.
+ /// Additonal CAN-XL related fields are ignored
+ /// </summary>
+ public struct TPCANMsgXL
+ {
+ /// <summary>
+ /// CAN-XL: Priority ID (physical layer) (0..0x7FF)
+ /// CAN-CC/CAN-FD: 11/29-bit message identifier
+ /// </summary>
+ public uint PID;
+ /// <summary>
+ /// Virtual CAN network ID
+ /// </summary>
+ public byte VCID;
+ /// <summary>
+ /// Type of the message
+ /// </summary>
+ [MarshalAs(UnmanagedType.U2)]
+ public TPCANMessageTypeXL MSGTYPE;
+ /// <summary>
+ /// Data Length Code of the message (0..2047)
+ /// </summary>
+ public ushort DLC;
+ /// <summary>
+ /// Service Data unit(SDU) protocol Type
+ /// </summary>
+ public byte SDT;
+ /// <summary>
+ /// Acceptance Field, SDU - specific high - layer ID
+ /// </summary>
+ public uint AF;
+ /// <summary>
+ /// Remote Request Substitution flag (0..1)
+ /// </summary>
+ [MarshalAs(UnmanagedType.U1)]
+ public bool RRS;
+ /// <summary>
+ /// Simple Extended Content flag (0..1)
+ /// </summary>
+ [MarshalAs(UnmanagedType.U1)]
+ public bool SEC;
+ /// <summary>
+ /// Data of the message (DATA[0]..DATA[2047])
+ /// </summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)]
+ public byte[] DATA;
+ }
+
+ /// <summary>
+ /// Describes an available PCAN channel
+ /// </summary>
+ public struct TPCANChannelInformation
+ {
+ /// <summary>
+ /// PCAN channel handle
+ /// </summary>
+ [MarshalAs(UnmanagedType.U2)]
+ public TPCANHandle channel_handle;
+ /// <summary>
+ /// Kind of PCAN device
+ /// </summary>
+ [MarshalAs(UnmanagedType.U1)]
+ public TPCANDevice device_type;
+ /// <summary>
+ /// CAN-Controller number
+ /// </summary>
+ public byte controller_number;
+ /// <summary>
+ /// Device capabilities flag (see FEATURE_*)
+ /// </summary>
+ public uint device_features;
+ /// <summary>
+ /// Device name
+ /// </summary>
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = PCANBasic.MAX_LENGTH_HARDWARE_NAME)]
+ public string device_name;
+ /// <summary>
+ /// Device number
+ /// </summary>
+ public uint device_id;
+ /// <summary>
+ /// Availability status of a PCAN-Channel
+ /// </summary>
+ public uint channel_condition;
+ }
+ #endregion
+
+ #region PCANBasic class
+ /// <summary>
+ /// PCAN-Basic API class implementation
+ /// </summary>
+ public static class PCANBasic
+ {
+ #region PCAN-BUS Handles Definition
+ /// <summary>
+ /// Undefined/default value for a PCAN bus
+ /// </summary>
+ public const TPCANHandle PCAN_NONEBUS = 0x00;
+
+ /// <summary>
+ /// PCAN-PCI interface, channel 1
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS1 = 0x41;
+ /// <summary>
+ /// PCAN-PCI interface, channel 2
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS2 = 0x42;
+ /// <summary>
+ /// PCAN-PCI interface, channel 3
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS3 = 0x43;
+ /// <summary>
+ /// PCAN-PCI interface, channel 4
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS4 = 0x44;
+ /// <summary>
+ /// PCAN-PCI interface, channel 5
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS5 = 0x45;
+ /// <summary>
+ /// PCAN-PCI interface, channel 6
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS6 = 0x46;
+ /// <summary>
+ /// PCAN-PCI interface, channel 7
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS7 = 0x47;
+ /// <summary>
+ /// PCAN-PCI interface, channel 8
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS8 = 0x48;
+ /// <summary>
+ /// PCAN-PCI interface, channel 9
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS9 = 0x409;
+ /// <summary>
+ /// PCAN-PCI interface, channel 10
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS10 = 0x40A;
+ /// <summary>
+ /// PCAN-PCI interface, channel 11
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS11 = 0x40B;
+ /// <summary>
+ /// PCAN-PCI interface, channel 12
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS12 = 0x40C;
+ /// <summary>
+ /// PCAN-PCI interface, channel 13
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS13 = 0x40D;
+ /// <summary>
+ /// PCAN-PCI interface, channel 14
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS14 = 0x40E;
+ /// <summary>
+ /// PCAN-PCI interface, channel 15
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS15 = 0x40F;
+ /// <summary>
+ /// PCAN-PCI interface, channel 16
+ /// </summary>
+ public const TPCANHandle PCAN_PCIBUS16 = 0x410;
+
+ /// <summary>
+ /// PCAN-USB interface, channel 1
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS1 = 0x51;
+ /// <summary>
+ /// PCAN-USB interface, channel 2
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS2 = 0x52;
+ /// <summary>
+ /// PCAN-USB interface, channel 3
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS3 = 0x53;
+ /// <summary>
+ /// PCAN-USB interface, channel 4
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS4 = 0x54;
+ /// <summary>
+ /// PCAN-USB interface, channel 5
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS5 = 0x55;
+ /// <summary>
+ /// PCAN-USB interface, channel 6
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS6 = 0x56;
+ /// <summary>
+ /// PCAN-USB interface, channel 7
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS7 = 0x57;
+ /// <summary>
+ /// PCAN-USB interface, channel 8
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS8 = 0x58;
+ /// <summary>
+ /// PCAN-USB interface, channel 9
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS9 = 0x509;
+ /// <summary>
+ /// PCAN-USB interface, channel 10
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS10 = 0x50A;
+ /// <summary>
+ /// PCAN-USB interface, channel 11
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS11 = 0x50B;
+ /// <summary>
+ /// PCAN-USB interface, channel 12
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS12 = 0x50C;
+ /// <summary>
+ /// PCAN-USB interface, channel 13
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS13 = 0x50D;
+ /// <summary>
+ /// PCAN-USB interface, channel 14
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS14 = 0x50E;
+ /// <summary>
+ /// PCAN-USB interface, channel 15
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS15 = 0x50F;
+ /// <summary>
+ /// PCAN-USB interface, channel 16
+ /// </summary>
+ public const TPCANHandle PCAN_USBBUS16 = 0x510;
+
+ /// <summary>
+ /// PCAN-LAN interface, channel 1
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS1 = 0x801;
+ /// <summary>
+ /// PCAN-LAN interface, channel 2
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS2 = 0x802;
+ /// <summary>
+ /// PCAN-LAN interface, channel 3
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS3 = 0x803;
+ /// <summary>
+ /// PCAN-LAN interface, channel 4
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS4 = 0x804;
+ /// <summary>
+ /// PCAN-LAN interface, channel 5
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS5 = 0x805;
+ /// <summary>
+ /// PCAN-LAN interface, channel 6
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS6 = 0x806;
+ /// <summary>
+ /// PCAN-LAN interface, channel 7
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS7 = 0x807;
+ /// <summary>
+ /// PCAN-LAN interface, channel 8
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS8 = 0x808;
+ /// <summary>
+ /// PCAN-LAN interface, channel 9
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS9 = 0x809;
+ /// <summary>
+ /// PCAN-LAN interface, channel 10
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS10 = 0x80A;
+ /// <summary>
+ /// PCAN-LAN interface, channel 11
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS11 = 0x80B;
+ /// <summary>
+ /// PCAN-LAN interface, channel 12
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS12 = 0x80C;
+ /// <summary>
+ /// PCAN-LAN interface, channel 13
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS13 = 0x80D;
+ /// <summary>
+ /// PCAN-LAN interface, channel 14
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS14 = 0x80E;
+ /// <summary>
+ /// PCAN-LAN interface, channel 15
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS15 = 0x80F;
+ /// <summary>
+ /// PCAN-LAN interface, channel 16
+ /// </summary>
+ public const TPCANHandle PCAN_LANBUS16 = 0x810;
+ #endregion
+
+ #region Bit rate frequency parameters
+ /// <summary>
+ /// Clock frequency in Herz (160000000, 80000000, 60000000, 40000000, 30000000, 24000000, 20000000)
+ /// </summary>
+ public const string PCAN_BR_CLOCK = "f_clock";
+ /// <summary>
+ /// Clock frequency in Megaherz (160, 80, 60, 40, 30, 24, 20)
+ /// </summary>
+ public const string PCAN_BR_CLOCK_MHZ = "f_clock_mhz";
+ #endregion
+
+ #region FD Bit rate specific parameters
+ /// <summary>
+ /// Clock prescaler for nominal time quantum
+ /// </summary>
+ public const string PCAN_BR_NOM_BRP = "nom_brp";
+ /// <summary>
+ /// TSEG1 segment for nominal bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_NOM_TSEG1 = "nom_tseg1";
+ /// <summary>
+ /// TSEG2 segment for nominal bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_NOM_TSEG2 = "nom_tseg2";
+ /// <summary>
+ /// Synchronization Jump Width for nominal bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_NOM_SJW = "nom_sjw";
+ /// <summary>
+ /// Sample point for nominal bit rate
+ /// </summary>
+ public const string PCAN_BR_NOM_SAMPLE = "nom_sam";
+ /// <summary>
+ /// Clock prescaler for highspeed data time quantum
+ /// </summary>
+ public const string PCAN_BR_DATA_BRP = "data_brp";
+ /// <summary>
+ /// TSEG1 segment for fast data bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_DATA_TSEG1 = "data_tseg1";
+ /// <summary>
+ /// TSEG2 segment for fast data bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_DATA_TSEG2 = "data_tseg2";
+ /// <summary>
+ /// Synchronization Jump Width for highspeed data bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_DATA_SJW = "data_sjw";
+ /// <summary>
+ /// DEPRECATED: Secondary sample point delay for highspeed data bit rate in cycles
+ /// <remarks>Use <see cref="PCANBasic.PCAN_BR_DATA_SSP_OFFSET"/> instead</remarks>
+ /// </summary>
+ public const string PCAN_BR_DATA_SAMPLE = "data_ssp_offset";
+ /// <summary>
+ /// Secondary sample point delay for highspeed data bit rate in cycles
+ /// </summary>
+ public const string PCAN_BR_DATA_SSP_OFFSET = "data_ssp_offset";
+ #endregion
+
+ #region XL Bit rate specific parameters
+ /// <summary>
+ /// Clock prescaler for nominal, CAN FD and CAN XL bit rates
+ /// </summary>
+ public const string PCAN_BR_BRP = "brp";
+ /// <summary>
+ /// Clock prescaler for fast data time quantum
+ /// </summary>
+ public const string PCAN_BR_FD_TSEG1 = "fd_tseg1";
+ /// <summary>
+ /// Clock prescaler for fast data time quantum
+ /// </summary>
+ public const string PCAN_BR_FD_TSEG2 = "fd_tseg2";
+ /// <summary>
+ /// Synchronization Jump Width for fast data bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_FD_SJW = "fd_sjw";
+ /// <summary>
+ /// Secondary sample point delay for fast data bit rate in cycles
+ /// </summary>
+ public const string PCAN_BR_FD_SSP_OFFSET = "fd_ssp_offset";
+ /// <summary>
+ /// Clock prescaler for XL time quantum
+ /// </summary>
+ public const string PCAN_BR_XL_TSEG1 = "xl_tseg1";
+ /// <summary>
+ /// Clock prescaler for XL time quantum
+ /// </summary>
+ public const string PCAN_BR_XL_TSEG2 = "xl_tseg2";
+ /// <summary>
+ /// Synchronization Jump Width for XL bit rate in time quanta
+ /// </summary>
+ public const string PCAN_BR_XL_SJW = "xl_sjw";
+ /// <summary>
+ /// Secondary sample point delay for XL bit rate in cycles
+ /// </summary>
+ public const string PCAN_BR_XL_SSP_OFFSET = "xl_ssp_offset";
+ /// <summary>
+ /// CAN XL PWM Offset in mtq ticks == f_cancore cycles
+ /// </summary>
+ public const string PCAN_BR_XL_PWM_OFFSET = "xl_pwm_offset";
+ /// <summary>
+ /// CAN XL PWM Short phase in mtq ticks == f_cancore cycles
+ /// </summary>
+ public const string PCAN_BR_XL_PWM_SHORT = "xl_pwm_short";
+ /// <summary>
+ /// CAN XL PWM Long phase in mtq ticks == f_cancore cycles
+ /// </summary>
+ public const string PCAN_BR_XL_PWM_LONG = "xl_pwm_long";
+ /// <summary>
+ /// 1 = CAN XL Data Phase uses 'fast TX' or 'fast RX' with PWM encoding
+ /// 0 = CAN XL Data Phase uses no PWM encoding (recessive/dominant only, like CAN FD)
+ /// </summary>
+ public const string PCAN_BR_XL_TRANSCEIVER_MODE_SWITCH = "xl_transceiver_mode_switch";
+ /// <summary>
+ /// 1 = Error Signaling with Error Frame in case of bus errors
+ /// 0 = No Error Signaling
+ /// </summary>
+ public const string PCAN_BR_XL_ERROR_SIGNALING = "xl_error_signaling";
+ #endregion
+
+ #region Parameter values definition
+ /// <summary>
+ /// The PCAN parameter is not set (inactive)
+ /// </summary>
+ public const int PCAN_PARAMETER_OFF = 0;
+ /// <summary>
+ /// The PCAN parameter is set (active)
+ /// </summary>
+ public const int PCAN_PARAMETER_ON = 1;
+ /// <summary>
+ /// The PCAN filter is closed. No messages will be received
+ /// </summary>
+ public const int PCAN_FILTER_CLOSE = 0;
+ /// <summary>
+ /// The PCAN filter is fully opened. All messages will be received
+ /// </summary>
+ public const int PCAN_FILTER_OPEN = 1;
+ /// <summary>
+ /// The PCAN filter is custom configured. Only registered
+ /// messages will be received
+ /// </summary>
+ public const int PCAN_FILTER_CUSTOM = 2;
+ /// <summary>
+ /// The PCAN-Channel handle is illegal, or its associated hardware is not available
+ /// </summary>
+ public const int PCAN_CHANNEL_UNAVAILABLE = 0;
+ /// <summary>
+ /// The PCAN-Channel handle is available to be connected (PnP Hardware: it means furthermore that the hardware is plugged-in)
+ /// </summary>
+ public const int PCAN_CHANNEL_AVAILABLE = 1;
+ /// <summary>
+ /// The PCAN-Channel handle is valid, and is already being used
+ /// </summary>
+ public const int PCAN_CHANNEL_OCCUPIED = 2;
+ /// <summary>
+ /// The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect
+ /// </summary>
+ public const int PCAN_CHANNEL_PCANVIEW = (PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED);
+
+ /// <summary>
+ /// Logs system exceptions / errors
+ /// </summary>
+ public const int LOG_FUNCTION_DEFAULT = 0x00;
+ /// <summary>
+ /// Logs the entries to the PCAN-Basic API functions
+ /// </summary>
+ public const int LOG_FUNCTION_ENTRY = 0x01;
+ /// <summary>
+ /// Logs the parameters passed to the PCAN-Basic API functions
+ /// </summary>
+ public const int LOG_FUNCTION_PARAMETERS = 0x02;
+ /// <summary>
+ /// Logs the exits from the PCAN-Basic API functions
+ /// </summary>
+ public const int LOG_FUNCTION_LEAVE = 0x04;
+ /// <summary>
+ /// Logs the CAN messages passed to the CAN_Write function
+ /// </summary>
+ public const int LOG_FUNCTION_WRITE = 0x08;
+ /// <summary>
+ /// Logs the CAN messages received within the CAN_Read function
+ /// </summary>
+ public const int LOG_FUNCTION_READ = 0x10;
+ /// <summary>
+ /// Logs all possible information within the PCAN-Basic API functions
+ /// </summary>
+ public const int LOG_FUNCTION_ALL = 0xFFFF;
+
+ /// <summary>
+ /// A single file is written until it size reaches PAN_TRACE_SIZE
+ /// </summary>
+ public const int TRACE_FILE_SINGLE = 0x00;
+ /// <summary>
+ /// Traced data is distributed in several files with size PAN_TRACE_SIZE
+ /// </summary>
+ public const int TRACE_FILE_SEGMENTED = 0x01;
+ /// <summary>
+ /// Includes the date into the name of the trace file
+ /// </summary>
+ public const int TRACE_FILE_DATE = 0x02;
+ /// <summary>
+ /// Includes the start time into the name of the trace file
+ /// </summary>
+ public const int TRACE_FILE_TIME = 0x04;
+ /// <summary>
+ /// Causes the overwriting of available traces (same name)
+ /// </summary>
+ public const int TRACE_FILE_OVERWRITE = 0x80;
+ /// <summary>
+ /// Causes using the data length column ('l') instead of the DLC column ('L') in the trace file
+ /// </summary>
+ public const int TRACE_FILE_DATA_LENGTH = 0x100;
+
+ /// <summary>
+ /// Device supports the subsequent development of the classic CAN bus (CAN FD)
+ /// </summary>
+ public const int FEATURE_FD_CAPABLE = 0x01;
+ /// <summary>
+ /// Device supports a delay between sending frames (FPGA based USB devices)
+ /// </summary>
+ public const int FEATURE_DELAY_CAPABLE = 0x02;
+ /// <summary>
+ /// Device supports I/O functionality for electronic circuits (USB-Chip devices)
+ /// </summary>
+ public const int FEATURE_IO_CAPABLE = 0x04;
+ /// <summary>
+ /// Device supports the subsequent development of the classic CAN bus (CAN XL)
+ /// </summary>
+ public const int FEATURE_XL_CAPABLE = 0x08;
+
+ /// <summary>
+ /// The service is not running
+ /// </summary>
+ public const int SERVICE_STATUS_STOPPED = 0x01;
+ /// <summary>
+ /// The service is running
+ /// </summary>
+ public const int SERVICE_STATUS_RUNNING = 0x04;
+
+ /// <summary>
+ /// The PCAN-Channel is limited to incoming communication only
+ /// </summary>
+ public const int LAN_DIRECTION_READ = 0x01;
+ /// <summary>
+ /// The PCAN-Channel is limited to outgoing communication only
+ /// </summary>
+ public const int LAN_DIRECTION_WRITE = 0x02;
+ /// <summary>
+ /// The PCAN-Channel communication is bidirectional
+ /// </summary>
+ public const int LAN_DIRECTION_READ_WRITE = (LAN_DIRECTION_READ | LAN_DIRECTION_WRITE);
+ #endregion
+
+ #region Lookup Parameters
+ /// <summary>
+ /// Lookup channel by Device type (see PCAN devices e.g. PCAN_USB)
+ /// </summary>
+ public const string LOOKUP_DEVICE_TYPE = "devicetype";
+ /// <summary>
+ /// Lookup channel by device id
+ /// </summary>
+ public const string LOOKUP_DEVICE_ID = "deviceid";
+ /// <summary>
+ /// Lookup channel by CAN controller 0-based index
+ /// </summary>
+ public const string LOOKUP_CONTROLLER_NUMBER = "controllernumber";
+ /// <summary>
+ /// Lookup channel by IP address (LAN channels only)
+ /// </summary>
+ public const string LOOKUP_IP_ADDRESS = "ipaddress";
+ /// <summary>
+ /// Lookup channel by device unique identifier (USB channels only)
+ /// </summary>
+ public const string LOOKUP_DEVICE_GUID = "deviceguid";
+ #endregion
+
+ #region Other Constants
+ /// <summary>
+ /// Maximum length of the name of a device: 32 characters + terminator
+ /// </summary>
+ public const int MAX_LENGTH_HARDWARE_NAME = 33;
+ /// <summary>
+ /// Maximum length of a version string: 255 characters + terminator
+ /// </summary>
+ public const int MAX_LENGTH_VERSION_STRING = 256;
+ /// <summary>
+ /// Maximum amount of data bytes of a CAN-XL message
+ /// </summary>
+ public const int MAX_LENGTH_DATA_XL = 2048;
+ /// <summary>
+ /// Maximum value for a standard CAN ID of a CAN CC/FD message
+ /// </summary>
+ public const int MAX_VALUE_STANDARD_ID = 0x7FF;
+ /// <summary>
+ /// Maximum value for an extended CAN ID of a CAN CC/FD message
+ /// </summary>
+ public const int MAX_VALUE_EXTENDED_ID = 0x1FFFFFFF;
+ /// <summary>
+ /// Maximum value for a Priority ID of a CAN XL message
+ /// </summary>
+ public const int MAX_VALUE_PRIORITY_ID = 0x7FF;
+ #endregion
+
+ #region PCANBasic API Implementation
+ /// <summary>
+ /// Initializes a PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Btr0Btr1">The speed for the communication (BTR0BTR1 code)</param>
+ /// <param name="deprecated1">Deprecated. Parameter is ignored</param>
+ /// <param name="deprecated2">Deprecated. Parameter is ignored</param>
+ /// <param name="deprecated3">Deprecated. Parameter is ignored</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_Initialize")]
+ private static extern TPCANStatus Initialize(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANBaudrate Btr0Btr1,
+ Byte deprecated1,
+ UInt32 deprecated2,
+ UInt16 deprecated3);
+
+ /// <summary>
+ /// Initializes a PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Btr0Btr1">The speed for the communication (BTR0BTR1 code)</param>
+ /// <returns>A TPCANStatus error code</returns>
+ public static TPCANStatus Initialize(
+ TPCANHandle Channel,
+ TPCANBaudrate Btr0Btr1)
+ {
+ return Initialize(Channel, Btr0Btr1, 0, 0, 0);
+ }
+
+ /// <summary>
+ /// Initializes a FD capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a FD capable PCAN Channel</param>
+ /// <param name="BitrateFD">The speed for the communication (FD bit rate string)</param>
+ /// <remarks> See PCAN_BR_* values
+ /// Bit rate string must follow the following construction rules:
+ /// * parameters and values must be separated by '='
+ /// * Couples of Parameter/value must be separated by ','
+ /// * Following Parameter must be filled out: f_clock, data_brp, data_sjw, data_tseg1, data_tseg2,
+ /// nom_brp, nom_sjw, nom_tseg1, nom_tseg2.
+ /// * Following Parameters are optional (not used yet): data_ssp_offset, nom_sam</remarks>
+ /// <example>f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1</example>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_InitializeFD")]
+ public static extern TPCANStatus InitializeFD(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ TPCANBitrateFD BitrateFD);
+
+ /// <summary>
+ /// Initializes an XL capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of an XL capable PCAN Channel</param>
+ /// <param name="BitrateXL">The speed for the communication (XL bit rate string)</param>
+ /// <remarks>See PCAN_BR_* values
+ /// * Parameter and values must be separated by '='
+ /// * Couples of Parameter/value must be separated by ','
+ /// * Following Parameter must be filled out: f_clock, brp, nom_brp, nom_sjw, nom_tseg1, nom_tseg2.
+ /// If xl_transceiver_mode_switch is active, also the parameters xl_sjw, xl_tseg1, and xl_tseg2, must be present.
+ /// If error_signaling is active, also the parameters fd_sjw, fd_tseg1, and fd_tseg2, must be present.
+ /// * Following Parameters are optional: fd_ssp_offset, xl_ssp_offset, xl_transceiver_mode_switch, error_signaling,
+ /// xl_pwm_offset, xl_pwm_short, and xl_pwm_long</remarks>
+ /// <example>f_clock=160000000,brp=1,nom_tseg1=255,nom_tseg2=64,nom_sjw=64,fd_tseg1=63,fd_tseg2=16,fd_sjw=16,fd_ssp_offset=0,xl_tseg1=10,
+ /// xl_tseg2=9,xl_sjw=9,xl_ssp_offset=10,xl_error_signaling=1,xl_transceiver_mode_switch=0</example>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_InitializeXL")]
+ public static extern TPCANStatus InitializeXL(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ TPCANBitrateXL BitrateXL);
+
+ /// <summary>
+ /// Uninitializes one or all PCAN Channels initialized by CAN_Initialize
+ /// </summary>
+ /// <remarks>Giving the TPCANHandle value "PCAN_NONEBUS",
+ /// uninitialize all initialized channels</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_Uninitialize")]
+ public static extern TPCANStatus Uninitialize(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel);
+
+ /// <summary>
+ /// Resets the receive and transmit queues of the PCAN Channel
+ /// </summary>
+ /// <remarks>A reset of the CAN controller is not performed</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_Reset")]
+ public static extern TPCANStatus Reset(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel);
+
+ /// <summary>
+ /// Gets the current status of a PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_GetStatus")]
+ public static extern TPCANStatus GetStatus(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel);
+
+ /// <summary>
+ /// Reads a CAN message from the receive queue of a PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsg structure buffer to store the CAN message</param>
+ /// <param name="TimestampBuffer">A TPCANTimestamp structure buffer to get
+ /// the reception time of the message</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_Read")]
+ public static extern TPCANStatus Read(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ out TPCANMsg MessageBuffer,
+ out TPCANTimestamp TimestampBuffer);
+
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_Read")]
+ private static extern TPCANStatus Read(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ out TPCANMsg MessageBuffer,
+ IntPtr bufferPointer);
+
+ /// <summary>
+ /// Reads a CAN message from the receive queue of a PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsg structure buffer to store the CAN message</param>
+ /// <returns>A TPCANStatus error code</returns>
+ public static TPCANStatus Read(
+ TPCANHandle Channel,
+ out TPCANMsg MessageBuffer)
+ {
+ return Read(Channel, out MessageBuffer, IntPtr.Zero);
+ }
+
+ /// <summary>
+ /// Reads a CAN message from the receive queue of a FD capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a FD capable PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsgFD structure buffer to store the CAN message</param>
+ /// <param name="TimestampBuffer">A TPCANTimestampFD buffer to get the
+ /// reception time of the message</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_ReadFD")]
+ public static extern TPCANStatus ReadFD(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ out TPCANMsgFD MessageBuffer,
+ out TPCANTimestampFD TimestampBuffer);
+
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_ReadFD")]
+ private static extern TPCANStatus ReadFD(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ out TPCANMsgFD MessageBuffer,
+ IntPtr TimestampBuffer);
+
+ /// <summary>
+ /// Reads a CAN message from the receive queue of a FD capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a FD capable PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsgFD structure buffer to store the CAN message</param>
+ /// <returns>A TPCANStatus error code</returns>
+ public static TPCANStatus ReadFD(
+ TPCANHandle Channel,
+ out TPCANMsgFD MessageBuffer)
+ {
+ return ReadFD(Channel, out MessageBuffer, IntPtr.Zero);
+ }
+
+ /// <summary>
+ /// Reads a CAN message from the receive queue of an XL capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of an XL capable PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsgXL structure buffer to store the CAN message</param>
+ /// <param name="TimestampBuffer">A TPCANTimestampXL buffer to get
+ /// the reception time of the message. If this value is not desired, this parameter
+ /// should be passed as NULL</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_ReadXL")]
+ public static extern TPCANStatus ReadXL(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ out TPCANMsgXL MessageBuffer,
+ out TPCANTimestampXL TimestampBuffer);
+
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_ReadXL")]
+ private static extern TPCANStatus ReadXL(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ out TPCANMsgXL MessageBuffer,
+ IntPtr TimestampBuffer);
+
+ /// <summary>
+ /// Reads a CAN message from the receive queue of an XL capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of an XL capable PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsgXL structure buffer to store the CAN message</param>
+ /// <returns>A TPCANStatus error code</returns>
+ public static TPCANStatus ReadXL(
+ TPCANHandle Channel,
+ out TPCANMsgXL MessageBuffer)
+ {
+ return ReadXL(Channel, out MessageBuffer, IntPtr.Zero);
+ }
+
+ /// <summary>
+ /// Transmits a CAN message
+ /// </summary>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsg buffer with the message to be sent</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_Write")]
+ public static extern TPCANStatus Write(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ ref TPCANMsg MessageBuffer);
+
+ /// <summary>
+ /// Transmits a CAN message over a FD capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of a FD capable PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsgFD buffer with the message to be sent</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_WriteFD")]
+ public static extern TPCANStatus WriteFD(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ ref TPCANMsgFD MessageBuffer);
+
+ /// <summary>
+ /// Transmits a CAN message over an XL capable PCAN Channel
+ /// </summary>
+ /// <param name="Channel">The handle of an XL capable PCAN Channel</param>
+ /// <param name="MessageBuffer">A TPCANMsgXL buffer with the message to be sent</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_WriteXL")]
+ public static extern TPCANStatus WriteXL(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ ref TPCANMsgXL MessageBuffer);
+
+ /// <summary>
+ /// Configures the reception filter
+ /// </summary>
+ /// <remarks>The message filter will be expanded with every call to
+ /// this function. If it is desired to reset the filter, please use
+ /// the 'SetValue' function</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="FromID">The lowest CAN ID to be received</param>
+ /// <param name="ToID">The highest CAN ID to be received</param>
+ /// <param name="Mode">Message type, Standard (11-bit identifier) or
+ /// Extended (29-bit identifier)</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_FilterMessages")]
+ public static extern TPCANStatus FilterMessages(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ UInt32 FromID,
+ UInt32 ToID,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANMode Mode);
+
+ /// <summary>
+ /// Retrieves a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to get</param>
+ /// <param name="StringBuffer">Buffer for the parameter value</param>
+ /// <param name="BufferLength">Size in bytes of the buffer</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_GetValue")]
+ public static extern TPCANStatus GetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ StringBuilder StringBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Retrieves a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to get</param>
+ /// <param name="NumericBuffer">Buffer for the parameter value</param>
+ /// <param name="BufferLength">Size in bytes of the buffer</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_GetValue")]
+ public static extern TPCANStatus GetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ out UInt32 NumericBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Retrieves a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to get</param>
+ /// <param name="NumericBuffer">Buffer for the parameter value</param>
+ /// <param name="BufferLength">Size in bytes of the buffer</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_GetValue")]
+ public static extern TPCANStatus GetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ out UInt64 NumericBuffer,
+ UInt32 BufferLength);
+
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_GetValue")]
+ private static extern TPCANStatus GetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)]
+ [In, Out]TPCANChannelInformation[] ChannelsBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Retrieves a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to get</param>
+ /// <param name="ChannelsBuffer">Buffer for the parameter value</param>
+ /// <returns>A TPCANStatus error code</returns>
+ public static TPCANStatus GetValue(
+ TPCANHandle Channel,
+ TPCANParameter Parameter,
+ TPCANChannelInformation[] ChannelsBuffer)
+ {
+ if (ChannelsBuffer == null)
+ return TPCANStatus.PCAN_ERROR_ILLPARAMVAL;
+ return GetValue(Channel, Parameter, ChannelsBuffer, (uint)(ChannelsBuffer.Length * Marshal.SizeOf(typeof(TPCANChannelInformation))));
+ }
+
+ /// <summary>
+ /// Configures or sets a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to set</param>
+ /// <param name="NumericBuffer">Buffer with the value to be set</param>
+ /// <param name="BufferLength">Size in bytes of the buffer</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_SetValue")]
+ public static extern TPCANStatus SetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ ref UInt32 NumericBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Configures or sets a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to set</param>
+ /// <param name="NumericBuffer">Buffer with the value to be set</param>
+ /// <param name="BufferLength">Size in bytes of the buffer</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_SetValue")]
+ public static extern TPCANStatus SetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ ref UInt64 NumericBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Configures or sets a PCAN Channel value
+ /// </summary>
+ /// <remarks>Parameters can be present or not according with the kind
+ /// of Hardware (PCAN Channel) being used. If a parameter is not available,
+ /// a PCAN_ERROR_ILLPARAMTYPE error will be returned</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter"></param>
+ /// <param name="StringBuffer">Buffer with the value to be set</param>
+ /// <param name="BufferLength">Size in bytes of the buffer</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_SetValue")]
+ public static extern TPCANStatus SetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ [MarshalAs(UnmanagedType.LPStr,SizeParamIndex=3)]
+ string StringBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Configures or sets a PCAN Channel value using a native handle
+ /// </summary>
+ /// <remarks>此重载专用于传递句柄类参数(如 PCAN_RECEIVE_EVENT 的 Windows 事件句柄)。
+ /// 句柄在 64 位进程下为 8 字节,不能使用 UInt32 重载,否则会被截断。</remarks>
+ /// <param name="Channel">The handle of a PCAN Channel</param>
+ /// <param name="Parameter">The TPCANParameter parameter to set</param>
+ /// <param name="NumericBuffer">Handle (Windows event) to be set</param>
+ /// <param name="BufferLength">Size in bytes of the handle</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_SetValue")]
+ public static extern TPCANStatus SetValue(
+ [MarshalAs(UnmanagedType.U2)]
+ TPCANHandle Channel,
+ [MarshalAs(UnmanagedType.U1)]
+ TPCANParameter Parameter,
+ IntPtr NumericBuffer,
+ UInt32 BufferLength);
+
+ /// <summary>
+ /// Returns a descriptive text of a given TPCANStatus error
+ /// code, in any desired language
+ /// </summary>
+ /// <remarks>The current languages available for translation are:
+ /// Neutral (0x00), German (0x07), English (0x09), Spanish (0x0A),
+ /// Italian (0x10) and French (0x0C)</remarks>
+ /// <param name="Error">A TPCANStatus error code</param>
+ /// <param name="Language">Indicates a 'Primary language ID'</param>
+ /// <param name="StringBuffer">Buffer for the text (must be at least 256 in length)</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_GetErrorText")]
+ public static extern TPCANStatus GetErrorText(
+ [MarshalAs(UnmanagedType.U4)]
+ TPCANStatus Error,
+ UInt16 Language,
+ StringBuilder StringBuffer);
+
+
+ /// <summary>
+ /// Finds a PCAN-Basic channel that matches with the given parameters
+ /// </summary>
+ /// <param name="Parameters">A comma separated string contained pairs of
+ /// parameter-name/value to be matched within a PCAN-Basic channel</param>
+ /// <param name="FoundChannel">Buffer for returning the PCAN-Basic channel,
+ /// when found</param>
+ /// <returns>A TPCANStatus error code</returns>
+ [DllImport("PCANBasic.dll", EntryPoint = "CAN_LookUpChannel")]
+ public static extern TPCANStatus LookUpChannel(
+ String Parameters,
+ out TPCANHandle FoundChannel);
+ #endregion
+ }
+ #endregion
+}
diff --git a/A1Lib/CAN/PCAN/PCANBasic.zip b/A1Lib/CAN/PCAN/PCANBasic.zip
new file mode 100644
index 0000000..52ad50a
Binary files /dev/null and b/A1Lib/CAN/PCAN/PCANBasic.zip differ
diff --git a/A1Lib/CAN/PCAN/PeakCanPort.cs b/A1Lib/CAN/PCAN/PeakCanPort.cs
new file mode 100644
index 0000000..788e9bd
--- /dev/null
+++ b/A1Lib/CAN/PCAN/PeakCanPort.cs
@@ -0,0 +1,450 @@
+using ALib;
+using ALib.Can;
+using ALib.CAN;
+using NewLife.Log;
+using Peak.Can.Basic;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+
+namespace ALib.PEAK_CAN
+{
+ public class PeakCanPort : ICanPort
+ {
+ /// <summary>构造,指定枚举名称</summary>
+ /// <param name="name">枚举时候的名字</param>
+ public PeakCanPort(String name) : base(name) { }
+
+ #region DLL管理
+
+ private static readonly string DllFileName = "PCANBasic.dll";
+
+ /// <summary>
+ /// 静态构造函数,确保DLL可用
+ /// </summary>
+ static PeakCanPort()
+ {
+ // 使用通用DLL资源管理器确保DLL可用(资源名 ALib.PCAN.PCANBasic.dll,见 A1Lib.csproj)
+ bool success = DllResourceManager.EnsureDllAvailable("ALib.PCAN", DllFileName);
+ if (!success)
+ {
+ XTrace.WriteLine($"PCANBasic.dll 加载失败");
+ throw new InvalidOperationException("PCANBasic.dll 加载失败");
+ }
+ }
+
+ #endregion
+
+ #region 硬件相关
+
+ /// <summary>PCAN 通道句柄,默认 USB 通道1(可按需改为 PCI/局域网通道)</summary>
+ public UInt16 Channel = PCANBasic.PCAN_USBBUS1;
+
+ /// <summary>打开标记</summary>
+ public bool Opened = false;
+
+ #endregion
+
+ #region 设备枚举
+
+ /// <summary>枚举当前计算机上所有可用的 PCAN 通道(未打开,仅列出)</summary>
+ /// <returns>key = "PCAN-{设备名}-{控制器号}",value = 可配置的 ICanPort 实例</returns>
+ public new static Dictionary<String, ICanPort> EnumDevices()
+ {
+ var dict = new Dictionary<String, ICanPort>();
+ try
+ {
+ TPCANChannelInformation[] buffer = new TPCANChannelInformation[16];
+ var st = PCANBasic.GetValue(PCANBasic.PCAN_NONEBUS, TPCANParameter.PCAN_ATTACHED_CHANNELS, buffer);
+ if (st != TPCANStatus.PCAN_ERROR_OK) return dict;
+
+ foreach (var ci in buffer)
+ {
+ if (ci.channel_handle == PCANBasic.PCAN_NONEBUS) continue;
+ // 跳过不可用通道(PCAN_CHANNEL_UNAVAILABLE = 0)
+ if (ci.channel_condition == 0) continue;
+
+ var name = $"PCAN-{ci.device_name}-{ci.controller_number}";
+ var port = new PeakCanPort(name);
+ port.Channel = ci.channel_handle;
+ dict[name] = port;
+ }
+ }
+ catch (Exception ex)
+ {
+ // 输出完整异常,便于定位(如 DllNotFoundException / EntryPointNotFoundException)
+ XTrace.WriteLine("枚举PCAN设备失败: " + ex.ToString());
+ }
+ return dict;
+ }
+
+ #endregion
+
+ #region 打开/关闭
+
+ /// <summary>打开</summary>
+ /// <returns></returns>
+ public override bool Open()
+ {
+ try
+ {
+ var st = PCANBasic.Initialize(Channel, MapBaudrate());
+ if (st != TPCANStatus.PCAN_ERROR_OK)
+ {
+ WriteLog("PCAN 初始化失败: " + GetErrorText(st));
+ return false;
+ }
+
+ Opened = true;
+ // 复位停止标志,恢复正常收发
+ _ResumeReceive();
+ // 先注册接收事件,再启动线程,避免线程读到空的 _evtRx 而退化为轮询
+ _RegisterRxEvent();
+ // 注册失败不阻断打开流程:线程侧会自动退化为定时轮询
+ _StartRx();
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ WriteLog("PCAN 打开异常: " + ex.Message);
+ return false;
+ }
+ }
+
+ /// <summary>关闭</summary>
+ public override void Close()
+ {
+ // 先置停止标志并发出取消,接收线程不会再分发消息,也会尽快退出
+ _StopRx();
+
+ // 等接收线程真正退出后再卸载通道,避免线程仍在 Read 中访问已卸载的通道。
+ // 停止标志已置位,线程最多跑完当前一次 Read 便会退出,不会长时间阻塞。
+ var thr = _thrRx;
+ if ((thr != null) && thr.IsAlive)
+ {
+ try { thr.Join(1000); } catch { }
+ }
+
+ // 线程已退出,此时解除事件注册并释放句柄是安全的
+ _UnregisterRxEvent();
+
+ try
+ {
+ PCANBasic.Uninitialize(Channel);
+ }
+ catch { }
+
+ Opened = false;
+ }
+
+ /// <summary>释放资源</summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(bool disposing)
+ {
+ // 仅显式释放时清理;终结器/进程退出阶段不触碰 _ctsRx(等待句柄)与原生 DLL,
+ // 否则会导致进程崩溃(0xC0000409 STATUS_STACK_BUFFER_OVERRUN)
+ if (disposing)
+ {
+ _ctsRx?.Dispose();
+ _ctsRx = null;
+ }
+
+ base.Dispose(disposing); // 会调用 Close() -> _StopRx()(仅 disposing 时真正关闭硬件)
+ }
+
+ /// <summary>波特率映射</summary>
+ /// <returns></returns>
+ private TPCANBaudrate MapBaudrate()
+ {
+ switch (Baudrate)
+ {
+ case Baudrate_e.B1000K: return TPCANBaudrate.PCAN_BAUD_1M;
+ case Baudrate_e.B500K: return TPCANBaudrate.PCAN_BAUD_500K;
+ case Baudrate_e.B250K: return TPCANBaudrate.PCAN_BAUD_250K;
+ case Baudrate_e.B125K: return TPCANBaudrate.PCAN_BAUD_125K;
+ case Baudrate_e.B100K: return TPCANBaudrate.PCAN_BAUD_100K;
+ case Baudrate_e.B200K:
+ WriteLog("PCAN 无 200K 标准波特率,回退 250K");
+ return TPCANBaudrate.PCAN_BAUD_250K;
+ default: return TPCANBaudrate.PCAN_BAUD_250K;
+ }
+ }
+
+ #endregion
+
+ #region 接收线程
+
+ /// <summary>后台接收线程</summary>
+ private Thread _thrRx;
+
+ /// <summary>终止线程</summary>
+ private CancellationTokenSource _ctsRx = new CancellationTokenSource();
+
+ /// <summary>PCAN 接收事件句柄(驱动在有报文时置位,由接收线程等待)</summary>
+ private ManualResetEvent _evtRx;
+
+ /// <summary>等待事件超时时间(毫秒),仅用于周期性检查退出标志,避免漏停</summary>
+ private const Int32 RxWaitTimeoutMs = 200;
+
+ /// <summary>启动接收线程</summary>
+ private void _StartRx()
+ {
+ if ((_thrRx != null) && (_thrRx.IsAlive))
+ {
+ _StopRx();
+ // 旧线程已置停止标志,会很快退出;短暂等待避免新旧线程同时访问通道
+ _thrRx.Join(1000);
+ _thrRx = null;
+ }
+
+ _ctsRx?.Dispose();
+ _ctsRx = new CancellationTokenSource();
+ _thrRx = new Thread(() => _RxThreadFunc());
+ _thrRx.IsBackground = true;
+ _thrRx.Start();
+ }
+
+ /// <summary>
+ /// 停止接收线程。
+ /// 只发出停止信号并置位停止标志,不在调用线程上阻塞等待。
+ /// 原因:关闭通常由 UI 线程发起,接收线程可能正阻塞在向 UI 线程 Invoke 上,
+ /// 若此处同步等待线程退出,会与 UI 线程形成双向死等,表现为主窗口关闭卡顿。
+ /// </summary>
+ private void _StopRx()
+ {
+ // 先置位停止标志,后续收到的帧不再分发(也不会再 Invoke 到 UI 线程)
+ _StopReceive();
+
+ if (_thrRx == null) return;
+ if (_ctsRx == null) return;
+
+ try { _ctsRx.Cancel(); } catch { }
+
+ // 唤醒可能正阻塞在 WaitOne 上的接收线程,使其立即检查停止标志
+ try { _evtRx?.Set(); } catch { }
+ }
+
+ /// <summary>
+ /// 注册 PCAN 接收事件。驱动在接收队列非空时置位该事件,
+ /// 接收线程据此阻塞等待,避免轮询空转。
+ /// </summary>
+ /// <returns>是否注册成功</returns>
+ private bool _RegisterRxEvent()
+ {
+ _UnregisterRxEvent();
+
+ try
+ {
+ // ManualReset:一次通知可覆盖多帧,线程被唤醒后抽干队列,读完自行 Reset
+ _evtRx = new ManualResetEvent(false);
+ var h = _evtRx.SafeWaitHandle.DangerousGetHandle();
+ var st = PCANBasic.SetValue(
+ Channel,
+ TPCANParameter.PCAN_RECEIVE_EVENT,
+ h,
+ (UInt32)IntPtr.Size);
+
+ if (st != TPCANStatus.PCAN_ERROR_OK)
+ {
+ WriteLog("PCAN 注册接收事件失败: " + GetErrorText(st));
+ _UnregisterRxEvent();
+ return false;
+ }
+ return true;
+ }
+ catch (Exception ex)
+ {
+ WriteLog("PCAN 注册接收事件异常: " + ex.Message);
+ _UnregisterRxEvent();
+ return false;
+ }
+ }
+
+ /// <summary>注销 PCAN 接收事件</summary>
+ private void _UnregisterRxEvent()
+ {
+ try
+ {
+ // 传 NULL 句柄即注销,BufferLength 必须为 0
+ PCANBasic.SetValue(Channel, TPCANParameter.PCAN_RECEIVE_EVENT, IntPtr.Zero, 0);
+ }
+ catch { }
+
+ try { _evtRx?.Dispose(); } catch { }
+ _evtRx = null;
+ }
+
+ /// <summary>接收线程函数(事件驱动 + 批量抽干队列)</summary>
+ private void _RxThreadFunc()
+ {
+ var cts = _ctsRx;
+ var evt = _evtRx;
+
+ try
+ {
+ while (true)
+ {
+ if (cts == null || cts.Token.IsCancellationRequested) break;
+ // 收到停止信号立即退出
+ if (RxStopped) break;
+
+ try
+ {
+ // 阻塞等待驱动通知(带超时以便定期检查退出标志)。
+ // 事件缺失时退化为普通轮询,保证仍能收帧。
+ if (evt != null) evt.WaitOne(RxWaitTimeoutMs);
+ else Thread.Sleep(RxWaitTimeoutMs);
+
+ if (cts.Token.IsCancellationRequested) break;
+ if (RxStopped) break;
+
+ // 一次唤醒,抽干队列:状态帧/错误帧不入队
+ TPCANMsg msg;
+ TPCANTimestamp ts;
+ while (PCANBasic.Read(Channel, out msg, out ts) == TPCANStatus.PCAN_ERROR_OK)
+ {
+ // 状态帧/错误帧不向上分发
+ if ((msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_STATUS) != 0)
+ continue;
+ if ((msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_ERRFRAME) != 0)
+ continue;
+
+ RxMsgProcess(msg, ts);
+ }
+ }
+ catch
+ {
+ Thread.Sleep(10);
+ }
+ finally
+ {
+ // 必须在读完后复位,否则下一批报文将不再触发通知(漏帧)
+ try { evt?.Reset(); } catch { }
+ }
+ }
+ }
+ finally
+ {
+ // 线程退出前解除事件注册,避免驱动继续向已释放的句柄置位
+ try { _UnregisterRxEvent(); } catch { }
+ }
+ }
+
+ #endregion
+
+ #region 发送
+
+ /// <summary>发送数据</summary>
+ /// <param name="id">CANID</param>
+ /// <param name="data">数据</param>
+ /// <param name="remote">远程帧</param>
+ /// <param name="ext">扩展帧</param>
+ public override void Send(UInt32 id, byte[] data, bool remote = false, bool ext = false)
+ {
+ if (!Opened)
+ {
+ WriteLog("发送失败,端口未打开");
+ return;
+ }
+
+ var msg = new TPCANMsg();
+ msg.ID = (uint)id;
+ msg.MSGTYPE = ext
+ ? TPCANMessageType.PCAN_MESSAGE_EXTENDED
+ : TPCANMessageType.PCAN_MESSAGE_STANDARD;
+ if (remote) msg.MSGTYPE |= TPCANMessageType.PCAN_MESSAGE_RTR;
+ msg.LEN = (byte)data.Length;
+ if (msg.LEN > 8) msg.LEN = 8;
+ msg.DATA = new byte[8];
+ Array.Copy(data, msg.DATA, msg.LEN);
+
+ WriteLog(MsgToString(msg));
+
+ var st = PCANBasic.Write(Channel, ref msg);
+ if (st != TPCANStatus.PCAN_ERROR_OK)
+ {
+ WriteLog("PCAN 发送失败: " + GetErrorText(st));
+ }
+ }
+
+ #endregion
+
+ #region 处理消息,软件逻辑
+
+ /// <summary>处理消息</summary>
+ /// <param name="msg"></param>
+ /// <param name="ts"></param>
+ /// <returns></returns>
+ private bool RxMsgProcess(TPCANMsg msg, TPCANTimestamp ts)
+ {
+ return ProcessReceived(ToCanMsg(msg, ts));
+ }
+
+ /// <summary>TPCANMsg -> CanMsg</summary>
+ /// <param name="msg"></param>
+ /// <param name="ts"></param>
+ /// <returns></returns>
+ private CanMsg ToCanMsg(TPCANMsg msg, TPCANTimestamp ts)
+ {
+ var ext = (msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_EXTENDED) != 0;
+ var rtr = (msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_RTR) != 0;
+ var len = msg.LEN > 8 ? 8 : msg.LEN;
+ var data = new byte[len];
+ Array.Copy(msg.DATA, data, len);
+
+ var pkt = new CanMsg((Int32)msg.ID, data, rtr, ext);
+ // 总微秒 = micros + 1000*millis + 0x100000000*1000*millis_overflow
+ var micros = ts.micros + 1000UL * ts.millis + 0x100000000UL * 1000UL * ts.millis_overflow;
+ pkt.TimeStamp = TimeSpan.FromMilliseconds(micros / 1000.0);
+ return pkt;
+ }
+
+ #endregion
+
+ #region 日志
+
+ /// <summary>日志前缀</summary>
+ protected override String LogPre => "PCAN ";
+
+ /// <summary>格式化字符串</summary>
+ /// <param name="msg"></param>
+ /// <returns></returns>
+ private String MsgToString(TPCANMsg msg)
+ {
+ var sb = new StringBuilder();
+
+ if ((msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_EXTENDED) != 0)
+ sb.Append(msg.ID.ToString("X8") + " ");
+ else
+ sb.Append(msg.ID.ToString("X3") + " ");
+
+ var R = (msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_RTR) != 0 ? "R " : " ";
+ sb.Append(R);
+
+ var E = (msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_EXTENDED) != 0 ? "E " : " ";
+ sb.Append(E);
+
+ sb.Append("[" + msg.LEN + "] ");
+ for (int i = 0; i < msg.LEN; i++)
+ {
+ sb.Append(msg.DATA[i].ToString("X2") + " ");
+ }
+ return sb.ToString();
+ }
+
+ /// <summary>错误描述</summary>
+ /// <param name="st"></param>
+ /// <returns></returns>
+ private String GetErrorText(TPCANStatus st)
+ {
+ var sb = new StringBuilder(256);
+ if (PCANBasic.GetErrorText(st, 0, sb) == TPCANStatus.PCAN_ERROR_OK)
+ return sb.ToString();
+ return st.ToString();
+ }
+
+ #endregion
+ }
+}
diff --git a/A1Lib/CAN/ZLG_CAN/KernelDllExtractor.cs b/A1Lib/CAN/ZLG_CAN/KernelDllExtractor.cs
new file mode 100644
index 0000000..e0952e0
--- /dev/null
+++ b/A1Lib/CAN/ZLG_CAN/KernelDllExtractor.cs
@@ -0,0 +1,209 @@
+using System;
+using System.IO;
+using System.Reflection;
+using NewLife.Log;
+
+namespace ALib.ZLG_CAN
+{
+ /// <summary>
+ /// 把嵌入的 ZLG kerneldlls 驱动目录释放到磁盘。
+ /// zlgcan.dll 运行时固定按「自身同级目录\kerneldlls\」查找内核驱动与 dll_cfg.ini,
+ /// 因此必须还原出真实的目录结构,不能只把 dll 平铺到程序目录。
+ /// </summary>
+ internal static class KernelDllExtractor
+ {
+ /// <summary>驱动目录名(zlgcan.dll 约定,不可更改)</summary>
+ public const String DirName = "kerneldlls";
+
+ /// <summary>驱动映射配置文件,用于判断目录是否已释放完整</summary>
+ public const String CfgFileName = "dll_cfg.ini";
+
+ /// <summary>
+ /// 嵌入资源前缀。csproj 中 LogicalName 为
+ /// <c>ALib.ZLG_CAN.kerneldlls.%(RecursiveDir)%(Filename)%(Extension)</c>,
+ /// 生成结果形如:
+ /// <c>ALib.ZLG_CAN.kerneldlls.dll_cfg.ini</c>(根目录文件)
+ /// <c>ALib.ZLG_CAN.kerneldlls.devices_property\default\usbcan2.xml</c>(子目录文件)
+ /// 即前缀之后「反斜杠」保留为路径分隔符,可据此精确还原目录层级。
+ /// </summary>
+ private const String ResourcePrefix = "ALib.ZLG_CAN.kerneldlls";
+
+ /// <summary>释放结果缓存</summary>
+ private static Boolean _extracted;
+ private static readonly Object _lock = new Object();
+
+ /// <summary>把 kerneldlls 下所有嵌入资源释放到 应用目录\kerneldlls\</summary>
+ /// <param name="appDir">应用目录</param>
+ /// <returns>是否全部就位</returns>
+ public static Boolean Extract(String appDir)
+ {
+ lock (_lock)
+ {
+ if (_extracted) return true;
+ if (String.IsNullOrEmpty(appDir)) return false;
+
+ try
+ {
+ var root = Path.Combine(appDir, DirName);
+
+ // 1)优先使用 zip 打包形式:kerneldlls 内含大量 DLL/XML,
+ // 整目录压缩后体积可减小数倍,且解压逻辑更简单。
+ var zipName = $"{ResourcePrefix}.zip";
+ if (Assembly.GetExecutingAssembly().GetManifestResourceStream(zipName) != null)
+ {
+ if (ZipResourceExtractor.ExtractToDirectory(zipName, root, CfgFileName))
+ {
+ _extracted = true;
+ return true;
+ }
+ XTrace.WriteLine("zip 形式释放失败,回退逐个资源释放");
+ }
+
+ // 2)回退:逐个嵌入资源释放(兼容未打包的旧配置)
+ return ExtractIndividual(appDir, root);
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine("释放 ZLG kerneldlls 异常: " + ex);
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// 逐个嵌入资源释放(kerneldlls 未打包为 zip 时的兼容路径)。
+ /// </summary>
+ /// <param name="appDir">应用目录</param>
+ /// <param name="root">目标根目录</param>
+ /// <returns>是否全部就位</returns>
+ private static Boolean ExtractIndividual(String appDir, String root)
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ var names = asm.GetManifestResourceNames();
+
+ var count = 0;
+ var skipped = 0;
+
+ foreach (var name in names)
+ {
+ if (!name.StartsWith(ResourcePrefix, StringComparison.OrdinalIgnoreCase)) continue;
+
+ var rel = ToRelativePath(name);
+ if (String.IsNullOrEmpty(rel)) continue;
+
+ count++;
+
+ var dest = Path.Combine(root, rel);
+
+ // 已存在且大小一致则跳过写入,减少重复磁盘写入
+ if (IsUpToDate(asm, name, dest))
+ {
+ skipped++;
+ continue;
+ }
+
+ if (!TryExtractOne(asm, name, dest))
+ {
+ XTrace.WriteLine($"释放 ZLG 驱动失败: {rel}");
+ return false;
+ }
+ }
+
+ if (count == 0)
+ {
+ XTrace.WriteLine("未找到 ZLG kerneldlls 嵌入资源,请检查 A1Lib.csproj 的 EmbeddedResource 配置");
+ return false;
+ }
+
+ var ok = File.Exists(Path.Combine(root, CfgFileName));
+ if (ok) XTrace.WriteLine($"ZLG 驱动就绪: 共 {count} 个文件(复用 {skipped},写入 {count - skipped})于 {root}");
+ else XTrace.WriteLine($"ZLG 驱动目录缺少 {CfgFileName},zlgcan.dll 可能无法加载设备驱动");
+
+ _extracted = ok;
+ return ok;
+ }
+
+ /// <summary>目标文件是否已是最新(存在且长度与嵌入资源一致)</summary>
+ /// <param name="asm">程序集</param>
+ /// <param name="resourceName">资源名</param>
+ /// <param name="destPath">磁盘路径</param>
+ /// <returns></returns>
+ private static Boolean IsUpToDate(Assembly asm, String resourceName, String destPath)
+ {
+ try
+ {
+ var fi = new FileInfo(destPath);
+ if (!fi.Exists) return false;
+
+ using var stream = asm.GetManifestResourceStream(resourceName);
+ if (stream == null) return false;
+
+ return fi.Length == stream.Length;
+ }
+ catch { return false; }
+ }
+
+ /// <summary>资源名 -> 相对路径(如 devices_property\default\usbcan2.xml)</summary>
+ /// <param name="resourceName">嵌入资源名</param>
+ /// <returns>相对路径,无法解析返回 null</returns>
+ private static String ToRelativePath(String resourceName)
+ {
+ // 前缀之后的部分:".dll_cfg.ini" 或 ".devices_property\default\usbcan2.xml"
+ var tail = resourceName.Substring(ResourcePrefix.Length);
+ tail = tail.TrimStart('.');
+
+ if (String.IsNullOrEmpty(tail)) return null;
+
+ // 根目录文件:资源名中不含反斜杠,直接就是文件名;
+ // 子目录文件:反斜杠已被 MSBuild 保留,天然就是相对路径。
+ return tail;
+ }
+
+ /// <summary>释放单个资源到指定路径</summary>
+ /// <param name="asm">程序集</param>
+ /// <param name="resourceName">资源名</param>
+ /// <param name="destPath">目标路径</param>
+ /// <returns>是否成功</returns>
+ private static Boolean TryExtractOne(Assembly asm, String resourceName, String destPath)
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(destPath);
+ if (!String.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
+
+ using var stream = asm.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ XTrace.WriteLine($"找不到嵌入资源: {resourceName}");
+ return false;
+ }
+
+ // 先写临时文件再原子替换,避免驱动正被加载时写入失败
+ var tmp = destPath + ".tmp";
+ using (var fs = new FileStream(tmp, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ stream.CopyTo(fs);
+ fs.Flush();
+ }
+
+ try
+ {
+ File.Move(tmp, destPath, true);
+ }
+ catch
+ {
+ // 目标文件被占用(如驱动已加载)时保留旧文件,视为成功
+ try { File.Delete(tmp); } catch { }
+ if (!File.Exists(destPath)) throw;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"写入 {Path.GetFileName(destPath)} 失败: {ex.Message}");
+ return false;
+ }
+ }
+ }
+}
diff --git a/A1Lib/CAN/ZLG_CAN/kerneldlls.zip b/A1Lib/CAN/ZLG_CAN/kerneldlls.zip
new file mode 100644
index 0000000..1784359
Binary files /dev/null and b/A1Lib/CAN/ZLG_CAN/kerneldlls.zip differ
diff --git "a/A1Lib/CAN/ZLG_CAN/kerneldlls\347\262\276\347\256\200\350\257\264\346\230\216.md" "b/A1Lib/CAN/ZLG_CAN/kerneldlls\347\262\276\347\256\200\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..b66aeab
--- /dev/null
+++ "b/A1Lib/CAN/ZLG_CAN/kerneldlls\347\262\276\347\256\200\350\257\264\346\230\216.md"
@@ -0,0 +1,196 @@
+# kerneldlls 目录精简说明
+
+本文档说明 `kerneldlls/` 目录的作用、精简依据,以及后续扩展(更换设备型号)时的恢复方法。
+
+---
+
+## 一、kerneldlls 是什么
+
+ZLG(致远)CAN 接口卡的原生 SDK 分为两层:
+
+```
+zlgcan.dll 分发层 / 统一 API(ZCAN_OpenDevice、ZCAN_Transmit ...)
+ └── kerneldlls/ 内核驱动层(按设备型号分别提供)
+ ├── dll_cfg.ini 设备类型码 → 驱动文件 的映射表
+ ├── USBCAN.dll 某一类设备的实际收发实现
+ └── ...
+```
+
+`zlgcan.dll` 自身**不含**任何设备逻辑。它启动时读取 `kerneldlls/dll_cfg.ini`,
+按映射表加载对应型号的驱动,再由驱动完成与硬件的实际通信。
+
+**关键约束**:`zlgcan.dll` 按「自身所在目录 / kerneldlls /」这一**固定相对路径**查找,
+不支持通过参数指定其它位置。因此 kerneldlls 必须与 `zlgcan.dll` 落在同一个目录。
+
+---
+
+## 二、为什么要精简
+
+官方发布的 kerneldlls 覆盖**全部产品线**,包含大量本项目用不到的驱动:
+
+| 类别 | 举例 | 本项目是否需要 |
+|---|---|---|
+| PCI / PCIe 插卡 | `PCI9810.dll`、`pcie9120.dll` | 否 |
+| 以太网 / 无线 | `CANET_TCP.dll`、`CANFDNET.dll`、`CANWIFI_TCP.dll` | 否 |
+| CANFD 系列 | `USBCANFD.dll`、`xpcfd.dll`、`ZPSCANFD.dll` | 否 |
+| 其它 | `ZlgCloud.dll`、`CAN_REPLAY.dll`、`CANDTU_*.dll` | 否 |
+| **USB 经典 CAN** | **`USBCAN.dll`** | **是** |
+
+精简带来的收益:
+
+- 嵌入资源由 **135 个文件降为 3 个**,首次运行释放耗时显著降低
+- 发布体积减小
+- 目录结构清晰,便于维护与排查
+
+---
+
+## 三、当前精简结果
+
+### 3.1 保留的文件
+
+```
+kerneldlls/
+├── dll_cfg.ini 驱动映射表(已改写,见 3.2)
+├── USBCAN.dll 本项目实际使用的驱动,支持 USBCAN-I / USBCAN-II
+└── USBCAN.xml USBCAN.dll 对应的属性描述文件
+```
+
+### 3.2 dll_cfg.ini 的改法
+
+原始文件按段组织,每段内的条目是**顺序索引**(`1=`、`2=` ……),
+条目的序号与段头的 `COUNT` 必须自洽,**因此不能简单删行**,
+必须同时修改 `COUNT`,否则 `zlgcan.dll` 解析会出错。
+
+段名(`[KERNELDLL_VCI_CAN]` 等)是 `zlgcan.dll` 内部约定的**固定标识,不可删除**,
+没有对应设备时把 `COUNT` 置 `0` 即可。
+
+精简后的内容:
+
+```ini
+[KERNELDLL_VCI_CAN]
+COUNT=2
+START=1
+1=USBCAN.dll 3 32 1
+2=USBCAN.dll 4 32 1
+
+[KERNELDLL_ZLG_CAN]
+COUNT=0
+START=1
+
+[KERNELDLL_CANSCOPE]
+COUNT=0
+START=1
+```
+
+三个段的作用:
+
+| 段名 | 用途 | 精简前条数 |
+|---|---|---|
+| `KERNELDLL_VCI_CAN` | 经典 CAN 设备 | 33 |
+| `KERNELDLL_ZLG_CAN` | CANFD 设备 | 49 |
+| `KERNELDLL_CANSCOPE` | CANScope 分析仪 | 1 |
+
+### 3.3 条目字段含义
+
+```
+1=USBCAN.dll 3 32 1
+│ │ │ │ └─ USB 标志(1 = USB 接口设备)
+│ │ │ └──── 通道数上限
+│ │ └────── 设备类型码(对应 ZlgCanApi 中的 ZCAN_* 常量)
+│ └───────────────── 驱动文件名
+└──────────────────── 段内序号
+```
+
+保留的两条分别对应:
+
+| 序号 | 驱动 | 设备类型码 | 常量 | 含义 |
+|---|---|---|---|---|
+| 1 | `USBCAN.dll` | 3 | `ZCAN_USBCAN1` | USBCAN-I,单通道 |
+| 2 | `USBCAN.dll` | 4 | `ZCAN_USBCAN2` | USBCAN-II,双通道 |
+
+> **注意**:这两条**共用同一个 `USBCAN.dll`**,是同一驱动注册的两个类型别名。
+> 因此枚举时同一台物理设备会被识别两次(`ZLG-3-*` 与 `ZLG-4-*`)。
+> 代码侧已在 `ZlgCanPort.EnumDevices()` 中按「硬件类型 + 序列号」去重,
+> 并优先保留通道数与实际相符的类型(双通道设备归 `ZCAN_USBCAN2`)。
+> 详见 `ZlgCanPort._ChannelFit()` 与 `IsBetterType()`。
+
+---
+
+## 四、精简是如何验证的
+
+为避免「删出问题」,采取了先验证后替换的步骤:
+
+1. **备份**:整份复制原始 kerneldlls(135 个文件)到源码树外的
+ `ZLG_USBCAN/kerneldlls_original/`
+ (放在源码树外是刻意的 —— `A1Lib.csproj` 用 `kerneldlls\**\*.*` 通配符嵌入,
+ 若备份留在目录内会被一并打包)
+
+2. **边界测试**:在临时目录构造最激进的配置(仅 `USBCAN.dll` + `dll_cfg.ini`
+ 两条记录,不含 `CANDevCore.dll`、不含 `devices_property/`),
+ 实测设备仍能正常枚举 → 证明这些文件对**枚举与收发**并非必需
+
+3. **替换并端到端验证**:精简实际目录后,从零释放再跑完整枚举流程
+
+验证结果:
+
+```
+[before] kerneldlls=False ← 运行前物理目录不存在(走资源释放路径)
+[after ] files=3 ← 仅释放 3 个文件
+[enum ] count=2
+ ZLG-4-0-0 ← 设备正常识别
+ ZLG-4-0-1 ← 且已按序列号去重
+```
+
+---
+
+## 五、关于 devices_property 目录
+
+原始 kerneldlls 下的 `devices_property/` 存放各型号的**属性描述 XML**
+(波特率选项、参数范围等元数据),供 ZLG 官方上位机绘制配置界面使用。
+
+本项目的波特率由代码中的 `ZlgCanApi.TryGetTiming()` 直接映射为寄存器值,
+**不读取这些 XML**,故已全部删除。
+
+---
+
+## 六、后续扩展:更换设备型号
+
+如需接入其它型号,按以下步骤操作:
+
+1. 从 `ZLG_USBCAN/kerneldlls_original/` 取回对应驱动 `.dll`
+ (以及可选的同名 `.xml`)放回 `kerneldlls/`
+
+2. 在 `kerneldlls/dll_cfg.ini` 的相应段中**恢复或追加**条目,
+ 并**同步更新该段的 `COUNT`**
+
+3. 如新增的是 USB 设备,确认其类型码已列入
+ `ZlgCanPort.ScanDeviceTypes`,否则不会被枚举
+
+常用型号对照(摘自原始 dll_cfg.ini):
+
+| 型号 | 驱动文件 | 类型码 |
+|---|---|---|
+| USBCAN-I | `USBCAN.dll` | 3 |
+| USBCAN-II | `USBCAN.dll` | 4 |
+| USBCAN-II E-U(单通道) | `USBCAN_E_64.dll` | 20 |
+| USBCAN-II 2E-U(双通道) | `USBCAN_E_64.dll` | 21 |
+| USBCAN-4E-U | `USBCAN_4E_U_X64.dll` | 31 |
+| USBCAN-8E-U | `USBCAN_8E_U_x64.dll` | 34 |
+| USBCANFD-200U | `USBCANFD.dll` | 41 |
+| USBCANFD-100U | `USBCANFD.dll` | 42 |
+| USBCANFD-MINI | `USBCANFD.dll` | 43 |
+| USBCANFD-800U | `USBCANFD800U.dll` | 59 |
+
+> CANFD 设备属于 `[KERNELDLL_ZLG_CAN]` 段,且初始化时
+> `ZCAN_CHANNEL_INIT_CONFIG.can_type` 需传 `TYPE_CANFD`(当前实现固定为 `TYPE_CAN`)。
+
+---
+
+## 七、相关代码位置
+
+| 功能 | 文件 |
+|---|---|
+| P/Invoke 声明、设备类型常量、波特率寄存器表 | `ZlgCanApi.cs` |
+| 端口实现(枚举、去重、打开、收发、关闭) | `ZlgCanPort.cs` |
+| kerneldlls 嵌入资源的运行时释放 | `KernelDllExtractor.cs` |
+| 资源嵌入配置(csproj) | `A1Lib/A1Lib.csproj` |
diff --git a/A1Lib/CAN/ZLG_CAN/zlgcan.zip b/A1Lib/CAN/ZLG_CAN/zlgcan.zip
new file mode 100644
index 0000000..488c4e1
Binary files /dev/null and b/A1Lib/CAN/ZLG_CAN/zlgcan.zip differ
diff --git a/A1Lib/CAN/ZLG_CAN/ZlgCanApi.cs b/A1Lib/CAN/ZLG_CAN/ZlgCanApi.cs
new file mode 100644
index 0000000..cc59ab9
--- /dev/null
+++ b/A1Lib/CAN/ZLG_CAN/ZlgCanApi.cs
@@ -0,0 +1,348 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace ALib.ZLG_CAN
+{
+ /// <summary>
+ /// ZLG(广州致远)CAN 接口卡驱动 zlgcan.dll 的 P/Invoke 声明。
+ /// 对应官方 SDK 头文件 zlgcan.h / canframe.h / typedef.h,x64 版本。
+ /// 注意:本类只声明,不做任何托管包装;所有调用均由 <see cref="ZlgCanPort"/> 完成。
+ /// </summary>
+ public static class ZlgCanApi
+ {
+ #region 设备类型
+
+ /// <summary>USBCAN-I 单通道。与 USBCAN2 共用 USBCAN.dll,枚举同一设备会重复,需去重</summary>
+ public const UInt32 ZCAN_USBCAN1 = 3;
+
+ /// <summary>USBCAN-II 双通道。与 USBCAN1 共用 USBCAN.dll,枚举同一设备会重复,需去重</summary>
+ public const UInt32 ZCAN_USBCAN2 = 4;
+
+ /// <summary>USBCAN-II 单通道(E-U 系列)</summary>
+ public const UInt32 ZCAN_USBCAN_E_U = 20;
+
+ /// <summary>USBCAN-II 双通道(2E-U 系列)</summary>
+ public const UInt32 ZCAN_USBCAN_2E_U = 21;
+
+ /// <summary>USBCANFD-200U</summary>
+ public const UInt32 ZCAN_USBCANFD_200U = 41;
+
+ /// <summary>USBCANFD-100U</summary>
+ public const UInt32 ZCAN_USBCANFD_100U = 42;
+
+ /// <summary>USBCANFD-MINI</summary>
+ public const UInt32 ZCAN_USBCANFD_MINI = 43;
+
+ /// <summary>虚拟设备,用于无硬件时自测</summary>
+ public const UInt32 ZCAN_VIRTUAL_DEVICE = 99;
+
+ #endregion
+
+ #region 常量
+
+ /// <summary>无效设备句柄</summary>
+ public static readonly IntPtr INVALID_DEVICE_HANDLE = IntPtr.Zero;
+
+ /// <summary>无效通道句柄</summary>
+ public static readonly IntPtr INVALID_CHANNEL_HANDLE = IntPtr.Zero;
+
+ /// <summary>CAN 帧</summary>
+ public const byte TYPE_CAN = 0;
+
+ /// <summary>CANFD 帧</summary>
+ public const byte TYPE_CANFD = 1;
+
+ /// <summary>发送类型:正常发送</summary>
+ public const UInt32 TRANSMIT_NORMAL = 0;
+
+ /// <summary>标准帧最大数据长度</summary>
+ public const Int32 CAN_MAX_DLEN = 8;
+
+ /// <summary>扩展帧格式标志(bit31)</summary>
+ public const UInt32 CAN_EFF_FLAG = 0x80000000U;
+
+ /// <summary>远程帧标志(bit30)</summary>
+ public const UInt32 CAN_RTR_FLAG = 0x40000000U;
+
+ /// <summary>错误帧标志(bit29)</summary>
+ public const UInt32 CAN_ERR_FLAG = 0x20000000U;
+
+ /// <summary>ID 掩码</summary>
+ public const UInt32 CAN_ID_FLAG = 0x1FFFFFFFU;
+
+ /// <summary>调用成功</summary>
+ public const UInt32 STATUS_OK = 1;
+
+ #endregion
+
+ #region 结构体
+
+ /// <summary>CAN 帧(对应 can_frame)</summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct can_frame
+ {
+ /// <summary>32 位 ID,含 EFF/RTR/ERR 标志,见 MAKE_CAN_ID</summary>
+ public UInt32 can_id;
+
+ /// <summary>数据长度 0~8</summary>
+ public byte can_dlc;
+
+ /// <summary>填充</summary>
+ public byte __pad;
+
+ /// <summary>保留</summary>
+ public byte __res0;
+
+ /// <summary>保留</summary>
+ public byte __res1;
+
+ /// <summary>数据区,固定 8 字节</summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = CAN_MAX_DLEN)]
+ public byte[] data;
+
+ /// <summary>组装 32 位 ID(含标志位)</summary>
+ /// <param name="id">CAN ID(不高于 29 位)</param>
+ /// <param name="eff">是否扩展帧</param>
+ /// <param name="rtr">是否远程帧</param>
+ /// <param name="err">是否错误帧</param>
+ /// <returns></returns>
+ public static UInt32 MakeCanId(UInt32 id, bool eff, bool rtr, bool err)
+ {
+ var v = id & CAN_ID_FLAG;
+ if (eff) v |= CAN_EFF_FLAG;
+ if (rtr) v |= CAN_RTR_FLAG;
+ if (err) v |= CAN_ERR_FLAG;
+ return v;
+ }
+
+ /// <summary>是否扩展帧</summary>
+ public bool IsExtend => (can_id & CAN_EFF_FLAG) != 0;
+
+ /// <summary>是否远程帧</summary>
+ public bool IsRemote => (can_id & CAN_RTR_FLAG) != 0;
+
+ /// <summary>是否错误帧</summary>
+ public bool IsError => (can_id & CAN_ERR_FLAG) != 0;
+
+ /// <summary>纯 CAN ID(去除标志位)</summary>
+ public UInt32 PureId => can_id & CAN_ID_FLAG;
+ }
+
+ /// <summary>CAN 通道初始化配置(对应 ZCAN_CHANNEL_INIT_CONFIG)</summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_CHANNEL_INIT_CONFIG
+ {
+ /// <summary>帧类型,TYPE_CAN 或 TYPE_CANFD</summary>
+ public UInt32 can_type;
+
+ // ---- union { struct can; struct canfd; } ----
+ public UInt32 acc_code;
+ public UInt32 acc_mask;
+ public UInt32 reserved;
+
+ /// <summary>滤波方式,1 为单滤波</summary>
+ public byte filter;
+
+ /// <summary>波特率定时器 0</summary>
+ public byte timing0;
+
+ /// <summary>波特率定时器 1</summary>
+ public byte timing1;
+
+ /// <summary>模式,0 为正常模式</summary>
+ public byte mode;
+
+ /// <summary>CANFD 仲裁域波特率(can_type = TYPE_CANFD 时使用)</summary>
+ public UInt32 abit_timing;
+
+ /// <summary>CANFD 数据域波特率(can_type = TYPE_CANFD 时使用)</summary>
+ public UInt32 dbit_timing;
+
+ /// <summary>CANFD 波特率分频</summary>
+ public UInt32 brp;
+
+ /// <summary>CANFD 填充</summary>
+ public UInt16 pad;
+
+ /// <summary>CANFD 保留</summary>
+ public UInt32 reserved2;
+ }
+
+ /// <summary>设备信息(对应 ZCAN_DEVICE_INFO)</summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_DEVICE_INFO
+ {
+ /// <summary>硬件版本</summary>
+ public UInt16 hw_Version;
+
+ /// <summary>固件版本</summary>
+ public UInt16 fw_Version;
+
+ /// <summary>驱动版本</summary>
+ public UInt16 dr_Version;
+
+ /// <summary>动态库版本</summary>
+ public UInt16 in_Version;
+
+ /// <summary>中断号</summary>
+ public UInt16 irq_Num;
+
+ /// <summary>CAN 通道数</summary>
+ public byte can_Num;
+
+ /// <summary>序列号</summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
+ public byte[] str_Serial_Num;
+
+ /// <summary>硬件类型</summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 40)]
+ public byte[] str_hw_Type;
+
+ /// <summary>保留</summary>
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
+ public UInt16[] reserved;
+ }
+
+ /// <summary>发送数据(对应 ZCAN_Transmit_Data)</summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_Transmit_Data
+ {
+ /// <summary>CAN 帧</summary>
+ public can_frame frame;
+
+ /// <summary>发送类型:0 正常发送、1 单次发送、2 自发自收</summary>
+ public UInt32 transmit_type;
+ }
+
+ /// <summary>接收数据(对应 ZCAN_Receive_Data)</summary>
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct ZCAN_Receive_Data
+ {
+ /// <summary>CAN 帧</summary>
+ public can_frame frame;
+
+ /// <summary>时间戳,单位微秒</summary>
+ public UInt64 timestamp;
+ }
+
+ #endregion
+
+ #region 设备与通道
+
+ /// <summary>打开设备</summary>
+ /// <param name="device_type">设备类型,见 ZCAN_USBCAN* 常量</param>
+ /// <param name="device_index">同类型设备的索引,从 0 开始</param>
+ /// <param name="reserved">保留,传 0</param>
+ /// <returns>设备句柄,失败返回 IntPtr.Zero</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_OpenDevice(UInt32 device_type, UInt32 device_index, UInt32 reserved);
+
+ /// <summary>按自定义名称打开设备</summary>
+ /// <param name="device_type">设备类型</param>
+ /// <param name="name">设备名称</param>
+ /// <returns>设备句柄,失败返回 IntPtr.Zero</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_OpenDeviceByName(UInt32 device_type, [MarshalAs(UnmanagedType.LPStr)] String name);
+
+ /// <summary>关闭设备</summary>
+ /// <param name="device_handle">设备句柄</param>
+ /// <returns>STATUS_OK 表示成功</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_CloseDevice(IntPtr device_handle);
+
+ /// <summary>获取设备信息</summary>
+ /// <param name="device_handle">设备句柄</param>
+ /// <param name="pInfo">输出设备信息</param>
+ /// <returns>STATUS_OK 表示成功</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_GetDeviceInf(IntPtr device_handle, ref ZCAN_DEVICE_INFO pInfo);
+
+ /// <summary>设备是否在线</summary>
+ /// <param name="device_handle">设备句柄</param>
+ /// <returns>非 0 表示在线</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_IsDeviceOnLine(IntPtr device_handle);
+
+ /// <summary>初始化 CAN 通道</summary>
+ /// <param name="device_handle">设备句柄</param>
+ /// <param name="can_index">通道索引,从 0 开始</param>
+ /// <param name="pInitConfig">通道初始化配置</param>
+ /// <returns>通道句柄,失败返回 IntPtr.Zero</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern IntPtr ZCAN_InitCAN(IntPtr device_handle, UInt32 can_index, ref ZCAN_CHANNEL_INIT_CONFIG pInitConfig);
+
+ /// <summary>启动 CAN 通道</summary>
+ /// <param name="channel_handle">通道句柄</param>
+ /// <returns>STATUS_OK 表示成功</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_StartCAN(IntPtr channel_handle);
+
+ /// <summary>复位 CAN 通道</summary>
+ /// <param name="channel_handle">通道句柄</param>
+ /// <returns>STATUS_OK 表示成功</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_ResetCAN(IntPtr channel_handle);
+
+ /// <summary>清空接收缓冲</summary>
+ /// <param name="channel_handle">通道句柄</param>
+ /// <returns>STATUS_OK 表示成功</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_ClearBuffer(IntPtr channel_handle);
+
+ /// <summary>获取接收缓冲区中的帧数</summary>
+ /// <param name="channel_handle">通道句柄</param>
+ /// <param name="type">TYPE_CAN 或 TYPE_CANFD</param>
+ /// <returns>帧数</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_GetReceiveNum(IntPtr channel_handle, byte type);
+
+ #endregion
+
+ #region 收发
+
+ /// <summary>发送 CAN 帧</summary>
+ /// <param name="channel_handle">通道句柄</param>
+ /// <param name="pTransmit">待发送数据指针(指向首个元素的非托管地址,由调用方 fixed 后传入)</param>
+ /// <param name="len">帧数</param>
+ /// <returns>实际发送成功的帧数</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_Transmit(IntPtr channel_handle, IntPtr pTransmit, UInt32 len);
+
+ /// <summary>接收 CAN 帧</summary>
+ /// <param name="channel_handle">通道句柄</param>
+ /// <param name="pReceive">接收缓冲指针(指向首个元素的非托管地址,由调用方 fixed 后传入)</param>
+ /// <param name="len">缓冲可容纳的帧数</param>
+ /// <param name="wait_time">等待超时(毫秒),-1 表示直到收到数据才返回</param>
+ /// <returns>实际接收到的帧数</returns>
+ [DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
+ public static extern UInt32 ZCAN_Receive(IntPtr channel_handle, IntPtr pReceive, UInt32 len, Int32 wait_time);
+
+ #endregion
+
+ #region 波特率
+
+ // 波特率寄存器值取自官方应用手册《CAN 波特率计算》,timing0 为 BTR0、timing1 为 BTR1。
+ // 参考时钟 36MHz(USBCAN 系列与 PCAN 一致)。
+
+ /// <summary>按波特率取 (timing0, timing1)</summary>
+ /// <param name="baudrate">波特率索引,与 CAN.Baudrate_e 取值一致</param>
+ /// <param name="timing0">输出 timing0</param>
+ /// <param name="timing1">输出 timing1</param>
+ /// <returns>是否支持该波特率</returns>
+ public static Boolean TryGetTiming(Int32 baudrate, out byte timing0, out byte timing1)
+ {
+ switch (baudrate)
+ {
+ case 0: timing0 = 0x00; timing1 = 0x14; return true; // 1000K
+ case 1: timing0 = 0x00; timing1 = 0x1C; return true; // 500K
+ case 2: timing0 = 0x01; timing1 = 0x1C; return true; // 250K
+ case 3: timing0 = 0x81; timing1 = 0xFA; return true; // 200K
+ case 4: timing0 = 0x03; timing1 = 0x1C; return true; // 125K
+ case 5: timing0 = 0x04; timing1 = 0x1C; return true; // 100K
+ default: timing0 = 0x01; timing1 = 0x1C; return false; // 回退 250K
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/A1Lib/CAN/ZLG_CAN/ZlgCanPort.cs b/A1Lib/CAN/ZLG_CAN/ZlgCanPort.cs
new file mode 100644
index 0000000..b871251
--- /dev/null
+++ b/A1Lib/CAN/ZLG_CAN/ZlgCanPort.cs
@@ -0,0 +1,717 @@
+using ALib;
+using ALib.Can;
+using ALib.CAN;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using static ALib.ZLG_CAN.ZlgCanApi;
+
+namespace ALib.ZLG_CAN
+{
+ /// <summary>
+ /// ZLG(致远)CAN 接口卡端口,基于 zlgcan.dll(第二代句柄式 API)。
+ /// 每个实例对应设备上的一个 CAN 通道。
+ /// </summary>
+ public class ZlgCanPort : ICanPort
+ {
+ /// <summary>构造,指定枚举名称</summary>
+ /// <param name="name">枚举时候的名字</param>
+ public ZlgCanPort(String name) : base(name) { }
+
+ #region DLL管理
+
+ private static readonly String DllFileName = "zlgcan.dll";
+
+ /// <summary>
+ /// 静态构造函数,确保DLL可用
+ /// </summary>
+ static ZlgCanPort() => _Prepare();
+
+ /// <summary>
+ /// 一次性完成运行环境准备,保证「先释放驱动目录、再加载 zlgcan.dll」的严格顺序。
+ /// 顺序不可颠倒:zlgcan.dll 加载时会探测 kerneldlls 并缓存结果,
+ /// 若此时驱动尚未就位,本次进程内 ZCAN_OpenDevice 将一直返回 0(表现为枚举为空)。
+ /// </summary>
+ private static void _Prepare()
+ {
+ // 幂等:静态构造函数、EnumDevices()、Open() 都可能触发,只做一次
+ if (_prepared) return;
+ lock (_prepareLock)
+ {
+ if (_prepared) return;
+
+ try
+ {
+ // 1)先把 kerneldlls 释放到位(zlgcan.dll 初始化时依赖它)
+ // 释放失败必须直接返回:一旦 zlgcan.dll 被加载,它会把「驱动缺失」
+ // 的结果缓存到本进程,之后即便 kerneldlls 补齐,枚举也仍为空。
+ if (!_EnsureKernelDlls())
+ {
+ XTrace.WriteLine("ZLG 驱动目录未就位,跳过 zlgcan.dll 加载");
+ return;
+ }
+
+ // 2)再确保 zlgcan.dll 可用(资源名 ALib.ZLG_CAN.zlgcan.dll,见 A1Lib.csproj)
+ var success = DllResourceManager.EnsureDllAvailable("ALib.ZLG_CAN", DllFileName);
+ if (!success)
+ {
+ XTrace.WriteLine($"{DllFileName} 加载失败");
+ throw new InvalidOperationException($"{DllFileName} 加载失败");
+ }
+
+ _prepared = true;
+ }
+ catch (InvalidOperationException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // 无硬件等场景下不应因准备失败而中断,仅记录
+ XTrace.WriteLine("ZLG 运行环境准备失败: " + ex.Message);
+ }
+ }
+ }
+
+ /// <summary>运行环境是否已准备(驱动目录 + zlgcan.dll)</summary>
+ private static Boolean _prepared;
+
+ /// <summary>准备过程锁</summary>
+ private static readonly Object _prepareLock = new Object();
+
+ /// <summary>
+ /// 确保 zlgcan.dll 能找到同目录下的 kerneldlls 驱动目录。
+ /// zlgcan.dll 运行时按「自身同级目录\kerneldlls\xxx.dll」加载各设备内核驱动
+ /// (USBCAN.dll、USBCANFD.dll 等),并读取 kerneldlls\dll_cfg.ini 决定设备类型到驱动的映射。
+ /// 缺少该目录时 ZCAN_OpenDevice 一律返回 0,表现为「枚举不到任何设备」。
+ /// </summary>
+ /// <returns>kerneldlls 目录是否就位</returns>
+ private static Boolean _EnsureKernelDlls()
+ {
+ try
+ {
+ var appDir = AppContext.BaseDirectory;
+ if (String.IsNullOrEmpty(appDir)) return false;
+
+ var dir = Path.Combine(appDir, KernelDllExtractor.DirName);
+
+ // 已存在且含配置文件的目录直接复用,避免重复遍历全部嵌入资源
+ if (File.Exists(Path.Combine(dir, KernelDllExtractor.CfgFileName))) return true;
+
+ return KernelDllExtractor.Extract(appDir);
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine("释放 ZLG kerneldlls 失败: " + ex.Message);
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region 硬件相关
+
+ /// <summary>设备类型,见 ZlgCanApi.ZCAN_USBCAN* 常量</summary>
+ public UInt32 DeviceType = ZCAN_USBCAN2;
+
+ /// <summary>同类型设备索引,从 0 开始</summary>
+ public UInt32 DeviceIndex = 0;
+
+ /// <summary>设备句柄(由首个打开的通道创建,引用计数归零时关闭)</summary>
+ public IntPtr DeviceHandle = IntPtr.Zero;
+
+ /// <summary>通道句柄(本实例独占)</summary>
+ public IntPtr ChannelHandle = IntPtr.Zero;
+
+ /// <summary>打开标记</summary>
+ public Boolean Opened = false;
+
+ /// <summary>接收缓冲区可容纳的帧数</summary>
+ private const Int32 RxBufferSize = 1000;
+
+ /// <summary>接收等待超时,用于周期性检查退出标志</summary>
+ private const Int32 RxWaitTimeoutMs = 100;
+
+ #endregion
+
+ #region 设备枚举
+
+ /// <summary>枚举当前计算机上所有可用的 ZLG CAN 口(未打开,仅列出)</summary>
+ /// <returns>key = "ZLG-{设备类型}-{设备索引}-{通道号}",value = 可配置的 ICanPort 实例</returns>
+ public new static Dictionary<String, ICanPort> EnumDevices()
+ {
+ var dict = new Dictionary<String, ICanPort>();
+
+ // 必须最先执行:枚举内部会立即调用 ZCAN_OpenDevice,
+ // 而它要求 kerneldlls 已就位且 zlgcan.dll 已在驱动目录就绪之后加载。
+ // 未就绪时直接返回空表:此时调用 ZCAN_OpenDevice 会因找不到驱动而失败,
+ // 且可能让 zlgcan.dll 把故障状态缓存下来。
+ _Prepare();
+ if (!_prepared) return dict;
+
+ // 物理设备唯一键 -> 已选中的探测结果。
+ // 同一个 ZCAN_USBCAN1(3) 与 ZCAN_USBCAN2(4) 在 dll_cfg.ini 里指向同一个
+ // USBCAN.dll,两种类型去打开同一台设备都会成功,若不去重会重复列出。
+ var devices = new Dictionary<String, DeviceCandidate>();
+
+ // zlgcan.dll 没有类似 VCI_FindUsbDevice 的枚举接口,
+ // 采用「按设备类型逐索引试探打开」的方式枚举:打不开说明该索引没有设备。
+ foreach (var devtype in ScanDeviceTypes)
+ {
+ // 单类型设备索引通常不超过 16 个
+ for (UInt32 index = 0; index < MaxDeviceIndex; index++)
+ {
+ IntPtr dev = IntPtr.Zero;
+ try
+ {
+ dev = ZCAN_OpenDevice(devtype, index, 0);
+ if (dev == IntPtr.Zero) break;
+
+ var info = new ZCAN_DEVICE_INFO();
+ UInt32 canNum = 0;
+ if (ZCAN_GetDeviceInf(dev, ref info) == STATUS_OK)
+ {
+ canNum = info.can_Num;
+ }
+ if (canNum == 0) canNum = 1;
+
+ var serial = info.str_Serial_Num == null
+ ? ""
+ : System.Text.Encoding.Default.GetString(info.str_Serial_Num).TrimEnd('\0', ' ');
+ var hwType = info.str_hw_Type == null
+ ? ""
+ : System.Text.Encoding.Default.GetString(info.str_hw_Type).TrimEnd('\0', ' ');
+
+ // 序列号为空的设备无法作为唯一键,退化为「类型+索引」以免误合并不同设备
+ var key = String.IsNullOrEmpty(serial)
+ ? $"{hwType}|{devtype}|{index}"
+ : $"{hwType}|{serial}";
+
+ var cur = new DeviceCandidate
+ {
+ DeviceType = devtype,
+ DeviceIndex = index,
+ CanNum = canNum,
+ Serial = serial,
+ HwType = hwType,
+ };
+
+ if (devices.TryGetValue(key, out var old))
+ {
+ // 同一物理设备被多种类型识别:保留更贴切的一个。
+ // 例:USBCAN-II 是双通道,can_Num=2,应归到 ZCAN_USBCAN2 而非单通道的 USBCAN1。
+ if (!IsBetterType(cur, old))
+ {
+ continue;
+ }
+ }
+
+ devices[key] = cur;
+ }
+ catch (Exception ex)
+ {
+ // 输出完整异常(含 DllNotFoundException / EntryPointNotFoundException 等类型),便于定位
+ XTrace.WriteLine($"枚举ZLG设备失败(type={devtype}, index={index}): {ex}");
+ break;
+ }
+ finally
+ {
+ // 枚举只是探测,立刻关闭,避免占用设备导致后续真正 Open 失败
+ if (dev != IntPtr.Zero)
+ {
+ try { ZCAN_CloseDevice(dev); } catch { }
+ }
+ }
+ }
+ }
+
+ // 去重后按设备生成各通道
+ foreach (var d in devices.Values)
+ {
+ for (UInt32 ch = 0; ch < d.CanNum; ch++)
+ {
+ var name = $"ZLG-{d.DeviceType}-{d.DeviceIndex}-{ch}";
+ var port = new ZlgCanPort(name)
+ {
+ DeviceType = d.DeviceType,
+ DeviceIndex = d.DeviceIndex,
+ PortId = (Int32)ch,
+ };
+ port._Serial = d.Serial;
+ port._HwType = d.HwType;
+ dict[name] = port;
+ }
+ }
+
+ return dict;
+ }
+
+ /// <summary>探测到的物理设备候选(同一设备可能被多种设备类型识别)</summary>
+ private struct DeviceCandidate
+ {
+ /// <summary>设备类型</summary>
+ public UInt32 DeviceType;
+
+ /// <summary>同类型设备索引</summary>
+ public UInt32 DeviceIndex;
+
+ /// <summary>通道数</summary>
+ public UInt32 CanNum;
+
+ /// <summary>序列号</summary>
+ public String Serial;
+
+ /// <summary>硬件类型</summary>
+ public String HwType;
+ }
+
+ /// <summary>
+ /// 比较同一物理设备下两种设备类型的贴切程度。
+ /// 规则:通道数能对上硬件实际的类型优先(USBCAN1 为单通道型号,
+ /// 双通道设备应归 ZCAN_USBCAN2);其次按类型码排序保证结果稳定。
+ /// </summary>
+ /// <param name="cur">当前候选</param>
+ /// <param name="old">已记录候选</param>
+ /// <returns>cur 是否更优</returns>
+ private static Boolean IsBetterType(DeviceCandidate cur, DeviceCandidate old)
+ {
+ // 单通道设备类型若声称的通道数与实际不符,说明是别名,判为劣
+ var curFit = _ChannelFit(cur.DeviceType, cur.CanNum);
+ var oldFit = _ChannelFit(old.DeviceType, old.CanNum);
+ if (curFit != oldFit) return curFit > oldFit;
+
+ // 通道数一致时取较大的类型码(USBCAN2=4 优于 USBCAN1=3)
+ return cur.DeviceType > old.DeviceType;
+ }
+
+ /// <summary>设备类型与通道数的匹配度,越大越贴切</summary>
+ /// <param name="devtype">设备类型</param>
+ /// <param name="canNum">实际通道数</param>
+ /// <returns>匹配度</returns>
+ private static Int32 _ChannelFit(UInt32 devtype, UInt32 canNum)
+ {
+ // 已知单通道型号
+ if (devtype == ZCAN_USBCAN1 || devtype == ZCAN_USBCAN_E_U) return canNum == 1 ? 2 : 0;
+
+ // 已知双通道型号
+ if (devtype == ZCAN_USBCAN2 || devtype == ZCAN_USBCAN_2E_U) return canNum == 2 ? 2 : 0;
+
+ // 其它类型不做通道数假设
+ return 1;
+ }
+
+ /// <summary>枚举时探测的设备类型(仅 USB 系列,PCI/网络系列按需追加)</summary>
+ private static readonly UInt32[] ScanDeviceTypes =
+ {
+ ZCAN_USBCAN1,
+ ZCAN_USBCAN2,
+ ZCAN_USBCAN_E_U,
+ ZCAN_USBCAN_2E_U,
+ ZCAN_USBCANFD_200U,
+ ZCAN_USBCANFD_100U,
+ ZCAN_USBCANFD_MINI,
+ };
+
+ /// <summary>单类型最多探测的设备索引数</summary>
+ private const UInt32 MaxDeviceIndex = 16;
+
+ /// <summary>序列号(枚举时读取,仅用于展示)</summary>
+ private String _Serial;
+
+ /// <summary>硬件类型(枚举时读取,仅用于展示)</summary>
+ private String _HwType;
+
+ #endregion
+
+ #region 设备引用计数(一个CAN工具有多个CAN口,需所有口都关闭才能关闭设备)
+
+ /// <summary>设备打开引用计数,key = (设备类型 << 32) | 设备索引</summary>
+ private static readonly Dictionary<UInt64, Int32> _devRefCount = new Dictionary<UInt64, Int32>();
+
+ /// <summary>设备句柄缓存,key 同上</summary>
+ private static readonly Dictionary<UInt64, IntPtr> _devHandles = new Dictionary<UInt64, IntPtr>();
+
+ /// <summary>引用计数锁</summary>
+ private static readonly Object _devLock = new Object();
+
+ /// <summary>设备key</summary>
+ private static UInt64 _DevKey(UInt32 devtype, UInt32 devindex) => ((UInt64)devtype << 32) | devindex;
+
+ /// <summary>打开设备(引用计数,仅首个CAN口真正调用 ZCAN_OpenDevice)</summary>
+ private static IntPtr _OpenDevice(UInt32 devtype, UInt32 devindex)
+ {
+ lock (_devLock)
+ {
+ var key = _DevKey(devtype, devindex);
+ if (_devRefCount.TryGetValue(key, out var cnt) && cnt > 0)
+ {
+ // 设备已打开,仅增加引用
+ _devRefCount[key] = cnt + 1;
+ return _devHandles[key];
+ }
+
+ // 首次打开设备
+ var handle = ZCAN_OpenDevice(devtype, devindex, 0);
+ if (handle == IntPtr.Zero) return IntPtr.Zero;
+
+ _devRefCount[key] = 1;
+ _devHandles[key] = handle;
+ return handle;
+ }
+ }
+
+ /// <summary>关闭设备(引用计数,仅最后一个CAN口真正调用 ZCAN_CloseDevice)</summary>
+ private static void _CloseDevice(UInt32 devtype, UInt32 devindex)
+ {
+ lock (_devLock)
+ {
+ var key = _DevKey(devtype, devindex);
+ if (!_devRefCount.TryGetValue(key, out var cnt) || cnt <= 0) return;
+
+ cnt--;
+ if (cnt > 0)
+ {
+ // 还有其它CAN口在用,仅减少引用
+ _devRefCount[key] = cnt;
+ return;
+ }
+
+ // 最后一个CAN口,真正关闭设备
+ _devRefCount.Remove(key);
+ if (_devHandles.TryGetValue(key, out var handle))
+ {
+ _devHandles.Remove(key);
+ try { ZCAN_CloseDevice(handle); } catch { }
+ }
+ }
+ }
+
+ #endregion
+
+ #region 打开/关闭
+
+ /// <summary>打开</summary>
+ /// <returns></returns>
+ public override Boolean Open()
+ {
+ if (Opened) return true;
+
+ try
+ {
+ // 枚举得到的实例可能被直接 Open(未经 EnumDevices),此处兜底准备
+ _Prepare();
+ if (!_prepared)
+ {
+ WriteLog("ZLG 运行环境未就绪,无法打开端口");
+ return false;
+ }
+
+ // 打开(引用计数:一个设备有多个CAN口,仅首个口真正打开设备)
+ var dev = _OpenDevice(DeviceType, DeviceIndex);
+ if (dev == IntPtr.Zero)
+ {
+ WriteLog("打开ZLG设备失败");
+ return false;
+ }
+ DeviceHandle = dev;
+
+ // 配置通道:can_type 只取决于硬件类型,非 CANFD 设备必须传 TYPE_CAN
+ var cfg = new ZCAN_CHANNEL_INIT_CONFIG
+ {
+ can_type = TYPE_CAN,
+ acc_code = 0,
+ acc_mask = 0xFFFFFFFF,
+ };
+
+ if (!TryGetTiming((Int32)Baudrate, out var t0, out var t1))
+ {
+ WriteLog($"不支持的波特率 {Baudrate},回退 250K");
+ }
+ cfg.timing0 = t0;
+ cfg.timing1 = t1;
+ cfg.filter = 1;
+ cfg.mode = 0;
+
+ var ch = ZCAN_InitCAN(dev, (UInt32)PortId, ref cfg);
+ if (ch == IntPtr.Zero)
+ {
+ WriteLog("ZCAN_InitCAN 失败");
+ _CloseDevice(DeviceType, DeviceIndex);
+ DeviceHandle = IntPtr.Zero;
+ return false;
+ }
+ ChannelHandle = ch;
+
+ if (ZCAN_StartCAN(ch) != STATUS_OK)
+ {
+ WriteLog("ZCAN_StartCAN 失败");
+ _CloseDevice(DeviceType, DeviceIndex);
+ DeviceHandle = IntPtr.Zero;
+ ChannelHandle = IntPtr.Zero;
+ return false;
+ }
+
+ // 丢弃打开前的残留报文
+ try { ZCAN_ClearBuffer(ch); } catch { }
+ }
+ catch (Exception ex)
+ {
+ WriteLog("ZLG 打开异常: " + ex.Message);
+ _CloseDevice(DeviceType, DeviceIndex);
+ DeviceHandle = IntPtr.Zero;
+ ChannelHandle = IntPtr.Zero;
+ return false;
+ }
+
+ Opened = true;
+ // 复位停止标志,恢复正常收发
+ _ResumeReceive();
+ _StartRx();
+
+ return true;
+ }
+
+ /// <summary>关闭</summary>
+ public override void Close()
+ {
+ // 先置停止标志并发出取消,接收线程不会再分发消息,也会尽快退出
+ _StopRx();
+
+ // 必须等接收线程真正退出后再释放通道,否则线程仍在 ZCAN_Receive 中访问已复位的通道。
+ // 此处不会长时间阻塞:停止标志已置位,线程最多跑完当前一次 ZCAN_Receive 便会退出。
+ var thr = _thrRx;
+ if ((thr != null) && thr.IsAlive)
+ {
+ try { thr.Join(1000); } catch { }
+ }
+
+ if (Opened)
+ {
+ try
+ {
+ // 停止本CAN口
+ ZCAN_ResetCAN(ChannelHandle);
+ }
+ catch { }
+
+ // 关闭设备(引用计数:一个设备多个CAN口,需所有口都关闭后才真正关闭设备)
+ _CloseDevice(DeviceType, DeviceIndex);
+
+ ChannelHandle = IntPtr.Zero;
+ DeviceHandle = IntPtr.Zero;
+ }
+
+ Opened = false;
+ }
+
+ /// <summary>释放资源</summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(Boolean disposing)
+ {
+ // 仅显式释放时清理;终结器/进程退出阶段不触碰 _ctsRx(等待句柄)与原生 DLL,
+ // 否则会导致进程崩溃(0xC0000409 STATUS_STACK_BUFFER_OVERRUN)
+ if (disposing)
+ {
+ _ctsRx?.Dispose();
+ _ctsRx = null;
+ }
+
+ base.Dispose(disposing); // 会调用 Close() -> _StopRx()(仅 disposing 时真正关闭硬件)
+ }
+
+ #endregion
+
+ #region 接收线程
+
+ /// <summary>后台接收线程</summary>
+ private Thread _thrRx;
+
+ /// <summary>终止线程</summary>
+ private CancellationTokenSource _ctsRx = new CancellationTokenSource();
+
+ /// <summary>启动接收线程</summary>
+ private void _StartRx()
+ {
+ if ((_thrRx != null) && (_thrRx.IsAlive))
+ {
+ _StopRx();
+ // 旧线程已置停止标志,会很快退出;短暂等待避免新旧线程同时访问硬件
+ _thrRx.Join(1000);
+ _thrRx = null;
+ }
+
+ _ctsRx?.Dispose();
+ _ctsRx = new CancellationTokenSource();
+ _thrRx = new Thread(() => _RxThreadFunc());
+ _thrRx.IsBackground = true;
+ _thrRx.Start();
+ }
+
+ /// <summary>
+ /// 停止接收线程。
+ /// 只发出停止信号并置位停止标志,不在调用线程上阻塞等待。
+ /// 原因:关闭通常由 UI 线程发起,接收线程可能正阻塞在向 UI 线程 Invoke 上,
+ /// 若此处同步等待线程退出,会与 UI 线程形成双向死等,表现为主窗口关闭卡顿。
+ /// </summary>
+ private void _StopRx()
+ {
+ // 先置位停止标志,后续收到的帧不再分发(也不会再 Invoke 到 UI 线程)
+ _StopReceive();
+
+ if (_thrRx == null) return;
+ if (_ctsRx == null) return;
+
+ try { _ctsRx.Cancel(); } catch { }
+ }
+
+ /// <summary>接收线程函数(阻塞式批量接收)</summary>
+ private void _RxThreadFunc()
+ {
+ var cts = _ctsRx;
+
+ // ZCAN_Receive 需要一段连续的非托管缓冲,一次性分配并复用。
+ // 每帧结构体大小固定,按 ZCAN_Receive_Data 布局写入后按同样布局读回。
+ var frameSize = Marshal.SizeOf<ZCAN_Receive_Data>();
+ var bufSize = frameSize * RxBufferSize;
+ var buf = Marshal.AllocHGlobal(bufSize);
+
+ try
+ {
+ while (true)
+ {
+ if (cts == null || cts.Token.IsCancellationRequested) break;
+ // 收到停止信号立即退出,不再处理本批剩余帧
+ if (RxStopped) break;
+
+ var ch = ChannelHandle;
+ if (ch == IntPtr.Zero) break;
+
+ try
+ {
+ // wait_time 给超时值,既能及时响应退出,也能在超时内被数据唤醒
+ var res = ZCAN_Receive(ch, buf, RxBufferSize, RxWaitTimeoutMs);
+ if (res == 0xFFFFFFFF) res = 0;
+ if (res > RxBufferSize) res = RxBufferSize;
+
+ for (UInt32 i = 0; i < res; i++)
+ {
+ // 停止过程中丢弃剩余帧,避免继续向 UI 线程 Invoke
+ if (RxStopped) break;
+
+ var pkt = Marshal.PtrToStructure<ZCAN_Receive_Data>(buf + (Int32)i * frameSize);
+
+ // 错误帧不向上分发
+ if (pkt.frame.IsError) continue;
+
+ ProcessReceived(pkt.ToCanMsg());
+ }
+ }
+ catch
+ {
+ Thread.Sleep(10);
+ }
+
+ if (RxStopped) break;
+ }
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(buf);
+ }
+ }
+
+ #endregion
+
+ #region 发送
+
+ /// <summary>发送数据</summary>
+ /// <param name="id">CANID</param>
+ /// <param name="data">数据</param>
+ /// <param name="remote">远程帧</param>
+ /// <param name="ext">扩展帧</param>
+ public override void Send(UInt32 id, byte[] data, bool remote = false, bool ext = false)
+ {
+ if (!Opened)
+ {
+ WriteLog("发送失败,端口未打开");
+ return;
+ }
+
+ var len = data?.Length ?? 0;
+ if (len > CAN_MAX_DLEN) len = CAN_MAX_DLEN;
+
+ var tx = new ZCAN_Transmit_Data
+ {
+ frame = new can_frame
+ {
+ can_id = can_frame.MakeCanId(id, ext, remote, false),
+ can_dlc = (byte)len,
+ data = new byte[CAN_MAX_DLEN],
+ },
+ transmit_type = TRANSMIT_NORMAL,
+ };
+ if (len > 0) Array.Copy(data, tx.frame.data, len);
+
+ WriteLog(tx.ToCanMsg().ToString() + " TX");
+
+ // ZCAN_Transmit 需要连续的结构体地址,用非托管内存承载,避免取托管结构体地址
+ var size = Marshal.SizeOf<ZCAN_Transmit_Data>();
+ var ptr = Marshal.AllocHGlobal(size);
+ try
+ {
+ Marshal.StructureToPtr(tx, ptr, false);
+ var n = ZCAN_Transmit(ChannelHandle, ptr, 1);
+ if (n != 1) WriteLog("ZLG 发送失败");
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(ptr);
+ }
+ }
+
+ #endregion
+
+ #region 日志
+
+ /// <summary>日志前缀</summary>
+ protected override String LogPre => "ZLG ";
+
+ #endregion
+ }
+
+ /// <summary>ZLG 帧与通用 CanMsg 的转换辅助</summary>
+ internal static class ZlgCanConvert
+ {
+ /// <summary>接收数据 -> CanMsg</summary>
+ /// <param name="pkt">ZLG 接收数据</param>
+ /// <returns></returns>
+ public static CanMsg ToCanMsg(this ZCAN_Receive_Data pkt)
+ {
+ var f = pkt.frame;
+ var len = f.can_dlc > CAN_MAX_DLEN ? CAN_MAX_DLEN : f.can_dlc;
+ var data = new byte[len];
+ if (len > 0 && f.data != null) Array.Copy(f.data, data, len);
+
+ var msg = new CanMsg((Int32)f.PureId, data, f.IsRemote, f.IsExtend);
+ // ZLG 时间戳单位为微秒
+ if (pkt.timestamp > 0) msg.TimeStamp = TimeSpan.FromMilliseconds(pkt.timestamp / 1000.0);
+ return msg;
+ }
+
+ /// <summary>发送数据 -> CanMsg(仅用于日志格式化)</summary>
+ /// <param name="pkt">ZLG 发送数据</param>
+ /// <returns></returns>
+ public static CanMsg ToCanMsg(this ZCAN_Transmit_Data pkt)
+ {
+ var f = pkt.frame;
+ var len = f.can_dlc > CAN_MAX_DLEN ? CAN_MAX_DLEN : f.can_dlc;
+ var data = new byte[len];
+ if (len > 0 && f.data != null) Array.Copy(f.data, data, len);
+
+ return new CanMsg((Int32)f.PureId, data, f.IsRemote, f.IsExtend);
+ }
+ }
+}
diff --git a/A1Lib/CanOpen/CanPacket.cs b/A1Lib/CanOpen/CanPacket.cs
new file mode 100644
index 0000000..423bb43
--- /dev/null
+++ b/A1Lib/CanOpen/CanPacket.cs
@@ -0,0 +1,97 @@
+using ALib.CAN;
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Can
+{
+ /// <summary>Can报文格式</summary>
+ public class CanPacket
+ {
+ /// <summary>功能码,4bit</summary>
+ public byte Func;
+ /// <summary>节点ID 7bit</summary>
+ public int Node_Id = 0;
+
+ public int COB_ID
+ {
+ get { return ((int)Func << 7) + Node_Id; }
+ set { Func = (byte)(value >> 7); Node_Id = value & 0x7f; }
+ }
+
+ /// <summary>远程帧,默认false</summary>
+ public bool RTR = false;
+ /// <summary>报文长度</summary>
+ public int DLC => Data.Length;
+ /// <summary>负载数据</summary>
+ public byte[] Data = new byte[0];
+
+ public CanPacket() { }
+
+ public CanPacket(int cobId, byte[] py)
+ {
+ COB_ID = cobId;
+
+ RTR = false;
+ if (py.Length > 8) Data = py.ReadBytes(0, 8);
+ else Data = py;
+ }
+
+ public CanPacket(byte func, int nodeId, byte[] py)
+ {
+ Func = func;
+ Node_Id = nodeId;
+
+ RTR = false;
+ if (py.Length > 8) Data = py.ReadBytes(0, 8);
+ else Data = py;
+ }
+
+ public CanPacket(CanMsg msg)
+ {
+ COB_ID = msg.ID;
+ RTR = msg.RemoteFlag;
+ Data = msg.Data;
+ }
+
+ protected virtual void OnReadPkt(CanPacket pkt) { }
+
+ /// <summary>copy 内容,子类拿数据用</summary>
+ /// <param name="pkt"></param>
+ public void ReadPkt(CanPacket pkt)
+ {
+ this.Func = pkt.Func;
+ this.Node_Id = pkt.Node_Id;
+
+ this.RTR = pkt.RTR;
+ this.Data = (byte[])pkt.Data.Clone();
+
+ OnReadPkt(pkt);
+ }
+
+ protected virtual void WritePkt() { }
+
+ /// <summary>转化为子类,具体类型的数据包</summary>
+ /// <typeparam name="T"></typeparam>
+ /// <returns></returns>
+ public T ToPacket<T>() where T : CanPacket, new()
+ {
+ var obj = new T();
+ obj.ReadPkt(this);
+ return obj;
+ }
+
+ /// <summary>序列化为驱动要求的包,用于发送数据(会先按子类规则填充 Data)</summary>
+ /// <returns></returns>
+ public CanMsg ToCanMsg()
+ {
+ // 必须先 WritePkt(),否则 Data 为空(默认 byte[0]),会发出 DLC=0 的空帧
+ WritePkt();
+
+ return new CanMsg(COB_ID, Data, RTR, false);
+ }
+ }
+}
diff --git a/A1Lib/CanOpen/SDO_Read.cs b/A1Lib/CanOpen/SDO_Read.cs
new file mode 100644
index 0000000..22492f5
--- /dev/null
+++ b/A1Lib/CanOpen/SDO_Read.cs
@@ -0,0 +1,343 @@
+
+using ALib.Can;
+using ALib.CAN;
+using NewLife;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.CanOpen
+{
+ /// <summary>
+ /// 发送 cobid = 0x600+nodeid, byte0 = 0x40
+ /// 接收 cobid = 0x580+nodeid
+ /// </summary>
+ public class SDO_ReadPacket : CanPacket
+ {
+ #region byte 0
+
+ /// <summary>第一个字节,控制字节</summary>
+ public byte Byte0 = 0;
+
+ #endregion
+
+ #region byte 1 2 3
+
+ /// <summary>索引, ushort 类型</summary>
+ public int Index = 0;
+ /// <summary>子索引, byte 类型</summary>
+ public int SubIndex = 0;
+
+ #endregion
+
+ #region 两种负载
+
+ /// <summary>单包负载</summary>
+ public byte[] Payload = new byte[0];
+ /// <summary>TransferTypeFast=false,SizeIndicator=true。
+ /// 给定大小,后续数据包负责传输</summary>
+ public int Length = 0;
+
+ #endregion
+
+ public SDO_ReadPacket() { }
+
+ public SDO_ReadPacket(int nodId, int index, int subidx)
+ {
+ COB_ID = 0x600 + nodId;
+ Byte0 = 0x40;
+
+ Index = index;
+ SubIndex = subidx;
+ }
+
+ /// <summary>读操作,分片传输的中间ACK(客户端发送)</summary>
+ /// <param name="t"></param>
+ /// <returns></returns>
+ public void SDO_ReadContinue(bool t)
+ {
+ // xCs=3 T=t
+ Byte0 = (byte)(t ? 0x60 : 0x70);
+ }
+
+ /// <summary>下一个包</summary>
+ /// <returns></returns>
+ public SDO_ReadPacket NextPkt()
+ {
+ if (IsEndPacket) return null;
+ if (Error) return null;
+
+ var pkt = new SDO_ReadPacket(Node_Id, Index, SubIndex);
+
+ if (Byte0 == 0x41)
+ {
+ // 第一个 60/70 包是 60
+ pkt.Byte0 = 0x60;
+ }
+ else
+ {
+ // T bit4 需要翻转
+ if ((Byte0 & 0x10) == 0) pkt.Byte0 = 0x70;
+ else pkt.Byte0 = 0x60;
+ }
+
+ pkt.Func = 0x600 >> 7;
+ pkt.Node_Id = Node_Id;
+ pkt.RTR = RTR;
+
+ return pkt;
+ }
+
+ /// <summary>这是结尾包</summary>
+ public bool IsEndPacket = false;
+ /// <summary>出错,错误结束,数据无效</summary>
+ public bool Error = false;
+
+ /// <summary>错误码</summary>
+ public UInt32 ErrCode = 0;
+
+
+ protected override void OnReadPkt(CanPacket pkt)
+ {
+ // 判断 COB_ID 是不是 580 基底
+ if (Func != 0xB)
+ {
+ IsEndPacket = true;
+ Error = true;
+
+ return;
+ }
+
+ var byte0 = Data[0];
+ Byte0 = byte0;
+ if ((byte0 & 0x43) == 0x43)
+ {
+ // byte0 = 43 + (BytesNotData << 2)
+ // 快速包。 负载最大 4 byte
+ var BytesNotData = (byte0 >> 2) & 0x03;
+ Payload = Data.ReadBytes(4, 4 - BytesNotData);
+ IsEndPacket = true;
+ }
+ else if (byte0 == 0x41)
+ {
+ // 分片数据包,首包。 负载为数据长度。
+ Length = Data.ReadBytes(4, 4).ToInt();
+ }
+ else if ((byte0 & 0xef) == 0)
+ {
+ // byte0 = 00 or 10
+ // 分片数据包,后续包。 负载 7 字节。
+ Payload = Data.ReadBytes(1, 7);
+ }
+ else if ((byte0 & 0xe1) == 0x01)
+ {
+ // byte0 = 01 or 11 + (bytesNotData << 1)
+ // 分片数据包,尾包。 负载最大 7 字节。
+ var bytesNotData = (byte0 >> 1) & 0x07;
+ Payload = Data.ReadBytes(1, 7 - bytesNotData);
+ IsEndPacket = true;
+ }
+ else if (byte0 == 0x80)
+ {
+ // 错误数据,有错误码可以提取
+ ErrCode = Data.ReadBytes(4, 4).ToUInt32();
+ IsEndPacket = true;
+ Error = true;
+ }
+ else
+ {
+ IsEndPacket = true;
+ Error = true;
+ }
+ }
+
+ protected override void WritePkt()
+ {
+ byte _byte0 = Byte0;
+
+ // 60 70 读数据的 ACK 包
+ if ((_byte0 == 0x60) || (_byte0 == 0x70))
+ {
+ Data = new byte[8];
+ Data[0] = _byte0;
+
+ base.WritePkt();
+ return;
+ }
+
+ // 正常传输内容
+ var ms = new MemoryStream(8);
+ ms.WriteByte(_byte0);
+ ms.WriteByte((byte)Index);
+ ms.WriteByte((byte)(Index >> 8));
+ ms.WriteByte((byte)(SubIndex));
+ ms.Write(new byte[4]);
+
+ Data = ms.ToArray();
+ base.WritePkt();
+ }
+ }
+
+ /*
+ 用法示例
+
+ var reader = new SDO_Read(_Port, 2);
+
+ // 12字节负载,普通读
+ // var data = reader.Read(0x100b, 0, 500);
+
+ // 4字节负载,快速读
+ // var data = reader.Read(0x100a, 3, 500);
+
+ WriteLog(data?.ToHex());
+
+ */
+
+ /// <summary>SDO 读数据,常规模式/快速模式</summary>
+ public class SDO_Read : ICanRx
+ {
+ ICanPort _Port;
+
+ public int Node_Id;
+ /// <summary>索引, ushort 类型</summary>
+ public int Index = 0;
+ /// <summary>子索引, byte 类型</summary>
+ public int SubIndex = 0;
+
+ public SDO_Read(ICanPort port, int nodId)
+ {
+ _Port = port;
+ Node_Id = nodId;
+
+ CanIdMask = -1;
+ CanId = 0x580 + nodId;
+ }
+
+ /// <summary>收数据流</summary>
+ public MemoryStream ObjMs = new MemoryStream(0);
+
+ public enum State_e
+ {
+ /// <summary>初始化</summary>
+ Init = 0,
+ /// <summary>工作中</summary>
+ Working = 1,
+ /// <summary>完成</summary>
+ OK = 6,
+ /// <summary>超时失败</summary>
+ Timeout = 7,
+ /// <summary>错误</summary>
+ Error = 8,
+ }
+
+ /// <summary>状态</summary>
+ public State_e State = State_e.Init;
+
+ public byte[] Read(int idx, int subidx, int timeout)
+ {
+ Index = idx;
+ SubIndex = subidx;
+
+ if (_Port == null)
+ {
+ State = State_e.Error;
+ return new byte[0];
+ }
+
+ // 发送启动包
+ {
+ var pkt = new SDO_ReadPacket(Node_Id, Index, subidx);
+ _Port?.Send(pkt.ToCanMsg());
+ State = State_e.Working;
+ }
+
+ // 等待回复
+ _Port?.Register(this);
+
+ var tend = DateTime.Now.AddMilliseconds(timeout);
+ while (true)
+ {
+ Task.Delay(5).Wait();
+
+ if (State != State_e.Working) break;
+
+ // 超时判断
+ if (DateTime.Now > tend)
+ {
+ State = State_e.Timeout;
+ break;
+ }
+ }
+
+ _Port?.Unregister(this);
+
+ if (State == State_e.OK) return ObjMs.ToArray();
+ else return new byte[0];
+ }
+
+ public override bool Process(CanMsg cmsg, ICanPort port)
+ {
+ var msg = new CanPacket(cmsg);
+ // base.Process(msg, port);
+ var pkt = msg.ToPacket<SDO_ReadPacket>();
+ if (pkt.Error)
+ {
+ State = State_e.Error;
+ return true;
+ }
+
+ if (pkt.IsEndPacket)
+ {
+ if (ObjMs == null)
+ {
+ ObjMs = new MemoryStream(pkt.Payload.Length);
+ ObjMs.Write(pkt.Payload);
+ }
+ else
+ {
+ ObjMs.Write(pkt.Payload);
+ }
+
+ State = State_e.OK;
+ return true;
+ }
+ else
+ {
+ if (pkt.Length > 0)
+ {
+ ObjMs = new MemoryStream(pkt.Length);
+ }
+ else
+ {
+ if (ObjMs == null)
+ {
+ State = State_e.Error;
+ return true;
+ }
+
+ ObjMs.Write(pkt.Payload);
+ }
+
+ port.Send(pkt.NextPkt().ToCanMsg());
+ return true;
+ }
+ }
+
+
+ #region 日志
+
+ /// <summary>日志</summary>
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>写日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteLog(String format, params Object[] args) => Log?.Info($"Upgrade {format}", args);
+
+ #endregion
+ }
+}
diff --git a/A1Lib/CanOpen/SDO_Write.cs b/A1Lib/CanOpen/SDO_Write.cs
new file mode 100644
index 0000000..f4daa9b
--- /dev/null
+++ b/A1Lib/CanOpen/SDO_Write.cs
@@ -0,0 +1,289 @@
+
+using ALib.Can;
+using ALib.CAN;
+using NewLife;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.CanOpen
+{
+ public class SDO_WritePacket : CanPacket
+ {
+ #region byte 0
+
+ /// <summary>第一个字节,控制字节</summary>
+ public byte Byte0 = 0;
+
+ #endregion
+
+ #region byte 1 2 3
+
+ /// <summary>索引, ushort 类型</summary>
+ public int Index = 0;
+ /// <summary>子索引, byte 类型</summary>
+ public int SubIndex = 0;
+
+ #endregion
+
+ #region 负载
+
+ /// <summary>总负载数据,可能需要分包</summary>
+ public MemoryStream ObjMs = null;
+
+ #endregion
+
+
+ public SDO_WritePacket() { }
+
+ public SDO_WritePacket(int nodId, int index, int subidx, byte[] data)
+ {
+ COB_ID = 0x600 + nodId;
+ Byte0 = 0x21;
+
+ Index = index;
+ SubIndex = subidx;
+
+ if (data != null) ObjMs = new MemoryStream(data);
+ }
+
+ public bool First()
+ {
+ if (ObjMs.Capacity <= 4)
+ {
+ int len = ObjMs.Capacity;
+ int bytesNotData = 4 - len;
+ Byte0 = (byte)(0x23 + (bytesNotData << 2));
+
+ Data = new byte[8];
+ var ms = new MemoryStream(Data);
+ ms.Write(Byte0);
+ ms.WriteByte((byte)Index);
+ ms.WriteByte((byte)(Index >> 8));
+ ms.WriteByte((byte)(SubIndex));
+ ms.Write(ObjMs.ReadBytes(len));
+ }
+ else
+ {
+ int len = ObjMs.Capacity;
+ int bytesNotData = 4 - len;
+ Byte0 = (byte)0x21;
+
+ Data = new byte[8];
+ var ms = new MemoryStream(Data);
+ ms.Write(Byte0);
+ ms.WriteByte((byte)Index);
+ ms.WriteByte((byte)(Index >> 8));
+ ms.WriteByte((byte)(SubIndex));
+ ms.Write(len.GetBytes());
+ }
+
+ return true;
+ }
+
+ private bool T;
+
+ public bool Continue(bool t)
+ {
+ T = t;
+ var remian = ObjMs.Capacity - ObjMs.Position;
+ if (remian < 1) return false;
+
+ if (remian > 7)
+ {
+ Byte0 = (byte)(t ? 0x10 : 0x00);
+
+ Data = new byte[8];
+ var ms = new MemoryStream(Data);
+ ms.WriteByte(Byte0);
+ ms.Write(ObjMs.ReadBytes(7));
+ }
+ else
+ {
+ int bytesNotData = (int)(7 - remian);
+ Byte0 = 0x01;
+ Byte0 += (byte)(bytesNotData << 1);
+ if (t) Byte0 += 0x10;
+
+ Data = new byte[8];
+ var ms = new MemoryStream(Data);
+ ms.WriteByte(Byte0);
+ ms.Write(ObjMs.ReadBytes(remian));
+ }
+
+ return true;
+ }
+
+ /// <summary>出错,错误结束,数据无效</summary>
+ public bool Error = false;
+ /// <summary>错误码</summary>
+ public Int32 ErrCode = 0;
+ /// <summary>这是结尾包</summary>
+ public bool IsEndPacket = false;
+
+ protected override void OnReadPkt(CanPacket pkt)
+ {
+ // 判断 COB_ID 是不是 580 基底
+ if (Func != 0xB)
+ {
+ IsEndPacket = true;
+ Error = true;
+ return;
+ }
+
+ var byte0 = Data[0];
+ Byte0 = byte0;
+
+ if (byte0 == 0x80)
+ {
+ // 快速包,异常
+ // 分段包,异常
+ Error = true;
+ ErrCode = Data.ReadBytes(4, 4).ToInt();
+ IsEndPacket = true;
+ return;
+ }
+ else if (byte0 == 0x60)
+ {
+ // 快速包,结束
+ IsEndPacket = true;
+ return;
+ }
+ else if ((byte0 & 0xef) == 0x20)
+ {
+ // 0x20 0x30
+ // 分段包,中间的包,结尾的包
+ }
+ else
+ {
+ IsEndPacket = true;
+ Error = true;
+ }
+ }
+ }
+
+ public class SDO_Write : ICanRx
+ {
+ ICanPort _Port;
+
+ public int Node_Id;
+ /// <summary>索引, ushort 类型</summary>
+ public int Index = 0;
+ /// <summary>子索引, byte 类型</summary>
+ public int SubIndex = 0;
+
+ private SDO_WritePacket Packet;
+
+ private bool T = false;
+
+ public SDO_Write(ICanPort port, int nodId)
+ {
+ _Port = port;
+ Node_Id = nodId;
+
+ CanIdMask = -1;
+ CanId = 0x580 + nodId;
+ }
+
+ public enum State_e
+ {
+ /// <summary>初始化</summary>
+ Init = 0,
+ /// <summary>工作中</summary>
+ Working = 1,
+ /// <summary>完成</summary>
+ OK = 6,
+ /// <summary>超时失败</summary>
+ Timeout = 7,
+ /// <summary>错误</summary>
+ Error = 8,
+ }
+
+ /// <summary>状态</summary>
+ public State_e State = State_e.Init;
+
+ public bool Write(int idx, int subidx, byte[] data, int timeout)
+ {
+ Index = idx;
+ SubIndex = subidx;
+
+ if (_Port == null)
+ {
+ State = State_e.Error;
+ return false;
+ }
+
+ var pkt = new SDO_WritePacket(Node_Id, Index, subidx, data);
+ Packet = pkt;
+
+ // 等待回复
+ _Port?.Register(this);
+ // 发送启动包
+ {
+ pkt.First();
+ _Port?.Send(pkt.ToCanMsg());
+ State = State_e.Working;
+ }
+
+
+ var tend = DateTime.Now.AddMilliseconds(timeout);
+ while (true)
+ {
+ Task.Delay(5).Wait();
+
+ if (State != State_e.Working) break;
+
+ // 超时判断
+ if (DateTime.Now > tend)
+ {
+ State = State_e.Timeout;
+ break;
+ }
+ }
+
+ _Port?.Unregister(this);
+ return State == State_e.OK;
+ }
+
+
+ public override bool Process(CanMsg cmsg, ICanPort port)
+ {
+ var msg = new CanPacket(cmsg);
+ // base.Process(msg, port);
+ var pkt = msg.ToPacket<SDO_WritePacket>();
+ if (pkt.Error)
+ {
+ State = State_e.Error;
+ return true;
+ }
+
+ if (!Packet.Continue(T))
+ {
+ State = State_e.OK;
+ return true;
+ }
+
+ T = !T;
+ _Port?.Send(Packet.ToCanMsg());
+ return true;
+ }
+
+
+ #region 日志
+
+ /// <summary>日志</summary>
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>写日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteLog(String format, params Object[] args) => Log?.Info($"Upgrade {format}", args);
+
+ #endregion
+ }
+}
diff --git a/A1Lib/CanOpen/SdoPacket.cs b/A1Lib/CanOpen/SdoPacket.cs
new file mode 100644
index 0000000..6cbc746
--- /dev/null
+++ b/A1Lib/CanOpen/SdoPacket.cs
@@ -0,0 +1,165 @@
+using ALib.Can;
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.CanOpen
+{
+ public class SDO_Base : CanPacket
+ {
+ #region byte 0
+
+ /// <summary>数据包类型</summary>
+ public enum xCs_e
+ {
+ Error = 4,
+ /// <summary>向SDO上传附带数据(快速)。 发送2,应答2</summary>
+ UploadExpedited = 2,
+ /// <summary>向SDO上传 常规模式。 发送3,应答0</summary>
+ UploadNormal = 3,
+ /// <summary>SDO应答 常规模式。 发送3,应答0</summary>
+ UploadRes = 0,
+ }
+
+ /// <summary>bit7 6 5。 xCs_e </summary>
+ public int xCs = 1;
+ /// <summary>bit4 。翻转位,根上一个数据包相关,收发需要一致</summary>
+ public bool T = false;
+
+ /// <summary>bit 2,3 。值 = 4 - Payload.Length </summary>
+ public int BytesNotData = 0;
+ /// <summary>bit1。 传输类型, 正常(后续包附带) / 加速(本数据包携带)</summary>
+ public bool TransferTypeFast = false;
+ /// <summary>bit0。 包内含 Payload 数据</summary>
+ public bool SizeIndicator = false;
+
+ /// <summary>处理 byte0</summary>
+ /// <returns></returns>
+ protected byte Byte0
+ {
+ get
+ {
+ int _byte0 = 0;
+ _byte0 |= (xCs << 5);
+ if (T) _byte0 |= 0x10;
+ // if (TransferTypeFast)
+ _byte0 |= ((BytesNotData & 3) << 2);
+ if (TransferTypeFast) _byte0 |= 0x02;
+ if (SizeIndicator) _byte0 |= 0x01;
+
+ return (byte)_byte0;
+ }
+ set
+ {
+ xCs = value >> 5;
+ T = (value & 0x10) > 0;
+ BytesNotData = (value >> 2) & 3;
+ TransferTypeFast = (value & 2) > 0;
+ SizeIndicator = (value & 1) > 0;
+ }
+ }
+
+ #endregion
+
+ #region byte 1 2 3
+
+ /// <summary>索引, ushort 类型</summary>
+ public int Index = 0;
+ /// <summary>子索引, byte 类型</summary>
+ public int SubIndex = 0;
+
+ #endregion
+
+ #region other
+
+ /// <summary>TransferTypeFast=true。
+ /// 负载数据,最大4字节</summary>
+ public byte[] Payload = new byte[4];
+ /// <summary>TransferTypeFast=false,SizeIndicator=true。
+ /// 给定大小,后续数据包负责传输</summary>
+ public int Length;
+
+ #endregion
+
+ protected override void OnReadPkt(CanPacket pkt)
+ {
+ Byte0 = Data[0];
+ Index = Data[2] * 256 + Data[1];
+ SubIndex = Data[3];
+
+ // if (TransferTypeFast && SizeIndicator)
+ // {
+ // Payload = Data.ReadBytes(4, 4 - BytesNotData);
+ // }
+ // if (SizeIndicator && (!TransferTypeFast))
+ // {
+ // Length = Data.ReadBytes(4, 4).ToInt();
+ // }
+ }
+
+ protected override void WritePkt()
+ {
+ byte _byte0 = Byte0;
+
+ // 60 70 读数据的 ACK 包
+ if ((_byte0 == 0x60) || (_byte0 == 0x70))
+ {
+ Data = new byte[4];
+ Data[0] = _byte0;
+
+ base.WritePkt();
+ return;
+ }
+
+ // 正常传输内容
+ var ms = new MemoryStream(8);
+ ms.WriteByte(_byte0);
+ ms.WriteByte((byte)Index);
+ ms.WriteByte((byte)(Index >> 8));
+ ms.WriteByte((byte)(SubIndex));
+ if (TransferTypeFast && SizeIndicator)
+ {
+ if (BytesNotData < 4) { ms.Write(Payload.ReadBytes(0, 4 - BytesNotData)); }
+ }
+
+ Data = ms.ToArray();
+ base.WritePkt();
+ }
+
+
+ /// <summary>启动SDO读</summary>
+ /// <param name="nodId"></param>
+ /// <param name="index"></param>
+ /// <param name="subidx"></param>
+ /// <returns></returns>
+ public static SDO_Base SDO_ReadInit(int nodId, int index, int subidx)
+ {
+ var pkt = new SDO_Base();
+ pkt.COB_ID = 0x600 + nodId;
+ // xCs=1
+ pkt.Byte0 = 0x40;
+ pkt.Index = index;
+ pkt.SubIndex = subidx;
+
+ return pkt;
+ }
+
+ /// <summary>读操作,分片传输的中间ACK(客户端发送)</summary>
+ /// <param name="nodId"></param>
+ /// <param name="t"></param>
+ /// <returns></returns>
+ public static SDO_Base SDO_ReadContinue(int nodId, bool t)
+ {
+ var pkt = new SDO_Base();
+
+ pkt.COB_ID = 0x600 + nodId;
+ // xCs=3 T=t
+ pkt.Byte0 = (byte)(t ? 0x60 : 0x70);
+ return pkt;
+ }
+ }
+}
diff --git a/A1Lib/CircularQueue.cs b/A1Lib/CircularQueue.cs
new file mode 100644
index 0000000..71aaa0d
--- /dev/null
+++ b/A1Lib/CircularQueue.cs
@@ -0,0 +1,456 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Security.Permissions;
+using System.Text;
+using NewLife.Collections;
+
+namespace ALib
+{
+ /// <summary>环形队列接口</summary>
+ public interface ICircularQueue<T>
+ {
+ /// <summary>进队</summary>
+ /// <param name="item"></param>
+ /// <param name="expan">是否扩容</param>
+ /// <returns></returns>
+ public bool Enqueue(T item, bool expan = false);
+
+ /// <summary>出队</summary>
+ /// <returns></returns>
+ public T Dequeue();
+
+ /// <summary>设置容量</summary>
+ /// <param name="size"></param>
+ /// <returns></returns>
+ public bool SetCapacity(int size);
+
+ /// <summary>丢弃,单向的。</summary>
+ /// <remarks>环形队列的特殊性,seek出来的位置会被head写入。必须不能倒退</remarks>
+ /// <param name="capacity">负值清空所有</param>
+ public void Seek(int capacity);
+
+ /// <summary>序列化为数组,队列内容不丢失</summary>
+ /// <returns></returns>
+ public T[] ToArray();
+
+ /// <summary>进队多个</summary>
+ /// <param name="item"></param>
+ /// <param name="expan">是否扩容</param>
+ /// <returns></returns>
+ public bool WriteArray(T[] item, bool expan = false);
+
+ /// <summary>出队多个</summary>
+ /// <param name="size"></param>
+ /// <returns></returns>
+ public T[] ReadArray(int size = -1);
+ }
+
+ /// <summary>环形队列,不带锁</summary>
+ /// <remarks>写、读 不冲突。多读/多写 冲突。</remarks>
+ /// <typeparam name="T"></typeparam>
+ public class CircularQueue<T> : ICircularQueue<T>
+ {
+ /// <summary>内部缓冲</summary>
+ protected T[] _queue;
+ /// <summary>头,写入下标</summary>
+ protected volatile int _head;
+ /// <summary>尾,读取下标</summary>
+ protected volatile int _tail;
+
+ // <summary>不使用 count 变量维护内部数量,避免读写操作count变量造成竞争</summary>
+ // protected int _count;
+
+ /// <summary>是否为基元类型,决定是否可用 Buffer.BlockCopy</summary>
+ protected static readonly bool _isPrimitive = typeof(T).IsPrimitive;
+
+ /// <summary>每个元素的字节大小,用于 Buffer.BlockCopy</summary>
+ protected static readonly int _sizeOfT = _isPrimitive ? System.Runtime.InteropServices.Marshal.SizeOf<T>() : -1;
+
+ /// <summary>复制数组,根据类型选择最佳方式</summary>
+ private void CopyArray(T[] src, int srcIdx, T[] dst, int dstIdx, int len)
+ {
+ if (_isPrimitive)
+ {
+ Buffer.BlockCopy(src, srcIdx * _sizeOfT, dst, dstIdx * _sizeOfT, len * _sizeOfT);
+ }
+ else
+ {
+ Array.Copy(src, srcIdx, dst, dstIdx, len);
+ }
+ }
+
+ /// <summary>构造</summary>
+ /// <param name="capacity">最少32个吧</param>
+ public CircularQueue(int capacity = -1)
+ {
+ if (capacity < 8) capacity = 8;
+
+ // 必须留一个空位置,否则无法使用 _head _tial 区分空和满。
+ _queue = new T[capacity + 1];
+ _head = 0;
+ _tail = 0;
+ }
+
+ /// <summary>清空内容</summary>
+ public void Clear()
+ {
+ lock (this)
+ {
+ Array.Clear(_queue, 0, _queue.Length);
+
+ _head = 0;
+ _tail = 0;
+ }
+ }
+
+ /// <summary>队列内容量</summary>
+ /// <returns></returns>
+ public int Count()
+ {
+ // 变量copy处理,避免计算过程中其他线程修改。
+ var head = _head;
+ var tail = _tail;
+
+ if (head >= tail) return head - tail;
+
+ return head + _queue.Length - tail;
+ }
+
+ /// <summary>剩余空间</summary>
+ /// <returns></returns>
+ public int Remain()
+ {
+ // 必须留一个空位置,否则无法使用 _head _tial 区分空和满。
+ return _queue.Length - Count() - 1;
+ }
+
+ /// <summary>空</summary>
+ public bool IsEmpty()
+ {
+ return _tail == _head;
+ }
+
+ /// <summary>满的</summary>
+ public bool IsFull()
+ {
+ return Remain() == 0;
+ }
+
+ /// <summary>进队</summary>
+ /// <param name="item"></param>
+ /// <param name="expan">是否扩容</param>
+ /// <returns></returns>
+ public virtual bool Enqueue(T item, bool expan = false)
+ {
+ if (IsFull())
+ {
+ if (!expan) return false;
+ // 扩容一倍
+ SetCapacity((_queue.Length - 1) * 2);
+ }
+
+ _queue[_head] = item;
+
+ var head = _head + 1;
+ // _head = head % _queue.Length;
+ // 语法上看,取余是最好的写法。但是执行效率不一定合适。
+ // 如果是ARM处理器,取余/除法 执行效率都不高。
+ // 还是加减法吧。
+ if (head < _queue.Length)
+ _head = head;
+ else
+ _head = head - _queue.Length;
+
+ return true;
+ }
+
+ /// <summary>出队</summary>
+ /// <returns></returns>
+ public virtual T Dequeue()
+ {
+ if (IsEmpty()) return default(T);
+
+ var temp = _queue[_tail];
+ _queue[_tail] = default(T);
+
+ var tail = _tail + 1;
+ // _tail = tail % _queue.Length;
+ // 语法上看,取余是最好的写法。但是执行效率不一定合适。
+ // 如果是ARM处理器,取余/除法 执行效率都不高。
+ // 还是加减法吧。
+ if (tail < _queue.Length)
+ _tail = tail;
+ else
+ _tail = tail - _queue.Length;
+
+ return temp;
+ }
+
+ /// <summary>读一个,但不出队</summary>
+ /// <returns></returns>
+ public T Peek()
+ {
+ if (IsEmpty()) return default(T);
+
+ return _queue[_tail];
+ }
+
+ /// <summary>读多个,但不出队列</summary>
+ /// <param name="size"></param>
+ /// <returns></returns>
+ public T[] PeekArray(int size = -1)
+ {
+ var cnt = Count();
+ var len = (size == -1) ? cnt : size;
+ if (len > cnt) len = cnt;
+
+ if (len == 0) return new T[0];
+
+ // 变量copy处理,避免计算过程中其他线程修改。
+ var tail = _tail;
+ var result = new T[len];
+
+ if (tail + len <= _queue.Length)
+ {
+ CopyArray(_queue, tail, result, 0, len);
+ }
+ else
+ {
+ // _queue 后半截
+ var cplen = _queue.Length - tail;
+ CopyArray(_queue, tail, result, 0, cplen);
+
+ // _queue 前半截
+ var cplen2 = len - cplen;
+ CopyArray(_queue, 0, result, cplen, cplen2);
+ }
+
+ return result;
+ }
+
+
+ /// <summary>读最后写入的一个,不出队(不允许从_head出队)</summary>
+ /// <returns></returns>
+ public T PeekHead()
+ {
+ if (IsEmpty()) return default(T);
+
+ var n = _queue.Length;
+ var idx = _head - 1;
+ if (idx < 0) idx += n;
+
+ return _queue[idx];
+ }
+
+ /// <summary>读最后最后写入的多个,不出队(不允许从_head出队)</summary>
+ /// <param name="size"></param>
+ /// <returns></returns>
+ public T[] PeekHeadArray(int size = -1)
+ {
+ var cnt = Count();
+ var len = (size == -1) ? cnt : size;
+ if (len > cnt) len = cnt;
+ if (len == 0) return new T[0];
+
+ var n = _queue.Length;
+ // 最近写入的元素在 (_head - 1),窗口起点(最旧的一个)为 (_head - len)
+ var start = _head - len;
+ if (start < 0) start += n;
+
+ var result = new T[len];
+
+ if (start + len <= n)
+ {
+ CopyArray(_queue, start, result, 0, len);
+ }
+ else
+ {
+ var cplen = n - start;
+ CopyArray(_queue, start, result, 0, cplen);
+
+ var cplen2 = len - cplen;
+ CopyArray(_queue, 0, result, cplen, cplen2);
+ }
+
+ return result;
+ }
+
+ /// <summary>进队多个</summary>
+ /// <param name="item"></param>
+ /// <param name="expan">是否扩容</param>
+ /// <returns></returns>
+ public virtual bool WriteArray(T[] item, bool expan = false)
+ {
+ var remian = Remain();
+ var wrlen = item.Length;
+ if (remian < wrlen)
+ {
+ if (!expan) return false;
+
+ // 计算扩容到多少才够
+ var needlen = Count() + wrlen;
+ var len = _queue.Length - 1;
+ while (true)
+ {
+ len *= 2;
+ if (needlen < len) break;
+ }
+
+ SetCapacity(len);
+ }
+
+ var head = _head;
+ if (head + wrlen <= _queue.Length)
+ {
+ CopyArray(item, 0, _queue, head, wrlen);
+ }
+ else
+ {
+ // 后半部
+ var cplen = _queue.Length - head;
+ CopyArray(item, 0, _queue, head, cplen);
+ // 前半部
+ var cplen2 = wrlen - cplen;
+ CopyArray(item, cplen, _queue, 0, cplen2);
+ }
+
+ head += wrlen;
+ // _head = head % _queue.Length;
+ // 语法上看,取余是最好的写法。但是执行效率不一定合适。
+ // 如果是ARM处理器,取余/除法 执行效率都不高。
+ // 还是加减法吧。
+ if (head < _queue.Length)
+ _head = head;
+ else
+ _head = head - _queue.Length;
+
+ return true;
+ }
+
+ /// <summary>出队多个</summary>
+ /// <param name="size"></param>
+ /// <returns></returns>
+ public virtual T[] ReadArray(int size = -1)
+ {
+ var result = PeekArray(size);
+
+ var tail = _tail;
+ var len = result.Length;
+
+ tail += len;
+ // _tail = tail % _queue.Length;
+ // 语法上看,取余是最好的写法。但是执行效率不一定合适。
+ // 如果是ARM处理器,取余/除法 执行效率都不高。
+ // 还是加减法吧。
+ if (tail < _queue.Length)
+ _tail = tail;
+ else
+ _tail = tail - _queue.Length;
+
+ return result;
+ }
+
+ /// <summary>设置容量</summary>
+ /// <param name="size"></param>
+ /// <returns></returns>
+ public bool SetCapacity(int size)
+ {
+ lock (this)
+ {
+ var cnt = Count();
+ // 设置容量不能小于当前队列内容大小。
+ if (size < cnt) return false;
+
+ // 读出内容
+ var all = ToArray();
+
+ // 新建缓冲
+ // 必须+1留一个空位置,否则无法使用 _head _tial 区分空和满。
+ var queue = new T[size + 1];
+ // 放入新缓冲
+ CopyArray(all, 0, queue, 0, all.Length);
+
+ // 更新 queue tail head
+ _queue = queue;
+ _tail = 0;
+ _head = all.Length;
+ }
+
+ return true;
+ }
+
+ /// <summary>丢弃,单向的。</summary>
+ /// <remarks>环形队列的特殊性,seek出来的位置会被head写入。必须不能倒退</remarks>
+ /// <param name="capacity">负值清空所有</param>
+ public virtual void Seek(int capacity)
+ {
+ if (capacity < 0)
+ {
+ Clear();
+ return;
+ }
+
+ var cnt = Count();
+ var len = cnt > capacity ? capacity : cnt;
+ if (len == 0) return;
+
+ // 变量copy处理,避免计算过程中其他线程修改。
+ var tail = _tail;
+ if (tail + len <= _queue.Length)
+ {
+ Array.Clear(_queue, tail, len);
+ }
+ else
+ {
+ // _queue 后半截
+ var cplen = _queue.Length - tail;
+ Array.Clear(_queue, tail, cplen);
+
+ // _queue 前半截
+ var cplen2 = len - cplen;
+ Array.Clear(_queue, 0, cplen2);
+ }
+
+ tail += len;
+ // _tail = tail % _queue.Length;
+ // 语法上看,取余是最好的写法。但是执行效率不一定合适。
+ // 如果是ARM处理器,取余/除法 执行效率都不高。
+ // 还是加减法吧。
+ if (tail < _queue.Length)
+ _tail = tail;
+ else
+ _tail = tail - _queue.Length;
+ }
+
+ /// <summary>序列化为数组,队列内容不丢失</summary>
+ /// <returns></returns>
+ public virtual T[] ToArray()
+ {
+ var len = Count();
+ if (len == 0) return new T[0];
+
+ // 变量copy处理,避免计算过程中其他线程修改。
+ var tail = _tail;
+ var result = new T[len];
+
+ if (tail + len <= _queue.Length)
+ {
+ CopyArray(_queue, tail, result, 0, len);
+ }
+ else
+ {
+ // _queue 后半截
+ var cplen = _queue.Length - tail;
+ CopyArray(_queue, tail, result, 0, cplen);
+
+ // _queue 前半截
+ var cplen2 = len - cplen;
+ CopyArray(_queue, 0, result, cplen, cplen2);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/A1Lib/ComPort/Com_WithUnpack.cs b/A1Lib/ComPort/Com_WithUnpack.cs
new file mode 100644
index 0000000..a215d43
--- /dev/null
+++ b/A1Lib/ComPort/Com_WithUnpack.cs
@@ -0,0 +1,126 @@
+using NewLife;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.ComPort
+{
+ public abstract class Com_WithUnpack : DisposeBase
+ {
+ /// <summary>消息命令</summary>
+ public CircularQueue<byte[]> RxMsg { get; set; } = new CircularQueue<byte[]>(128);
+ /// <summary>收数据缓冲</summary>
+ public CircularQueue<byte> RxCache { get; set; } = new CircularQueue<byte>(256);
+
+ private SerialTransport _Port = new SerialTransport();
+
+ public Com_WithUnpack(String comName, int baudrate = 115200)
+ {
+ _Port.PortName = comName;
+ _Port.BaudRate = baudrate;
+ }
+
+ public Com_WithUnpack() { }
+
+ public void Init(String comName, int baudrate = 115200)
+ {
+ _Port.PortName = comName;
+ _Port.BaudRate = baudrate;
+ }
+
+
+ #region 开关析构
+
+ private void _Port_Disconnected(object sender, EventArgs e)
+ {
+ // throw new NotImplementedException();
+ RxCache.Clear();
+ WriteLog("串口断开连接");
+ }
+
+ public bool Open()
+ {
+ _Port.Disconnected += _Port_Disconnected;
+ _Port.Received += _Port_Received;
+ return _Port.Open();
+ }
+
+ public void Close()
+ {
+ _Port.Received -= _Port_Received;
+ _Port.Disconnected -= _Port_Disconnected;
+ _Port.Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ Close();
+ }
+
+ /// <summary>收数据</summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ private void _Port_Received(object sender, NewLife.Net.ReceivedEventArgs e)
+ {
+ var bs = e.GetBytes();
+ WriteLog(" RX " + bs.ToHex());
+
+ RxCache.WriteArray(bs, true);
+ while (true)
+ {
+ var pk = TryUnpack(RxCache);
+ if (pk == null) break;
+ if (pk.Length == 0) break;
+
+ if (RxMsg.Remain() < 1) RxMsg.Seek(1);
+ RxMsg.Enqueue(pk);
+
+ // 所有消息都处理抛
+ MsgReceived?.Invoke(this, pk);
+ }
+
+ if (RxCache.IsEmpty()) RxCache.Clear();
+ }
+
+ #endregion
+
+ /// <summary>
+ /// 拆包,需要丢弃不合法数据,别被塞死。
+ /// 返回有效包,返回null表示一条完整的都没有,结束拆包。
+ /// 需要回复ACK的时候直接Send发送。
+ /// </summary>
+ public abstract byte[] TryUnpack(CircularQueue<byte> queue);
+
+ /// <summary>收到消息事件</summary>
+ public event EventHandler<byte[]> MsgReceived;
+
+ /// <summary>清空接收数据包</summary>
+ public void RxMsgClear() => RxMsg.Clear();
+
+ /// <summary>发送</summary>
+ /// <param name="msg"></param>
+ /// <param name="log"></param>
+ /// <returns></returns>
+ public bool Send(byte[] msg, bool log = true)
+ {
+ if (log) WriteLog(" TX " + msg.ToHex());
+ lock (_Port) { return _Port.Send(msg) == msg.Length; }
+ }
+
+ #region 日志
+
+ /// <summary>日志前缀</summary>
+ public String LogPre = " ";
+
+ public ILog Log { get; set; }
+
+ void WriteLog(String format, params Object[] args) => Log?.Write(LogLevel.Info, LogPre + format, args);
+
+ #endregion
+ }
+}
diff --git a/A1Lib/ComPort/ComHelper.cs b/A1Lib/ComPort/ComHelper.cs
new file mode 100644
index 0000000..3b13edf
--- /dev/null
+++ b/A1Lib/ComPort/ComHelper.cs
@@ -0,0 +1,174 @@
+using ALib.UsbHub;
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Management;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.ComPort
+{
+ public static class ComHelper
+ {
+ /// <summary>获取所有COM,多串口的复合设备COM顺序是按照子设备MI排序的</summary>
+ /// <returns>ContainerId,Name</returns>
+ public static Dictionary<string, string> GetAllCom()
+ {
+ var dic = new Dictionary<string, string>();
+ var midic = new Dictionary<string, List<(int, string)>>();
+
+ // 使用 WMI 查询 Win32_PnPEntity 中所有 COM 口
+ ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE Name LIKE '%(COM%)'");
+ foreach (ManagementObject device in searcher.Get())
+ {
+ var did = device["DeviceID"] as string;
+ var key = UsbHelper.GetContainerId(did);
+ var value = device["Name"] as string;
+
+ if ((key == null) || (value == null)) continue;
+
+ if (did.IndexOf("&MI_") > 0)
+ {
+ var mi = -1;
+ var miStr = did.Split('&', '\\').FirstOrDefault(s => s.StartsWith("MI_"));
+ mi = miStr != null ? miStr.TrimStart("MI_").ToInt() : -1;
+
+ List<(int, string)> list;
+ if (midic.ContainsKey(key))
+ {
+ list = midic[key];
+ list.Add((mi, value));
+ }
+ else
+ {
+ list = new List<(int, string)>();
+ list.Add((mi, value));
+ midic[key] = list;
+ }
+ }
+ else
+ {
+ if (dic.ContainsKey(key))
+ {
+ var oldStr = dic[key];
+ var newStr = oldStr + " + " + value;
+ dic[key] = newStr;
+ }
+ else
+ {
+ dic.Add(key, value);
+ }
+ }
+ }
+
+ if (midic.Count > 0)
+ {
+ foreach (var item in midic)
+ {
+ var list = item.Value.OrderBy(x => x.Item1).ToList();
+ var outstr = string.Join(" + ", list.Select(x => x.Item2));
+ dic[item.Key] = outstr;
+ }
+ }
+
+ return dic;
+ }
+
+ /// <summary>获取串口名称</summary>
+ /// <param name="idx">COM口顺序号,不是COM口的不计入</param>
+ /// <returns>不是串口返回 ""</returns>
+ public static String GetComName(this UsbDevice dev, int idx = 0)
+ {
+ var str = dev.ToString();
+
+ if (str.IndexOf("(COM") < 0)
+ {
+ // FTDIBUS
+ if (dev.VID == "0403")
+ {
+ var all = GetAllCom();
+ if (all.ContainsKey(dev.ContainerId)) str = all[dev.ContainerId];
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ var strs = new List<string>();
+ var offset = 0;
+ while (true)
+ {
+ var head = str.IndexOf("(COM", offset);
+ if (head < 0) break;
+
+ var tail = str.IndexOf(")", head);
+
+ var comName = str.Substring(head + 1, tail - head - 1);
+ strs.Add(comName);
+ offset = tail + 1;
+ }
+
+ if (strs.Count < 1) return "";
+
+ return strs[idx];
+ }
+
+ /// <summary>从HUB端口,获取串口名称</summary>
+ /// <param name="dev"></param>
+ /// <param name="idx"></param>
+ /// <returns></returns>
+ public static String GetComName(this UsbHubPort dev)
+ {
+ if (dev == null) return "";
+ if (!dev.Exists()) return "";
+ return GetComName(dev.GetUsbDevice(), dev.SubDev);
+ }
+
+ /// <summary>从USB创建SerialTransport</summary>
+ /// <param name="dev"></param>
+ /// <param name="idx"></param>
+ /// <returns></returns>
+ public static SerialTransport CreateSerialTransport(this UsbDevice dev, int idx = 0)
+ {
+ var com = GetComName(dev, idx);
+ if (!com.StartsWith("COM")) return null;
+
+ var port = new SerialTransport();
+ port.PortName = com;
+
+ return port;
+ }
+
+ /// <summary>创建解码串口</summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="dev"></param>
+ /// <param name="idx"></param>
+ /// <param name="baudrate"></param>
+ /// <returns></returns>
+ public static T Create<T>(this UsbDevice dev, int idx = 0, int baudrate = 115200) where T : Com_WithUnpack, new()
+ {
+ var com = dev.GetComName(idx);
+ if (!com.StartsWith("COM")) return null;
+
+ var t = new T();
+ t.Init(com, baudrate);
+
+ return t;
+ }
+
+ /// <summary>从USB HUB端口创建解码对象</summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="dev"></param>
+ /// <param name="baudrate"></param>
+ /// <returns></returns>
+ public static T Create<T>(this UsbHubPort dev, int baudrate = 115200) where T : Com_WithUnpack, new()
+ {
+ if (!dev.Exists()) return null;
+ return Create<T>(dev.GetUsbDevice(), dev.SubDev, baudrate);
+ }
+ }
+}
diff --git "a/A1Lib/ComPort/FCT \346\211\253\347\240\201\346\236\252.pdf" "b/A1Lib/ComPort/FCT \346\211\253\347\240\201\346\236\252.pdf"
new file mode 100644
index 0000000..16cf729
Binary files /dev/null and "b/A1Lib/ComPort/FCT \346\211\253\347\240\201\346\236\252.pdf" differ
diff --git a/A1Lib/ComPort/Grbl_Com.cs b/A1Lib/ComPort/Grbl_Com.cs
new file mode 100644
index 0000000..5b9a687
--- /dev/null
+++ b/A1Lib/ComPort/Grbl_Com.cs
@@ -0,0 +1,611 @@
+using ALib.UsbHub;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace ALib.ComPort
+{
+ /// <summary>GRBL机器状态枚举</summary>
+ public enum GrblMachineState
+ {
+ /// <summary>空闲状态</summary>
+ Idle,
+ /// <summary>运行状态</summary>
+ Run,
+ /// <summary>暂停状态,急停按键或者!触发,使用~清除</summary>
+ Hold,
+ /// <summary>点动状态</summary>
+ Jog,
+ /// <summary>报警状态,通过$X解锁能到Idle,建议再$H命令归位一下</summary>
+ Alarm,
+ /// <summary>门状态,防护门没关</summary>
+ Door,
+ /// <summary>检查状态</summary>
+ Check,
+ /// <summary>回零状态</summary>
+ Home,
+ /// <summary>睡眠状态</summary>
+ Sleep
+ }
+
+ /// <summary>GRBL状态报告数据</summary>
+ public class GrblStatus
+ {
+ /// <summary>机器状态</summary>
+ public GrblMachineState MachineState { get; set; } = GrblMachineState.Idle;
+
+ /// <summary>子状态</summary>
+ public int SubState { get; set; } = -1;
+
+ /// <summary>MPos 机器位置 (X,Y,Z)</summary>
+ public double[] MachinePosition { get; set; } = new double[3];
+
+ /// <summary>WPos 工作位置 (X,Y,Z)</summary>
+ public double[] WorkPosition { get; set; } = new double[3];
+
+ /// <summary>WCO 工作坐标偏移 (X,Y,Z)</summary>
+ public double[] WorkCoordinateOffset { get; set; } = new double[3];
+
+ /// <summary>缓冲区状态 (可用块数, 可用字节数)</summary>
+ public int[] BufferState { get; set; } = new int[2];
+
+ /// <summary>当前行号</summary>
+ public int LineNumber { get; set; } = -1;
+
+ /// <summary>进给速度</summary>
+ public double FeedRate { get; set; } = 0;
+
+ /// <summary>主轴转速</summary>
+ public double SpindleSpeed { get; set; } = 0;
+
+ /// <summary>输入引脚状态</summary>
+ public string PinState { get; set; } = "";
+
+ /// <summary>进给倍率</summary>
+ public int FeedOverride { get; set; } = 100;
+
+ /// <summary>快速移动倍率</summary>
+ public int RapidOverride { get; set; } = 100;
+
+ /// <summary>主轴倍率</summary>
+ public int SpindleOverride { get; set; } = 100;
+
+ /// <summary>主轴状态</summary>
+ public bool SpindleCW { get; set; } = false;
+
+ /// <summary>主轴逆时针状态</summary>
+ public bool SpindleCCW { get; set; } = false;
+
+ /// <summary>冷却液状态</summary>
+ public bool FloodCoolant { get; set; } = false;
+
+ /// <summary>雾状冷却液状态</summary>
+ public bool MistCoolant { get; set; } = false;
+
+ /// <summary>返回状态信息的字符串表示</summary>
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"机器状态: {MachineState}");
+ if (SubState >= 0)
+ {
+ sb.AppendLine($"子状态: {SubState}");
+ }
+
+ sb.AppendLine($"机器位置: X={MachinePosition[0]:F3}, Y={MachinePosition[1]:F3}, Z={MachinePosition[2]:F3}");
+ sb.AppendLine($"工作位置: X={WorkPosition[0]:F3}, Y={WorkPosition[1]:F3}, Z={WorkPosition[2]:F3}");
+ sb.AppendLine($"工作坐标偏移: X={WorkCoordinateOffset[0]:F3}, Y={WorkCoordinateOffset[1]:F3}, Z={WorkCoordinateOffset[2]:F3}");
+
+ if (BufferState[0] >= 0 || BufferState[1] >= 0)
+ {
+ sb.AppendLine($"缓冲区状态: 可用块数={BufferState[0]}, 可用字节数={BufferState[1]}");
+ }
+
+ if (LineNumber >= 0)
+ {
+ sb.AppendLine($"当前行号: {LineNumber}");
+ }
+
+ sb.AppendLine($"进给速度: {FeedRate:F1}");
+ sb.AppendLine($"主轴转速: {SpindleSpeed:F0} RPM");
+
+ if (!string.IsNullOrEmpty(PinState))
+ {
+ sb.AppendLine($"输入引脚状态: {PinState}");
+ }
+
+ sb.AppendLine($"进给倍率: {FeedOverride}%");
+ sb.AppendLine($"快速移动倍率: {RapidOverride}%");
+ sb.AppendLine($"主轴倍率: {SpindleOverride}%");
+
+ sb.AppendLine($"主轴状态: {(SpindleCW ? "顺时针" : "停止")}{(SpindleCCW ? " + 逆时针" : "")}");
+ sb.AppendLine($"冷却液: {(FloodCoolant ? "液冷开" : "液冷关")}, {(MistCoolant ? "雾冷开" : "雾冷关")}");
+
+ return sb.ToString();
+ }
+ }
+
+ public class Grbl_Com : DisposeBase
+ {
+ public SerialTransport ComPort { get; private set; } = new SerialTransport();
+
+ public GrblStatus CurrStatus;
+
+ public CircularQueue<byte> RxQueue { get; private set; } = new CircularQueue<byte>(256);
+
+ public Grbl_Com(String comName)
+ {
+ ComPort.PortName = comName;
+ ComPort.BaudRate = 115200;
+ ComPort.Timeout = 1;
+ ComPort.ByteTimeout = 10;
+ }
+
+ public Grbl_Com(SerialTransport com)
+ {
+ ComPort = com;
+
+ ComPort.BaudRate = 115200;
+ ComPort.Timeout = 1;
+ ComPort.ByteTimeout = 10;
+ }
+
+ public static Grbl_Com CreateByUsb(UsbDevice dev, int subidx)
+ {
+ var com = ComHelper.CreateSerialTransport(dev, subidx);
+ if (com == null) return null;
+ return new Grbl_Com(com);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ Close();
+ }
+
+ private void ComPort_Received(object sender, NewLife.Net.ReceivedEventArgs e)
+ {
+ RxQueue.WriteArray(e.GetBytes());
+ }
+
+ private async Task<bool> WaitWith(string str, int timout = 100)
+ {
+ var tout = DateTime.Now.AddMilliseconds(timout);
+ while (true)
+ {
+ if (DateTime.Now > tout) return false;
+ await Task.Delay(20);
+
+ if (RxQueue.Count() < str.Length) continue;
+ var bs = RxQueue.ToArray();
+ if (bs.ToStr().IndexOf(str) != -1) return true;
+ }
+ }
+
+ public bool Open()
+ {
+ ComPort.Received += ComPort_Received;
+ return ComPort.Open();
+ }
+
+ public bool Close()
+ {
+ ComPort.Received -= ComPort_Received;
+ return ComPort.Close();
+ }
+
+ /// <summary>发送一行Gcode命令</summary>
+ /// <param name="str"></param>
+ /// <param name="hasRN">有回车</param>
+ /// <param name="tout">超时,ms</param>
+ /// <returns></returns>
+ public async Task<bool> SendGcodeLine(String str, bool hasRN = true, int tout = 200)
+ {
+ // 去除 \r\n \r \n 等几种形式
+ str = str.Replace("\n", "");
+ str = str.Replace("\r", "");
+
+ WriteLog(str);
+
+ // 加上 \n ,确保格式正确
+ if (hasRN) str += '\n';
+
+ RxQueue.Clear();
+ var bs = Encoding.ASCII.GetBytes(str);
+ lock (ComPort) ComPort.Send(bs);
+ if (await WaitWith("ok", tout)) return true;
+ return false;
+ }
+
+ /// <summary>设置参数</summary>
+ /// <param name="idx"></param>
+ /// <param name="param"></param>
+ /// <param name="tout">超时,ms</param>
+ /// <returns></returns>
+ public async Task<bool> SendParam(int idx, int param, int tout = 200)
+ {
+ var cmd = $"${idx}={param}\n";
+ WriteLog(cmd);
+ var bs = Encoding.ASCII.GetBytes(cmd);
+ // var res = await ComPort.SendAsync((ArrayPacket)bs);
+
+ RxQueue.Clear();
+ lock (ComPort) ComPort.Send(bs);
+ if (await WaitWith("ok", tout)) return true;
+ return false;
+ }
+
+ /// <summary>解锁</summary>
+ /// <returns></returns>
+ public async Task<bool> UnLock() => await SendGcodeLine("$X");
+
+ /// <summary>解除Hold</summary>
+ /// <returns></returns>
+ public async Task<bool> Resume() => await SendGcodeLine("~");
+
+ /// <summary>丢弃所有已发送未执行的gcode,并进入Alarm状态</summary>
+ public void Reset()
+ {
+ var bs = new byte[] { 0x18 };
+ lock (ComPort) ComPort.Send(bs);
+ }
+
+ /// <summary>硬重启,GBRL-HAL可能支持,可能会断开USB连接</summary>
+ public void HardwareReset()
+ {
+ var bs = new byte[] { 0x1B, 0x14 };
+ lock (ComPort) ComPort.Send(bs);
+ }
+
+ /// <summary>单轴回零</summary>
+ /// <param name="xyz">轴编号,xyzabc</param>
+ /// <param name="tSec">超时时间</param>
+ /// <returns></returns>
+ public async Task<bool> GoHome(char xyz, int tSec = 30)
+ {
+ var tout = DateTime.Now.AddSeconds(tSec);
+ // 轴名称过滤
+ var chs = "xXyYzZaAbBcC";
+ if (chs.IndexOf(xyz) == -1) return false;
+
+ // 启动
+ var cmd = ("$H" + xyz).ToUpper();
+ if (!await SendGcodeLine(cmd, true, tSec * 1000)) return false;
+
+ // 以获取状态成功作为结束
+ if (await GetStatusReport() != null) return true;
+ return false;
+ }
+
+ /// <summary>多轴回零</summary>
+ /// <param name="xyz">回零的坐标轴,字符串 XYZABC 进行组合,默认X</param>
+ /// <param name="tSec">每轴超时时间,秒</param>
+ /// <returns></returns>
+ public async Task<bool> GoHome(String xyz = "X", int tSec = 30)
+ {
+ if (xyz.IsNullOrEmpty()) return true;
+
+ foreach (var ch in xyz)
+ {
+ if (!await GoHome(ch, tSec)) return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>获取GRBL状态报告 (?命令)</summary>
+ /// <returns>状态报告对象,失败返回null</returns>
+ public async Task<GrblStatus> GetStatusReport()
+ {
+ // 发送?命令(不需要换行符)
+ var queryCmd = "?";
+ var bs = Encoding.ASCII.GetBytes(queryCmd);
+
+ RxQueue.Clear();
+ lock (ComPort) ComPort.Send(bs);
+ if (await WaitWith(">", 400))
+ {
+ var resStr = RxQueue.ToArray().ToStr();
+
+ int hidx = resStr.IndexOf('<');
+ if (hidx < 0) return null;
+ int tidx = resStr.IndexOf('>');
+ if (tidx < 0) return null;
+
+ resStr = resStr.Substring(hidx, tidx - hidx + 1);
+ WriteLog(" " + resStr);
+ return ParseStatusReport(resStr);
+ }
+
+ return null;
+ }
+
+ #region ?命令解码
+ /// <summary>解析状态报告字符串</summary>
+ /// <param name="statusStr">状态报告字符串</param>
+ /// <returns>状态报告对象</returns>
+ private GrblStatus ParseStatusReport(string statusStr)
+ {
+ if (CurrStatus == null) CurrStatus = new GrblStatus();
+ var status = CurrStatus;
+
+ try
+ {
+ // 移除<>符号
+ string content = statusStr.Trim('<', '>');
+
+ // 按|分割各个字段
+ string[] fields = content.Split('|');
+
+ foreach (string field in fields)
+ {
+ if (string.IsNullOrEmpty(field)) continue;
+
+ // 机器状态字段(第一个字段,可能包含子状态)
+ if (field.Contains("Idle") || field.Contains("Run") || field.Contains("Hold") ||
+ field.Contains("Jog") || field.Contains("Alarm") || field.Contains("Door") ||
+ field.Contains("Check") || field.Contains("Home") || field.Contains("Sleep"))
+ {
+ ParseMachineState(status, field);
+ }
+ // 位置字段
+ else if (field.StartsWith("MPos:"))
+ {
+ ParsePosition(status.MachinePosition, field.Substring(5));
+ }
+ else if (field.StartsWith("WPos:"))
+ {
+ ParsePosition(status.WorkPosition, field.Substring(5));
+ }
+ else if (field.StartsWith("WCO:"))
+ {
+ ParsePosition(status.WorkCoordinateOffset, field.Substring(4));
+ }
+ // 缓冲区字段
+ else if (field.StartsWith("Bf:"))
+ {
+ ParseBufferState(status, field.Substring(3));
+ }
+ // 行号字段
+ else if (field.StartsWith("Ln:"))
+ {
+ if (int.TryParse(field.Substring(3), out int lineNum))
+ {
+ status.LineNumber = lineNum;
+ }
+ }
+ // 进给和主轴速度字段
+ else if (field.StartsWith("F:"))
+ {
+ if (double.TryParse(field.Substring(2), out double feedRate))
+ {
+ status.FeedRate = feedRate;
+ }
+ }
+ else if (field.StartsWith("FS:"))
+ {
+ string[] fsValues = field.Substring(3).Split(',');
+ if (fsValues.Length >= 2)
+ {
+ if (double.TryParse(fsValues[0], out double feedRate)) status.FeedRate = feedRate;
+ if (double.TryParse(fsValues[1], out double spindleSpeed)) status.SpindleSpeed = spindleSpeed;
+ }
+ }
+ // 引脚状态字段
+ else if (field.StartsWith("Pn:"))
+ {
+ status.PinState = field.Substring(3);
+ }
+ // 倍率字段
+ else if (field.StartsWith("Ov:"))
+ {
+ string[] ovValues = field.Substring(3).Split(',');
+ if (ovValues.Length >= 3)
+ {
+ if (int.TryParse(ovValues[0], out int feedOv)) status.FeedOverride = feedOv;
+ if (int.TryParse(ovValues[1], out int rapidOv)) status.RapidOverride = rapidOv;
+ if (int.TryParse(ovValues[2], out int spindleOv)) status.SpindleOverride = spindleOv;
+ }
+ }
+ // 附件状态字段
+ else if (field.StartsWith("A:"))
+ {
+ ParseAccessoryState(status, field.Substring(2));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // 解析出错,返回部分解析的结果
+ XTrace.WriteLine($"解析GRBL状态报告出错: {ex.Message}");
+ }
+
+ return status;
+ }
+
+ /// <summary>解析机器状态</summary>
+ private void ParseMachineState(GrblStatus status, string stateStr)
+ {
+ // 检查是否包含子状态(格式:State:SubState)
+ if (stateStr.Contains(":"))
+ {
+ string[] parts = stateStr.Split(':');
+ status.MachineState = ParseMachineStateEnum(parts[0]);
+ if (parts.Length > 1 && int.TryParse(parts[1], out int subState))
+ {
+ status.SubState = subState;
+ }
+ }
+ else
+ {
+ status.MachineState = ParseMachineStateEnum(stateStr);
+ }
+ }
+
+ /// <summary>将状态字符串转换为枚举</summary>
+ private GrblMachineState ParseMachineStateEnum(string stateStr)
+ {
+ return stateStr switch
+ {
+ "Idle" => GrblMachineState.Idle,
+ "Run" => GrblMachineState.Run,
+ "Hold" => GrblMachineState.Hold,
+ "Jog" => GrblMachineState.Jog,
+ "Alarm" => GrblMachineState.Alarm,
+ "Door" => GrblMachineState.Door,
+ "Check" => GrblMachineState.Check,
+ "Home" => GrblMachineState.Home,
+ "Sleep" => GrblMachineState.Sleep,
+ _ => GrblMachineState.Idle
+ };
+ }
+
+ /// <summary>解析位置信息</summary>
+ private void ParsePosition(double[] position, string posStr)
+ {
+ string[] coords = posStr.Split(',');
+ for (int i = 0; i < Math.Min(coords.Length, 3); i++)
+ {
+ if (double.TryParse(coords[i], out double value))
+ {
+ position[i] = value;
+ }
+ }
+ }
+
+ /// <summary>解析缓冲区状态</summary>
+ private void ParseBufferState(GrblStatus status, string bfStr)
+ {
+ string[] bfValues = bfStr.Split(',');
+ if (bfValues.Length >= 2)
+ {
+ if (int.TryParse(bfValues[0], out int val1)) status.BufferState[0] = val1;
+ if (int.TryParse(bfValues[1], out int val2)) status.BufferState[1] = val2;
+ }
+ }
+
+ /// <summary>解析附件状态</summary>
+ private void ParseAccessoryState(GrblStatus status, string accStr)
+ {
+ status.SpindleCW = accStr.Contains("S");
+ status.SpindleCCW = accStr.Contains("C");
+ status.FloodCoolant = accStr.Contains("F");
+ status.MistCoolant = accStr.Contains("M");
+ }
+ #endregion
+
+ #region G 指令发送
+
+ /// <summary>快速移动过去</summary>
+ /// <param name="p"></param>
+ /// <param name="speed"></param>
+ /// <returns></returns>
+ public async Task<bool> G0_Point(PlaneCoordinate p, double speed = 0)
+ {
+ string gcode = "";
+ if (speed < 1) gcode = $"G0 X{p.X:F4} Y{p.Y:F4}";
+ gcode = $"G0 X{p.X:F4} Y{p.Y:F4} F{speed}";
+
+ return await SendGcodeLine(gcode);
+ }
+
+ /// <summary>切削过去</summary>
+ /// <param name="p"></param>
+ /// <param name="speed"></param>
+ /// <returns></returns>
+ public async Task<bool> G1_Point(PlaneCoordinate p, double speed = 5000)
+ {
+ string gcode = $"G1 X{p.X:F4} Y{p.Y:F4} F{speed}";
+ return await SendGcodeLine(gcode);
+ }
+
+ /// <summary>延迟</summary>
+ /// <param name="s">秒</param>
+ /// <returns></returns>
+ public async Task<bool> G4_Delay(double s)
+ {
+ // 遵循RS274/NGC标准,P单位是秒
+ string gcode = $"G4 P{s:F3}";
+ return await SendGcodeLine(gcode);
+ }
+
+ #endregion
+
+ #region GCODE + 完成
+
+ /// <summary>执行到完成退出</summary>
+ /// <param name="p">目标</param>
+ /// <param name="speed">速度</param>
+ /// <param name="tsec">等待时间</param>
+ /// <returns></returns>
+ public async Task<bool> WaitPoint_G1(PlaneCoordinate p, double speed = 800, int tsec = 10)
+ {
+ var tout = DateTime.Now.AddSeconds(tsec);
+
+ // 确保机器空闲
+ for (int i = 0; i < int.MaxValue; i++)
+ {
+ if (DateTime.Now > tout) return false;
+ if (i > 0) await Task.Delay(300);
+
+ // 获取当前坐标
+ var info2 = await GetStatusReport();
+ if (info2 == null) continue;
+ if (info2.MachineState != GrblMachineState.Idle) continue;
+
+ break;
+ }
+
+ // 坐标判断
+ var info = CurrStatus;
+ if ((Math.Abs(p.X - info.MachinePosition[0]) > 1) ||
+ (Math.Abs(p.Y - info.MachinePosition[1]) > 1))
+ {
+ // 运动
+ await G1_Point(p);
+ }
+
+ // 等待运行到目的地
+ for (int i = 0; i < int.MaxValue; i++)
+ {
+ if (DateTime.Now > tout) return false;
+ if (i > 0) await Task.Delay(300);
+
+ // 获取当前坐标
+ var info2 = await GetStatusReport();
+ if (info2 == null) continue;
+ if (info2.MachineState != GrblMachineState.Idle) continue;
+
+ // // 坐标判断
+ // if ((Math.Abs(p.X - info2.MachinePosition[0]) > 1) ||
+ // (Math.Abs(p.Y - info2.MachinePosition[1]) > 1))
+ // {
+ // continue;
+ // }
+
+ break;
+ }
+
+ return true;
+ }
+
+ #endregion
+
+ #region 日志
+
+ /// <summary>日志前缀</summary>
+ public String LogPre = " ";
+
+ public ILog Log { get; set; }
+
+ void WriteLog(String format, params Object[] args) => Log?.Write(LogLevel.Info, format, args);
+
+ #endregion
+ }
+}
diff --git a/A1Lib/ComPort/PlaneCoordinate.cs b/A1Lib/ComPort/PlaneCoordinate.cs
new file mode 100644
index 0000000..4181550
--- /dev/null
+++ b/A1Lib/ComPort/PlaneCoordinate.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.ComPort
+{
+ /// <summary>物理坐标点,单位毫米</summary>
+ public class PlaneCoordinate
+ {
+ public double X { get; set; } = 0.0;
+ public double Y { get; set; } = 0.0;
+
+ public PlaneCoordinate() { }
+
+ public PlaneCoordinate(double x, double y)
+ {
+ X = x;
+ Y = y;
+ }
+
+ /// <summary>加法运算</summary>
+ public static PlaneCoordinate operator +(PlaneCoordinate a, PlaneCoordinate b) => new PlaneCoordinate(a.X + b.X, a.Y + b.Y);
+
+ /// <summary>减法运算</summary>
+ public static PlaneCoordinate operator -(PlaneCoordinate a, PlaneCoordinate b) => new PlaneCoordinate(a.X - b.X, a.Y - b.Y);
+
+ /// <summary>取反运算</summary>
+ public static PlaneCoordinate operator -(PlaneCoordinate a) => new PlaneCoordinate(-a.X, -a.Y);
+
+ /// <summary>数乘运算</summary>
+ public static PlaneCoordinate operator *(double scalar, PlaneCoordinate a) => new PlaneCoordinate(a.X * scalar, a.Y * scalar);
+
+ /// <summary>数除运算</summary>
+ public static PlaneCoordinate operator /(PlaneCoordinate a, double scalar)
+ {
+ if (scalar == 0) throw new DivideByZeroException("Cannot divide PlaneCoordinate by zero");
+ return new PlaneCoordinate(a.X / scalar, a.Y / scalar);
+ }
+
+ /// <summary>相等比较</summary>
+ public static bool operator ==(PlaneCoordinate a, PlaneCoordinate b)
+ {
+ // 物理坐标,小数点后面6位即可。
+ const double epsilon = 1e-6;
+ return Math.Abs(a.X - b.X) < epsilon && Math.Abs(a.Y - b.Y) < epsilon;
+ }
+
+ /// <summary>不等比较</summary>
+ public static bool operator !=(PlaneCoordinate a, PlaneCoordinate b) => !(a == b);
+
+ /// <summary>重写Equals方法</summary>
+ public override bool Equals(object obj) => obj is PlaneCoordinate other && this == other;
+
+ /// <summary>转换为字符串</summary>
+ public override string ToString() => $"({X:F3}, {Y:F3})";
+
+ public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
+ }
+
+ /// <summary>平面物理大小,单位毫米</summary>
+ public struct PhysicalSize
+ {
+ public double Width;
+ public double Height;
+
+ public PhysicalSize(double w, double h)
+ {
+ Width = w;
+ Height = h;
+ }
+ }
+}
diff --git a/A1Lib/ComPort/QrScan.cs b/A1Lib/ComPort/QrScan.cs
new file mode 100644
index 0000000..af4ce3a
--- /dev/null
+++ b/A1Lib/ComPort/QrScan.cs
@@ -0,0 +1,83 @@
+using ALib.ComPort;
+using ALib.UsbHub;
+using NewLife;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib
+{
+ /// <summary>
+ /// 京东条码商城的扫码枪,命令模式读码封装,注意不是刘强东的京东
+ /// 型号 U100/U200 等
+ /// 参数:虚拟串口模式 115200-8n1,命令模式
+ /// </summary>
+ public class QrScan : Com_WithUnpack
+ {
+ public QrScan() { }
+
+ public QrScan(String comName) : base(comName) { }
+
+ /// <summary>读 二维码/条码</summary>
+ /// <returns>返回条码</returns>
+ public String ReadQr()
+ {
+ RxCache.Clear();
+ RxMsgClear();
+
+ // 发送识读指令
+ // 7E 00 08 01 00 02 01 AB CD
+ var cmd = new byte[] { 0x7E, 0x00, 0x08, 0x01, 0x00, 0x02, 0x01, 0xAB, 0xCD };
+ Send(cmd);
+
+ var tend = DateTime.Now.AddSeconds(2);
+ while (true)
+ {
+ if (DateTime.Now > tend) break;
+
+ Task.Delay(50).Wait();
+ if (RxMsg.Count() > 0)
+ {
+ return Encoding.ASCII.GetString(RxMsg.Dequeue());
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>解包</summary>
+ /// <param name="queue"></param>
+ /// <returns></returns>
+ public override byte[] TryUnpack(CircularQueue<byte> queue)
+ {
+ // 02 00 00 01 00 33 31
+ var reply = new byte[] { 0x02, 0x00, 0x00, 0x01, 0x00, 0x33, 0x31 };
+
+ while (true)
+ {
+ var allrx = queue.ToArray();
+ // var idx = allrx.IndexOf(reply);
+ // var idx = MemoryExtensions.IndexOf(allrx.AsSpan(), reply.AsSpan());
+ var idx = MemoryExtensions.LastIndexOf(allrx.AsSpan(), reply.AsSpan());
+
+ if (idx < 0) return null;
+ if (idx > 0)
+ {
+ queue.Seek(idx);
+ continue;
+ }
+
+ if (allrx[allrx.Length - 1] == 0x0d)
+ {
+ queue.Seek(allrx.Length);
+ return allrx.ReadBytes(reply.Length, -1);
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/A1Lib/ComPort/SerialTransport.cs b/A1Lib/ComPort/SerialTransport.cs
new file mode 100644
index 0000000..b17d9b0
--- /dev/null
+++ b/A1Lib/ComPort/SerialTransport.cs
@@ -0,0 +1,532 @@
+using System.IO.Ports;
+using Microsoft.Win32;
+using NewLife;
+using NewLife.Data;
+using NewLife.Log;
+using NewLife.Net;
+using NewLife.Threading;
+
+namespace ALib.ComPort;
+
+public class SerialTransport : DisposeBase, ITransport
+{
+ #region 属性
+ private SerialPort _Serial;
+ /// <summary>串口对象</summary>
+ public SerialPort Serial
+ {
+ get { return _Serial; }
+ set
+ {
+ _Serial = value;
+ if (_Serial != null)
+ {
+ PortName = _Serial.PortName;
+ BaudRate = _Serial.BaudRate;
+ Parity = _Serial.Parity;
+ DataBits = _Serial.DataBits;
+ StopBits = _Serial.StopBits;
+ }
+ }
+ }
+
+ /// <summary>端口名称。默认COM1</summary>
+ public string PortName { get; set; } = "COM1";
+
+ /// <summary>波特率。默认115200</summary>
+ public int BaudRate { get; set; } = 115200;
+
+ /// <summary>奇偶校验位。默认None</summary>
+ public Parity Parity { get; set; } = Parity.None;
+
+ /// <summary>数据位。默认8</summary>
+ public int DataBits { get; set; } = 8;
+
+ /// <summary>停止位。默认One</summary>
+ public StopBits StopBits { get; set; } = StopBits.One;
+
+ /// <summary>超时时间。超过该大小未收到数据,说明是另一帧。默认10ms</summary>
+ public int Timeout { get; set; } = 10;
+
+ private string _Description;
+ /// <summary>描述信息</summary>
+ public string Description
+ {
+ get
+ {
+ if (_Description == null)
+ {
+ var dic = GetNames();
+ if (!dic.TryGetValue(PortName, out _Description))
+ _Description = "";
+ }
+ return _Description;
+ }
+ }
+
+ ///// <summary>粘包处理接口</summary>
+ //public IPacket Packet { get; set; }
+
+ /// <summary>字节超时。数据包间隔,默认20ms</summary>
+ public int ByteTimeout { get; set; } = 20;
+ #endregion
+
+ #region 构造
+ /// <summary>串口传输</summary>
+ public SerialTransport()
+ {
+ // 每隔一段时间检查一次串口是否已经关闭,如果串口已经不存在,则关闭该传输口
+ timer = new TimerX(CheckDisconnect, null, 3000, 3000) { Async = true };
+ }
+
+ /// <summary>销毁</summary>
+ /// <param name="disposing"></param>
+ protected override void Dispose(Boolean disposing)
+ {
+ base.Dispose(disposing);
+
+ try
+ {
+ if (Serial != null) Close();
+ if (timer != null) timer.Dispose();
+ }
+ catch { }
+ }
+
+ #endregion
+
+ #region 方法
+ /// <summary>确保创建</summary>
+ public virtual void EnsureCreate()
+ {
+ if (Serial == null)
+ {
+ Serial = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits);
+
+ _Description = null;
+ }
+ }
+
+ /// <summary>打开</summary>
+ public virtual bool Open()
+ {
+ EnsureCreate();
+
+ if (!Serial.IsOpen)
+ {
+ Serial.Open();
+ if (Received != null) Serial.DataReceived += DataReceived;
+ }
+
+ return true;
+ }
+
+ /// <summary>关闭</summary>
+ public virtual bool Close()
+ {
+ // 关闭时必须清空,否则更换属性后再次打开也无法改变属性
+ var sp = Serial;
+ if (sp != null)
+ {
+ Serial = null;
+ if (Received != null) sp.DataReceived -= DataReceived;
+ if (sp.IsOpen) sp.Close();
+
+ OnDisconnect();
+ }
+
+ return true;
+ }
+ #endregion
+
+ #region 发送
+ /// <summary>写入数据</summary>
+ /// <param name="pk">数据包</param>
+ public virtual int Send(IPacket pk)
+ {
+ if (!Open()) return -1;
+
+ WriteLog("Send:{0}", pk.ToHex());
+
+ var sp = Serial;
+ try
+ {
+ lock (sp)
+ {
+ if (pk.TryGetArray(out var seg))
+ {
+ sp.Write(seg.Array, seg.Offset, seg.Count);
+ }
+ else
+ {
+ var buf = pk.ReadBytes();
+ sp.Write(buf, 0, buf.Length);
+ }
+ }
+ }
+ catch
+ {
+ XTrace.WriteLine("Send:{0} ERROR", pk.ToHex());
+ }
+
+ return pk.Total;
+ }
+
+ public int Send(byte[] buf) => Send((ArrayPacket)buf);
+
+ /// <summary>异步发送数据并等待响应,不好使,不要使用!!!</summary>
+ /// <param name="pk"></param>
+ /// <returns></returns>
+ public virtual async Task<IOwnerPacket> SendAsync(IPacket pk)
+ {
+ if (!Open()) return null;
+
+ //if (Packet == null) Packet = new PacketProvider();
+
+ //var task = Packet.Add(pk, null, Timeout);
+
+ _Source = new TaskCompletionSource<IOwnerPacket>();
+
+ if (pk != null)
+ {
+ WriteLog("SendAsync:{0}", pk.ToHex());
+
+ // 发送数据
+ //Serial.Write(pk.Data, pk.Offset, pk.Count);
+ var sp = Serial;
+ if (pk.TryGetArray(out var seg))
+ sp.Write(seg.Array, seg.Offset, seg.Count);
+ else
+ {
+ var buf = pk.ReadBytes();
+ sp.Write(buf, 0, buf.Length);
+ }
+ }
+
+ return await _Source.Task;
+ }
+
+ /// <summary>接收数据</summary>
+ /// <returns></returns>
+ public virtual IOwnerPacket Receive()
+ {
+ if (!Open()) return null;
+
+ var task = SendAsync(null);
+ if (Timeout > 0 && !task.Wait(Timeout)) return null;
+
+ //return task.Result;
+ var rs = task.ConfigureAwait(false).GetAwaiter().GetResult();
+ if (rs is OwnerPacket op) return op;
+
+ return rs;
+ }
+ #endregion
+
+ #region 异步接收
+ void DataReceived(object sender, SerialDataReceivedEventArgs e)
+ {
+ // 发送者必须保持一定间隔,每个报文不能太大,否则会因为粘包拆包而出错
+ try
+ {
+ var sp = sender as SerialPort;
+ WaitMore();
+ if (sp.BytesToRead > 0)
+ {
+ //var buf = new Byte[sp.BytesToRead];
+ var pk = new OwnerPacket(sp.BytesToRead);
+
+ var count = sp.Read(pk.Buffer, 0, pk.Length);
+ //if (count != buf.Length) buf = buf.ReadBytes(0, count);
+ //var ms = new MemoryStream(buf, 0, count, false);
+ //var pk = new ArrayPacket(buf, 0, count);
+ pk.Resize(count);
+
+ ProcessReceive(pk);
+ }
+ }
+ catch (Exception ex)
+ {
+ //WriteLog("Error " + ex.Message);
+ if (Log != null) Log.Error("DataReceived Error {0}", ex.Message);
+ }
+ }
+
+ void WaitMore()
+ {
+ var sp = Serial;
+
+ var ms = ByteTimeout;
+ var end = DateTime.Now.AddMilliseconds(ms);
+ var count = sp.BytesToRead;
+ while (sp.IsOpen && end > DateTime.Now)
+ {
+ //Thread.SpinWait(1);
+ Thread.Sleep(ms);
+ if (count != sp.BytesToRead)
+ {
+ end = DateTime.Now.AddMilliseconds(ms);
+ count = sp.BytesToRead;
+ }
+ }
+ }
+
+ void ProcessReceive(IOwnerPacket pk)
+ {
+ try
+ {
+ //if (Packet == null)
+ OnReceive(pk);
+ //else
+ //{
+ // // 拆包,多个包多次调用处理程序
+ // foreach (var msg in Packet.Parse(pk))
+ // {
+ // OnReceive(msg);
+ // }
+ //}
+ }
+ catch (Exception ex)
+ {
+ if (!ex.IsDisposed()) Log.Error("{0}.OnReceive {1}", PortName, ex.Message);
+ }
+ }
+
+ private TaskCompletionSource<IOwnerPacket> _Source;
+ /// <summary>处理收到的数据。默认匹配同步接收委托</summary>
+ /// <param name="pk"></param>
+ internal virtual void OnReceive(IOwnerPacket pk)
+ {
+ //// 同步匹配
+ //if (Packet != null && Packet.Match(pk, null)) return;
+
+ if (_Source != null)
+ {
+ _Source.SetResult(pk);
+ _Source = null;
+ return;
+ }
+
+ // 触发事件
+ Received?.Invoke(this, new ReceivedEventArgs { Packet = pk });
+ }
+
+ /// <summary>数据到达事件</summary>
+ public event EventHandler<ReceivedEventArgs> Received;
+ #endregion
+
+ #region 自动检测串口断开
+ /// <summary>断开时触发,可能是人为断开,也可能是串口链路断开</summary>
+ public event EventHandler Disconnected;
+
+ bool isInEvent;
+ void OnDisconnect()
+ {
+ if (Disconnected != null)
+ {
+ // 判断是否在事件中,避免外部在断开时间中调用Close造成死循环
+ if (!isInEvent)
+ {
+ isInEvent = true;
+
+ Disconnected(this, EventArgs.Empty);
+
+ isInEvent = false;
+ }
+ }
+ }
+
+ TimerX timer;
+ /// <summary>检查串口是否已经断开</summary>
+ /// <remarks>
+ /// FX串口异步操作有严重的泄漏缺陷,如果外部硬件长时间断开,
+ /// SerialPort.IsOpen检测不到,并且会无限大占用内存。
+ /// </remarks>
+ /// <param name="state"></param>
+ void CheckDisconnect(object state)
+ {
+ if (string.IsNullOrEmpty(PortName) || Serial == null || !Serial.IsOpen) return;
+
+ // 如果端口已经不存在,则断开吧
+ if (!SerialPort.GetPortNames().Contains(PortName))
+ {
+ WriteLog("串口{0}已经不存在,准备关闭!", PortName);
+
+ //OnDisconnect();
+ Close();
+ }
+ }
+
+ #endregion
+
+ #region 辅助
+ /// <summary>获取带有描述的串口名,没有时返回空数组</summary>
+ /// <returns></returns>
+ public static string[] GetPortNames()
+ {
+ var list = new List<string>();
+ foreach (var item in GetNames())
+ {
+ list.Add(string.Format("{0}({1})", item.Key, item.Value));
+ }
+ return list.ToArray();
+ }
+
+ /// <summary>获取串口列表,名称和描述</summary>
+ /// <returns></returns>
+ public static Dictionary<string, string> GetNames()
+ {
+ var dic = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+ foreach (var item in SerialPort.GetPortNames())
+ {
+ dic.Add(item, "");
+ }
+
+ if (Runtime.Windows)
+ {
+ using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DEVICEMAP\SERIALCOMM", false);
+ using var usb = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\USB", false);
+ if (key != null)
+ {
+ foreach (var item in key.GetValueNames())
+ {
+ var name = key.GetValue(item) + "";
+ var des = "";
+
+ // 尝试枚举USB串口
+ foreach (var vid in usb.GetSubKeyNames())
+ {
+ var usbvid = usb.OpenSubKey(vid);
+ foreach (var elm in usbvid.GetSubKeyNames())
+ {
+ var sub = usbvid.OpenSubKey(elm);
+ //if (sub.GetValue("Class") + "" == "Ports")
+ {
+ var FriendlyName = sub.GetValue("FriendlyName") + "";
+ if (FriendlyName.Contains($"({name})"))
+ {
+ des = FriendlyName.TrimEnd($"({name})").Trim();
+ break;
+ }
+ }
+ }
+ if (!des.IsNullOrEmpty()) break;
+ }
+
+ // 最后选择设备映射的串口名
+ if (des.IsNullOrEmpty())
+ {
+ des = item;
+ var p = item.LastIndexOf('\\');
+ if (p >= 0) des = des.Substring(p + 1);
+ }
+
+ //dic.Add(name, des);
+ // 某台机器上发现,串口有重复
+ dic[name] = des;
+ }
+ }
+ }
+
+ return dic;
+ }
+
+ /// <summary>从串口列表选择串口,支持自动选择关键字</summary>
+ /// <param name="keyWord">串口名称或者描述符的关键字</param>
+ /// <returns></returns>
+ public static SerialTransport Choose(string keyWord = null)
+ {
+ var ns = GetNames();
+ if (ns.Count == 0)
+ {
+ Console.WriteLine("没有可用串口!");
+ return null;
+ }
+
+ var name = "";
+ var des = "";
+
+ Console.WriteLine("可用串口:");
+ Console.ForegroundColor = ConsoleColor.Green;
+ foreach (var item in ns)
+ {
+ if (item.Value == "Serial0") continue;
+
+ if (keyWord != null && (item.Key.EqualIgnoreCase(keyWord) || item.Value.Contains(keyWord)))
+ {
+ name = item.Key;
+ des = item.Value;
+ }
+
+ //Console.WriteLine(item);
+ Console.WriteLine("{0,5}({1})", item.Key, item.Value);
+ }
+ // 没有自动选择,则默认最后一个
+ if (name.IsNullOrEmpty())
+ {
+ var item = ns.Last();
+ name = item.Key;
+ des = item.Value;
+ }
+ while (true)
+ {
+ Console.ResetColor();
+ Console.Write("请输入串口名称(默认 ");
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.Write("{0}", name);
+ Console.ResetColor();
+ Console.Write("):");
+
+ var str = Console.ReadLine();
+ if (str.IsNullOrEmpty()) break;
+
+ // 只有输入有效串口名称才行
+ if (ns.ContainsKey(str))
+ {
+ name = str;
+ des = ns[str];
+ break;
+ }
+ }
+
+ Console.WriteLine();
+ Console.Write("正在打开串口 ");
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.WriteLine("{0}({1})", name, des);
+
+ Console.ResetColor();
+
+ var sp = new SerialTransport
+ {
+ PortName = name
+ };
+
+ return sp;
+ }
+ #endregion
+
+ #region 日志
+ /// <summary>日志对象</summary>
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>输出日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteLog(string format, params object[] args)
+ {
+ if (Log != null && Log.Enable) Log.Info(format, args);
+ }
+
+ /// <summary>已重载</summary>
+ /// <returns></returns>
+ public override string ToString()
+ {
+ if (!string.IsNullOrEmpty(PortName))
+ return PortName;
+ else
+ return "(SerialPort)";
+ }
+
+
+ #endregion
+}
\ No newline at end of file
diff --git a/A1Lib/DllResourceManager.cs b/A1Lib/DllResourceManager.cs
new file mode 100644
index 0000000..cd7201a
--- /dev/null
+++ b/A1Lib/DllResourceManager.cs
@@ -0,0 +1,377 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using NewLife.Log;
+
+namespace ALib
+{
+ /// <summary>
+ /// DLL资源管理器 - 提供通用的DLL嵌入资源释放功能
+ /// </summary>
+ public static class DllResourceManager
+ {
+ private static readonly Dictionary<string, bool> _loadedDlls = new Dictionary<string, bool>();
+ private static readonly Dictionary<string, IntPtr> _loadedHandles = new Dictionary<string, IntPtr>();
+ private static readonly object _lockObject = new object();
+
+ /// <summary>
+ /// 已释放DLL的绝对路径缓存:文件名(小写) -> 绝对路径。
+ /// 供 DllImport 解析器使用,保证原生库能被按绝对路径正确加载。
+ /// </summary>
+ private static readonly Dictionary<string, string> _resolvedPaths = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>DllImport 解析器是否已注册(只会注册一次)</summary>
+ private static bool _resolverRegistered;
+
+ /// <summary>
+ /// 确保DLL可用,如果不存在则从嵌入资源中释放并显式加载
+ /// </summary>
+ /// <param name="resourcePrefix">资源前缀(通常是命名空间路径)</param>
+ /// <param name="dllFileName">DLL文件名</param>
+ /// <returns>是否成功</returns>
+ public static bool EnsureDllAvailable(string resourcePrefix, string dllFileName)
+ {
+ lock (_lockObject)
+ {
+ if (_loadedDlls.ContainsKey(dllFileName) && _loadedDlls[dllFileName])
+ {
+ return true;
+ }
+
+ try
+ {
+ RegisterResolver();
+
+ // 1)优先应用目录:Release 后与主程序同目录,DllImport 默认搜索顺序即可命中
+ // 注意:单文件发布时 Assembly.Location 为空,必须用 AppContext.BaseDirectory
+ var appDir = GetAppDirectory();
+ if (!string.IsNullOrEmpty(appDir))
+ {
+ var appDllPath = Path.Combine(appDir, dllFileName);
+ if (File.Exists(appDllPath))
+ {
+ CacheResolvedPath(dllFileName, appDllPath);
+ _loadedDlls[dllFileName] = true;
+ return true;
+ }
+ }
+
+ // 2)系统路径(应用目录 / PATH)中已存在,直接使用
+ var existPath = FindDllInSystem(dllFileName);
+ if (!string.IsNullOrEmpty(existPath))
+ {
+ CacheResolvedPath(dllFileName, existPath);
+ _loadedDlls[dllFileName] = true;
+ return true;
+ }
+
+ // 3)从嵌入资源中释放(释放到应用目录,失败则回退临时目录)
+ var dllPath = ExtractDllFromResource(resourcePrefix, dllFileName);
+ CacheResolvedPath(dllFileName, dllPath);
+
+ // 显式加载DLL
+ return LoadDllExplicitly(dllFileName, dllPath);
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"DLL加载失败 {dllFileName}: {ex.Message}");
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// 记录已解析到的DLL绝对路径,供 DllImport 解析器回查。
+ /// </summary>
+ private static void CacheResolvedPath(string dllFileName, string dllPath)
+ {
+ if (string.IsNullOrEmpty(dllPath)) return;
+ _resolvedPaths[dllFileName] = dllPath;
+ _resolvedPaths[Path.GetFileName(dllPath)] = dllPath;
+ }
+
+ /// <summary>
+ /// 为当前程序集注册 DllImport 解析器。
+ /// 单文件发布时原生DLL可能被释放到临时目录,而 DllImport 默认搜索顺序不包含该目录,
+ /// 通过解析器把逻辑名映射到释放后的绝对路径,避免 DllNotFoundException。
+ /// </summary>
+ private static void RegisterResolver()
+ {
+ if (_resolverRegistered) return;
+ _resolverRegistered = true;
+
+ try
+ {
+ NativeLibrary.SetDllImportResolver(typeof(DllResourceManager).Assembly, ResolveNativeLibrary);
+ }
+ catch (InvalidOperationException)
+ {
+ // 同一程序集重复注册会抛此异常,忽略即可
+ _resolverRegistered = true;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"注册DllImport解析器失败: {ex.Message}");
+ }
+ }
+
+ /// <summary>
+ /// DllImport 解析回调:先查缓存路径,再回退到默认加载逻辑。
+ /// </summary>
+ private static IntPtr ResolveNativeLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
+ {
+ if (!string.IsNullOrEmpty(libraryName))
+ {
+ string cached;
+ if (_resolvedPaths.TryGetValue(libraryName, out cached) && File.Exists(cached))
+ {
+ if (NativeLibrary.TryLoad(cached, out var handle)) return handle;
+ }
+ }
+
+ // 回退:按系统默认搜索顺序查找
+ string fallback = libraryName;
+ if (!string.IsNullOrEmpty(fallback) && NativeLibrary.TryLoad(fallback, assembly, searchPath, out var defaultHandle))
+ {
+ return defaultHandle;
+ }
+
+ return IntPtr.Zero;
+ }
+
+ /// <summary>
+ /// 获取应用目录。单文件发布时 Assembly.Location 为空字符串,必须回退到 AppContext.BaseDirectory。
+ /// </summary>
+ private static string GetAppDirectory()
+ {
+ try
+ {
+ var location = Assembly.GetExecutingAssembly().Location;
+ if (!string.IsNullOrEmpty(location))
+ {
+ var dir = Path.GetDirectoryName(location);
+ if (!string.IsNullOrEmpty(dir)) return dir;
+ }
+ }
+ catch { }
+
+ // 单文件发布会走到这里:BaseDirectory 指向 exe 所在目录
+ return AppContext.BaseDirectory;
+ }
+
+ /// <summary>
+ /// 在系统路径中查找DLL文件
+ /// </summary>
+ private static string FindDllInSystem(string dllFileName)
+ {
+ // 检查应用程序目录
+ string appDir = GetAppDirectory();
+ if (string.IsNullOrEmpty(appDir)) return null;
+
+ string dllPathInAppDir = Path.Combine(appDir, dllFileName);
+ if (File.Exists(dllPathInAppDir))
+ {
+ return dllPathInAppDir;
+ }
+
+ // 检查系统PATH中的目录
+ string[] paths = Environment.GetEnvironmentVariable("PATH")?.Split(';') ?? Array.Empty<string>();
+ foreach (string path in paths)
+ {
+ if (string.IsNullOrEmpty(path)) continue;
+ string fullPath = Path.Combine(path.Trim(), dllFileName);
+ if (File.Exists(fullPath))
+ {
+ return fullPath;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// 显式加载DLL到内存
+ /// </summary>
+ private static bool LoadDllExplicitly(string dllFileName, string dllPath)
+ {
+ try
+ {
+ // 如果已经加载过,先释放
+ if (_loadedHandles.ContainsKey(dllFileName) && _loadedHandles[dllFileName] != IntPtr.Zero)
+ {
+ NativeLibrary.Free(_loadedHandles[dllFileName]);
+ _loadedHandles.Remove(dllFileName);
+ }
+
+ // 显式加载DLL
+ IntPtr handle = NativeLibrary.Load(dllPath);
+ if (handle == IntPtr.Zero)
+ {
+ throw new InvalidOperationException($"无法加载DLL: {dllPath}");
+ }
+
+ _loadedHandles[dllFileName] = handle;
+ _loadedDlls[dllFileName] = true;
+
+ XTrace.WriteLine($"DLL已显式加载: {dllPath}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"显式加载DLL失败 {dllFileName}: {ex.Message}");
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// 从嵌入资源中释放DLL。
+ /// 优先释放到应用目录(DllImport 默认搜索顺序第一步即可命中);若应用目录不可写,则回退到临时目录。
+ /// 支持两种嵌入形态,按「zip 优先」选择:
+ /// 1)zip 压缩:{前缀}.{dll名去扩展名}.zip,内含同名 dll,体积可减小数倍(推荐)
+ /// 2)裸文件:{前缀}.{dll文件名}
+ /// </summary>
+ private static string ExtractDllFromResource(string resourcePrefix, string dllFileName)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+
+ // 约定:zlgcan.dll -> ALib.ZLG_CAN.zlgcan.zip
+ var baseName = Path.GetFileNameWithoutExtension(dllFileName);
+ var zipResourceName = $"{resourcePrefix}.{baseName}.zip";
+
+ // 优先释放到应用目录,确保 DllImport("xxx.dll") 能被默认搜索路径命中
+ // (临时目录不在原生DLL默认搜索路径中,会导致 DllImport 运行时找不到库而抛 DllNotFoundException)
+ var appDir = GetAppDirectory();
+ if (!string.IsNullOrEmpty(appDir))
+ {
+ var appDllPath = Path.Combine(appDir, dllFileName);
+ if (TryExtract(assembly, zipResourceName, $"{resourcePrefix}.{dllFileName}", appDllPath))
+ {
+ XTrace.WriteLine($"DLL已释放到: {appDllPath}");
+ return appDllPath;
+ }
+ }
+
+ // 回退:应用目录不可写(如单文件放在 Program Files、只读盘)时释放到临时目录。
+ // 释放到 Temp 后必须依赖已注册的 DllImport 解析器按绝对路径加载,否则 DllImport 找不到库。
+ var tempDir = Path.Combine(Path.GetTempPath(), "ALib.Native");
+ string tempDllPath = Path.Combine(tempDir, dllFileName);
+ if (!TryExtract(assembly, zipResourceName, $"{resourcePrefix}.{dllFileName}", tempDllPath))
+ {
+ throw new InvalidOperationException($"无法释放嵌入资源: {resourcePrefix}.{dllFileName} 或 {zipResourceName}");
+ }
+
+ // 进程退出时清理临时目录内本次释放的文件
+ var pathToDelete = tempDllPath;
+ AppDomain.CurrentDomain.ProcessExit += (s, e) =>
+ {
+ try { if (File.Exists(pathToDelete)) File.Delete(pathToDelete); } catch { }
+ };
+
+ XTrace.WriteLine($"DLL已释放到: {tempDllPath}");
+ return tempDllPath;
+ }
+
+ /// <summary>
+ /// 释放单个 DLL:优先尝试 zip 资源,不存在或失败则回退裸文件资源。
+ /// </summary>
+ /// <param name="assembly">程序集</param>
+ /// <param name="zipResourceName">zip 资源名(可不存在)</param>
+ /// <param name="rawResourceName">裸文件资源名(可不存在)</param>
+ /// <param name="destPath">目标路径</param>
+ /// <returns>是否成功</returns>
+ private static bool TryExtract(Assembly assembly, string zipResourceName, string rawResourceName, string destPath)
+ {
+ // 目标已存在(且非本次写入)时直接复用,避免文件被占用导致写入失败
+ if (File.Exists(destPath) && new FileInfo(destPath).Length > 0) return true;
+
+ // 1)zip 形式
+ if (!string.IsNullOrEmpty(zipResourceName) &&
+ assembly.GetManifestResourceStream(zipResourceName) != null)
+ {
+ if (ZipResourceExtractor.ExtractFile(zipResourceName, destPath)) return true;
+ XTrace.WriteLine($"zip 释放失败,尝试裸文件资源: {zipResourceName}");
+ }
+
+ // 2)裸文件形式
+ return TryExtractTo(assembly, rawResourceName, destPath);
+ }
+
+ /// <summary>将嵌入资源写入指定路径,成功返回 true</summary>
+ private static bool TryExtractTo(Assembly assembly, string resourceName, string destPath)
+ {
+ try
+ {
+ // 目标已存在(且非本次写入)时直接复用,避免文件被占用导致写入失败
+ if (File.Exists(destPath)) return true;
+
+ using Stream stream = assembly.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ throw new InvalidOperationException($"无法找到嵌入资源: {resourceName}");
+ }
+
+ // 确保目录存在
+ var dir = Path.GetDirectoryName(destPath);
+ if (!String.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
+
+ // 写临时文件后原子替换,避免并发/占用
+ var tmp = destPath + ".tmp";
+ using (var fileStream = new FileStream(tmp, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ stream.CopyTo(fileStream);
+ fileStream.Flush();
+ }
+ File.Move(tmp, destPath, true);
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"释放 {Path.GetFileName(destPath)} 失败: {ex.Message}");
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// 重置DLL加载状态(主要用于测试或重新加载)
+ /// </summary>
+ public static void ResetLoadStatus(string dllFileName = null)
+ {
+ lock (_lockObject)
+ {
+ if (dllFileName != null)
+ {
+ if (_loadedHandles.ContainsKey(dllFileName))
+ {
+ NativeLibrary.Free(_loadedHandles[dllFileName]);
+ _loadedHandles.Remove(dllFileName);
+ }
+ _loadedDlls.Remove(dllFileName);
+ }
+ else
+ {
+ // 释放所有句柄
+ foreach (var handle in _loadedHandles.Values)
+ {
+ if (handle != IntPtr.Zero)
+ {
+ NativeLibrary.Free(handle);
+ }
+ }
+ _loadedHandles.Clear();
+ _loadedDlls.Clear();
+ }
+ }
+ }
+
+ /// <summary>
+ /// 程序退出时清理资源(可选)
+ /// </summary>
+ public static void Cleanup()
+ {
+ ResetLoadStatus();
+ }
+ }
+}
\ No newline at end of file
diff --git a/A1Lib/Hid/UsbHidHelper.cs b/A1Lib/Hid/UsbHidHelper.cs
new file mode 100644
index 0000000..dd92a24
--- /dev/null
+++ b/A1Lib/Hid/UsbHidHelper.cs
@@ -0,0 +1,162 @@
+using ALib.UsbHub;
+using HidSharp;
+using Microsoft.Win32;
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Hid
+{
+ public static class UsbHidHelper
+ {
+ /// <summary>获取所有USB HID,不考虑复合设备</summary>
+ /// <remarks>注意,只包含HidDevice支持的HID设备</remarks>
+ /// <param name="devs">从这些设备里面获取,没有就从所有设备里面筛选</param>
+ /// <returns>ContainerId,HidDevice</returns>
+ public static Dictionary<string, List<HidDevice>> GetAllUsbHid(List<UsbDevice> devs = null)
+ {
+ // 筛选范围
+ if (devs == null)
+ {
+ var all = UsbHelper.GetAllUsbInfo();
+ devs = new List<UsbDevice>();
+ foreach (var item in all)
+ {
+ devs.AddRange(item.Value);
+ }
+ }
+
+ // 所有有效 usb hid 设备 cid
+ var allcid = GetAllHidCid();
+ // 转换为小写
+ var temp = new Dictionary<string, string>();
+ foreach (var item in allcid)
+ {
+ temp.Add(item.Key.ToLower(), item.Value);
+ }
+ allcid = temp;
+
+ // 使用 HidSharp 获取所有 HID 设备
+ var deviceList = DeviceList.Local;
+ var hidDevices = deviceList.GetHidDevices().ToArray();
+
+ var dic = new Dictionary<string, List<HidDevice>>();
+ foreach (var hid in hidDevices)
+ {
+ // path = @"\\?\hid#vid_09da&pid_c10a&col01#9&54744d2&1&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"
+ var path = hid.DevicePath;
+ // 去掉 @"\\?\"
+ // path = @"hid#vid_09da&pid_c10a&col01#9&54744d2&1&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}"
+ path = path.Replace(@"\\?\", "");
+ // # 变成 \
+ // path = @"hid\vid_09da&pid_c10a&col01\9&54744d2&1&0000\{4d1e55b2-f16f-11cf-88cb-001111000030}"
+ path = path.Replace("#", @"\");
+ // 去掉最后面的 \ 和后面的值
+ // path = @"hid\vid_09da&pid_c10a&col01\9&54744d2&1&0000"
+ path = path.Substring(0, path.LastIndexOf('\\'));
+ // 拿到cid
+ var cid = GetCid(allcid, path);
+ if (string.IsNullOrEmpty(cid)) continue;
+ // 再从 devs 里面找对应的设备
+ var dev = devs.Find(d => d.ContainerId == cid);
+ if (dev == null) continue;
+
+ // 添加到字典
+ if (dic.ContainsKey(cid)) dic[cid].Add(hid);
+ else dic.Add(cid, new List<HidDevice> { hid });
+ }
+
+ return dic;
+ }
+
+ /// <summary>从字典里面拿到cid</summary>
+ /// <param name="path_cid">注册表路径(纯小写),CID</param>
+ /// <param name="path">需要的路径(纯小写)</param>
+ /// <returns></returns>
+ private static string GetCid(Dictionary<string, string> path_cid, string path)
+ {
+ foreach (var item in path_cid)
+ {
+ var dicpath = item.Key;
+ if (dicpath.IndexOf(path) != -1) return item.Value;
+ }
+
+ return "";
+ }
+
+ /// <summary>从注册表获取所有hid设备的ContainerId</summary>
+ /// <remarks>注意,包含了键盘鼠标等标准设备</remarks>
+ /// <param name="onlyUsb">只拿usb的hid设备</param>
+ /// <returns>注册表路径,ContainerId</returns>
+ public static Dictionary<string, string> GetAllHidCid(bool onlyUsb = true)
+ {
+ var dic = new Dictionary<string, string>();
+
+ // Registry.LocalMachine 注册表搜索 @"SYSTEM\CurrentControlSet\Enum\HID\" 目录下 ContainerID 字段
+ // 目录 和 ContainerID 值作为 dic 的 key 和 value
+ using (var hidKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\HID"))
+ {
+ if (hidKey == null) return dic;
+
+ // 遍历所有 VID_xxxx&PID_xxxx 子项
+ foreach (var vidPidName in hidKey.GetSubKeyNames())
+ {
+ using (var vidPidKey = hidKey.OpenSubKey(vidPidName))
+ {
+ if (vidPidKey == null) continue;
+
+ // 遍历具体设备实例
+ foreach (var instanceName in vidPidKey.GetSubKeyNames())
+ {
+ var fullPath = $"HID\\{vidPidName}\\{instanceName}";
+
+ using (var instanceKey = vidPidKey.OpenSubKey(instanceName))
+ {
+ if (instanceKey == null) continue;
+
+ // 只拿 USB HID 设备
+ // if (onlyUsb)
+ // {
+ // if (fullPath.IndexOf("PID") < 0) continue;
+ // }
+
+ // 获取 ContainerID
+ var containerId = instanceKey.GetValue("ContainerID") as string;
+ if (containerId == null) continue;
+ // 打开注册表看到的,这个值的都不是 USB 设备
+ if (onlyUsb)
+ {
+ if (containerId == @"{00000000-0000-0000-ffff-ffffffffffff}") continue;
+ }
+
+ if (!string.IsNullOrEmpty(containerId))
+ {
+ dic[fullPath] = containerId;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return dic;
+ }
+
+ /// <summary>获取HID设备</summary>
+ /// <param name="dev">USB设备</param>
+ /// <param name="idx">符合设备序号,按照注册表顺序,mi_col一起排序的</param>
+ /// <remarks>注意,idx下标是只是支持HidDevice的HID设备的排序</remarks>
+ /// <returns></returns>
+ public static HidDevice GetHidDevice(this UsbDevice dev, int idx = 0)
+ {
+ var dic = GetAllUsbHid(new List<UsbDevice> { dev });
+ if(dic.Count < idx)return null;
+
+ return dic[dev.ContainerId][idx];
+ }
+ }
+}
diff --git a/A1Lib/Image/ImageHelper.cs b/A1Lib/Image/ImageHelper.cs
new file mode 100644
index 0000000..f951eda
--- /dev/null
+++ b/A1Lib/Image/ImageHelper.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Drawing;
+using System.Drawing.Imaging;
+
+namespace ALib.Image
+{
+ public static class ImageHelper
+ {
+ /// <summary>
+ /// 将 Bitmap 以指定质量保存为 JPG 文件
+ /// </summary>
+ /// <param name="bitmap">要保存的 Bitmap 对象</param>
+ /// <param name="outputPath">输出文件路径</param>
+ /// <param name="quality">压缩质量 (0-100)</param>
+ public static void SaveAsJpeg(this Bitmap bitmap, string outputPath, long quality)
+ {
+ // 1. 获取 JPG 格式的图像编码器
+ ImageCodecInfo jpegEncoder = GetEncoder(ImageFormat.Jpeg);
+
+ // 2. 创建并设置编码参数,这里设置压缩质量
+ EncoderParameters encoderParams = new EncoderParameters(1);
+ encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
+
+ // 3. 使用指定的编码器和参数保存图像
+ bitmap.Save(outputPath, jpegEncoder, encoderParams);
+ }
+
+ /// <summary>
+ /// 辅助方法:根据 ImageFormat 获取对应的 ImageCodecInfo
+ /// </summary>
+ private static ImageCodecInfo GetEncoder(ImageFormat format)
+ {
+ ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
+ foreach (ImageCodecInfo codec in codecs)
+ {
+ if (codec.FormatID == format.Guid)
+ {
+ return codec;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/A1Lib/Net/NetworkHelper.cs b/A1Lib/Net/NetworkHelper.cs
new file mode 100644
index 0000000..b93182d
--- /dev/null
+++ b/A1Lib/Net/NetworkHelper.cs
@@ -0,0 +1,160 @@
+using ALib.UsbHub;
+using NewLife.Configuration;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Management;
+using System.Net;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.Net
+{
+ public static class NetworkHelper
+ {
+ /*
+ AI 修改前的版本,未测试。 此处留作备份, AI 软件不许改。
+
+ /// <summary>从USB获取网卡对象</summary>
+ /// <param name="dev"></param>
+ /// <returns></returns>
+ public static ManagementObject GetNetworkCard(this UsbDevice dev)
+ {
+ var dst_cid = dev.ContainerId;
+
+ // 查询所有网口适配器
+ using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionStatus IS NOT NULL"))
+ {
+ foreach (ManagementObject adapter in searcher.Get())
+ {
+ string pnpId = adapter["PNPDeviceID"]?.ToString();
+ var cid = UsbHelper.GetContainerId(pnpId);
+
+ if (dst_cid == cid) return adapter;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>设置静态IP</summary>
+ /// <param name="card">网卡</param>
+ /// <param name="ip">IP</param>
+ /// <param name="mask">子网掩码</param>
+ /// <param name="gw">网关</param>
+ /// <returns></returns>
+ public static bool SetStaticIp(this ManagementObject card, IPAddress ip, IPAddress mask, IPAddress gw)
+ {
+ if (card == null) return false;
+
+ try
+ {
+ // 设置 IP
+ ManagementBaseObject ipParams = card.GetMethodParameters("EnableStatic");
+ ipParams["IPAddress"] = new string[] { ip.ToString() };
+ ipParams["SubnetMask"] = new string[] { mask.ToString() };
+ ManagementBaseObject ipResult = card.InvokeMethod("EnableStatic", ipParams, null);
+ uint ipRes = Convert.ToUInt32(ipResult["ReturnValue"]);
+ if (ipRes != 0)
+ {
+ // XTrace.WriteLine($"设置IP失败, 错误码: {ipRes}");
+ return false;
+ }
+
+ // 设置网关
+ ManagementBaseObject gatewayParams = card.GetMethodParameters("SetGateways");
+ gatewayParams["DefaultIPGateway"] = new string[] { gw.ToString() };
+ gatewayParams["GatewayCostMetric"] = new int[] { 1 };
+ ManagementBaseObject gatewayResult = card.InvokeMethod("SetGateways", gatewayParams, null);
+ uint gwRes = Convert.ToUInt32(gatewayResult["ReturnValue"]);
+ if (gwRes != 0)
+ {
+ // XTrace.WriteLine($"设置网关失败, 错误码: {gwRes}");
+ return false;
+ }
+
+ // XTrace.WriteLine($"设置静态IP成功: {ip}/{mask} GW:{gw}");
+ return true;
+ }
+ catch { return false; }
+ }
+
+ */
+
+
+ /// <summary>从USB获取网卡对象</summary>
+ /// <param name="dev"></param>
+ /// <returns></returns>
+ public static ManagementObject GetNetworkCard(this UsbDevice dev)
+ {
+ var dst_cid = dev.ContainerId;
+
+ // 查询所有网口适配器
+ using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionStatus IS NOT NULL"))
+ {
+ foreach (ManagementObject adapter in searcher.Get())
+ {
+ string pnpId = adapter["PNPDeviceID"]?.ToString();
+ var cid = UsbHelper.GetContainerId(pnpId);
+
+ if (dst_cid == cid) return adapter;
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>设置静态IP</summary>
+ /// <param name="card">网卡 (Win32_NetworkAdapter)</param>
+ /// <param name="ip">IP</param>
+ /// <param name="mask">子网掩码</param>
+ /// <param name="gw">网关</param>
+ /// <returns></returns>
+ public static bool SetStaticIp(this ManagementObject card, IPAddress ip, IPAddress mask, IPAddress gw)
+ {
+ if (card == null) return false;
+
+ try
+ {
+ // 通过关联关系直接获取 Configuration 对象
+ using (var relatedConfigs = card.GetRelated("Win32_NetworkAdapterConfiguration"))
+ {
+ foreach (ManagementObject config in relatedConfigs)
+ {
+ // 设置 IP
+ ManagementBaseObject ipParams = config.GetMethodParameters("EnableStatic");
+ ipParams["IPAddress"] = new string[] { ip.ToString() };
+ ipParams["SubnetMask"] = new string[] { mask.ToString() };
+ ManagementBaseObject ipResult = config.InvokeMethod("EnableStatic", ipParams, null);
+ uint ipRes = Convert.ToUInt32(ipResult["ReturnValue"]);
+ if (ipRes != 0)
+ {
+ // XTrace.WriteLine($"设置IP失败, 错误码: {ipRes}");
+ return false;
+ }
+
+ // 设置网关
+ ManagementBaseObject gatewayParams = config.GetMethodParameters("SetGateways");
+ gatewayParams["DefaultIPGateway"] = new string[] { gw.ToString() };
+ gatewayParams["GatewayCostMetric"] = new int[] { 1 };
+ ManagementBaseObject gatewayResult = config.InvokeMethod("SetGateways", gatewayParams, null);
+ uint gwRes = Convert.ToUInt32(gatewayResult["ReturnValue"]);
+ if (gwRes != 0)
+ {
+ // XTrace.WriteLine($"设置网关失败, 错误码: {gwRes}");
+ return false;
+ }
+
+ // XTrace.WriteLine($"设置静态IP成功: {ip}/{mask} GW:{gw}");
+ return true;
+ }
+ }
+
+ return false;
+ }
+ catch { return false; }
+ }
+ }
+}
diff --git a/A1Lib/pack_native_res.ps1 b/A1Lib/pack_native_res.ps1
new file mode 100644
index 0000000..2da1bcf
--- /dev/null
+++ b/A1Lib/pack_native_res.ps1
@@ -0,0 +1,132 @@
+<#
+.SYNOPSIS
+ 把原生库打包为 zip,供以嵌入资源形式随程序集发布。
+
+.DESCRIPTION
+ 原生 DLL 多为未压缩 PE 文件,zip 压缩率通常可达 3~8 倍。
+ 打包后由 ZipResourceExtractor 在运行时解压还原,可显著减小程序集体积。
+
+ 打包产物命名需与代码约定一致:
+ · 单文件:{目录}.{文件名去扩展名}.zip,zip 内仅含该文件
+ 例:CAN\ZLG_CAN\zlgcan.dll -> CAN\ZLG_CAN\zlgcan.zip
+ · 目录 :{目录名}.zip,zip 内含目录内全部文件(保留相对结构)
+ 例:CAN\ZLG_CAN\kerneldlls -> CAN\ZLG_CAN\kerneldlls.zip
+
+.PARAMETER Target
+ 可选,指定要打包的目标(相对本脚本目录)。缺省打包全部已配置项。
+
+.EXAMPLE
+ .\pack_native_res.ps1
+
+.EXAMPLE
+ .\pack_native_res.ps1 -Target 'CAN\ZLG_CAN\kerneldlls'
+#>
+[CmdletBinding()]
+param(
+ [string]$Target
+)
+
+$ErrorActionPreference = 'Stop'
+Set-StrictMode -Version Latest
+
+$root = $PSScriptRoot
+
+# 待打包项:与 A1Lib.csproj 中的嵌入配置一一对应
+$items = @(
+ @{ Path = 'CAN\ZLG_CAN\zlgcan.dll'; Kind = 'file' }
+ @{ Path = 'CAN\ZLG_CAN\kerneldlls'; Kind = 'dir' }
+ @{ Path = 'CAN\CX_CAN\ControlCAN.dll'; Kind = 'file' }
+ @{ Path = 'CAN\PCAN\PCANBasic.dll'; Kind = 'file' }
+)
+
+function Get-ZipPath([string]$relPath, [string]$kind) {
+ $full = Join-Path $root $relPath
+ if ($kind -eq 'dir') {
+ return "$full.zip"
+ }
+ # 单文件:同名但扩展名换成 .zip
+ $dir = Split-Path $full -Parent
+ $base = [IO.Path]::GetFileNameWithoutExtension($full)
+ return (Join-Path $dir "$base.zip")
+}
+
+function Write-Zip([string]$srcPath, [string]$kind, [string]$zipPath) {
+ if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
+
+ if ($kind -eq 'dir') {
+ # 目录:以目录内容为根,保留相对结构(解压后直接铺在目标目录下)
+ Compress-Archive -Path (Join-Path $srcPath '*') -DestinationPath $zipPath -CompressionLevel Optimal
+ }
+ else {
+ # 单文件:以文件名作为 zip 内的条目名
+ Compress-Archive -Path $srcPath -DestinationPath $zipPath -CompressionLevel Optimal
+ }
+ return $zipPath
+}
+
+function Format-Size([long]$n) {
+ if ($n -ge 1MB) { return ('{0:N2} MB' -f ($n / 1MB)) }
+ if ($n -ge 1KB) { return ('{0:N1} KB' -f ($n / 1KB)) }
+ return "$n B"
+}
+
+$results = @()
+
+foreach ($it in $items) {
+ $rel = $it.Path
+ $kind = $it.Kind
+
+ if ($Target -and ($rel -ne $Target)) { continue }
+
+ $src = Join-Path $root $rel
+ if (-not (Test-Path $src)) {
+ Write-Warning "跳过(不存在): $rel"
+ continue
+ }
+
+ # 源大小
+ if ($kind -eq 'dir') {
+ $rawSize = (Get-ChildItem $src -Recurse -File | Measure-Object -Property Length -Sum).Sum
+ $fileCount = (Get-ChildItem $src -Recurse -File).Count
+ }
+ else {
+ $rawSize = (Get-Item $src).Length
+ $fileCount = 1
+ }
+
+ $zipPath = Get-ZipPath $rel $kind
+ Write-Zip $src $kind $zipPath | Out-Null
+
+ $zipSize = (Get-Item $zipPath).Length
+ $ratio = if ($rawSize -gt 0) { $zipSize / $rawSize } else { 0 }
+
+ $results += [pscustomobject]@{
+ 目标 = $rel
+ 类型 = $kind
+ 文件数 = $fileCount
+ 原始 = Format-Size $rawSize
+ 压缩后 = Format-Size $zipSize
+ 压缩率 = ('{0:P0}' -f $ratio)
+ Zip = [IO.Path]::GetFileName($zipPath)
+ RawBytes = [long]$rawSize
+ ZipBytes = [long]$zipSize
+ }
+}
+
+if ($results.Count -eq 0) {
+ Write-Host '没有可打包的目标。' -ForegroundColor Yellow
+ return
+}
+
+$results | Select-Object 目标, 类型, 文件数, 原始, 压缩后, 压缩率 | Format-Table -AutoSize
+
+$sumRaw = ($results | Measure-Object -Property RawBytes -Sum).Sum
+$sumZip = ($results | Measure-Object -Property ZipBytes -Sum).Sum
+$sumRatio = if ($sumRaw -gt 0) { $sumZip / $sumRaw } else { 0 }
+
+Write-Host ''
+Write-Host ('合计:{0} -> {1}(压缩率 {2:P0})' -f (Format-Size $sumRaw), (Format-Size $sumZip), $sumRatio) -ForegroundColor Green
+
+Write-Host ''
+Write-Host '说明:zip 与原始文件同时存在于源码目录时,A1Lib.csproj 只嵌入 zip。'
+Write-Host ' 原始文件保留供打包与排查使用,不参与编译。'
diff --git a/A1Lib/StructuralLog.cs b/A1Lib/StructuralLog.cs
new file mode 100644
index 0000000..43158bf
--- /dev/null
+++ b/A1Lib/StructuralLog.cs
@@ -0,0 +1,170 @@
+using NewLife;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib
+{
+ public class _StructuralLog<T> : DisposeBase where T : _StructuralLog<T>
+ {
+
+ #region 属性
+
+ /// <summary>目录</summary>
+ public String BasePath { get; set; } = "StrtLog/";
+
+ /// <summary>名称,日志目录名称</summary>
+ public String Name { get; set; } = "123";
+
+ /// <summary>日志路径</summary>
+ public String LogPath { get; private set; }
+
+ /// <summary>根日志</summary>
+ public CompositeLog RootLog { get; private set; } = new CompositeLog();
+
+ /// <summary>子项日志</summary>
+ private Dictionary<String, TextFileLog> _Dic = new();
+
+ #endregion
+
+ /// <summary>构造</summary>
+ /// <param name="sn">S/N号</param>
+ /// <param name="basepath">日志路径</param>
+ /// <param name="time">目录时间戳</param>
+ public _StructuralLog(string sn, string basepath = "StrtLog/", bool addtime = true)
+ {
+ if (sn == null) sn = "123";
+ if (sn.Length < 1) sn = "123";
+ Name = sn;
+
+ if (basepath == null) basepath = "StrtLog/";
+ if (basepath.Length < 1) basepath = "StrtLog/";
+ if (!basepath.EndsWith("/")) basepath += '/';
+ BasePath = basepath;
+
+ var path = PathHelper.GetFullPath(BasePath);
+ if (!Directory.Exists(path)) Directory.CreateDirectory(path);
+
+ var time = DateTime.Now;
+ // 生成适合文件目录的时间戳格式: _yyMMdd_HHmmss
+ var tstr = time.ToString("_yyMMdd_HHmmss");
+
+ var snpath = path + Name + tstr;
+ if (!addtime) snpath = path + Name;
+
+ if (!snpath.EndsWith('/')) snpath = snpath + "/";
+ if (!Directory.Exists(snpath)) Directory.CreateDirectory(snpath);
+
+ LogPath = snpath;
+
+ // 根日志的文件部分
+ var zroot = LogPath + "z.root.txt";
+ var tlg = TextFileLog.CreateFile(zroot);
+ // 装进去
+ RootLog.Add(tlg);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ RootLogClear();
+ }
+
+ /// <summary>清空日志输出途径</summary>
+ public void RootLogClear()
+ {
+ RootLog.Logs.Clear();
+ }
+
+ #region 写日志
+
+ /// <summary>写根日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteRootLog(String format, params Object[] args) => RootLog?.Info(format, args);
+
+ /// <summary>写日志</summary>
+ /// <param name="name">日志,root为根日志,其余为子项</param>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WriteLog(String name, String format, params Object[] args)
+ {
+ if (name == null) name = "root";
+ if (name.Length < 1) name = "root";
+ if (name == "root")
+ {
+ WriteRootLog(format, args);
+ return;
+ }
+
+ ILog log = SubItemLog(name);
+ log?.Info(format, args);
+ }
+
+ /// <summary>当前日志</summary>
+ public String CurrWork { get; set; } = "root";
+
+ /// <summary>按照CurrWork写日志</summary>
+ /// <param name="format"></param>
+ /// <param name="args"></param>
+ public void WorkLog(String format, params Object[] args)
+ {
+ var work = SubItemLog(CurrWork);
+ work?.Info(format, args);
+ }
+
+ #endregion
+
+ /// <summary>子项的日志路径</summary>
+ /// <param name="name"></param>
+ /// <returns></returns>
+ public String SubItemPath(string name)
+ {
+ if (name == null) return "";
+ return LogPath + name + ".txt";
+ }
+
+ /// <summary>子项的文本日志对象</summary>
+ /// <param name="name">子项名称</param>
+ /// <returns></returns>
+ public TextFileLog SubItemLog(string name)
+ {
+ if (name == null) return null;
+
+ if (_Dic.ContainsKey(name)) return _Dic[name];
+
+ var file = LogPath + name + ".txt";
+ var log = TextFileLog.CreateFile(file);
+ _Dic.Add(name, log);
+ return log;
+ }
+
+ private static T _curr;
+ /// <summary>当前项</summary>
+ public static T Current
+ {
+ get { return _curr; }
+ set
+ {
+ var old = _curr;
+ _curr = value;
+
+ if (old != null)
+ {
+ old.TryDispose();
+ // var flog = old.RootLog.Get<TextFileLog>();
+ // flog?.Dispose();
+ }
+ }
+ }
+ }
+
+ public class StructuralLog : _StructuralLog<StructuralLog>
+ {
+ public StructuralLog(string sn, string basepath = "StrtLog/", bool addtime = true) : base(sn, basepath, addtime) { }
+ }
+}
diff --git a/XCoder/Windows/UsbHelper.cs b/A1Lib/UsbHub/UsbHelper.cs
similarity index 80%
rename from XCoder/Windows/UsbHelper.cs
rename to A1Lib/UsbHub/UsbHelper.cs
index bc357a4..dcb7e54 100644
--- a/XCoder/Windows/UsbHelper.cs
+++ b/A1Lib/UsbHub/UsbHelper.cs
@@ -8,7 +8,7 @@ using System.Linq;
using System.Management;
using System.Text;
-namespace XCoder
+namespace ALib.UsbHub
{
/// <summary>USB复合设备</summary>
public class UsbSubDev
@@ -28,7 +28,7 @@ namespace XCoder
public class UsbDevice
{
- /// <summary>端口号</summary>
+ /// <summary>物理端口号,插口编号</summary>
public int Port { get; set; }
/// <summary>设备名称</summary>
@@ -46,12 +46,6 @@ namespace XCoder
/// <summary>复合设备,设备名称</summary>
public List<UsbSubDev> SubDev { get; set; }
- /// <summary>本设备是HUB时有效,无效为-1</summary>
- public int HubId { get; set; } = -1;
-
- /// <summary>注册表ParentIdPrefix字段</summary>
- public String ParentIdPrefix = "";
-
public override string ToString()
{
@@ -110,6 +104,7 @@ namespace XCoder
var hub = strs[1].TrimStart(hhead).ToInt();
var cid = GetContainerId(pnpId);
var (vid, pid) = GetVidPid(pnpId);
+
var dev = new UsbDevice
{
Port = port,
@@ -122,9 +117,6 @@ namespace XCoder
// ParentIdPrefix = GetParentIdPrefix(pnpId),
};
- var pre = GetParentIdPrefix(pnpId);
- if(!pre.IsNullOrEmpty())dev.ParentIdPrefix = pre;
-
if (roots.ContainsKey(hub)) { roots[hub].Add(dev); }
else { roots[hub] = new List<UsbDevice> { dev }; }
}
@@ -186,41 +178,9 @@ namespace XCoder
}
}
- // UpdateHubInfo(roots);
-
return roots;
}
- /* 方法不对,获取的id不对,屏蔽之
- /// <summary>更新HUB之间的关系</summary>
- /// <param name="dic"></param>
- public static void UpdateHubInfo(Dictionary<int, List<UsbDevice>> roots)
- {
- foreach (var item in roots)
- {
- var id = item.Key;
- var pre = GetParentIdPrefix(item.Value[0].PNPDeviceID);
- if (pre.IsNullOrEmpty()) continue;
-
- foreach (var item2 in roots)
- {
- foreach (var dev in item2.Value)
- {
- if (!IsHub(dev.PNPDeviceID)) continue;
-
- if (dev.PNPDeviceID.IndexOf(pre) != -1)
- {
- dev.HubId = id;
- dev.Name += $" {id}";
-
- break;
- }
- }
- }
- }
- }
- */
-
/// <summary>获取集线集下的所有设备</summary>
/// <param name="hubid">集线集编号</param>
/// <returns></returns>
@@ -280,39 +240,6 @@ namespace XCoder
}
}
- /// <summary>父关系前缀</summary>
- /// <param name="devInstPath"></param>
- /// <returns></returns>
- public static string GetParentIdPrefix(string devInstPath)
- {
- if (string.IsNullOrEmpty(devInstPath)) return null;
-
- using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\" + devInstPath))
- {
- return key?.GetValue("ParentIdPrefix") as string;
- }
- }
-
- /// <summary>判断是不是hub</summary>
- /// <param name="devInstPath"></param>
- /// <returns></returns>
- public static bool IsHub(string devInstPath)
- {
- if (string.IsNullOrEmpty(devInstPath)) return false;
-
- using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Enum\" + devInstPath))
- {
- var val = key?.GetValue("CompatibleIDs") as string[];
- if (val == null) return false;
- foreach (var id in val)
- {
- if (id.IndexOf("USB20_HUB") >= 0 || id.IndexOf("USB30_HUB") >= 0) return true;
- }
- }
-
- return false;
- }
-
/// <summary>提取VID PID</summary>
/// <param name="pnpId"></param>
/// <returns></returns>
diff --git a/A1Lib/UsbHub/UsbHubPort.cs b/A1Lib/UsbHub/UsbHubPort.cs
new file mode 100644
index 0000000..c53804f
--- /dev/null
+++ b/A1Lib/UsbHub/UsbHubPort.cs
@@ -0,0 +1,253 @@
+using ALib.Audio;
+using ALib.Camera;
+using ALib.ComPort;
+using ALib.Hid;
+using ALib.Net;
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.UsbHub
+{
+ /// <summary>
+ /// 本类描述 USB_HUB 物理插口上的 设备/子设备
+ /// 作用:描述一个HUB的插口情况,多个插口一起描述的时候可以直接判断此HUB是否存在
+ /// </summary>
+ public class UsbHubPort
+ {
+ [Description("设备名")]
+ public String Name { get; set; } = "USB";
+
+ [Description("HUB编组")]
+ public int HubGroup { get; set; } = 0;
+
+ [Description("HUB物理端口号,插口号。通常从1开始,没有看到0的")]
+ public int Port { get; set; } = 0;
+
+ [Description("复合设备的子设备编号")]
+ public int SubDev { get; set; } = 0;
+
+ [Description("实际接入的HUB")]
+ public int HubId = -1;
+
+ /// <summary>构造</summary>
+ /// <param name="grp">编组</param>
+ /// <param name="port">插口号</param>
+ /// <param name="type">类型</param>
+ /// <param name="sub">子设备编号</param>
+ public UsbHubPort(int grp, int port, DevType type, int sub = 0)
+ {
+ HubGroup = grp;
+ Port = port;
+ SubDev = sub;
+ Type = type;
+ }
+
+ /// <summary>获取USB设备</summary>
+ /// <returns></returns>
+ public UsbDevice GetUsbDevice()
+ {
+ if(HubId < 0) return null;
+
+ var all = UsbHelper.GetAllUsbInfo();
+ if (!all.ContainsKey(HubId)) return null;
+
+ var hubdevs = all[HubId];
+ return hubdevs.FirstOrDefault(e => e.Port == Port);
+ }
+
+ #region 判定设备是否存在
+
+ public enum DevType
+ {
+ Hid,
+ Camera,
+ Audio,
+ Com,
+ Network,
+ Hub,
+ // 判断 VID PID
+ Other
+ };
+
+ /// <summary>设备类型</summary>
+ public DevType Type { get; set; } = DevType.Other;
+
+ /// <summary>type=Other时判定</summary>
+ public ushort VID { get; set; }
+ /// <summary>type=Other时判定</summary>
+ public ushort PID { get; set; }
+
+ /// <summary>判断设备在这个位置是否存在</summary>
+ /// <param name="devs">hub下所有设备集合</param>
+ /// <returns></returns>
+ public bool Exists(List<UsbDevice> devs)
+ {
+ return Exists(devs, Port, Type, SubDev, PID, VID);
+ }
+
+ /// <summary>判断设备在这个位置是否存在</summary>
+ /// <param name="hubid">系统中hub的编号,默认直接用HubId判断</param>
+ /// <returns></returns>
+ public bool Exists(int hubid = -1)
+ {
+ if (hubid < 0) hubid = HubId;
+ if (hubid < 0) return false;
+
+ return Exists(hubid, Port, Type, SubDev, PID, VID);
+ }
+
+ /// <summary>判定设备是否存在</summary>
+ /// <param name="hubdevs"></param>
+ /// <param name="port"></param>
+ /// <param name="type">类型</param>
+ /// <param name="subIdx">复合设备的子设备编号,默认0</param>
+ /// <param name="pid">type=Other时判定</param>
+ /// <param name="vid">type=Other时判定</param>
+ /// <returns></returns>
+ public static bool Exists(List<UsbDevice> hubdevs, int port, DevType type, int subIdx = 0, ushort pid = 0, ushort vid = 0)
+ {
+ /*
+ var devs = hubdevs;
+ var dic = devs.ToDictionary(e => e.Port, e => e);
+ // 端口没有插入设备
+ if (!dic.ContainsKey(port)) return false;
+ var usbdev = dic[port];
+ */
+ // 端口没有插入设备
+ var usbdev = hubdevs.FirstOrDefault(e => e.Port == port);
+ if (usbdev == null) return false;
+
+ bool res = false;
+ // 根据类型判断是否存在
+ switch (type)
+ {
+ case DevType.Hid:
+ var hiddev = usbdev.GetHidDevice(subIdx);
+ if (hiddev != null) res = true;
+ break;
+ case DevType.Camera:
+ var pnp = usbdev.GetCameraPnp();
+ if (!pnp.IsNullOrEmpty()) res = true;
+ break;
+ case DevType.Audio:
+ var ad = usbdev.GetAudioDevice();
+ if (ad != null) res = true;
+ break;
+ case DevType.Com:
+ var cname = usbdev.GetComName(subIdx);
+ if (cname.StartsWith("COM")) res = true;
+ break;
+ case DevType.Network:
+ var netobj = usbdev.GetNetworkCard();
+ if (netobj != null) res = true;
+ break;
+ case DevType.Hub:
+ if (usbdev.Name.Contains("集线器")) res = true;
+ break;
+ case DevType.Other:
+ var _pid = BitConverter.ToUInt16(usbdev.PID.GetBytes(), 0);
+ var _vid = BitConverter.ToUInt16(usbdev.VID.GetBytes(), 0);
+ if ((_pid == pid) && (_vid == vid)) res = true;
+ break;
+ default: break;
+ }
+
+ return res;
+ }
+
+ /// <summary>判定设备是否存在</summary>
+ /// <param name="hubid"></param>
+ /// <param name="port"></param>
+ /// <param name="type">类型</param>
+ /// <param name="subIdx">复合设备的子设备编号,默认0</param>
+ /// <param name="pid">type=Other时判定</param>
+ /// <param name="vid">type=Other时判定</param>
+ /// <returns></returns>
+ public static bool Exists(int hubid, int port, DevType type, int subIdx = 0, ushort pid = 0, ushort vid = 0)
+ {
+ if (hubid < 0) return false;
+
+ var all = UsbHelper.GetAllUsbInfo();
+ if (!all.ContainsKey(hubid)) return false;
+
+ return Exists(all[hubid], port, type, subIdx, pid, vid);
+ }
+
+ #endregion
+
+ #region 查找HUB
+
+ /// <summary>获取检查有效的hub</summary>
+ /// <param name="pdevs"></param>
+ /// <param name="ignore">pdevs中不参与匹配的。 注意:成员和pdevs内的是同一个对象</param>
+ /// <returns>key是group,value是满足条件的hubids</returns>
+ public static Dictionary<int,List<int>> FindHubs(UsbHubPort[] pdevs, UsbHubPort[] ignore = null)
+ {
+ // 拿到所有去重后的 group id
+ var gps = pdevs.Select(e => e.HubGroup).Distinct().ToList();
+
+ // 满足hub筛查条件的hub集合
+ var dic = new Dictionary<int, List<int>>();
+ foreach (var hub in UsbHelper.GetAllUsbInfo())
+ {
+ // 分别判断是否满足分组
+ foreach (var gp in gps)
+ {
+ bool grpOk = true;
+ foreach (var udev in pdevs)
+ {
+ if ((ignore!=null) && ignore.Contains(udev)) continue;
+
+ // 是相关分组
+ if (udev.HubGroup != gp) continue;
+ if (!udev.Exists(hub.Value))
+ {
+ grpOk = false;
+ break;
+ }
+ }
+
+ if (grpOk)
+ {
+ if (!dic.ContainsKey(gp))
+ {
+ var list = new List<int>();
+ list.Add(hub.Key);
+ dic.Add(gp, list);
+ }
+ else
+ {
+ dic[gp].Add(hub.Key);
+ }
+ }
+ }
+ }
+
+ return dic;
+ }
+
+ /// <summary>获取检查有效的hub,每个Group只且只有一个匹配项</summary>
+ /// <param name="pdevs"></param>
+ /// <param name="ignore"></param>
+ /// <returns>key是group,value是满足条件的hubid</returns>
+ public static Dictionary<int, int> FindHubs_JustOne(UsbHubPort[] pdevs, UsbHubPort[] ignore = null)
+ {
+ var dic = FindHubs(pdevs, ignore);
+
+ if (dic == null || dic.Count < 1) return null;
+ foreach (var item in dic)
+ {
+ if (item.Value.Count != 1) return null;
+ }
+
+ return dic.ToDictionary(e => e.Key, e => e.Value[0]);
+ }
+
+ #endregion
+ }
+}
diff --git a/A1Lib/ZipResourceExtractor.cs b/A1Lib/ZipResourceExtractor.cs
new file mode 100644
index 0000000..c3a4717
--- /dev/null
+++ b/A1Lib/ZipResourceExtractor.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Reflection;
+using NewLife.Log;
+
+namespace ALib
+{
+ /// <summary>
+ /// 嵌入 ZIP 资源释放器。
+ /// 原生库多为未压缩的 PE 文件,ZIP 压缩率通常可达 3~8 倍,
+ /// 因此把成组文件(或单个大 DLL)打包为 zip 嵌入,可显著减小程序集体积;
+ /// 运行首次使用时再解压还原到磁盘。
+ /// 仅依赖 .NET 内置的 <see cref="System.IO.Compression"/>,无需第三方库。
+ /// </summary>
+ public static class ZipResourceExtractor
+ {
+ /// <summary>是否已释放过,避免重复解压</summary>
+ private static readonly HashSet<String> _done = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>释放过程锁</summary>
+ private static readonly Object _lock = new Object();
+
+ /// <summary>
+ /// 把嵌入的 zip 资源解压到指定目录(保留 zip 内的相对结构)。
+ /// </summary>
+ /// <param name="resourceName">嵌入资源完整名,如 ALib.ZLG_CAN.kerneldlls.zip</param>
+ /// <param name="destDir">目标目录</param>
+ /// <param name="checkFile">
+ /// 完整性校验用的相对文件名(如 dll_cfg.ini)。
+ /// 该文件存在即认为目录已释放完整,直接跳过解压;
+ /// 传 null 则每次都按缓存标记判断。
+ /// </param>
+ /// <returns>是否成功</returns>
+ public static Boolean ExtractToDirectory(String resourceName, String destDir, String checkFile = null)
+ {
+ if (String.IsNullOrEmpty(resourceName) || String.IsNullOrEmpty(destDir)) return false;
+
+ lock (_lock)
+ {
+ if (_done.Contains(resourceName)) return true;
+
+ // 已释放且关键文件在,直接复用,避免每次启动都解压
+ if (!String.IsNullOrEmpty(checkFile) && File.Exists(Path.Combine(destDir, checkFile)))
+ {
+ _done.Add(resourceName);
+ return true;
+ }
+
+ try
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ using var stream = asm.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ XTrace.WriteLine($"找不到嵌入资源: {resourceName}");
+ return false;
+ }
+
+ using var zip = new ZipArchive(stream, ZipArchiveMode.Read);
+
+ Directory.CreateDirectory(destDir);
+
+ var count = 0;
+ foreach (var entry in zip.Entries)
+ {
+ // 目录项(以 / 结尾)本身不写文件,但其下的文件会隐式建目录
+ if (String.IsNullOrEmpty(entry.Name)) continue;
+
+ var rel = entry.FullName.Replace('/', Path.DirectorySeparatorChar);
+
+ // 防目录穿越:只允许解压到 destDir 之内
+ var dest = Path.GetFullPath(Path.Combine(destDir, rel));
+ var rootFull = Path.GetFullPath(destDir);
+ if (!dest.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
+ {
+ XTrace.WriteLine($"跳过越界条目: {entry.FullName}");
+ continue;
+ }
+
+ var dir = Path.GetDirectoryName(dest);
+ if (!String.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
+
+ // 大小一致则跳过写入:驱动已被加载时无法覆盖,跳过也能保证可用
+ var fi = new FileInfo(dest);
+ if (fi.Exists && fi.Length == entry.Length) continue;
+
+ WriteEntry(entry, dest);
+ count++;
+ }
+
+ var ok = String.IsNullOrEmpty(checkFile) || File.Exists(Path.Combine(destDir, checkFile));
+ if (ok)
+ {
+ _done.Add(resourceName);
+ XTrace.WriteLine($"ZIP 资源已释放: {resourceName} -> {destDir}(解压 {count} 个文件)");
+ }
+ else
+ {
+ XTrace.WriteLine($"ZIP 释放后缺少校验文件 {checkFile},目录可能不完整: {destDir}");
+ }
+ return ok;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"释放 ZIP 资源失败 {resourceName}: {ex.Message}");
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// 把嵌入 zip 中的某个条目解压为单个文件(zip 内可只放这一个文件)。
+ /// </summary>
+ /// <param name="resourceName">嵌入资源完整名,如 ALib.ZLG_CAN.zlgcan.zip</param>
+ /// <param name="destPath">目标文件路径</param>
+ /// <returns>是否成功</returns>
+ public static Boolean ExtractFile(String resourceName, String destPath)
+ {
+ if (String.IsNullOrEmpty(resourceName) || String.IsNullOrEmpty(destPath)) return false;
+
+ lock (_lock)
+ {
+ var key = resourceName + "|" + destPath;
+ if (_done.Contains(key)) return true;
+
+ // 已存在且非空,直接复用
+ var fi = new FileInfo(destPath);
+ if (fi.Exists && fi.Length > 0)
+ {
+ _done.Add(key);
+ return true;
+ }
+
+ try
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ using var stream = asm.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ {
+ XTrace.WriteLine($"找不到嵌入资源: {resourceName}");
+ return false;
+ }
+
+ using var zip = new ZipArchive(stream, ZipArchiveMode.Read);
+
+ // 单文件模式:取第一个非目录条目
+ ZipArchiveEntry target = null;
+ foreach (var e in zip.Entries)
+ {
+ if (!String.IsNullOrEmpty(e.Name)) { target = e; break; }
+ }
+ if (target == null)
+ {
+ XTrace.WriteLine($"ZIP 中无可解压文件: {resourceName}");
+ return false;
+ }
+
+ var dir = Path.GetDirectoryName(destPath);
+ if (!String.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
+
+ WriteEntry(target, destPath);
+
+ _done.Add(key);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"释放 ZIP 文件失败 {resourceName}: {ex.Message}");
+ return false;
+ }
+ }
+ }
+
+ /// <summary>查找以指定后缀结尾的嵌入 zip 资源名,找不到返回 null</summary>
+ /// <param name="suffix">资源名后缀,如 ".ZLG_CAN.zlgcan.zip"</param>
+ /// <returns>完整资源名</returns>
+ public static String FindResource(String suffix)
+ {
+ if (String.IsNullOrEmpty(suffix)) return null;
+
+ try
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ foreach (var name in asm.GetManifestResourceNames())
+ {
+ if (name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) return name;
+ }
+ }
+ catch (Exception ex)
+ {
+ XTrace.WriteLine($"枚举嵌入资源失败: {ex.Message}");
+ }
+
+ return null;
+ }
+
+ /// <summary>把 zip 条目写入磁盘(先写临时文件再原子替换)</summary>
+ /// <param name="entry">zip 条目</param>
+ /// <param name="destPath">目标路径</param>
+ private static void WriteEntry(ZipArchiveEntry entry, String destPath)
+ {
+ var tmp = destPath + ".tmp";
+
+ using (var src = entry.Open())
+ using (var fs = new FileStream(tmp, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ src.CopyTo(fs);
+ fs.Flush();
+ }
+
+ try
+ {
+ File.Move(tmp, destPath, true);
+ }
+ catch
+ {
+ // 目标被占用(如驱动已加载)时保留旧文件;旧文件不存在才视为失败
+ try { File.Delete(tmp); } catch { }
+ if (!File.Exists(destPath)) throw;
+ }
+ }
+ }
+}
diff --git "a/A1Lib/\345\216\237\347\224\237\345\272\223\345\265\214\345\205\245\350\257\264\346\230\216.md" "b/A1Lib/\345\216\237\347\224\237\345\272\223\345\265\214\345\205\245\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..9994eed
--- /dev/null
+++ "b/A1Lib/\345\216\237\347\224\237\345\272\223\345\265\214\345\205\245\350\257\264\346\230\216.md"
@@ -0,0 +1,162 @@
+# 原生库嵌入与 zip 压缩说明
+
+本文档说明 A1Lib 中原生 DLL 的嵌入方式,以及如何通过 zip 压缩减小程序集体积。
+
+---
+
+## 一、背景
+
+A1Lib 需要随程序分发若干 ZLG / 创芯 / PEAK 的原生驱动 DLL:
+
+| DLL | 用途 | 原始体积 |
+|---|---|---|
+| `ControlCAN.dll` | 创芯科技 CAN 卡驱动 | 2.56 MB |
+| `PCANBasic.dll` | PEAK CAN 卡驱动 | 711 KB |
+| `zlgcan.dll` | ZLG 致远 CAN 卡驱动(分发层) | 308 KB |
+| `kerneldlls\*` | ZLG 各设备型号内核驱动 | 39 KB(精简后) |
+
+这些文件是**未压缩的 PE 二进制**,而程序集嵌入资源本身不做压缩,
+导致 `A1Lib.dll` 体积偏大。实测 ZIP 压缩率约 **45%**(约 2:1):
+
+```
+合计:3.59 MB -> 1.61 MB(压缩率 45%)
+```
+
+压缩率虽不及 7z(后者可达 3~8 倍),但 **.NET 内置 `System.IO.Compression`
+即可解压,无需引入第三方库**,因此优先采用 ZIP。
+
+---
+
+## 二、工作机制
+
+### 2.1 嵌入阶段(构建时)
+
+`A1Lib.csproj` 采用「**zip 优先,裸文件兜底**」的双条件配置:
+
+```xml
+<ItemGroup Condition="Exists('CAN\ZLG_CAN\zlgcan.zip')">
+ <EmbeddedResource Include="CAN\ZLG_CAN\zlgcan.zip" LogicalName="ALib.ZLG_CAN.zlgcan.zip" />
+</ItemGroup>
+<ItemGroup Condition="!Exists('CAN\ZLG_CAN\zlgcan.zip')">
+ <EmbeddedResource Include="CAN\ZLG_CAN\zlgcan.dll" LogicalName="ALib.ZLG_CAN.zlgcan.dll" />
+</ItemGroup>
+```
+
+- zip 存在 → 嵌入 zip,体积小
+- zip 不存在 → 嵌入原始 DLL,**保证任何情况下都能构建成功**
+
+因此 zip 是**可选优化**,删掉 zip 也能正常编译,只是程序集变大。
+
+### 2.2 释放阶段(运行时)
+
+首次使用某个原生库时自动解压还原到程序目录:
+
+```
+ZipResourceExtractor.ExtractFile() 单文件:{前缀}.{名去扩展名}.zip
+ZipResourceExtractor.ExtractToDirectory() 目录 :{前缀}.{目录名}.zip
+ ↑
+ DllResourceManager.ExtractDllFromResource() 按「zip 优先」选择资源
+ KernelDllExtractor.Extract() kerneldlls 目录
+```
+
+**资源名约定**(必须与 csproj 的 `LogicalName` 一致):
+
+| 形态 | 约定 | 示例 |
+|---|---|---|
+| 单文件 | `{前缀}.{文件名去扩展名}.zip` | `ALib.ZLG_CAN.zlgcan.zip` |
+| 目录 | `{前缀}.{目录名}.zip` | `ALib.ZLG_CAN.kerneldlls.zip` |
+
+---
+
+## 三、如何生成 zip
+
+在 `A1Lib` 目录下执行打包脚本:
+
+```powershell
+.\pack_native_res.ps1
+```
+
+脚本会为每个目标生成同名 zip 并输出压缩统计:
+
+```
+目标 类型 文件数 原始 压缩后 压缩率
+CAN\ZLG_CAN\zlgcan.dll file 1 307.5 KB 129.8 KB 42%
+CAN\ZLG_CAN\kerneldlls dir 3 38.8 KB 17.9 KB 46%
+CAN\CX_CAN\ControlCAN.dll file 1 2.56 MB 1.16 MB 45%
+CAN\PCAN\PCANBasic.dll file 1 711.1 KB 310.2 KB 44%
+合计:3.59 MB -> 1.61 MB(压缩率 45%)
+```
+
+仅打包单个目标:
+
+```powershell
+.\pack_native_res.ps1 -Target 'CAN\ZLG_CAN\kerneldlls'
+```
+
+> **注意**:脚本含中文输出,文件必须保存为 **UTF-8 with BOM**,
+> 否则 Windows PowerShell 5.1 会按 ANSI 解析而报语法错误。
+
+---
+
+## 四、注意事项
+
+### 4.1 原始文件保留,不参与编译
+
+zip 与原始 DLL 同时存在于源码目录:
+
+```
+CAN/ZLG_CAN/
+├── zlgcan.dll ← 保留:供打包、排查使用
+├── zlgcan.zip ← 嵌入:实际随程序集发布
+└── kerneldlls/ ← 保留:供打包、排查使用
+```
+
+csproj 通过 `Condition` 只嵌入其中之一,两者不会重复打包。
+
+### 4.2 zip 内的目录结构
+
+- **单文件 zip**:zip 内直接是该文件(`zlgcan.dll`)
+- **目录 zip**:以**目录内容为根**,不含目录名本身
+ (即 `kerneldlls.zip` 内的条目是 `USBCAN.dll`、`dll_cfg.ini` 等,
+ 而非 `kerneldlls/USBCAN.dll`),解压后直接铺到目标目录下。
+ 这样 `kerneldlls\` 的相对路径才能被 `zlgcan.dll` 正确找到。
+
+### 4.3 安全与健壮性
+
+解压实现已考虑以下情况:
+
+| 场景 | 处理 |
+|---|---|
+| zip 条目含 `..\` 路径穿越 | 校验目标路径必须落在解压目录内,否则跳过 |
+| 文件已存在且大小一致 | 跳过写入,减少磁盘操作 |
+| 目标文件被占用(驱动已加载) | 保留旧文件不报错,不影响使用 |
+| 解压中断导致目录不完整 | `kerneldlls` 用 `dll_cfg.ini` 作完整性标记,缺失则重新解压 |
+| 未打包 zip | 自动回退嵌入裸文件,功能不受影响 |
+
+### 4.4 更换设备型号后需重新打包
+
+若从 `ZLG_USBCAN/kerneldlls_original/` 取回了新的驱动文件,
+**必须重新运行打包脚本**,否则 zip 内仍是旧内容。
+
+---
+
+## 五、后续可改进项
+
+- **7z 支持**:7z 压缩率显著优于 ZIP(实测 kerneldlls 7.2MB → 932KB),
+ 但需引入 `SevenZipSharp` / `SharpCompress` 等第三方库,
+ 或改为调用外部 `7z.exe`。当前方案已用 ZIP 覆盖主要收益。
+- **增量打包**:目前每次全量重打包,可按文件时间戳跳过未变更项。
+- **构建时自动打包**:可加 MSBuild Target 在 `BeforeBuild` 自动调用脚本,
+ 省去手动执行(代价是每次构建增加压缩耗时)。
+
+---
+
+## 六、相关文件
+
+| 文件 | 职责 |
+|---|---|
+| `pack_native_res.ps1` | 打包脚本:生成各原生库的 zip |
+| `ZipResourceExtractor.cs` | 通用 zip 资源释放(单文件 / 目录两种模式) |
+| `DllResourceManager.cs` | 单个 DLL 的「zip 优先」释放与加载 |
+| `CAN/ZLG_CAN/KernelDllExtractor.cs` | kerneldlls 目录的释放(zip 优先,逐个兜底) |
+| `A1Lib.csproj` | 嵌入配置(zip 优先、裸文件兜底) |
diff --git a/A2Lib/A2Lib.csproj b/A2Lib/A2Lib.csproj
new file mode 100644
index 0000000..18c85de
--- /dev/null
+++ b/A2Lib/A2Lib.csproj
@@ -0,0 +1,48 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Library</OutputType>
+ <RootNamespace>ALib</RootNamespace>
+ <TargetFramework>net8.0-windows</TargetFramework>
+ <Nullable>disable</Nullable>
+ <UseWindowsForms>true</UseWindowsForms>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <OutputPath>..\Bin\A2Lib</OutputPath>
+ <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <ApplicationHighDpiMode>SystemAware</ApplicationHighDpiMode>
+ <ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Update="Audio\AudioRecordForm.cs">
+ <SubType>Form</SubType>
+ </Compile>
+ <Compile Update="Properties\Resources.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>Resources.resx</DependentUpon>
+ </Compile>
+ </ItemGroup>
+
+ <ItemGroup>
+ <EmbeddedResource Update="Properties\Resources.resx">
+ <Generator>PublicResXFileCodeGenerator</Generator>
+ <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
+ </ItemGroup>
+
+ <ItemGroup>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\A1Lib\A1Lib.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Folder Include="Hid\" />
+ </ItemGroup>
+
+</Project>
\ No newline at end of file
diff --git a/A2Lib/Audio/AudioRecordForm.cs b/A2Lib/Audio/AudioRecordForm.cs
new file mode 100644
index 0000000..34a2f89
--- /dev/null
+++ b/A2Lib/Audio/AudioRecordForm.cs
@@ -0,0 +1,287 @@
+using ALib.Audio;
+using ALib.WinForm;
+using NAudio.Wave;
+using NewLife.Log;
+using System.Drawing;
+
+namespace ALib.Audio
+{
+ public partial class AudioRecordForm : Form
+ {
+ public AudioRecordForm()
+ {
+ InitializeComponent();
+ }
+
+ private AudioRecord _recorder;
+
+ private float _leftVolume;
+ private float _rightVolume;
+
+ /// <summary>左声道梅尔特征提取器</summary>
+ private AudioFeatureExtractor _afeLeft;
+ /// <summary>右声道梅尔特征提取器</summary>
+ private AudioFeatureExtractor _afeRight;
+
+ /// <summary>左声道梅尔流动图</summary>
+ private MelStreamImage _melLeft;
+ /// <summary>右声道梅尔流动图</summary>
+ private MelStreamImage _melRight;
+
+ /// <summary>选中的声卡设备</summary>
+ public AudioDevice? SelectedDevice { get; set; }
+
+ private void AudioRecordForm_Load(object sender, EventArgs e)
+ {
+ // 显示设备信息
+ if (SelectedDevice != null)
+ {
+ Text = $"录音测试 - {SelectedDevice.Name}";
+ rtbInfo.Text = SelectedDevice.ToString();
+ }
+
+ // 初始化音量显示
+ UpdateVolumeDisplay();
+
+ // 初始化梅尔图
+ InitMelDisplay();
+ }
+
+ /// <summary>初始化梅尔图显示</summary>
+ private void InitMelDisplay()
+ {
+ _melLeft = new MelStreamImage(pbMelL.Width) { ColorMode = 0 };
+ _melRight = new MelStreamImage(pbMelR.Width) { ColorMode = 0 };
+
+ pbMelL.Image = null;
+ pbMelR.Image = null;
+ }
+
+ private void btnSelectDevice_Click(object sender, EventArgs e)
+ {
+ var form = new AudioSelect();
+ form.ShowDialog();
+
+ SelectedDevice = form.Devices[form.SelectedDevice];
+ rtbInfo.Text = SelectedDevice.ToString();
+ Text = $"录音测试 - {SelectedDevice.Name}";
+ }
+
+ private void btnStart_Click(object sender, EventArgs e)
+ {
+ if (SelectedDevice?.RecordingDevice == null)
+ {
+ MessageBox.Show("请先选择声卡设备", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
+ return;
+ }
+
+ try
+ {
+ WriteLog($"使用录音设备: {SelectedDevice.RecordingDevice.DeviceFriendlyName}");
+
+ // 使用 AudioRecord 创建录音实例
+ _recorder = new AudioRecord(SelectedDevice);
+
+ var recordFormat = _recorder.RecordFormat;
+ WriteLog($"设备实际格式: {recordFormat.Encoding}, {recordFormat.SampleRate}Hz, {recordFormat.Channels}声道, {recordFormat.BitsPerSample}位");
+ WriteLog($"保存文件: {(cbSaveFile.Checked ? "是" : "否")}");
+
+ // 初始化梅尔特征提取器(按设备实际格式与声道数)
+ CreateMelExtractors(recordFormat);
+
+ // 订阅音量事件
+ _recorder.VolumeUpdated += OnVolumeUpdated;
+
+ // 订阅音频数据事件(用于实时梅尔图)
+ _recorder.DataUpdated += OnDataUpdated;
+
+ // 订阅完成事件
+ _recorder.Stoped += OnRecorderStoped;
+
+ // 开始录音(-1 表示一直录,直到手动停止)
+ if (!_recorder.Start(-1))
+ {
+ throw new Exception("启动录音失败");
+ }
+
+ // 更新UI
+ btnStart.Enabled = false;
+ btnStop.Enabled = true;
+ btnSelectDevice.Enabled = false;
+ cbSaveFile.Enabled = false;
+ lbRecordStatus.Text = "正在录音...";
+ lbRecordStatus.ForeColor = Color.Red;
+
+ WriteLog("开始录音");
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"启动录音失败: {ex.Message}");
+ MessageBox.Show($"启动录音失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ /// <summary>音量更新回调</summary>
+ private void OnVolumeUpdated(Object sender, byte[] vols)
+ {
+ _leftVolume = vols[0];
+ _rightVolume = vols[1];
+
+ this.UpdateUIControl(UpdateVolumeDisplay);
+ }
+
+ /// <summary>创建梅尔特征提取器</summary>
+ /// <param name="format">设备录音格式</param>
+ private void CreateMelExtractors(WaveFormat format)
+ {
+ if (format == null) return;
+
+ _afeLeft = new AudioFeatureExtractor(format, 30);
+ _melLeft = new MelStreamImage(pbMelL.Width) { ColorMode = 0 };
+
+ // 单声道时右声道复用左声道数据
+ _afeRight = new AudioFeatureExtractor(format, 30);
+ _melRight = new MelStreamImage(pbMelR.Width) { ColorMode = 0 };
+ }
+
+ /// <summary>音频数据更新回调,实时刷新梅尔图</summary>
+ private void OnDataUpdated(Object sender, float[][] audioData)
+ {
+ if (audioData == null || audioData.Length == 0) return;
+ if (_afeLeft == null || _melLeft == null) return;
+
+ try
+ {
+ // 左声道
+ var dbsLeft = _afeLeft.ProcessAsDecibels(audioData[0]);
+ if (dbsLeft != null && dbsLeft.Length > 0)
+ {
+ _melLeft.PushDbs(dbsLeft);
+ var imgL = _melLeft.GetImage("L");
+ this.UpdateUIControl(() =>
+ {
+ var old = pbMelL.Image;
+ pbMelL.Image = imgL;
+ old?.Dispose();
+ });
+ }
+
+ // 右声道:多声道取第二声道,单声道复用第一声道
+ var rightIndex = audioData.Length > 1 ? 1 : 0;
+ var dbsRight = _afeRight.ProcessAsDecibels(audioData[rightIndex]);
+ if (dbsRight != null && dbsRight.Length > 0)
+ {
+ _melRight.PushDbs(dbsRight);
+ var imgR = _melRight.GetImage("R");
+ this.UpdateUIControl(() =>
+ {
+ var old = pbMelR.Image;
+ pbMelR.Image = imgR;
+ old?.Dispose();
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"梅尔图计算失败: {ex.Message}");
+ }
+ }
+
+ /// <summary>录音停止回调</summary>
+ private void OnRecorderStoped(object sender, EventArgs e)
+ {
+ // 清理梅尔图数据
+ _melLeft?.Clear();
+ _melRight?.Clear();
+
+ // 更新UI
+ this.UpdateUIControl(() =>
+ {
+ btnStart.Enabled = true;
+ btnStop.Enabled = false;
+ btnSelectDevice.Enabled = true;
+ cbSaveFile.Enabled = true;
+ lbRecordStatus.Text = "录音已停止";
+ lbRecordStatus.ForeColor = Color.Black;
+
+ // 重置音量显示
+ _leftVolume = 0;
+ _rightVolume = 0;
+ UpdateVolumeDisplay();
+
+ // 清理梅尔图显示
+ if (pbMelL.Image != null) { pbMelL.Image.Dispose(); pbMelL.Image = null; }
+ if (pbMelR.Image != null) { pbMelR.Image.Dispose(); pbMelR.Image = null; }
+
+ // 如果勾选了保存文件,转存到磁盘
+ if (cbSaveFile.Checked && _recorder != null && _recorder.AudioData.Length > 0)
+ {
+ try
+ {
+ var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
+ var path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"record_{timestamp}.wav");
+
+ if (_recorder.SaveTo(path))
+ {
+ WriteLog($"录音完成: {path}");
+ }
+ else
+ {
+ WriteLog("录音完成但保存文件失败");
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"保存文件失败: {ex.Message}");
+ }
+ }
+ else
+ {
+ WriteLog("录音完成(未保存)");
+ }
+ });
+ }
+
+ private void btnStop_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ _recorder?.Stop();
+ }
+ catch (Exception ex)
+ {
+ WriteLog($"停止录音失败: {ex.Message}");
+ }
+ }
+
+ private void UpdateVolumeDisplay()
+ {
+ progressLeft.Value = (int)Math.Min(_leftVolume, 100);
+ progressRight.Value = (int)Math.Min(_rightVolume, 100);
+ lbLeft.Text = $"L: {_leftVolume:F1}%";
+ lbRight.Text = $"R: {_rightVolume:F1}%";
+ }
+
+ private void AudioRecordForm_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ if (_recorder != null)
+ {
+ _recorder.DataUpdated -= OnDataUpdated;
+ if (_recorder.IsRecording) _recorder.Stop();
+ }
+ }
+
+ #region 日志
+
+ public ILog Log { get; set; } = Logger.Null;
+
+ private void WriteLog(String format, params Object[] args)
+ {
+ var msg = String.Format(format, args);
+ rtbLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}\r\n");
+ Log?.Info(format, args);
+ }
+
+ #endregion
+ }
+}
diff --git a/A2Lib/Audio/AudioRecordForm.Designer.cs b/A2Lib/Audio/AudioRecordForm.Designer.cs
new file mode 100644
index 0000000..b2bc00f
--- /dev/null
+++ b/A2Lib/Audio/AudioRecordForm.Designer.cs
@@ -0,0 +1,242 @@
+namespace ALib.Audio
+{
+ partial class AudioRecordForm
+ {
+ /// <summary>
+ /// Required designer variable.
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// Clean up any resources being used.
+ /// </summary>
+ /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ /// <summary>
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ /// </summary>
+ private void InitializeComponent()
+ {
+ rtbInfo = new RichTextBox();
+ grpVolume = new GroupBox();
+ pbMelL = new PictureBox();
+ pbMelR = new PictureBox();
+ progressLeft = new ProgressBar();
+ progressRight = new ProgressBar();
+ lbLeft = new Label();
+ lbRight = new Label();
+ grpRecord = new GroupBox();
+ btnSelectDevice = new Button();
+ btnStart = new Button();
+ btnStop = new Button();
+ lbRecordStatus = new Label();
+ cbSaveFile = new CheckBox();
+ rtbLog = new RichTextBox();
+ grpVolume.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)pbMelL).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)pbMelR).BeginInit();
+ grpRecord.SuspendLayout();
+ SuspendLayout();
+ //
+ // rtbInfo
+ //
+ rtbInfo.Location = new Point(12, 12);
+ rtbInfo.Name = "rtbInfo";
+ rtbInfo.ReadOnly = true;
+ rtbInfo.Size = new Size(560, 134);
+ rtbInfo.TabIndex = 0;
+ rtbInfo.Text = "";
+ //
+ // grpVolume
+ //
+ grpVolume.Controls.Add(pbMelL);
+ grpVolume.Controls.Add(pbMelR);
+ grpVolume.Controls.Add(progressLeft);
+ grpVolume.Controls.Add(progressRight);
+ grpVolume.Controls.Add(lbLeft);
+ grpVolume.Controls.Add(lbRight);
+ grpVolume.Location = new Point(12, 152);
+ grpVolume.Name = "grpVolume";
+ grpVolume.Size = new Size(560, 210);
+ grpVolume.TabIndex = 1;
+ grpVolume.TabStop = false;
+ grpVolume.Text = "音量显示";
+ //
+ // pbMelL
+ //
+ pbMelL.Location = new Point(15, 62);
+ pbMelL.Name = "pbMelL";
+ pbMelL.Size = new Size(250, 140);
+ pbMelL.TabIndex = 4;
+ pbMelL.TabStop = false;
+ //
+ // pbMelR
+ //
+ pbMelR.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ pbMelR.Location = new Point(295, 62);
+ pbMelR.Name = "pbMelR";
+ pbMelR.Size = new Size(250, 140);
+ pbMelR.TabIndex = 5;
+ pbMelR.TabStop = false;
+ //
+ // progressLeft
+ //
+ progressLeft.Location = new Point(15, 39);
+ progressLeft.Name = "progressLeft";
+ progressLeft.Size = new Size(250, 17);
+ progressLeft.TabIndex = 0;
+ //
+ // progressRight
+ //
+ progressRight.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ progressRight.Location = new Point(295, 39);
+ progressRight.Name = "progressRight";
+ progressRight.Size = new Size(250, 17);
+ progressRight.TabIndex = 1;
+ //
+ // lbLeft
+ //
+ lbLeft.AutoSize = true;
+ lbLeft.Location = new Point(15, 19);
+ lbLeft.Name = "lbLeft";
+ lbLeft.Size = new Size(49, 17);
+ lbLeft.TabIndex = 2;
+ lbLeft.Text = "L: 0.0%";
+ //
+ // lbRight
+ //
+ lbRight.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ lbRight.AutoSize = true;
+ lbRight.Location = new Point(295, 19);
+ lbRight.Name = "lbRight";
+ lbRight.Size = new Size(51, 17);
+ lbRight.TabIndex = 3;
+ lbRight.Text = "R: 0.0%";
+ //
+ // grpRecord
+ //
+ grpRecord.Controls.Add(btnSelectDevice);
+ grpRecord.Controls.Add(btnStart);
+ grpRecord.Controls.Add(btnStop);
+ grpRecord.Controls.Add(lbRecordStatus);
+ grpRecord.Controls.Add(cbSaveFile);
+ grpRecord.Location = new Point(12, 368);
+ grpRecord.Name = "grpRecord";
+ grpRecord.Size = new Size(560, 80);
+ grpRecord.TabIndex = 2;
+ grpRecord.TabStop = false;
+ grpRecord.Text = "录音控制";
+ //
+ // btnSelectDevice
+ //
+ btnSelectDevice.Location = new Point(15, 30);
+ btnSelectDevice.Name = "btnSelectDevice";
+ btnSelectDevice.Size = new Size(100, 30);
+ btnSelectDevice.TabIndex = 0;
+ btnSelectDevice.Text = "选择声卡";
+ btnSelectDevice.UseVisualStyleBackColor = true;
+ btnSelectDevice.Click += btnSelectDevice_Click;
+ //
+ // btnStart
+ //
+ btnStart.Location = new Point(130, 30);
+ btnStart.Name = "btnStart";
+ btnStart.Size = new Size(100, 30);
+ btnStart.TabIndex = 1;
+ btnStart.Text = "开始录音";
+ btnStart.UseVisualStyleBackColor = true;
+ btnStart.Click += btnStart_Click;
+ //
+ // btnStop
+ //
+ btnStop.Enabled = false;
+ btnStop.Location = new Point(245, 30);
+ btnStop.Name = "btnStop";
+ btnStop.Size = new Size(100, 30);
+ btnStop.TabIndex = 2;
+ btnStop.Text = "停止录音";
+ btnStop.UseVisualStyleBackColor = true;
+ btnStop.Click += btnStop_Click;
+ //
+ // lbRecordStatus
+ //
+ lbRecordStatus.AutoSize = true;
+ lbRecordStatus.Location = new Point(360, 38);
+ lbRecordStatus.Name = "lbRecordStatus";
+ lbRecordStatus.Size = new Size(68, 17);
+ lbRecordStatus.TabIndex = 3;
+ lbRecordStatus.Text = "未开始录音";
+ //
+ // cbSaveFile
+ //
+ cbSaveFile.AutoSize = true;
+ cbSaveFile.Checked = true;
+ cbSaveFile.CheckState = CheckState.Checked;
+ cbSaveFile.Location = new Point(130, 58);
+ cbSaveFile.Name = "cbSaveFile";
+ cbSaveFile.Size = new Size(75, 21);
+ cbSaveFile.TabIndex = 4;
+ cbSaveFile.Text = "保存文件";
+ cbSaveFile.UseVisualStyleBackColor = true;
+ //
+ // rtbLog
+ //
+ rtbLog.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left;
+ rtbLog.Location = new Point(12, 454);
+ rtbLog.Name = "rtbLog";
+ rtbLog.Size = new Size(560, 168);
+ rtbLog.TabIndex = 3;
+ rtbLog.Text = "";
+ //
+ // AudioRecordForm
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(584, 634);
+ Controls.Add(rtbLog);
+ Controls.Add(grpRecord);
+ Controls.Add(grpVolume);
+ Controls.Add(rtbInfo);
+ Name = "AudioRecordForm";
+ Text = "录音测试";
+ FormClosing += AudioRecordForm_FormClosing;
+ Load += AudioRecordForm_Load;
+ grpVolume.ResumeLayout(false);
+ grpVolume.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)pbMelL).EndInit();
+ ((System.ComponentModel.ISupportInitialize)pbMelR).EndInit();
+ grpRecord.ResumeLayout(false);
+ grpRecord.PerformLayout();
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private RichTextBox rtbInfo;
+ private GroupBox grpVolume;
+ private ProgressBar progressLeft;
+ private ProgressBar progressRight;
+ private Label lbLeft;
+ private Label lbRight;
+ private GroupBox grpRecord;
+ private Button btnSelectDevice;
+ private Button btnStart;
+ private Button btnStop;
+ private Label lbRecordStatus;
+ private CheckBox cbSaveFile;
+ private RichTextBox rtbLog;
+ private PictureBox pbMelR;
+ private PictureBox pbMelL;
+ }
+}
diff --git a/XCoder/Windows/AudioSelect.resx b/A2Lib/Audio/AudioRecordForm.resx
similarity index 100%
rename from XCoder/Windows/AudioSelect.resx
rename to A2Lib/Audio/AudioRecordForm.resx
diff --git a/XCoder/Windows/AudioSelect.cs b/A2Lib/Audio/AudioSelect.cs
similarity index 71%
rename from XCoder/Windows/AudioSelect.cs
rename to A2Lib/Audio/AudioSelect.cs
index 8be9501..5f72251 100644
--- a/XCoder/Windows/AudioSelect.cs
+++ b/A2Lib/Audio/AudioSelect.cs
@@ -1,4 +1,4 @@
-
+using ALib.Audio;
using NAudio.CoreAudioApi;
using NAudio.Utils;
using NAudio.Wave;
@@ -14,10 +14,9 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
-namespace XCoder
+namespace ALib.Audio
{
- [DisplayName("声卡选择器")]
- public partial class AudioSelect : Form,IXForm
+ public partial class AudioSelect : Form
{
public AudioSelect()
{
@@ -35,9 +34,7 @@ namespace XCoder
private void AudioSelect_Load(object sender, EventArgs e)
{
- rtb_Info.MouseWheel += Rtb_Show_MouseWheel;
-
- Devices = AudioHelper.GetAudioDevices();
+ Devices = AudioDeviceHelper.GetAudioDevices();
// 如果没有声卡,直接退出
if (Devices == null || Devices.Count < 1)
{
@@ -67,7 +64,6 @@ namespace XCoder
var dev = Devices[cid];
rtb_Info.Text = dev.ToString();
- /*
// 需要有播放和录音设备,且至少是立体声,才能启用确定按钮
bool en = true;
if (dev.PlaybackDevice == null) en = false;
@@ -75,7 +71,6 @@ namespace XCoder
if (dev.PlaybackDevice?.AudioClient.MixFormat.Channels < 2) en = false;
if (dev.RecordingDevice?.AudioClient.MixFormat.Channels < 2) en = false;
btn_ok.Enabled = en;
- */
// 如果有播放设备,启用测试按钮
btn_Spk.Enabled = dev.PlaybackDevice != null;
@@ -105,7 +100,7 @@ namespace XCoder
// 指定声卡播放测试音频
// var af = new AudioFileReader(@"playTest.wav");
- var af = new WaveFileReader(new MemoryStream(CrazyCoder.Properties.Resources.playTest));
+ var af = new WaveFileReader(new MemoryStream(ALib.Properties.Resources.playTest));
// af.TotalTime;
var waveOut = new WasapiOut(dev.PlaybackDevice, AudioClientShareMode.Shared, false, 100);
waveOut.Init(af);
@@ -119,72 +114,6 @@ namespace XCoder
waveOut.Play();
}
- protected override void WndProc(ref Message m)
- {
- const int WM_MOUSEWHEEL = 0x020A;
-
- if (m.Msg == WM_MOUSEWHEEL)
- {
- // 在这里处理你的全局滚轮事件
- short delta = 0;
- if (IntPtr.Size == 8)
- {
- // 64位系统
- delta = (short)(m.WParam.ToInt64() >> 16);
- }
- else
- {
- // 32位系统
- delta = (short)(m.WParam.ToInt32() >> 16);
- }
-
- // int delta = (short)(m.WParam.ToInt32() >> 16);
- // Console.WriteLine("Mouse wheel moved: " + delta);
-
- if (delta > 0)
- {
- // 向上滚动
- if (cb_Dev.SelectedIndex > 0)
- {
- cb_Dev.SelectedIndex--;
- }
- }
- else if (delta < 0)
- {
- // 向下滚动
- if (cb_Dev.SelectedIndex < cb_Dev.Items.Count - 1)
- {
- cb_Dev.SelectedIndex++;
- }
- }
-
- // 如果你想阻止事件传递给其他控件,可以在这里返回
- return;
- }
-
- base.WndProc(ref m);
- }
-
- private void Rtb_Show_MouseWheel(object sender, MouseEventArgs e)
- {
- if (e.Delta > 0)
- {
- // 向上滚动
- if (cb_Dev.SelectedIndex > 0)
- {
- cb_Dev.SelectedIndex--;
- }
- }
- else if (e.Delta < 0)
- {
- // 向下滚动
- if (cb_Dev.SelectedIndex < cb_Dev.Items.Count - 1)
- {
- cb_Dev.SelectedIndex++;
- }
- }
- }
-
#region 日志
public ILog Log { get; set; } = Logger.Null;
diff --git a/XCoder/Windows/AudioSelect.Designer.cs b/A2Lib/Audio/AudioSelect.Designer.cs
similarity index 91%
rename from XCoder/Windows/AudioSelect.Designer.cs
rename to A2Lib/Audio/AudioSelect.Designer.cs
index 708b24a..bc74efc 100644
--- a/XCoder/Windows/AudioSelect.Designer.cs
+++ b/A2Lib/Audio/AudioSelect.Designer.cs
@@ -1,4 +1,4 @@
-namespace XCoder
+namespace ALib.Audio
{
partial class AudioSelect
{
@@ -64,7 +64,7 @@
btn_Spk.Name = "btn_Spk";
btn_Spk.Size = new Size(91, 34);
btn_Spk.TabIndex = 2;
- btn_Spk.Text = "TEST";
+ btn_Spk.Text = "SPEAK TEST";
btn_Spk.UseVisualStyleBackColor = true;
btn_Spk.Click += btn_Spk_Click;
//
@@ -82,7 +82,6 @@
// rtb_Info
//
rtb_Info.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
- rtb_Info.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Regular, GraphicsUnit.Point);
rtb_Info.Location = new Point(57, 72);
rtb_Info.Name = "rtb_Info";
rtb_Info.ReadOnly = true;
@@ -112,7 +111,8 @@
//
// AudioSelect
//
- AutoScaleMode = AutoScaleMode.Dpi;
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(545, 311);
Controls.Add(lb_Volume);
Controls.Add(tb_Volume);
diff --git a/XCoder/Windows/MelForm.resx b/A2Lib/Audio/AudioSelect.resx
similarity index 100%
rename from XCoder/Windows/MelForm.resx
rename to A2Lib/Audio/AudioSelect.resx
diff --git a/XCoder/Windows/MelForm.cs b/A2Lib/Audio/MelForm.cs
similarity index 95%
rename from XCoder/Windows/MelForm.cs
rename to A2Lib/Audio/MelForm.cs
index df4d644..15699e9 100644
--- a/XCoder/Windows/MelForm.cs
+++ b/A2Lib/Audio/MelForm.cs
@@ -1,4 +1,5 @@
-using System;
+using ALib.Audio;
+using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@@ -8,10 +9,9 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
-namespace XCoder
+namespace ALib.Mel
{
- [DisplayName("音频梅尔频谱")]
- public partial class MelForm : Form, IXForm
+ public partial class MelForm : Form
{
public MelForm()
{
diff --git a/XCoder/Windows/MelForm.Designer.cs b/A2Lib/Audio/MelForm.Designer.cs
similarity index 95%
rename from XCoder/Windows/MelForm.Designer.cs
rename to A2Lib/Audio/MelForm.Designer.cs
index 6771869..ee61774 100644
--- a/XCoder/Windows/MelForm.Designer.cs
+++ b/A2Lib/Audio/MelForm.Designer.cs
@@ -1,4 +1,4 @@
-namespace XCoder
+namespace ALib.Mel
{
partial class MelForm
{
@@ -42,9 +42,9 @@
//
bt_Open.Location = new Point(12, 12);
bt_Open.Name = "bt_Open";
- bt_Open.Size = new Size(89, 25);
+ bt_Open.Size = new Size(75, 25);
bt_Open.TabIndex = 0;
- bt_Open.Text = "打开 wav";
+ bt_Open.Text = "打开";
bt_Open.UseVisualStyleBackColor = true;
bt_Open.Click += bt_Open_Click;
//
@@ -61,7 +61,6 @@
// pic_mel
//
pic_mel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
- pic_mel.BackColor = Color.LightGray;
pic_mel.Location = new Point(12, 71);
pic_mel.Name = "pic_mel";
pic_mel.Size = new Size(628, 80);
@@ -72,7 +71,6 @@
// pic_vol
//
pic_vol.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
- pic_vol.BackColor = Color.LightGray;
pic_vol.Location = new Point(12, 186);
pic_vol.Name = "pic_vol";
pic_vol.Size = new Size(628, 200);
@@ -102,7 +100,8 @@
//
// MelForm
//
- AutoScaleMode = AutoScaleMode.Dpi;
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(652, 404);
Controls.Add(label2);
Controls.Add(label1);
diff --git a/XCoder/Windows/UsbHubSelect.resx b/A2Lib/Audio/MelForm.resx
similarity index 100%
rename from XCoder/Windows/UsbHubSelect.resx
rename to A2Lib/Audio/MelForm.resx
diff --git a/A2Lib/Audio/VolumeBar.cs b/A2Lib/Audio/VolumeBar.cs
new file mode 100644
index 0000000..8863844
--- /dev/null
+++ b/A2Lib/Audio/VolumeBar.cs
@@ -0,0 +1,260 @@
+using System;
+using System.ComponentModel;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Windows.Forms;
+
+namespace ALib.Audio
+{
+ /// <summary>
+ /// 纵向音量条控件,用于显示音量级别
+ /// </summary>
+ public class VolumeBar : ProgressBar
+ {
+ private Color _volumeColor = Color.LimeGreen;
+ private Color _volumeColorHigh = Color.Yellow;
+ private Color _volumeColorMax = Color.Red;
+ private int _highThreshold = 70;
+ private int _maxThreshold = 90;
+
+ public VolumeBar()
+ {
+ this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
+ this.DoubleBuffered = true;
+ }
+
+ /// <summary>
+ /// 音量颜色(低音量)
+ /// </summary>
+ [Browsable(true)]
+ [Category("Appearance")]
+ [Description("低音量时的颜色")]
+ public Color VolumeColor
+ {
+ get { return _volumeColor; }
+ set { _volumeColor = value; this.Invalidate(); }
+ }
+
+ /// <summary>
+ /// 高音量颜色
+ /// </summary>
+ [Browsable(true)]
+ [Category("Appearance")]
+ [Description("高音量时的颜色")]
+ public Color VolumeColorHigh
+ {
+ get { return _volumeColorHigh; }
+ set { _volumeColorHigh = value; this.Invalidate(); }
+ }
+
+ /// <summary>
+ /// 最大音量颜色
+ /// </summary>
+ [Browsable(true)]
+ [Category("Appearance")]
+ [Description("最大音量时的颜色")]
+ public Color VolumeColorMax
+ {
+ get { return _volumeColorMax; }
+ set { _volumeColorMax = value; this.Invalidate(); }
+ }
+
+ /// <summary>
+ /// 高音量阈值(百分比)
+ /// </summary>
+ [Browsable(true)]
+ [Category("Behavior")]
+ [Description("高音量阈值百分比")]
+ public int HighThreshold
+ {
+ get { return _highThreshold; }
+ set { _highThreshold = value; this.Invalidate(); }
+ }
+
+ /// <summary>
+ /// 最大音量阈值(百分比)
+ /// </summary>
+ [Browsable(true)]
+ [Category("Behavior")]
+ [Description("最大音量阈值百分比")]
+ public int MaxThreshold
+ {
+ get { return _maxThreshold; }
+ set { _maxThreshold = value; this.Invalidate(); }
+ }
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ Graphics g = e.Graphics;
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ Rectangle rect = this.ClientRectangle;
+
+ // 绘制背景
+ using (SolidBrush bgBrush = new SolidBrush(this.BackColor))
+ {
+ g.FillRectangle(bgBrush, rect);
+ }
+
+ // 绘制边框
+ ControlPaint.DrawBorder(g, rect, Color.Gray, ButtonBorderStyle.Solid);
+
+ // 计算进度值百分比
+ double percent = (double)this.Value / this.Maximum;
+
+ // 计算进度条高度(从底部向上填充)
+ int progressHeight = (int)((rect.Height - 2) * percent);
+ int progressY = rect.Bottom - progressHeight - 1;
+
+ if (progressHeight > 0)
+ {
+ // 根据音量级别选择颜色
+ Color fillColor = GetVolumeColor(this.Value);
+
+ // 绘制进度条(纵向,从底部向上)
+ Rectangle progressRect = new Rectangle(rect.X + 1, progressY, rect.Width - 2, progressHeight);
+
+ // 使用渐变填充
+ using (LinearGradientBrush brush = new LinearGradientBrush(
+ progressRect,
+ ControlPaint.Light(fillColor),
+ fillColor,
+ LinearGradientMode.Horizontal))
+ {
+ g.FillRectangle(brush, progressRect);
+ }
+ }
+
+ // 绘制分段线(可选,增加视觉效果)
+ DrawScaleLines(g, rect);
+ }
+
+ /// <summary>
+ /// 根据音量值获取对应颜色
+ /// </summary>
+ private Color GetVolumeColor(int value)
+ {
+ int percent = value * 100 / this.Maximum;
+
+ if (percent >= _maxThreshold)
+ return _volumeColorMax;
+ else if (percent >= _highThreshold)
+ return _volumeColorHigh;
+ else
+ return _volumeColor;
+ }
+
+ /// <summary>
+ /// 绘制刻度线
+ /// </summary>
+ private void DrawScaleLines(Graphics g, Rectangle rect)
+ {
+ int segments = 10;
+ int segmentHeight = (rect.Height - 2) / segments;
+
+ using (Pen pen = new Pen(Color.FromArgb(60, Color.Gray), 1))
+ {
+ for (int i = 1; i < segments; i++)
+ {
+ int y = rect.Bottom - 1 - (segmentHeight * i);
+ g.DrawLine(pen, rect.X + 1, y, rect.Right - 1, y);
+ }
+ }
+ }
+
+ /// <summary>
+ /// 设置音量值(0-100)
+ /// </summary>
+ public void SetVolume(int volume)
+ {
+ if (volume < 0) volume = 0;
+ if (volume > 100) volume = 100;
+
+ if (this.InvokeRequired)
+ this.Invoke(() => this.Value = volume);
+ else
+ this.Value = volume;
+ }
+
+ private float _minDb = -60f;
+ private float _maxDb = 0f;
+
+ /// <summary>
+ /// 最小dB值(默认-60dB)
+ /// </summary>
+ [Browsable(true)]
+ [Category("Behavior")]
+ [Description("最小dB值,对应音量0%")]
+ public float MinDb
+ {
+ get { return _minDb; }
+ set { _minDb = value; }
+ }
+
+ /// <summary>
+ /// 最大dB值(默认0dB)
+ /// </summary>
+ [Browsable(true)]
+ [Category("Behavior")]
+ [Description("最大dB值,对应音量100%")]
+ public float MaxDb
+ {
+ get { return _maxDb; }
+ set { _maxDb = value; }
+ }
+
+ /// <summary>
+ /// 设置音量值(dB)
+ /// </summary>
+ /// <param name="db">dB值,范围由MinDb和MaxDb决定</param>
+ /// <param name="logarithmic">是否使用对数映射(更符合人耳感知),默认true</param>
+ public void SetVolumeDb(float db, bool logarithmic = true)
+ {
+ int volume;
+
+ if (logarithmic)
+ {
+ // 对数映射:linear = 10^(dB/20)
+ // 将dB转换为线性值后映射到百分比
+ if (db <= _minDb)
+ {
+ volume = 0;
+ }
+ else if (db >= _maxDb)
+ {
+ volume = 100;
+ }
+ else
+ {
+ // 计算相对于最大值的线性比例
+ float linear = (float)Math.Pow(10, db / 20);
+ float maxLinear = (float)Math.Pow(10, _maxDb / 20);
+ float minLinear = (float)Math.Pow(10, _minDb / 20);
+
+ // 映射到0-100
+ volume = (int)((linear - minLinear) / (maxLinear - minLinear) * 100);
+ if (volume < 0) volume = 0;
+ if (volume > 100) volume = 100;
+ }
+ }
+ else
+ {
+ // 线性映射
+ if (db <= _minDb)
+ {
+ volume = 0;
+ }
+ else if (db >= _maxDb)
+ {
+ volume = 100;
+ }
+ else
+ {
+ volume = (int)((db - _minDb) / (_maxDb - _minDb) * 100);
+ }
+ }
+
+ SetVolume(volume);
+ }
+ }
+}
diff --git a/A2Lib/BitmapImage.cs b/A2Lib/BitmapImage.cs
new file mode 100644
index 0000000..1348fa4
--- /dev/null
+++ b/A2Lib/BitmapImage.cs
@@ -0,0 +1,129 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib
+{
+ /// <summary>位图图像</summary>
+ public class BitmapImage
+ {
+ /// <summary>列数,每行个数,建议是8的倍数</summary>
+ public int Columns;
+ /// <summary>总bit数</summary>
+ public int BitCount;
+
+ /// <summary>位图数据</summary>
+ public Byte[] Bits;
+
+ /// <summary>位图 图</summary>
+ /// <param name="bits"></param>
+ /// <param name="bitCount">bit总数</param>
+ /// <param name="cols">列数</param>
+ public BitmapImage(byte[] bits, int bitCount, int cols = 8)
+ {
+ if (cols < 1) cols = 8;
+ if (bitCount > bits.Length * 8) bitCount = bits.Length * 8;
+
+ Bits = bits;
+ BitCount = bitCount;
+ Columns = cols;
+ }
+
+ /// <summary>位图,图</summary>
+ /// <param name="bitCount">bit数</param>
+ /// <param name="cols"></param>
+ public BitmapImage(int bitCount, int cols = 8)
+ {
+ if (cols < 1) cols = 8;
+
+ Bits = new Byte[bitCount];
+ BitCount = bitCount;
+ Columns = cols;
+ }
+
+ /// <summary>获取标志</summary>
+ /// <param name="idx"></param>
+ /// <returns></returns>
+ public bool GetBit(int idx)
+ {
+ idx %= BitCount;
+
+ int byteIdx = idx / 8;
+ var temp = Bits[byteIdx];
+
+ int bitIdx = idx % 8;
+ int mask = 1 << bitIdx;
+
+ return (mask & temp) > 0;
+ }
+
+ /// <summary>设置标志,测试用的</summary>
+ /// <param name="idx"></param>
+ /// <param name="bit"></param>
+ public void SetBit(int idx, bool bit)
+ {
+ idx %= BitCount;
+
+ int byteIdx = idx / 8;
+ var temp = Bits[byteIdx];
+
+ int bitIdx = idx % 8;
+ int bitMask = 1 << bitIdx;
+
+ if (bit)
+ {
+ temp = (byte)(temp | bitMask);
+ }
+ else
+ {
+ bitMask = ~bitMask;
+ temp = (byte)(temp & bitMask);
+ }
+
+ Bits[byteIdx] = temp;
+ }
+
+ /// <summary>获取图像</summary>
+ /// <returns></returns>
+ public Bitmap GetImage()
+ {
+ int rowCount = (BitCount + Columns - 1) / Columns;
+ // 每个标志占 10x10 pix ,计算整体大小。
+ var bmap = new Bitmap(Columns * 10, rowCount * 10);
+ var g = Graphics.FromImage(bmap);
+ g.Clear(Color.LightGray);
+
+ // 每个标志占 10x10 pix, 红色
+ for (int i = 0; i < BitCount; i++)
+ {
+ var brush = Brushes.Red;
+ if (!GetBit(i)) brush = Brushes.Green;
+
+ int x = i % Columns * 10;
+ int y = i / Columns * 10;
+ g.FillRectangle(brush, x, y, 10, 10);
+ }
+
+ // 画线分隔图块
+ // 竖线
+ var pen2 = new Pen(Color.White, 2);
+ for (int i = 1; i < Columns; i++)
+ {
+ int x = i * 10;
+ g.DrawLine(pen2, x, 0, x, rowCount * 10);
+ }
+ // 横线
+ for (int i = 1; i < rowCount; i++)
+ {
+ int y = i * 10;
+ g.DrawLine(pen2, 0, y, Columns * 10, y);
+ }
+
+ g.Dispose();
+
+ return bmap;
+ }
+ }
+}
diff --git a/A2Lib/CAN/CanMonitor.cs b/A2Lib/CAN/CanMonitor.cs
new file mode 100644
index 0000000..dfceba8
--- /dev/null
+++ b/A2Lib/CAN/CanMonitor.cs
@@ -0,0 +1,455 @@
+using ALib.WinForm;
+using NewLife;
+using NewLife.Collections;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Forms;
+
+namespace ALib.CAN
+{
+ public partial class CanMonitor : Form
+ {
+ public CanMonitor()
+ {
+ InitializeComponent();
+ }
+
+ #region 初始化界面部分
+
+ private void CanMonitor_Load(object sender, EventArgs e)
+ {
+ // 表格频繁刷新,开启双缓冲减少闪烁
+ lvMessage.EnableDoubleBuffer();
+
+ // 记录各列初始宽度与列表初始宽度,作为拖拽窗口时等比缩放的基准
+ _colBaseWidths = new int[lvMessage.Columns.Count];
+ for (int i = 0; i < lvMessage.Columns.Count; i++)
+ _colBaseWidths[i] = lvMessage.Columns[i].Width;
+ _listBaseWidth = lvMessage.ClientSize.Width;
+
+ // 拖拽窗口时按列表宽度等比缩放列宽(行高不变)
+ this.Resize += CanMonitor_Resize;
+ CanMonitor_Resize(null, null);
+
+ // 限制最小尺寸,避免窗口过小导致列宽被压得不可用
+ this.MinimumSize = new Size(640, 480);
+
+ // 日志转发到 CanSelect,便于在外部统一观察
+ canSelect.Log = Log;
+
+ cbSeCanId_DropDown(null, null);
+ }
+
+ /// <summary>各列初始宽度(设计基准),用于窗口缩放时按比例换算</summary>
+ private int[] _colBaseWidths;
+ /// <summary>列表初始可用宽度(设计基准)</summary>
+ private int _listBaseWidth;
+
+ /// <summary>
+ /// 拖拽窗口时按列表宽度等比缩放各列,最后一列填满剩余空间避免水平滚动条;
+ /// 仅调整列宽,不改变行高。
+ /// </summary>
+ private void CanMonitor_Resize(object sender, EventArgs e)
+ {
+ if (_colBaseWidths == null || _listBaseWidth <= 0) return;
+ if (lvMessage.Columns.Count != _colBaseWidths.Length) return;
+
+ int avail = lvMessage.ClientSize.Width;
+ double scale = (double)avail / _listBaseWidth;
+ if (scale <= 0) return;
+
+ int sum = 0;
+ for (int i = 0; i < lvMessage.Columns.Count - 1; i++)
+ {
+ int w = (int)(_colBaseWidths[i] * scale);
+ if (w < 10) w = 10;
+ lvMessage.Columns[i].Width = w;
+ sum += w;
+ }
+
+ // 最后一列填满剩余空间,避免出现水平滚动条
+ int last = lvMessage.Columns.Count - 1;
+ int lastW = avail - sum;
+ if (lastW < 10) lastW = 10;
+ lvMessage.Columns[last].Width = lastW;
+ }
+
+ /// <summary>
+ /// 拦截空格键:在按键消息分发到任何子控件之前处理,
+ /// 避免焦点在按钮上时被 Button 当成“空格=Click”而误触发
+ /// </summary>
+ protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
+ {
+ if ((keyData & Keys.KeyCode) == Keys.Space)
+ {
+ // 有下拉框展开时不拦截,交给下拉框自己处理
+ if (!cbSeCanId.DroppedDown && !canSelect.DroppedDown)
+ {
+ btPause_Click(null, null);
+ return true; // 已处理,不再分发,按钮不会收到空格
+ }
+ }
+
+ return base.ProcessCmdKey(ref msg, keyData);
+ }
+
+ #endregion
+
+ #region CAN 口
+
+ /// <summary>当前 CAN 口,随 <see cref="CanSelect"/> 打开/关闭变化</summary>
+ public ICanPort Port => canSelect.Port;
+
+ /// <summary>供外部注入已打开的 CAN 口(注入后由外部负责关闭)</summary>
+ public void AttachPort(ICanPort port) => canSelect.Port = port;
+
+ /// <summary>CAN 口打开成功</summary>
+ private void canSelect_PortOpened(object sender, EventArgs e)
+ {
+ WriteLog($"使用 CAN 设备: {Port?.Name}");
+ }
+
+ #endregion
+
+ #region 暂停接收
+
+ private void btPause_Click(object sender, EventArgs e)
+ {
+ var stop = cbStop.Checked;
+ stop = !stop;
+ cbStop.Checked = stop;
+
+ if (stop)
+ {
+ btPause.Text = "继续接收";
+ btPause.BackColor = Color.Gray;
+ }
+ else
+ {
+ btPause.Text = "暂停接收";
+ btPause.BackColor = this.BackColor;
+ }
+ }
+
+ #endregion
+
+ #region 接收与刷新
+
+ private int showNumber = 256;
+ public Int32 ShowNumber
+ {
+ get { return showNumber; }
+ set
+ {
+ if (value < 64) showNumber = 64;
+ else if (value > 1024 * 1024) showNumber = 1024 * 1024;
+ else showNumber = value;
+ }
+ }
+
+ private void canSelect_Received(object sender, CanMsg e)
+ {
+ if (cbStop.Checked) return;
+ AddMsg(e);
+ }
+
+ private void cbSeCanId_DropDown(object sender, EventArgs e)
+ {
+ var old = cbSeCanId.DataSource as string[];
+ var oldIdx = (int)cbSeCanId.SelectedIndex;
+
+ var ids = AllCanId();
+ // ids.Add(-2);
+ // ids.Add(-1);
+
+ var idsStr = ids.Select(x => x.ToString("X8")).ToList();
+ idsStr.Add("仅最新");
+ idsStr.Add("全部");
+
+ cbSeCanId.DataSource = idsStr;
+ cbSeCanId.Tag = ids;
+
+ if (old != null)
+ {
+ string oId = old[oldIdx];
+ int nId = idsStr.IndexOf(oId);
+ if (nId >= 0) cbSeCanId.SelectedIndex = nId;
+ }
+ }
+
+ /// <summary>用户当前选中的行所对应的消息,刷新时用它把选中行跟着数据一起挪动</summary>
+ private CanMsgInfo _followMsg = null;
+
+ private void lvMessage_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
+ {
+ if (!e.IsSelected) return;
+
+ // 记录选中行对应的消息对象(避免在刷新中赋值,刷新时 SelectedItems 已被程序改动)
+ _followMsg = e.Item.Tag as CanMsgInfo;
+ }
+
+ /// <summary>右键菜单:清空表格显示并清除缓存的消息</summary>
+ private void tsmiClearMsg_Click(object sender, EventArgs e)
+ {
+ lvMessage.BeginUpdate();
+ try
+ {
+ lvMessage.Items.Clear();
+ lvMessage.SelectedItems.Clear();
+ _followMsg = null;
+ }
+ finally { lvMessage.EndUpdate(); }
+
+ ClearMsgs();
+ cbSeCanId_DropDown(null, null);
+ }
+
+ /// <summary>
+ /// 让选中行跟着数据一起挪动。
+ /// 已在底部(自动跟随最新数据)时不跟随,避免和底部跟随打架;
+ /// 数据仍可见时保持选中;一旦滚出可见区(或已被挤出表格)就取消选中,不再滚动过去。
+ /// </summary>
+ /// <param name="atBottom">刷新前是否处于底部</param>
+ private void FollowSelection(bool atBottom)
+ {
+ // if (atBottom) return;
+ if (_followMsg == null) return;
+
+ // 数据跑出屏幕或已不在表格中:丢弃选中,避免跟随一个看不到的目标
+ if (!lvMessage.FollowSelection(_followMsg))
+ {
+ lvMessage.SelectedItems.Clear();
+ _followMsg = null;
+ }
+ }
+
+ private void lvtime1_Tick(object sender, EventArgs e)
+ {
+ lvMessage.BeginUpdate();
+ try
+ {
+ var str = cbSeCanId.SelectedItem as String;
+ if (str == "全部")
+ {
+ // 刷新前先判断是否已在底部,避免打断用户向上翻看
+ var atBottom = lvMessage.IsAtBottom();
+
+ var allmsgs = AllMsgs.PeekHeadArray(showNumber);
+ UpdateLatest(allmsgs);
+
+ // 表格显示最底部
+ if (atBottom) lvMessage.ScrollToBottom();
+
+ FollowSelection(atBottom);
+ }
+ else if (str == "仅最新")
+ {
+ // 刷新前先判断是否已在底部,避免打断用户向上翻看
+ // var atBottom = lvMessage.IsAtBottom();
+
+ var msgs = LastMsgs().ToArray();
+ UpdateLatest(msgs);
+ // FollowSelection(atBottom);
+ }
+ else
+ {
+ var ids = cbSeCanId.Tag as List<int>;
+ var id = ids == null ? -1 : ids[cbSeCanId.SelectedIndex];
+
+ // 刷新前先判断是否已在底部,避免打断用户向上翻看
+ var atBottom = lvMessage.IsAtBottom();
+
+ var allmsgs = GetMsgByCanId(id, showNumber);
+ UpdateLatest(allmsgs);
+
+ // 表格显示最底部
+ if (atBottom) lvMessage.ScrollToBottom();
+
+ FollowSelection(atBottom);
+ }
+ }
+ finally { lvMessage.EndUpdate(); }
+ }
+
+ #endregion
+
+ #region ListView 相关
+
+ /// <summary>按最新帧刷新表格(就地更新,避免增删行引起的闪烁)</summary>
+ /// <param name="msgs"></param>
+ private void UpdateLatest(CanMsgInfo[] msgs)
+ {
+ var lvCnt = lvMessage.Items.Count;
+
+ for (int i = 0; i < msgs.Length; i++)
+ {
+ var mi = msgs[i];
+ if (i < lvCnt)
+ {
+ // 内容未变则跳过,避免无谓重绘
+ if (ReferenceEquals(lvMessage.Items[i].Tag, mi)) continue;
+
+ FillRow(lvMessage.Items[i], mi);
+ }
+ else
+ {
+ lvMessage.Items.Add(lvMsgsView(mi));
+ }
+ }
+
+ // 多余的行一次删除(从尾部删)
+ while (lvMessage.Items.Count > msgs.Length)
+ lvMessage.Items.RemoveAt(lvMessage.Items.Count - 1);
+ }
+
+ /// <summary>就地填充一行,保留 ListViewItem 对象</summary>
+ /// <param name="it"></param>
+ /// <param name="mi"></param>
+ private void FillRow(ListViewItem it, CanMsgInfo mi)
+ {
+ var msg = mi.Msg;
+
+ var id = msg.ID.GetBytes(false).ReadBytes(0, 4).ToHex();
+ if (msg.RemoteFlag) id += " R";
+ if (msg.ExternFlag) id += " E";
+
+ var data = msg.Data.ToHex();
+ var time = msg.TimeStamp.ToString("hh\\:mm\\:ss\\.fff");
+ var interval = mi.Interval.TotalMilliseconds.ToString("F1") + "ms";
+
+ SetSubItem(it, 0, id);
+ SetSubItem(it, 1, data);
+ SetSubItem(it, 2, time);
+ SetSubItem(it, 3, interval);
+
+ it.Tag = mi;
+ }
+
+ private void SetSubItem(ListViewItem it, int index, string text)
+ {
+ while (it.SubItems.Count <= index) it.SubItems.Add("");
+ if (it.SubItems[index].Text != text) it.SubItems[index].Text = text;
+ }
+
+ /// <summary>消息转表格行</summary>
+ /// <param name="mi">消息</param>
+ /// <returns></returns>
+ private ListViewItem lvMsgsView(CanMsgInfo mi)
+ {
+ var it = new ListViewItem();
+ FillRow(it, mi);
+ return it;
+ }
+
+ #endregion
+
+ #region 消息部分
+
+ public class CanMsgInfo
+ {
+ /// <summary>消息本体</summary>
+ public CanMsg Msg;
+
+ /// <summary>和上一条同CAN ID消息的间隔</summary>
+ public TimeSpan Interval = TimeSpan.Zero;
+
+ public CanMsgInfo(CanMsg msg) { Msg = msg; }
+ }
+
+ #region 消息管理
+
+ /// <summary>各 CANID 的最新帧</summary>
+ public Dictionary<Int32, CircularQueue<CanMsgInfo>> Msgs = new Dictionary<Int32, CircularQueue<CanMsgInfo>>(1024 * 2);
+
+ /// <summary>全部消息,只按照时间排序</summary>
+ public CircularQueue<CanMsgInfo> AllMsgs = new CircularQueue<CanMsgInfo>(1024 * 100);
+
+ public List<int> AllCanId() { lock (Msgs) { return Msgs.Keys.ToList(); } }
+
+ public CanMsgInfo[] GetMsgByCanId(Int32 id, int count)
+ {
+ if (id < 0)
+ {
+ lock (AllMsgs) { return AllMsgs.PeekHeadArray(count); }
+ }
+ else
+ {
+ if (!Msgs.ContainsKey(id)) return null;
+
+ lock (Msgs)
+ {
+ var queue = Msgs[id];
+ return queue.PeekHeadArray(count);
+ }
+ }
+ }
+
+ public List<CanMsgInfo> LastMsgs()
+ {
+ lock (Msgs)
+ {
+ var list = new List<CanMsgInfo>();
+ foreach (var item in Msgs.Values) list.Add(item.PeekHead());
+ return list;
+ }
+ }
+
+ /// <summary>添加新消息</summary>
+ /// <param name="msg"></param>
+ /// <returns></returns>
+ public bool AddMsg(CanMsg msg)
+ {
+ lock (Msgs)
+ {
+ if (!Msgs.ContainsKey(msg.ID))
+ {
+ var cir = new CircularQueue<CanMsgInfo>(1000);
+ Msgs.Add(msg.ID, cir);
+ }
+
+ // 入队检查
+ var queue = Msgs[msg.ID];
+ if (queue.Remain() < 1) queue.Seek(1);
+
+ // 创建入队对象
+ var cin = new CanMsgInfo(msg);
+ var last = queue.PeekHead();
+ if (last != null) { cin.Interval = msg.TimeStamp - last.Msg.TimeStamp; }
+
+ // 入队
+ queue.Enqueue(cin);
+
+ // 全部消息入队
+ if (AllMsgs.Remain() < 1) AllMsgs.Seek(1);
+ AllMsgs.Enqueue(cin);
+ }
+
+ return true;
+ }
+
+ /// <summary>清空消息</summary>
+ public void ClearMsgs()
+ {
+ lock (Msgs)
+ {
+ foreach (var item in Msgs) { item.Value.Clear(); }
+ Msgs.Clear();
+ AllMsgs.Clear();
+ }
+ }
+
+ #endregion
+ #endregion
+
+ #region 日志
+
+ public ILog Log { get; set; } = Logger.Null;
+
+ public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
+
+ #endregion
+ }
+}
diff --git a/A2Lib/CAN/CanMonitor.Designer.cs b/A2Lib/CAN/CanMonitor.Designer.cs
new file mode 100644
index 0000000..3482b7d
--- /dev/null
+++ b/A2Lib/CAN/CanMonitor.Designer.cs
@@ -0,0 +1,201 @@
+namespace ALib.CAN
+{
+ partial class CanMonitor
+ {
+ /// <summary>
+ /// Required designer variable.
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// Clean up any resources being used.
+ /// </summary>
+ /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ /// <summary>
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ /// </summary>
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ lvMessage = new ListView();
+ lvCanId = new ColumnHeader();
+ lvData = new ColumnHeader();
+ lvTime = new ColumnHeader();
+ lvInv = new ColumnHeader();
+ cmsMessage = new ContextMenuStrip(components);
+ tsmiClearMsg = new ToolStripMenuItem();
+ cbSeCanId = new ComboBox();
+ label1 = new Label();
+ canSelect = new CanSelect();
+ lvtime1 = new System.Windows.Forms.Timer(components);
+ cbStop = new CheckBox();
+ btPause = new Button();
+ label5 = new Label();
+ cmsMessage.SuspendLayout();
+ SuspendLayout();
+ //
+ // lvMessage
+ //
+ lvMessage.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+ lvMessage.Columns.AddRange(new ColumnHeader[] { lvCanId, lvData, lvTime, lvInv });
+ lvMessage.ContextMenuStrip = cmsMessage;
+ lvMessage.Font = new Font("Cascadia Mono", 10.5F, FontStyle.Regular, GraphicsUnit.Point, 0);
+ lvMessage.FullRowSelect = true;
+ lvMessage.GridLines = true;
+ lvMessage.Location = new Point(192, 12);
+ lvMessage.MultiSelect = false;
+ lvMessage.Name = "lvMessage";
+ lvMessage.Size = new Size(566, 746);
+ lvMessage.TabIndex = 16;
+ lvMessage.UseCompatibleStateImageBehavior = false;
+ lvMessage.View = View.Details;
+ lvMessage.ItemSelectionChanged += lvMessage_ItemSelectionChanged;
+ //
+ // lvCanId
+ //
+ lvCanId.Text = "CAN ID";
+ lvCanId.Width = 120;
+ //
+ // lvData
+ //
+ lvData.Text = "数据";
+ lvData.Width = 180;
+ //
+ // lvTime
+ //
+ lvTime.Text = "时间戳";
+ lvTime.Width = 160;
+ //
+ // lvInv
+ //
+ lvInv.Text = "间隔";
+ lvInv.Width = 100;
+ //
+ // cmsMessage
+ //
+ cmsMessage.Items.AddRange(new ToolStripItem[] { tsmiClearMsg });
+ cmsMessage.Name = "cmsMessage";
+ cmsMessage.Size = new Size(101, 26);
+ //
+ // tsmiClearMsg
+ //
+ tsmiClearMsg.Name = "tsmiClearMsg";
+ tsmiClearMsg.Size = new Size(100, 22);
+ tsmiClearMsg.Text = "清空";
+ tsmiClearMsg.Click += tsmiClearMsg_Click;
+ //
+ // cbSeCanId
+ //
+ cbSeCanId.DropDownStyle = ComboBoxStyle.DropDownList;
+ cbSeCanId.Font = new Font("Cascadia Mono", 9F, FontStyle.Regular, GraphicsUnit.Point, 0);
+ cbSeCanId.FormattingEnabled = true;
+ cbSeCanId.Location = new Point(12, 373);
+ cbSeCanId.Name = "cbSeCanId";
+ cbSeCanId.Size = new Size(174, 24);
+ cbSeCanId.TabIndex = 17;
+ cbSeCanId.DropDown += cbSeCanId_DropDown;
+ //
+ // label1
+ //
+ label1.AutoSize = true;
+ label1.Location = new Point(12, 353);
+ label1.Name = "label1";
+ label1.Size = new Size(56, 17);
+ label1.TabIndex = 18;
+ label1.Text = "消息筛选";
+ //
+ // canSelect
+ //
+ canSelect.Location = new Point(12, 0);
+ canSelect.Name = "canSelect";
+ canSelect.Size = new Size(174, 164);
+ canSelect.TabIndex = 19;
+ canSelect.PortOpened += canSelect_PortOpened;
+ canSelect.Received += canSelect_Received;
+ //
+ // lvtime1
+ //
+ lvtime1.Enabled = true;
+ lvtime1.Interval = 50;
+ lvtime1.Tick += lvtime1_Tick;
+ //
+ // cbStop
+ //
+ cbStop.AutoSize = true;
+ cbStop.Enabled = false;
+ cbStop.Location = new Point(59, 445);
+ cbStop.Name = "cbStop";
+ cbStop.Size = new Size(75, 21);
+ cbStop.TabIndex = 32;
+ cbStop.Text = "暂停接收";
+ cbStop.UseVisualStyleBackColor = true;
+ //
+ // btPause
+ //
+ btPause.Location = new Point(12, 472);
+ btPause.Name = "btPause";
+ btPause.Size = new Size(174, 108);
+ btPause.TabIndex = 33;
+ btPause.Text = "暂停接收";
+ btPause.UseVisualStyleBackColor = true;
+ btPause.Click += btPause_Click;
+ //
+ // label5
+ //
+ label5.AutoSize = true;
+ label5.Location = new Point(12, 583);
+ label5.Name = "label5";
+ label5.Size = new Size(101, 17);
+ label5.TabIndex = 34;
+ label5.Text = "空格键 暂停/恢复";
+ //
+ // CanMonitor
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(770, 770);
+ Controls.Add(canSelect);
+ Controls.Add(label5);
+ Controls.Add(btPause);
+ Controls.Add(cbStop);
+ Controls.Add(label1);
+ Controls.Add(cbSeCanId);
+ Controls.Add(lvMessage);
+ Name = "CanMonitor";
+ Text = "CanMonitor";
+ Load += CanMonitor_Load;
+ cmsMessage.ResumeLayout(false);
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private ListView lvMessage;
+ private ContextMenuStrip cmsMessage;
+ private ToolStripMenuItem tsmiClearMsg;
+ private ColumnHeader lvTime;
+ private ColumnHeader lvCanId;
+ private ColumnHeader lvData;
+ private ComboBox cbSeCanId;
+ private Label label1;
+ private CanSelect canSelect;
+ private System.Windows.Forms.Timer lvtime1;
+ private ColumnHeader lvInv;
+ private CheckBox cbStop;
+ private Button btPause;
+ private Label label5;
+ }
+}
diff --git a/A2Lib/CAN/CanMonitor.resx b/A2Lib/CAN/CanMonitor.resx
new file mode 100644
index 0000000..91a8930
--- /dev/null
+++ b/A2Lib/CAN/CanMonitor.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <metadata name="cmsMessage.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+ <value>17, 17</value>
+ </metadata>
+ <metadata name="lvtime1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+ <value>148, 17</value>
+ </metadata>
+</root>
\ No newline at end of file
diff --git a/A2Lib/CAN/CanSelect.cs b/A2Lib/CAN/CanSelect.cs
new file mode 100644
index 0000000..b063405
--- /dev/null
+++ b/A2Lib/CAN/CanSelect.cs
@@ -0,0 +1,349 @@
+using ALib.WinForm;
+using NewLife.Log;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+using System.Windows.Forms;
+
+namespace ALib.CAN
+{
+ /// <summary>
+ /// CAN 口选择与开关控件。
+ /// 内部枚举 <see cref="ICanPort.EnumDevices"/> 得到的设备与波特率,供用户选择后打开/关闭 CAN 口。
+ /// 打开成功后通过 <see cref="PortOpened"/> 暴露 <see cref="ICanPort"/>;
+ /// 收到的 CAN 帧通过 <see cref="Received"/> 事件向外抛出(后台接收线程上触发,订阅方自行切回 UI 线程)。
+ /// </summary>
+ public partial class CanSelect : UserControl
+ {
+ #region 属性
+
+ private ICanPort _Port = null;
+
+ /// <summary>当前打开或外部注入的 CAN 口,未打开时为 null</summary>
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ICanPort Port
+ {
+ get => _Port;
+ set
+ {
+ if (ReferenceEquals(_Port, value)) return;
+
+ // 摘掉旧订阅;外部注入的端口不关闭,避免把宿主正在用的 CAN 口关掉
+ DetachPort();
+
+ _Port = value;
+ if (value == null)
+ {
+ _opened = false;
+ if (cbCan != null) cbCan.Enabled = AllowSelect;
+ if (cbBaudrate != null) cbBaudrate.Enabled = AllowSelect;
+ if (btCanOpen != null)
+ {
+ btCanOpen.Text = "打开";
+ btCanOpen.Enabled = true;
+ }
+ return;
+ }
+
+ // 外部注入的端口由外部负责关闭,不由本控件释放
+ _needClose = false;
+ _opened = true;
+
+ if (cbCan != null) cbCan.Text = value.Name;
+ if (cbBaudrate != null) cbBaudrate.Text = value.Baudrate.ToString();
+
+ if (btCanOpen != null)
+ {
+ btCanOpen.Text = "关闭";
+ btCanOpen.Enabled = false;
+ }
+ if (cbCan != null) cbCan.Enabled = false;
+ if (cbBaudrate != null) cbBaudrate.Enabled = false;
+
+ value.Received += Port_Received;
+ }
+ }
+
+ /// <summary>
+ /// 仅取消事件订阅并把内部状态复位,不关闭硬件。
+ /// 供 <see cref="Port"/> 换绑与 <see cref="ClosePort"/> 复用,保证订阅不会重复或残留。
+ /// </summary>
+ private void DetachPort()
+ {
+ var pt = _Port;
+ if (pt == null) return;
+
+ pt.Received -= Port_Received;
+ _Port = null;
+ _opened = false;
+ }
+
+ /// <summary>是否已打开</summary>
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool Opened => _opened;
+
+ /// <summary>任一下拉框是否处于展开状态,供宿主窗体拦截快捷键时判断</summary>
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public bool DroppedDown => cbCan.DroppedDown || cbBaudrate.DroppedDown;
+
+ /// <summary>是否允许选择波特率(外部注入端口时 false)</summary>
+ [Browsable(true)]
+ [DefaultValue(true)]
+ [Description("是否允许在界面上选择 CAN 设备与波特率")]
+ public bool AllowSelect { get; set; } = true;
+
+ /// <summary>是否在打开成功后把波特率选择框禁用</summary>
+ [Browsable(true)]
+ [DefaultValue(true)]
+ [Description("打开前是否禁用波特率与设备下拉框")]
+ public bool LockOnOpen { get; set; } = true;
+
+ /// <summary>控件显示名称,仅用于日志</summary>
+ [Browsable(true)]
+ [Description("控件显示名称,仅用于日志")]
+ public override string Text
+ {
+ get => base.Text;
+ set => base.Text = value;
+ }
+
+ #endregion
+
+ #region 事件
+
+ /// <summary>CAN 口打开成功后触发</summary>
+ [Category("CAN")]
+ [Description("CAN 口打开成功后触发")]
+ public event EventHandler PortOpened;
+
+ /// <summary>CAN 口已关闭后触发</summary>
+ [Category("CAN")]
+ [Description("CAN 口已关闭后触发")]
+ public event EventHandler PortClosed;
+
+ /// <summary>收到 CAN 帧时触发(后台线程)</summary>
+ [Category("CAN")]
+ [Description("收到 CAN 帧时触发")]
+ public event EventHandler<CanMsg> Received;
+
+ #endregion
+
+ #region 构造与初始化
+
+ public CanSelect()
+ {
+ InitializeComponent();
+ }
+
+ private void CanSelect_Load(object sender, EventArgs e)
+ {
+ // 已注入/已打开端口时不再枚举设备。
+ // 部分驱动(如 ZLG 的 VCI_FindUsbDevice2)在设备已打开的情况下再次枚举
+ // 会干扰正在运行的接收线程,导致宿主与监视器都收不到数据。
+ if (_Port == null)
+ {
+ InitBaudrate();
+ InitCanPort();
+ }
+ else
+ {
+ // 只填充波特率列表,不回写 Text,保持注入时的显示
+ InitBaudrate();
+ if (cbBaudrate != null) cbBaudrate.Text = _Port.Baudrate.ToString();
+ }
+
+ if (!AllowSelect)
+ {
+ cbCan.Enabled = false;
+ cbBaudrate.Enabled = false;
+ }
+
+ // 父窗体关闭时确保释放
+ if (FindForm() is Form frm) frm.FormClosed += Form_FormClosed;
+ }
+
+ private void Form_FormClosed(object sender, FormClosedEventArgs e) => ClosePort();
+
+ /// <summary>枚举 CAN 设备,填充设备下拉框</summary>
+ private void InitCanPort()
+ {
+ var devices = ICanPort.EnumDevices();
+ if (devices.Count == 0)
+ {
+ WriteLog("未发现 CAN 设备");
+ return;
+ }
+
+ cbCan.DataSource = devices.Keys.ToList();
+ cbCan.Tag = devices;
+ }
+
+ /// <summary>初始化波特率下拉框,默认 250K</summary>
+ private void InitBaudrate()
+ {
+ var list = new List<ICanPort.Baudrate_e>
+ {
+ ICanPort.Baudrate_e.B1000K,
+ ICanPort.Baudrate_e.B500K,
+ ICanPort.Baudrate_e.B250K,
+ ICanPort.Baudrate_e.B200K,
+ ICanPort.Baudrate_e.B125K,
+ ICanPort.Baudrate_e.B100K,
+ };
+
+ cbBaudrate.DataSource = list;
+ cbBaudrate.SelectedItem = ICanPort.Baudrate_e.B250K;
+ }
+
+ #endregion
+
+ #region 打开与关闭
+
+ private bool _opened = false;
+
+ /// <summary>
+ /// 外部注入的端口不需要本控件关闭(例如上层从别处拿到的共享 CAN 口)
+ /// </summary>
+ private bool _needClose = true;
+
+ /// <summary>重新枚举设备并刷新下拉框</summary>
+ public void RefreshDevices()
+ {
+ InitCanPort();
+ InitBaudrate();
+ }
+
+ /// <summary>
+ /// 按当前下拉框选择打开 CAN 口。
+ /// 未注入 <see cref="Port"/> 时使用设备下拉框当前选中项创建;
+ /// 已打开则返回 true。
+ /// </summary>
+ /// <returns></returns>
+ public bool Open()
+ {
+ if (_opened) return true;
+
+ // 未注入端口时,从下拉框取设备
+ if (_Port == null)
+ {
+ var devs = cbCan.Tag as Dictionary<String, ICanPort>;
+ if (devs == null || devs.Count == 0)
+ {
+ WriteLog("未发现 CAN 设备");
+ return false;
+ }
+
+ if (!devs.TryGetValue(cbCan.Text, out var dev))
+ {
+ WriteLog($"未找到 CAN 设备: {cbCan.Text}");
+ return false;
+ }
+
+ WriteLog($"使用 CAN 设备: {cbCan.Text}");
+ _Port = dev;
+ }
+
+ // 注入的端口可能已经打开,此时不重复 Open,避免打断其接收线程
+ if (!_opened)
+ {
+ if (cbBaudrate.SelectedItem is ICanPort.Baudrate_e bd) _Port.Baudrate = bd;
+ if (!_Port.Open())
+ {
+ WriteLog($"CAN 打开失败: {_Port.Name}");
+ return false;
+ }
+
+ _Port.Received += Port_Received;
+ _opened = true;
+ }
+
+ if (btCanOpen != null) btCanOpen.Text = "关闭";
+ if (LockOnOpen)
+ {
+ if (cbBaudrate != null) cbBaudrate.Enabled = false;
+ if (cbCan != null) cbCan.Enabled = false;
+ }
+
+ PortOpened?.Invoke(this, EventArgs.Empty);
+ return true;
+ }
+
+ /// <summary>关闭 CAN 口(外部注入的端口不会真正关闭,仅取消订阅)</summary>
+ public void ClosePort()
+ {
+ var pt = _Port;
+ if (pt == null) return;
+
+ // 是否需要真正关闭硬件,在 DetachPort 清空 _Port 之前先取
+ var needClose = _needClose;
+
+ DetachPort();
+
+ if (needClose)
+ {
+ try { pt.Close(); } catch { }
+ }
+
+ if (btCanOpen != null)
+ {
+ btCanOpen.Text = "打开";
+ btCanOpen.Enabled = true;
+ }
+ if (cbBaudrate != null) cbBaudrate.Enabled = AllowSelect;
+ if (cbCan != null) cbCan.Enabled = AllowSelect;
+
+ PortClosed?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void btCanOpen_Click(object sender, EventArgs e)
+ {
+ if (_opened) ClosePort();
+ else Open();
+ }
+
+ private void Port_Received(object sender, CanMsg e) => Received?.Invoke(this, e);
+
+ #endregion
+
+ #region 发送
+
+ /// <summary>发送 CAN 帧,未打开时返回 false</summary>
+ public bool Send(CanMsg msg)
+ {
+ var pt = _Port;
+ if (pt == null || !_opened || msg == null) return false;
+
+ pt.Send(msg);
+ return true;
+ }
+
+ /// <summary>发送数据,未打开时返回 false</summary>
+ public bool Send(UInt32 id, byte[] data, bool remote = false, bool ext = false)
+ {
+ var pt = _Port;
+ if (pt == null || !_opened) return false;
+
+ pt.Send(id, data, remote, ext);
+ return true;
+ }
+
+ #endregion
+
+ #region 日志
+
+ /// <summary>日志</summary>
+ [Browsable(false)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
+ public ILog Log { get; set; } = Logger.Null;
+
+ /// <summary>写日志</summary>
+ public void WriteLog(String format, params Object[] args) => Log?.Info(format, args);
+
+ #endregion
+ }
+}
diff --git a/A2Lib/CAN/CanSelect.Designer.cs b/A2Lib/CAN/CanSelect.Designer.cs
new file mode 100644
index 0000000..344a51e
--- /dev/null
+++ b/A2Lib/CAN/CanSelect.Designer.cs
@@ -0,0 +1,114 @@
+namespace ALib.CAN
+{
+ partial class CanSelect
+ {
+ /// <summary>
+ /// 必需的设计器变量。
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// 清理所有正在使用的资源。
+ /// </summary>
+ /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ // 先关硬件再走基类(UserControl 销毁不一定触发 FormClosed)
+ ClosePort();
+
+ if (components != null) components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region 组件设计器生成的代码
+
+ /// <summary>
+ /// 设计器支持所需的方法 - 不要修改
+ /// 使用代码编辑器修改此方法的内容。
+ /// </summary>
+ private void InitializeComponent()
+ {
+ cbCan = new ComboBox();
+ label4 = new Label();
+ label3 = new Label();
+ cbBaudrate = new ComboBox();
+ btCanOpen = new Button();
+ SuspendLayout();
+ //
+ // cbCan
+ //
+ cbCan.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ cbCan.DropDownStyle = ComboBoxStyle.DropDownList;
+ cbCan.FormattingEnabled = true;
+ cbCan.Location = new Point(0, 23);
+ cbCan.Name = "cbCan";
+ cbCan.Size = new Size(174, 25);
+ cbCan.TabIndex = 33;
+ //
+ // label4
+ //
+ label4.AutoSize = true;
+ label4.Location = new Point(0, 7);
+ label4.Name = "label4";
+ label4.Size = new Size(34, 17);
+ label4.TabIndex = 32;
+ label4.Text = "CAN";
+ //
+ // label3
+ //
+ label3.AutoSize = true;
+ label3.Location = new Point(0, 55);
+ label3.Name = "label3";
+ label3.Size = new Size(61, 17);
+ label3.TabIndex = 34;
+ label3.Text = "Baudrate";
+ //
+ // cbBaudrate
+ //
+ cbBaudrate.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ cbBaudrate.DropDownStyle = ComboBoxStyle.DropDownList;
+ cbBaudrate.FormattingEnabled = true;
+ cbBaudrate.Location = new Point(0, 75);
+ cbBaudrate.Name = "cbBaudrate";
+ cbBaudrate.Size = new Size(174, 25);
+ cbBaudrate.TabIndex = 35;
+ //
+ // btCanOpen
+ //
+ btCanOpen.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ btCanOpen.Location = new Point(0, 122);
+ btCanOpen.Name = "btCanOpen";
+ btCanOpen.Size = new Size(174, 29);
+ btCanOpen.TabIndex = 36;
+ btCanOpen.Text = "打开";
+ btCanOpen.UseVisualStyleBackColor = true;
+ btCanOpen.Click += btCanOpen_Click;
+ //
+ // CanSelect
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ Controls.Add(btCanOpen);
+ Controls.Add(cbBaudrate);
+ Controls.Add(label3);
+ Controls.Add(cbCan);
+ Controls.Add(label4);
+ Name = "CanSelect";
+ Size = new Size(174, 164);
+ Load += CanSelect_Load;
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private ComboBox cbCan;
+ private Label label4;
+ private Label label3;
+ private ComboBox cbBaudrate;
+ private Button btCanOpen;
+ }
+}
diff --git a/A2Lib/CAN/CanSelect.resx b/A2Lib/CAN/CanSelect.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/A2Lib/CAN/CanSelect.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/A2Lib/ComPort/ComForm.cs b/A2Lib/ComPort/ComForm.cs
new file mode 100644
index 0000000..f44fb96
--- /dev/null
+++ b/A2Lib/ComPort/ComForm.cs
@@ -0,0 +1,259 @@
+using ALib.ComPort;
+using NAudio.Mixer;
+using NewLife;
+using NewLife.Data;
+using NewLife.Net;
+using NewLife.Threading;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Xml.Linq;
+
+namespace ALib
+{
+ public partial class ComForm : UserControl
+ {
+ #region 属性
+ /// <summary>端口</summary>
+ public SerialTransport Port { get; set; }
+
+ /// <summary>选择的端口</summary>
+ public String SelectedPort { get { return cbName.SelectedItem + ""; } set { } }
+
+
+ /// <summary>波特率。默认115200</summary>
+ public Int32 BaudRate { get; set; } = 115200;
+
+ /// <summary>奇偶校验位。默认None</summary>
+ public Parity Parity { get; set; } = Parity.None;
+
+ /// <summary>数据位。默认8</summary>
+ public Int32 DataBits { get; set; } = 8;
+
+ /// <summary>停止位。默认One</summary>
+ public StopBits StopBits { get; set; } = StopBits.One;
+
+ #endregion
+
+ public ComForm()
+ {
+ InitializeComponent();
+ }
+
+ private void ComFrom_Load(object sender, EventArgs e)
+ {
+ cbName.DropDownStyle = ComboBoxStyle.DropDownList;
+
+ // 挂载定时器
+ _timer = new TimerX(OnCheck, null, 300, 300) { Async = true };
+
+ if (Parent is Form frm)
+ {
+ frm.FormClosing += frm_FormClosing;
+ }
+ }
+
+ #region 更新端口
+
+ TimerX _timer;
+ private void OnCheck(Object state)
+ {
+ if (Enabled) ShowPorts();
+ }
+
+ void frm_FormClosing(Object sender, FormClosingEventArgs e)
+ {
+ _timer.Dispose();
+ _timer = null;
+
+ Port?.Close();
+ }
+
+ String _ports = null;
+ DateTime _nextport = DateTime.MinValue;
+ /// <summary>下拉框显示串口</summary>
+ public void ShowPorts()
+ {
+ if (_nextport > DateTime.Now) return;
+ _nextport = DateTime.Now.AddSeconds(1);
+
+ var ps = SerialTransport.GetPortNames();
+ var str = String.Join(",", ps);
+ // 如果端口有所改变,则重新绑定
+ if (_ports != str)
+ {
+ // if (_ports != null) "串口有改变".SpeechTip();
+
+ _ports = str;
+
+ Invoke(() =>
+ {
+ var old = cbName.SelectedItem + "";
+ cbName.DataSource = ps;
+ if (!String.IsNullOrEmpty(old) && Array.IndexOf(ps, old) >= 0) cbName.SelectedItem = old;
+ });
+ }
+ }
+
+ #endregion
+
+ #region 窗体
+
+ /// <summary>连接串口</summary>
+ public void Connect()
+ {
+ var name = cbName.SelectedItem + "";
+ if (String.IsNullOrEmpty(name))
+ {
+ // "请选择串口".SpeechTip();
+ MessageBox.Show("请选择串口!", Text);
+ cbName.Focus();
+ return;
+ }
+ var p = name.IndexOf("(");
+ if (p > 0) name = name.Substring(0, p);
+
+ var pt = Port;
+ if (pt != null)
+ {
+ // 如果上次没有关闭,则关闭
+ try { pt.Close(); }
+ catch { pt = null; }
+ }
+
+ if (pt == null) pt = new SerialTransport();
+ pt.PortName = name;
+ pt.BaudRate = BaudRate;
+ pt.Parity = Parity;
+ pt.DataBits = DataBits;
+ pt.StopBits = StopBits;
+
+ //pt.Open();
+
+ pt.Disconnected += Port_Disconnected;
+ pt.Received += OnReceived;
+ pt.Open();
+
+ //pt.EnsureCreate();
+ Port = pt;
+ Enabled = false;
+ }
+
+ void Port_Disconnected(Object sender, EventArgs e)
+ {
+ Disconnect();
+ }
+
+ /// <summary>断开串口连接</summary>
+ public void Disconnect()
+ {
+ var pt = Port;
+ if (pt != null)
+ {
+ Port = null;
+ pt.Disconnected -= Port_Disconnected;
+ pt.Received -= OnReceived;
+ pt.Close();
+ }
+
+ ShowPorts();
+ Enabled = true;
+ }
+
+
+ protected override void WndProc(ref Message m)
+ {
+ const int WM_MOUSEWHEEL = 0x020A;
+
+ if (m.Msg == WM_MOUSEWHEEL)
+ {
+ // 在这里处理你的全局滚轮事件
+ short delta = 0;
+ if (IntPtr.Size == 8)
+ {
+ // 64位系统
+ delta = (short)(m.WParam.ToInt64() >> 16);
+ }
+ else
+ {
+ // 32位系统
+ delta = (short)(m.WParam.ToInt32() >> 16);
+ }
+
+ if (delta > 0)
+ {
+ // 向上滚动
+ if (cbName.SelectedIndex > 0)
+ {
+ cbName.SelectedIndex--;
+ }
+ }
+ else if (delta < 0)
+ {
+ // 向下滚动
+ if (cbName.SelectedIndex < cbName.Items.Count - 1)
+ {
+ cbName.SelectedIndex++;
+ }
+ }
+
+ // 如果你想阻止事件传递给其他控件,可以在这里返回
+ return;
+ }
+
+ base.WndProc(ref m);
+ }
+
+ #endregion
+
+ #region 收发数据
+ /// <summary>发送字节数组</summary>
+ /// <param name="data"></param>
+ public void Send(Byte[] data)
+ {
+ if (data == null || data.Length <= 0) return;
+
+ Port?.Send(data);
+ }
+
+ //public event SerialReceived Received;
+ /// <summary>收到数据时触发。第一个参数是数据,第二个参数返回是否继续往下传递数据</summary>
+ [Browsable(true)]
+ [EditorBrowsable(EditorBrowsableState.Always)]
+ public event EventHandler<BufferEventArgs> Received;
+
+ void OnReceived(Object sender, ReceivedEventArgs e)
+ {
+ var data = e.Packet.ReadBytes();
+ if (data == null || data.Length < 1) return;
+
+ // 处理数据委托
+ if (Received != null)
+ {
+ var e2 = new BufferEventArgs { Value = data };
+ Received(this, e2);
+ if (!e2.Cancel) return;
+ // 外部可能修改了数据
+ // data = e2.Value;
+ // if (!BufferEventArgs.Invoke(Received, data)) return null;
+ }
+ }
+ #endregion
+ }
+
+ /// <summary>缓冲区事件参数</summary>
+ public class BufferEventArgs : CancelEventArgs
+ {
+ private Byte[] _Value;
+
+ /// <summary>缓冲区数据</summary>
+ public Byte[] Value { get { return _Value; } set { _Value = value; } }
+ }
+}
diff --git a/A2Lib/ComPort/ComForm.Designer.cs b/A2Lib/ComPort/ComForm.Designer.cs
new file mode 100644
index 0000000..805288d
--- /dev/null
+++ b/A2Lib/ComPort/ComForm.Designer.cs
@@ -0,0 +1,71 @@
+namespace ALib
+{
+ partial class ComForm
+ {
+ /// <summary>
+ /// 必需的设计器变量。
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// 清理所有正在使用的资源。
+ /// </summary>
+ /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region 组件设计器生成的代码
+
+ /// <summary>
+ /// 设计器支持所需的方法 - 不要修改
+ /// 使用代码编辑器修改此方法的内容。
+ /// </summary>
+ private void InitializeComponent()
+ {
+ cbName = new ComboBox();
+ label1 = new Label();
+ SuspendLayout();
+ //
+ // cbName
+ //
+ cbName.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
+ cbName.FormattingEnabled = true;
+ cbName.Location = new Point(56, 12);
+ cbName.Name = "cbName";
+ cbName.Size = new Size(276, 25);
+ cbName.TabIndex = 0;
+ //
+ // label1
+ //
+ label1.AutoSize = true;
+ label1.Location = new Point(15, 15);
+ label1.Name = "label1";
+ label1.Size = new Size(35, 17);
+ label1.TabIndex = 1;
+ label1.Text = "串口:";
+ //
+ // ComFrom
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ Controls.Add(label1);
+ Controls.Add(cbName);
+ Name = "ComFrom";
+ Size = new Size(357, 49);
+ Load += ComFrom_Load;
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private ComboBox cbName;
+ private Label label1;
+ }
+}
diff --git a/A2Lib/ComPort/ComForm.resx b/A2Lib/ComPort/ComForm.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/A2Lib/ComPort/ComForm.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/A2Lib/ComPort/ComSelect.cs b/A2Lib/ComPort/ComSelect.cs
new file mode 100644
index 0000000..b1b1f7d
--- /dev/null
+++ b/A2Lib/ComPort/ComSelect.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace ALib.ComPort
+{
+ public partial class ComSelect : Form
+ {
+ public ComSelect()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/A2Lib/ComPort/ComSelect.Designer.cs b/A2Lib/ComPort/ComSelect.Designer.cs
new file mode 100644
index 0000000..248e494
--- /dev/null
+++ b/A2Lib/ComPort/ComSelect.Designer.cs
@@ -0,0 +1,63 @@
+namespace ALib.ComPort
+{
+ partial class ComSelect
+ {
+ /// <summary>
+ /// Required designer variable.
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// Clean up any resources being used.
+ /// </summary>
+ /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ /// <summary>
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ /// </summary>
+ private void InitializeComponent()
+ {
+ comFrom = new ComForm();
+ SuspendLayout();
+ //
+ // comFrom
+ //
+ comFrom.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ comFrom.BaudRate = 115200;
+ comFrom.DataBits = 8;
+ comFrom.Location = new Point(12, 12);
+ comFrom.Name = "comFrom";
+ comFrom.Parity = System.IO.Ports.Parity.None;
+ comFrom.Port = null;
+ comFrom.SelectedPort = "COM13(VCP0)";
+ comFrom.Size = new Size(357, 49);
+ comFrom.StopBits = System.IO.Ports.StopBits.One;
+ comFrom.TabIndex = 0;
+ //
+ // ComSelect
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(371, 74);
+ Controls.Add(comFrom);
+ Name = "ComSelect";
+ Text = "ComSelect";
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ public ComForm comFrom;
+ }
+}
\ No newline at end of file
diff --git a/A2Lib/ComPort/ComSelect.resx b/A2Lib/ComPort/ComSelect.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/A2Lib/ComPort/ComSelect.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/A2Lib/ComPort/GrblTool.cs b/A2Lib/ComPort/GrblTool.cs
new file mode 100644
index 0000000..7cf8e38
--- /dev/null
+++ b/A2Lib/ComPort/GrblTool.cs
@@ -0,0 +1,217 @@
+using ALib.WinForm;
+using NewLife.Log;
+using NewLife.Threading;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Xml.Linq;
+
+namespace ALib.ComPort
+{
+ public partial class GrblTool : Form
+ {
+ public GrblTool()
+ {
+ InitializeComponent();
+ }
+
+ public double X, Y, Z;
+
+ public Grbl_Com Grbl;
+
+ /// <summary>初始化,从GetStatusReport获取坐标给 XYZ赋值</summary>
+ private bool init = false;
+
+ private TimerX _time;
+
+ private int lockValue;
+
+ private void GrblTool_Load(object sender, EventArgs e)
+ {
+ cbMoveValue.SelectedIndex = 0;
+
+ foreach (Control item in this.Controls)
+ {
+ item.TabStop = false;
+ }
+ lbXYZ.TabIndex = 0;
+ cbMoveValue.TabStop = true;
+ this.ActiveControl = cbMoveValue;
+
+ _time = new TimerX(ReadInfo, null, 100, 1000);
+ this.FormClosing += GrblTool_FormClosing;
+ }
+
+ private void GrblTool_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ _time.Dispose();
+ }
+
+ private async Task ReadInfo(Object obj)
+ {
+ if (Grbl == null) return;
+
+ var _ = Grbl.GetStatusReport();
+ var info = Grbl.CurrStatus;
+ if (info != null)
+ {
+ var mco = info.MachinePosition;
+ if (!init)
+ {
+ X = mco[0];
+ Y = mco[1];
+ Z = mco[2];
+
+ init = true;
+ }
+ var mstr = $"MCO X{mco[0]:F4},Y{mco[1]:F4},Z{mco[2]:F4} | {info.MachineState}";
+
+ this.UpdateUIControl(() =>
+ {
+ lbCurrXYZ.Text = mstr;
+ });
+ }
+ }
+
+ private void GrblTool_KeyPress(object sender, KeyPressEventArgs e)
+ {
+
+ }
+
+ private void XYZChange()
+ {
+ lbXYZ.Text = $"X{X:F4},Y{Y:F4},Z{Z:F4}";
+
+ if (Grbl != null)
+ {
+ var g0 = $"G0 X{X:F4} Y{Y:F4} Z{Z:F4}";
+ var _ = Grbl.SendGcodeLine(g0);
+ }
+ }
+
+ private void GrblTool_KeyDown(object sender, KeyEventArgs e)
+ {
+ var move = Convert.ToDouble(cbMoveValue.Text);
+
+ bool xyzChange = false;
+ switch (e.KeyCode)
+ {
+ case Keys.Left: X -= move; xyzChange = true; break;
+ case Keys.Right: X += move; xyzChange = true; break;
+ case Keys.Up: Y += move; xyzChange = true; break;
+ case Keys.Down: Y -= move; xyzChange = true; break;
+ case Keys.PageUp: Z += move; xyzChange = true; break;
+ case Keys.PageDown: Z -= move; xyzChange = true; break;
+
+ case Keys.Add: if (cbMoveValue.SelectedIndex < cbMoveValue.Items.Count - 1) cbMoveValue.SelectedIndex++; break;
+ case Keys.Subtract: if (cbMoveValue.SelectedIndex > 0) cbMoveValue.SelectedIndex--; break;
+
+ case Keys.F1: cbMoveValue.SelectedIndex = 0; break;
+ case Keys.F2: cbMoveValue.SelectedIndex = 1; break;
+ case Keys.F3: cbMoveValue.SelectedIndex = 2; break;
+ case Keys.F4: cbMoveValue.SelectedIndex = 3; break;
+
+ case Keys.F9: btG92_Click(null, null); break;
+
+ // H 键回零
+ // case Keys.H: var _ = Grbl.GoHome(); break;
+ case Keys.X: var _ = Grbl.UnLock(); break;
+ // 清除所有未执行命令
+ case Keys.Escape: Grbl.Reset(); break;
+
+ default: break;
+ }
+
+ if (xyzChange)
+ {
+ XYZChange();
+ }
+
+ e.Handled = true;
+ // e.SuppressKeyPress = true;
+ }
+
+ /// <summary>把焦点移回单步值下拉框,避免方向键被按钮的焦点导航吞掉</summary>
+ private void FocusMoveValue()
+ {
+ this.ActiveControl = cbMoveValue;
+ }
+
+ private void btXInc_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var move = Convert.ToDouble(cbMoveValue.Text);
+ X += move;
+ XYZChange();
+ }
+ private void btXDec_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var move = Convert.ToDouble(cbMoveValue.Text);
+ X -= move;
+ XYZChange();
+ }
+
+ private void btYInc_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var move = Convert.ToDouble(cbMoveValue.Text);
+ Y += move;
+ XYZChange();
+ }
+
+ private void btYDec_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var move = Convert.ToDouble(cbMoveValue.Text);
+ Y -= move;
+ XYZChange();
+ }
+
+ private void btZInc_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var move = Convert.ToDouble(cbMoveValue.Text);
+ Z += move;
+ XYZChange();
+ }
+
+ private void btZDec_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var move = Convert.ToDouble(cbMoveValue.Text);
+ Z -= move;
+ XYZChange();
+ }
+
+ private void timer1_Tick(object sender, EventArgs e)
+ {
+ }
+
+ private void btG92_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var _ = Grbl.SendGcodeLine("G92 X0 Y0 Z0");
+ }
+
+ private void btUnlock_Click(object sender, EventArgs e)
+ {
+ FocusMoveValue();
+ var _ = Grbl.UnLock();
+ }
+
+ #region 日志
+
+ void WriteLog(String format, params Object[] args) => Grbl?.Log?.Write(LogLevel.Info, format, args);
+
+ #endregion
+
+
+ }
+}
diff --git a/A2Lib/ComPort/GrblTool.Designer.cs b/A2Lib/ComPort/GrblTool.Designer.cs
new file mode 100644
index 0000000..903cc91
--- /dev/null
+++ b/A2Lib/ComPort/GrblTool.Designer.cs
@@ -0,0 +1,276 @@
+namespace ALib.ComPort
+{
+ partial class GrblTool
+ {
+ /// <summary>
+ /// Required designer variable.
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// Clean up any resources being used.
+ /// </summary>
+ /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ /// <summary>
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ /// </summary>
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+ btYInc = new Button();
+ btYDec = new Button();
+ btXDec = new Button();
+ btXInc = new Button();
+ btZDec = new Button();
+ btZInc = new Button();
+ lbXYZ = new Label();
+ lbCurrXYZ = new Label();
+ cbMoveValue = new ComboBox();
+ label3 = new Label();
+ timer1 = new System.Windows.Forms.Timer(components);
+ label1 = new Label();
+ label2 = new Label();
+ label4 = new Label();
+ label6 = new Label();
+ btUnlock = new Button();
+ label7 = new Label();
+ SuspendLayout();
+ //
+ // btYInc
+ //
+ btYInc.Location = new Point(136, 145);
+ btYInc.Name = "btYInc";
+ btYInc.Size = new Size(60, 60);
+ btYInc.TabIndex = 0;
+ btYInc.TabStop = false;
+ btYInc.Text = "Y+";
+ btYInc.UseVisualStyleBackColor = true;
+ btYInc.Click += btYInc_Click;
+ btYInc.KeyDown += GrblTool_KeyDown;
+ //
+ // btYDec
+ //
+ btYDec.Location = new Point(136, 211);
+ btYDec.Name = "btYDec";
+ btYDec.Size = new Size(60, 60);
+ btYDec.TabIndex = 1;
+ btYDec.TabStop = false;
+ btYDec.Text = "Y-";
+ btYDec.UseVisualStyleBackColor = true;
+ btYDec.Click += btYDec_Click;
+ btYDec.KeyDown += GrblTool_KeyDown;
+ //
+ // btXDec
+ //
+ btXDec.Location = new Point(70, 211);
+ btXDec.Name = "btXDec";
+ btXDec.Size = new Size(60, 60);
+ btXDec.TabIndex = 2;
+ btXDec.TabStop = false;
+ btXDec.Text = "X-";
+ btXDec.UseVisualStyleBackColor = true;
+ btXDec.Click += btXDec_Click;
+ btXDec.KeyDown += GrblTool_KeyDown;
+ //
+ // btXInc
+ //
+ btXInc.Location = new Point(202, 211);
+ btXInc.Name = "btXInc";
+ btXInc.Size = new Size(60, 60);
+ btXInc.TabIndex = 3;
+ btXInc.TabStop = false;
+ btXInc.Text = "X+";
+ btXInc.UseVisualStyleBackColor = true;
+ btXInc.Click += btXInc_Click;
+ btXInc.KeyDown += GrblTool_KeyDown;
+ //
+ // btZDec
+ //
+ btZDec.Location = new Point(306, 211);
+ btZDec.Name = "btZDec";
+ btZDec.Size = new Size(60, 60);
+ btZDec.TabIndex = 5;
+ btZDec.TabStop = false;
+ btZDec.Text = "Z-";
+ btZDec.UseVisualStyleBackColor = true;
+ btZDec.Click += btZDec_Click;
+ btZDec.KeyDown += GrblTool_KeyDown;
+ //
+ // btZInc
+ //
+ btZInc.Location = new Point(306, 145);
+ btZInc.Name = "btZInc";
+ btZInc.Size = new Size(60, 60);
+ btZInc.TabIndex = 4;
+ btZInc.TabStop = false;
+ btZInc.Text = "Z+";
+ btZInc.UseVisualStyleBackColor = true;
+ btZInc.Click += btZInc_Click;
+ btZInc.KeyDown += GrblTool_KeyDown;
+ //
+ // lbXYZ
+ //
+ lbXYZ.AutoSize = true;
+ lbXYZ.Location = new Point(72, 55);
+ lbXYZ.Name = "lbXYZ";
+ lbXYZ.Size = new Size(57, 17);
+ lbXYZ.TabIndex = 0;
+ lbXYZ.Text = "X0,Y0,Z0";
+ //
+ // lbCurrXYZ
+ //
+ lbCurrXYZ.AutoSize = true;
+ lbCurrXYZ.Location = new Point(72, 84);
+ lbCurrXYZ.Name = "lbCurrXYZ";
+ lbCurrXYZ.Size = new Size(68, 17);
+ lbCurrXYZ.TabIndex = 7;
+ lbCurrXYZ.Text = "CURR_XYZ";
+ //
+ // cbMoveValue
+ //
+ cbMoveValue.DropDownStyle = ComboBoxStyle.DropDownList;
+ cbMoveValue.FormattingEnabled = true;
+ cbMoveValue.Items.AddRange(new object[] { "0.1", "1", "5", "10" });
+ cbMoveValue.Location = new Point(120, 304);
+ cbMoveValue.Name = "cbMoveValue";
+ cbMoveValue.Size = new Size(76, 25);
+ cbMoveValue.TabIndex = 9;
+ cbMoveValue.TabStop = false;
+ cbMoveValue.KeyDown += GrblTool_KeyDown;
+ //
+ // label3
+ //
+ label3.AutoSize = true;
+ label3.Location = new Point(70, 307);
+ label3.Name = "label3";
+ label3.Size = new Size(44, 17);
+ label3.TabIndex = 10;
+ label3.Text = "单步值";
+ //
+ // timer1
+ //
+ timer1.Enabled = false;
+ timer1.Tick += timer1_Tick;
+ //
+ // label1
+ //
+ label1.AutoSize = true;
+ label1.Location = new Point(372, 167);
+ label1.Name = "label1";
+ label1.Size = new Size(40, 17);
+ label1.TabIndex = 11;
+ label1.Text = "PgUp";
+ //
+ // label2
+ //
+ label2.AutoSize = true;
+ label2.Location = new Point(372, 233);
+ label2.Name = "label2";
+ label2.Size = new Size(39, 17);
+ label2.TabIndex = 12;
+ label2.Text = "PgDn";
+ //
+ // label4
+ //
+ label4.AutoSize = true;
+ label4.Location = new Point(202, 307);
+ label4.Name = "label4";
+ label4.Size = new Size(39, 17);
+ label4.TabIndex = 13;
+ label4.Text = "F1-F4";
+ //
+ // label6
+ //
+ label6.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
+ label6.AutoSize = true;
+ label6.Location = new Point(278, 361);
+ label6.Name = "label6";
+ label6.Size = new Size(164, 17);
+ label6.TabIndex = 15;
+ label6.Text = "方向键移动,F1-F4切换单步值。";
+ //
+ // btUnlock
+ //
+ btUnlock.Location = new Point(306, 41);
+ btUnlock.Name = "btUnlock";
+ btUnlock.Size = new Size(60, 60);
+ btUnlock.TabIndex = 17;
+ btUnlock.TabStop = false;
+ btUnlock.Text = "unlock";
+ btUnlock.UseVisualStyleBackColor = true;
+ btUnlock.Click += btUnlock_Click;
+ //
+ // label7
+ //
+ label7.AutoSize = true;
+ label7.Location = new Point(372, 63);
+ label7.Name = "label7";
+ label7.Size = new Size(16, 17);
+ label7.TabIndex = 18;
+ label7.Text = "X";
+ //
+ // GrblTool
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ ClientSize = new Size(454, 387);
+ Controls.Add(label7);
+ Controls.Add(btUnlock);
+ Controls.Add(label6);
+ Controls.Add(label4);
+ Controls.Add(label2);
+ Controls.Add(label1);
+ Controls.Add(label3);
+ Controls.Add(cbMoveValue);
+ Controls.Add(lbCurrXYZ);
+ Controls.Add(lbXYZ);
+ Controls.Add(btZDec);
+ Controls.Add(btZInc);
+ Controls.Add(btXInc);
+ Controls.Add(btXDec);
+ Controls.Add(btYDec);
+ Controls.Add(btYInc);
+ ImeMode = ImeMode.Disable;
+ KeyPreview = true;
+ Name = "GrblTool";
+ Text = "GrblTool";
+ Load += GrblTool_Load;
+ KeyDown += GrblTool_KeyDown;
+ KeyPress += GrblTool_KeyPress;
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private Button btYInc;
+ private Button btYDec;
+ private Button btXDec;
+ private Button btXInc;
+ private Button btZDec;
+ private Button btZInc;
+ private Label lbXYZ;
+ private Label lbCurrXYZ;
+ private ComboBox cbMoveValue;
+ private Label label3;
+ private System.Windows.Forms.Timer timer1;
+ private Label label1;
+ private Label label2;
+ private Label label4;
+ private Label label6;
+ private Button btUnlock;
+ private Label label7;
+ }
+}
\ No newline at end of file
diff --git a/A2Lib/ComPort/GrblTool.resx b/A2Lib/ComPort/GrblTool.resx
new file mode 100644
index 0000000..f2b86d4
--- /dev/null
+++ b/A2Lib/ComPort/GrblTool.resx
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <metadata name="timer1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+ <value>17, 17</value>
+ </metadata>
+</root>
\ No newline at end of file
diff --git a/A2Lib/FormComponent/BitmapSelect.cs b/A2Lib/FormComponent/BitmapSelect.cs
new file mode 100644
index 0000000..45a6ccf
--- /dev/null
+++ b/A2Lib/FormComponent/BitmapSelect.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ALib.FormComponent
+{
+ /// <summary>图片选择辅助</summary>
+ public class BitmapSelect
+ {
+ public BitmapSelect(PictureBox pb)
+ {
+ picBox = pb;
+ picBoxSize = new Size(pb.Size.Width, pb.Size.Height);
+
+ pb.MouseDown += pb_MouseDown;
+ pb.MouseMove += pb_MouseMove;
+ pb.MouseUp += pb_MouseUp;
+
+ var img = new Bitmap(pb.Width, pb.Height);
+ using (Graphics g = Graphics.FromImage(img)) { g.Clear(Color.White); }
+ pb.Image = img;
+
+ Src = img;
+ ShowBm = (bm) => pb.Image = bm;
+ }
+
+ /// <summary>显示窗</summary>
+ private PictureBox picBox;
+ /// <summary>大小</summary>
+ private Size picBoxSize;
+ /// <summary>原图</summary>
+ private Bitmap Src;
+
+ /// <summary>框选开始位置</summary>
+ public Point pt_Start { get; private set; } = new Point();
+ /// <summary>框选结束位置</summary>
+ public Point pt_End { get; private set; } = new Point();
+ /// <summary>鼠标当前位置</summary>
+ private Point pt_Now = new Point();
+
+ /// <summary>如果图片大于PictureBox是否支持剪裁</summary>
+ public bool SupportCutting = false;
+ /// <summary>如果在非UI线程执行,请写此函数</summary>
+ public Action<Bitmap> ShowBm;
+
+ /// <summary>给新图</summary>
+ /// <param name="src"></param>
+ public void SetImage(Bitmap src)
+ {
+ if (src == null) return;
+
+ // 大小有变化
+ if (src.Width != picBox.Width || src.Height != picBox.Height)
+ {
+ // 需要缩处理
+ if ((src.Width > picBoxSize.Width) || (src.Height > picBoxSize.Height))
+ {
+ if (!SupportCutting)
+ {
+ var str = $"图片过大无法处理 {src.Width},{src.Height}(src) > {picBoxSize.Width},{picBoxSize.Height}(picBox)";
+ throw new Exception(str);
+ }
+ else
+ {
+ // 最终大小
+ var w = src.Width;
+ if (w > picBoxSize.Width) w = picBoxSize.Width;
+ var h = src.Height;
+ if (h > picBoxSize.Height) h = picBoxSize.Height;
+
+ // 剪裁
+ var point = new System.Drawing.Point((src.Width - w) / 2, (src.Height - h) / 2);
+ var croRect = new Rectangle(point.X, point.Y, w, h);
+ src = src.Clone(croRect, src.PixelFormat);
+
+ // 剪裁后修改显示框大小
+ picBox.Width = src.Width;
+ picBox.Height = src.Height;
+ }
+ }
+ else
+ {
+ // 直接设置
+ picBox.Width = src.Width;
+ picBox.Height = src.Height;
+ }
+ }
+
+ // 可能剪裁过
+ Src = src;
+
+ // 先刷新一个
+ var bm = Src.Clone() as Bitmap;
+ pic_Draw(bm);
+ ShowBm?.Invoke(bm);
+ }
+
+ /// <summary>拿到选取区域</summary>
+ /// <returns></returns>
+ public Bitmap GetSelect()
+ {
+ Point pt = pt_Start;
+ Size size = new Size(pt_End.X - pt_Start.X, pt_End.Y - pt_Start.Y);
+ if (size.Width < 0) return null;
+ if (size.Height < 0) return null;
+
+ var croRect = new Rectangle(pt_Start, size);
+ return Src.Clone(croRect, Src.PixelFormat);
+ }
+
+ #region 干活的
+
+ private void pb_MouseDown(object sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ {
+ pt_Start = e.Location;
+ pt_End = e.Location;
+ pt_Now = e.Location;
+
+ var bm = Src.Clone() as Bitmap;
+ pic_Draw(bm);
+ ShowBm?.Invoke(bm);
+ }
+ }
+
+ private void pb_MouseMove(object sender, MouseEventArgs e)
+ {
+ pt_Now = e.Location;
+
+ if (e.Button == MouseButtons.Left)
+ {
+ pt_End = e.Location;
+ }
+
+ var bm = Src.Clone() as Bitmap;
+ pic_Draw(bm);
+ ShowBm?.Invoke(bm);
+ }
+
+ private void pb_MouseUp(object sender, MouseEventArgs e)
+ {
+ if (e.Button == MouseButtons.Left)
+ {
+ pt_End = e.Location;
+ pt_Now = e.Location;
+
+ var bm = Src.Clone() as Bitmap;
+ pic_Draw(bm);
+ ShowBm?.Invoke(bm);
+ }
+ }
+
+ private void pic_Draw(Bitmap bm)
+ {
+ if (bm == null) return;
+
+ using (var g = Graphics.FromImage(bm))
+ {
+ Point pt = pt_Start;
+ Size size = new Size(pt_End.X - pt_Start.X, pt_End.Y - pt_Start.Y);
+ if ((size.Width > 0) && (size.Height > 0))
+ {
+ var pen = new Pen(Color.Red, 2);
+ var rac = new Rectangle(pt, size);
+ g.DrawRectangle(pen, rac);
+ }
+
+ var pen2 = new Pen(Color.Black, 1);
+ g.DrawLine(pen2, 0, pt_Now.Y, bm.Width - 1, pt_Now.Y);
+ g.DrawLine(pen2, pt_Now.X, 0, pt_Now.X, bm.Height - 1);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/A2Lib/FormComponent/FormAction.cs b/A2Lib/FormComponent/FormAction.cs
new file mode 100644
index 0000000..119ea1e
--- /dev/null
+++ b/A2Lib/FormComponent/FormAction.cs
@@ -0,0 +1,134 @@
+using NewLife;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Alib.WinForm
+{
+ public class FormAction
+ {
+ #region IP/MAC <=> String 转换
+
+ public static IPAddress StringToIPAddress(String str)
+ {
+ try
+ {
+ var ip = IPAddress.Parse(str);
+ return ip;
+ }
+ catch
+ {
+ return new IPAddress(new Byte[] { 0x00, 0x00, 0x00, 0x00 });
+ }
+ }
+
+ public static string IpToString(byte[] ip, string def)
+ {
+ if (ip == null) return def;
+ if (ip.Length < 4) return def;
+
+ var xx = new IPAddress(ip).ToString();
+ return xx;
+ }
+
+ public static string MacToString(byte[] mac)
+ {
+ if (mac == null) return "";
+ if (mac.Length < 6) return "";
+
+ return mac.ToHex(":");
+ }
+
+ public static Byte[] StringToMac(String str)
+ {
+ var mac = new Byte[6];
+ if (str.IsNullOrWhiteSpace()) return mac;
+
+ str = str.Replace(",", "").Replace(":", "").Replace(" ", "");
+ var mac2 = str.ToHex();
+
+ var len = mac2.Length > 6 ? 6 : mac2.Length;
+ Array.Copy(mac2, mac, len);
+
+ return mac;
+ }
+
+ #endregion
+
+ #region TextBox 通用输入动作过滤
+
+ /// <summary>端口输入框 KeyPress 事件,限制输入仅为数字和小数点</summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ public static void IPInput_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ if (e.KeyChar == '\b') return;
+
+ if ((e.KeyChar < '0') || (e.KeyChar > '9'))
+ {
+ if (e.KeyChar != '.')
+ {
+ e.Handled = true;
+ return;
+ }
+ }
+
+ var box = sender as TextBox;
+ var str = box.Text;
+
+ var nums = str.Split('.');
+ if (e.KeyChar == '.')
+ {
+ if (nums.Count() > 3)
+ {
+ e.Handled = true;
+ return;
+ }
+ }
+
+ var strnum = nums[nums.Count() - 1] + e.KeyChar;
+ var num = strnum.ToInt();
+ if (num > 255) e.Handled = true;
+ }
+
+ /// <summary>端口输入过滤</summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ public static void PortInput_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ if (e.KeyChar == '\b') return;
+
+ if ((e.KeyChar < '0') || (e.KeyChar > '9'))
+ {
+ e.Handled = true;
+ return;
+ }
+
+ var box = sender as TextBox;
+ var str = box.Text + e.KeyChar;
+
+ var port = str.ToInt();
+ if (port > 65535) e.Handled = true;
+ }
+
+ /// <summary>数字输入过滤</summary>
+ /// <param name="sender"></param>
+ /// <param name="e"></param>
+ public static void NumInput_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ if (e.KeyChar == '\b') return;
+
+ if ((e.KeyChar < '0') || (e.KeyChar > '9'))
+ {
+ e.Handled = true;
+ return;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/A2Lib/FormComponent/FormHelper.cs b/A2Lib/FormComponent/FormHelper.cs
new file mode 100644
index 0000000..95b8264
--- /dev/null
+++ b/A2Lib/FormComponent/FormHelper.cs
@@ -0,0 +1,207 @@
+using NewLife.Log;
+using NewLife.Reflection;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Xml.Linq;
+
+namespace ALib.WinForm
+{
+ public static class FormHelper
+ {
+ /// <summary>非UI线程/UI线程,执行UI动作</summary>
+ /// <param name="fm"></param>
+ /// <param name="action"></param>
+ public static void UpdateUIControl(this Form fm, Action action)
+ {
+ if (fm.InvokeRequired)
+ {
+ try
+ {
+ fm.Invoke(() =>
+ {
+ if (fm.IsDisposed || fm.Disposing || !fm.IsHandleCreated) return;
+ action();
+ });
+ } catch { }
+ }
+ else
+ {
+ if (fm.IsDisposed || fm.Disposing || !fm.IsHandleCreated) return;
+ action();
+ }
+ }
+
+ /// <summary>给 PictureBox 设置图片,自动释放上一张(必须在 UI 线程调用;img 可为 null)</summary>
+ /// <param name="pic"></param>
+ /// <param name="img"></param>
+ public static void SetImage(this PictureBox pic, System.Drawing.Image img)
+ {
+ if (pic == null) { img?.Dispose(); return; }
+
+ var old = pic.Image;
+ pic.Image = img;
+ old?.Dispose();
+ }
+
+ /// <summary>开启控件双缓冲,减少频繁刷新时的闪烁</summary>
+ /// <param name="ctrl"></param>
+ public static void EnableDoubleBuffer(this Control ctrl)
+ {
+ if (ctrl == null) return;
+
+ // DoubleBuffered 是 protected 属性,反射设置
+ var pi = typeof(Control).GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic);
+ pi?.SetValue(ctrl, true, null);
+ }
+
+ /// <summary>非UI线程/UI线程,执行UI DialogShow函数</summary>
+ /// <param name="fm">主UI</param>
+ /// <param name="dialog">需要DialogShow的form</param>
+ /// <returns></returns>
+ public static DialogResult ShowModalDialogOnUIThread(this Form fm, Form dialog)
+ {
+ if (fm.InvokeRequired)
+ {
+ return fm.Invoke(() =>
+ {
+ // 外部设置吧
+ // dialog.StartPosition = FormStartPosition.CenterParent;
+ return dialog.ShowDialog(fm);
+ });
+ }
+ else
+ {
+ return dialog.ShowDialog(fm);
+ }
+ }
+
+ /// <summary>获取 ListView 当前可见的行区间(与是否有数据无关)</summary>
+ /// <param name="lv"></param>
+ /// <returns>(可见行首行行号,行数),无可见行时返回 (0, 0)</returns>
+ public static (int, int) Lines(this ListView lv)
+ {
+ if (lv == null || !lv.IsHandleCreated || lv.ClientSize.Height <= 0) return (0, 0);
+
+ // 行高与列头高度:有数据时用 GetItemRect 实测;无数据时按字体估算(GetItemRect 会抛异常)
+ int rowHeight = 0;
+ int headerHeight = 0;
+ if (lv.Items.Count > 0)
+ {
+ var r0 = lv.GetItemRect(0);
+ rowHeight = r0.Height;
+ headerHeight = r0.Top;
+ }
+ if (rowHeight <= 0)
+ {
+ rowHeight = (lv.Font != null ? lv.Font.Height : 12) + 6;
+ if (lv.SmallImageList != null && lv.SmallImageList.ImageSize.Height + 4 > rowHeight)
+ rowHeight = lv.SmallImageList.ImageSize.Height + 4;
+ }
+ if (rowHeight <= 0) return (0, 0);
+
+ // 可见区域:客户区去掉列头(若显示)
+ if (headerHeight <= 0 && lv.View == View.Details && lv.HeaderStyle != ColumnHeaderStyle.None)
+ headerHeight = rowHeight; // 无数据时的粗略估算
+
+ int areaHeight = lv.ClientSize.Height - headerHeight;
+ if (areaHeight <= 0) return (0, 0);
+
+ // 可见行数(至少 1 行,向上取整,避免末行只露一半被漏算)
+ int count = Math.Max(1, (areaHeight + rowHeight - 1) / rowHeight);
+
+ // 首个可见行行号:有数据时取 TopItem,否则为 0
+ int first = lv.Items.Count > 0 && lv.TopItem != null ? lv.TopItem.Index : 0;
+
+ return (first, count);
+ }
+
+ /// <summary>判断 ListView 是否已滚动到底部(可见区末行即末行)</summary>
+ /// <param name="lv"></param>
+ /// <returns></returns>
+ public static bool IsAtBottom(this ListView lv)
+ {
+ if (lv == null || lv.Items.Count == 0) return true;
+
+ // 控件尚未布局完成时无法测量,视为在底部,避免丢失跟随
+ var (start, count) = lv.Lines();
+ if (count <= 0) return true;
+
+ return start + count >= lv.Items.Count;
+ }
+
+ /// <summary>让 ListView 滚动到底部,显示最后一行</summary>
+ /// <param name="lv"></param>
+ public static void ScrollToBottom(this ListView lv)
+ {
+ if (lv == null || lv.Items.Count == 0) return;
+
+ lv.EnsureVisible(lv.Items.Count - 1);
+ }
+
+ /// <summary>判断指定行是否已在可见区内</summary>
+ /// <param name="lv"></param>
+ /// <param name="index">行号</param>
+ /// <returns></returns>
+ public static bool IsVisible(this ListView lv, int index)
+ {
+ if (lv == null || index < 0 || index >= lv.Items.Count) return false;
+
+ // 可见区间:IsAtBottom 已经把“控件未布局好”等情况兜住,此处仅做行号比较
+ var (start, count) = lv.Lines();
+ if (count <= 0) return false;
+
+ return index >= start && index < start + count;
+ }
+
+ /// <summary>
+ /// 让选中的行跟随其对应的数据(Tag 标识的对象)移动,保持选中。
+ /// 用于表格行位置会随数据刷新而变化的场景,例如新数据行插入到顶部时选中的旧行整体下移。
+ /// 行已滚出可见区时不再滚动滚动条,直接取消选中(丢弃跟随目标)。
+ /// </summary>
+ /// <param name="lv">ListView</param>
+ /// <param name="tag">选中行对应的标识对象(与 ListViewItem.Tag 比较,引用相同即认为同一行)</param>
+ /// <returns>该行仍在可见区内并保持选中返回 true,否则返回 false</returns>
+ public static bool FollowSelection(this ListView lv, Object tag)
+ {
+ if (lv == null || tag == null) return false;
+
+ for (int i = 0; i < lv.Items.Count; i++)
+ {
+ var it = lv.Items[i];
+ if (!ReferenceEquals(it.Tag, tag)) continue;
+
+ // 数据已跑出屏幕,取消选中,不再滚动滚动条把它拉回来
+ if (!lv.IsVisible(i))
+ {
+ it.Selected = false;
+ return false;
+ }
+
+ if (!it.Selected) it.Selected = true;
+
+ return true;
+ }
+
+ // 找不到对应行说明该数据已不在表格中
+ return false;
+ }
+
+ /// <summary>绑定输出到 RichTextBox</summary>
+ /// <param name="rtbLog"></param>
+ public static void RootLog2Rtb<T>(this _StructuralLog<T> slog, RichTextBox rtbLog) where T : _StructuralLog<T>
+ {
+ rtbLog.Clear();
+
+ var tcl = new TextControlLog() { Control = rtbLog, MaxLines = 1000 };
+ slog.RootLog.Add(tcl);
+
+ tcl.Write(LogLevel.Info, $"Name:{slog.Name} 日志");
+ }
+ }
+}
diff --git a/A2Lib/FormComponent/NumIncDec.cs b/A2Lib/FormComponent/NumIncDec.cs
new file mode 100644
index 0000000..260de4a
--- /dev/null
+++ b/A2Lib/FormComponent/NumIncDec.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace ALib.FormComponent
+{
+ /// <summary>
+ /// 数值增减微调控件。
+ /// 提供 <c><< < > >></c> 四个按钮,分别按 -10X、-1X、+1X、+10X 的倍数步进调整数值,
+ /// 也可在中间文本框内直接键入或粘贴数字(仅允许合法数字内容,含可选负号与单个小数点)。
+ /// 通过 <see cref="ValueChanged"/> 事件向外通知数值发生修改,修改来源不同触发时机也不同:
+ /// 按键修改立即触发;键入延迟 2 秒触发;粘贴延迟 200ms 触发;
+ /// 通过 <see cref="Value"/> 代码赋值不触发。
+ /// </summary>
+ public partial class NumIncDec : UserControl
+ {
+ /// <summary> 修改基准值 X。四个按键分别按 -10X、-1X、+1X、+10X 修改当前值。</summary>
+ [Browsable(true)]
+ [Description("修改基准值 X,按键按 X 的倍数调整数值")]
+ public float X { get; set; } = 1f;
+
+ /// <summary> 数值被修改后触发。按键修改立即触发;键入延迟 3 秒触发;粘贴延迟 200ms 触发;代码赋值不触发。</summary>
+ [Browsable(true)]
+ [Description("数值修改后触发")]
+ public event EventHandler ValueChanged;
+
+ /// <summary> 键入时触发数值修改事件的延迟(ms)。</summary>
+ private const int TYPE_DELAY_MS = 2000;
+
+ /// <summary> 粘贴时触发数值修改事件的延迟(ms)。</summary>
+ private const int PASTE_DELAY_MS = 200;
+
+ private const int WM_PASTE = 0x0302;
+
+ private readonly System.Windows.Forms.Timer _delayTimer;
+
+ /// <summary> 当前变更是否由按键(按钮)引起。</summary>
+ private bool _isButtonChange;
+
+ /// <summary> 当前变更是否由粘贴引起。</summary>
+ private bool _isPaste;
+
+ /// <summary> 当前变更是否由代码通过 <see cref="Value"/> setter 赋值引起。</summary>
+ private bool _isCodeSet;
+
+ public NumIncDec()
+ {
+ InitializeComponent();
+
+ _delayTimer = new System.Windows.Forms.Timer();
+ _delayTimer.Tick += (s, e) =>
+ {
+ _delayTimer.Stop();
+ RaiseValueChanged();
+ };
+
+ tbNumber.TextChanged += TbNumber_TextChanged;
+ tbNumber.KeyPress += TbNumber_KeyPress;
+
+ btDec10.Click += (s, e) => ChangeValue(-10f);
+ btDec1.Click += (s, e) => ChangeValue(-1f);
+ btInc1.Click += (s, e) => ChangeValue(1f);
+ btInc10.Click += (s, e) => ChangeValue(10f);
+ }
+
+ /// <summary> 拦截粘贴,仅允许粘贴合法数字内容。</summary>
+ protected override void WndProc(ref Message m)
+ {
+ if (m.Msg == WM_PASTE && m.HWnd == tbNumber.Handle)
+ {
+ if (!PasteAllowed())
+ {
+ return; // 拦截非法粘贴
+ }
+ _isPaste = true; // 合法的粘贴,交由 TextChanged 按粘贴延迟处理
+ }
+ base.WndProc(ref m);
+ }
+
+ private bool PasteAllowed()
+ {
+ string clip = Clipboard.ContainsText() ? Clipboard.GetText() : string.Empty;
+ string merged = tbNumber.Text.Substring(0, tbNumber.SelectionStart)
+ + clip
+ + tbNumber.Text.Substring(tbNumber.SelectionStart + tbNumber.SelectionLength);
+ return IsValidNumberText(merged);
+ }
+
+ /// <summary> 校验文本是否为合法数字(可选负号开头、单个小数点)。</summary>
+ private bool IsValidNumberText(string text)
+ {
+ text = text.Trim();
+ if (string.IsNullOrEmpty(text))
+ return true;
+
+ bool hasDot = false;
+ for (int i = 0; i < text.Length; i++)
+ {
+ char c = text[i];
+ if (c == '-')
+ {
+ if (i != 0)
+ return false;
+ }
+ else if (c == '.')
+ {
+ if (hasDot || i == 0)
+ return false;
+ hasDot = true;
+ }
+ else if (!char.IsDigit(c))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /// <summary> 当前数值。</summary>
+ public float Value
+ {
+ get
+ {
+ if (float.TryParse(tbNumber.Text.Trim(), out float v))
+ return v;
+ return 0f;
+ }
+ set
+ {
+ // 代码赋值:设置标志并取消可能挂起的延迟触发,不抛 ValueChanged 事件
+ _delayTimer.Stop();
+ _isCodeSet = true;
+ tbNumber.Text = value.ToString();
+ _isCodeSet = false;
+ }
+ }
+
+ private void ChangeValue(float multiplier)
+ {
+ float step = X * multiplier;
+ _isPaste = false; // 按钮修改不属于粘贴
+ _isButtonChange = true; // 标记为按钮变更,TextChanged 中跳过延迟处理
+ SetValue(Value + step);
+ _isButtonChange = false;
+
+ // 按键修改立即触发,并取消可能挂起的延迟触发
+ _delayTimer.Stop();
+ RaiseValueChanged();
+ }
+
+ private void SetValue(float v)
+ {
+ tbNumber.Text = v.ToString(CultureInfo.InvariantCulture);
+ }
+
+ /// <summary> 文本变化:区分按钮/粘贴/键入三种来源并据此触发数值修改事件。</summary>
+ private void TbNumber_TextChanged(object sender, EventArgs e)
+ {
+ if (_isCodeSet)
+ {
+ // 代码赋值来源(Value setter),不触发数值修改事件
+ _isCodeSet = false;
+ return;
+ }
+
+ if (_isButtonChange)
+ {
+ // 按钮修改路径已在 ChangeValue 中立即触发,这里直接跳过
+ _isButtonChange = false;
+ return;
+ }
+
+ if (_isPaste)
+ {
+ // 粘贴来源
+ _isPaste = false;
+ ScheduleValueChanged(PASTE_DELAY_MS);
+ return;
+ }
+
+ // 键入来源(按键输入,含退格/删除等编辑)
+ ScheduleValueChanged(TYPE_DELAY_MS);
+ }
+
+ /// <summary> 安排延迟触发(每次重新计时,实现 debounce)。</summary>
+ private void ScheduleValueChanged(int ms)
+ {
+ _delayTimer.Stop();
+ _delayTimer.Interval = ms;
+ _delayTimer.Start();
+ }
+
+ /// <summary> 触发数值修改事件。</summary>
+ private void RaiseValueChanged()
+ {
+ var handler = ValueChanged;
+ if (handler != null)
+ handler(this, EventArgs.Empty);
+ }
+
+ /// <summary> 只允许输入数字(含小数点和负号)。</summary>
+ private void TbNumber_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ // 只要发生了键入(含退格/删除),就不再按粘贴处理
+ _isPaste = false;
+
+ if (e.KeyChar == (char)Keys.Back || e.KeyChar == (char)Keys.Delete)
+ return;
+
+ TextBox tb = sender as TextBox;
+ if (tb == null)
+ return;
+
+ string text = tb.Text;
+ int selStart = tb.SelectionStart;
+ string newText = text.Remove(selStart, tb.SelectionLength).Insert(selStart, e.KeyChar.ToString());
+
+ if (e.KeyChar == '.')
+ {
+ // 只允许一个小数点,且不能作为首字符
+ if (text.Contains('.') || selStart == 0)
+ {
+ e.Handled = true;
+ return;
+ }
+ return;
+ }
+
+ if (e.KeyChar == '-')
+ {
+ // 只允许在首位输入负号
+ if (selStart != 0 || text.Contains('-') || newText == "-")
+ {
+ e.Handled = true;
+ }
+ return;
+ }
+
+ if (!char.IsDigit(e.KeyChar))
+ {
+ e.Handled = true;
+ return;
+ }
+
+ // 负号后面不允许再出现负号
+ if (newText.IndexOf('-') > 0)
+ {
+ e.Handled = true;
+ }
+ }
+ }
+}
diff --git a/A2Lib/FormComponent/NumIncDec.Designer.cs b/A2Lib/FormComponent/NumIncDec.Designer.cs
new file mode 100644
index 0000000..16a2419
--- /dev/null
+++ b/A2Lib/FormComponent/NumIncDec.Designer.cs
@@ -0,0 +1,111 @@
+namespace ALib.FormComponent
+{
+ partial class NumIncDec
+ {
+ /// <summary>
+ /// 必需的设计器变量。
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// 清理所有正在使用的资源。
+ /// </summary>
+ /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region 组件设计器生成的代码
+
+ /// <summary>
+ /// 设计器支持所需的方法 - 不要修改
+ /// 使用代码编辑器修改此方法的内容。
+ /// </summary>
+ private void InitializeComponent()
+ {
+ btDec10 = new Button();
+ btDec1 = new Button();
+ btInc10 = new Button();
+ btInc1 = new Button();
+ tbNumber = new TextBox();
+ SuspendLayout();
+ //
+ // btDec10
+ //
+ btDec10.Location = new Point(3, 3);
+ btDec10.Name = "btDec10";
+ btDec10.Size = new Size(36, 28);
+ btDec10.TabIndex = 0;
+ btDec10.Tag = "-10";
+ btDec10.Text = "<<";
+ btDec10.UseVisualStyleBackColor = true;
+ //
+ // btDec1
+ //
+ btDec1.Location = new Point(45, 3);
+ btDec1.Name = "btDec1";
+ btDec1.Size = new Size(36, 28);
+ btDec1.TabIndex = 1;
+ btDec1.Tag = "-1";
+ btDec1.Text = "<";
+ btDec1.UseVisualStyleBackColor = true;
+ //
+ // btInc10
+ //
+ btInc10.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ btInc10.Location = new Point(238, 3);
+ btInc10.Name = "btInc10";
+ btInc10.Size = new Size(36, 28);
+ btInc10.TabIndex = 3;
+ btInc10.Tag = "10";
+ btInc10.Text = ">>";
+ btInc10.UseVisualStyleBackColor = true;
+ //
+ // btInc1
+ //
+ btInc1.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ btInc1.Location = new Point(196, 3);
+ btInc1.Name = "btInc1";
+ btInc1.Size = new Size(36, 28);
+ btInc1.TabIndex = 2;
+ btInc1.Tag = "1";
+ btInc1.Text = ">";
+ btInc1.UseVisualStyleBackColor = true;
+ //
+ // tbNumber
+ //
+ tbNumber.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ tbNumber.Location = new Point(87, 6);
+ tbNumber.Name = "tbNumber";
+ tbNumber.Size = new Size(103, 23);
+ tbNumber.TabIndex = 4;
+ //
+ // NumIncDec
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ Controls.Add(tbNumber);
+ Controls.Add(btInc10);
+ Controls.Add(btInc1);
+ Controls.Add(btDec1);
+ Controls.Add(btDec10);
+ Name = "NumIncDec";
+ Size = new Size(277, 35);
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private Button btDec10;
+ private Button btDec1;
+ private Button btInc10;
+ private Button btInc1;
+ private TextBox tbNumber;
+ }
+}
diff --git a/A2Lib/FormComponent/NumIncDec.resx b/A2Lib/FormComponent/NumIncDec.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/A2Lib/FormComponent/NumIncDec.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/A2Lib/FormComponent/RemoteButton.cs b/A2Lib/FormComponent/RemoteButton.cs
new file mode 100644
index 0000000..9d82c8c
--- /dev/null
+++ b/A2Lib/FormComponent/RemoteButton.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace ALib.FormComponent
+{
+ public class RemoteButton : Button
+ {
+ public int RectMargin = 6;
+ public int RectHeight = 3;
+
+ protected override void OnPaint(PaintEventArgs pevent)
+ {
+ // 先按按钮默认外观绘制(基类 Button 的状态绘制)
+ base.OnPaint(pevent);
+
+ // 再在默认外观之上叠加自定义内容
+ int x = RectMargin;
+ int y = RectMargin;
+ int width = this.ClientSize.Width - RectMargin * 2;
+ int height = RectHeight;
+
+ Rectangle rect = new Rectangle(x, y, width, height);
+ pevent.Graphics.FillRectangle(Brushes.Red, rect);
+ }
+ }
+}
+
+
diff --git a/A2Lib/FormComponent/SN_Input.cs b/A2Lib/FormComponent/SN_Input.cs
new file mode 100644
index 0000000..d5a77be
--- /dev/null
+++ b/A2Lib/FormComponent/SN_Input.cs
@@ -0,0 +1,108 @@
+using NewLife.Reflection;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace ALib.Forms
+{
+ [DefaultEvent("Key_Click")]
+ public partial class SN_Input : UserControl
+ {
+ public SN_Input()
+ {
+ InitializeComponent();
+ }
+
+ #region 属性
+
+ /// <summary>名称</summary>
+ [Browsable(true)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
+ public override string Text
+ {
+ get { return tbSN.Text; }
+ set
+ {
+ var str = value ?? "";
+ tbSN.Text = str;
+ }
+ }
+
+ public event KeyPressEventHandler SN_KeyPress;
+
+ public event KeyEventHandler SN_KeyDown;
+
+ public event KeyEventHandler SN_KeyUp;
+
+ public String KeyName
+ {
+ get { return btKey.Text; }
+ set
+ {
+ var str = value ?? "";
+ btKey.Text = str;
+ }
+ }
+
+ public event EventHandler Key_Click;
+
+ public void Clear() => tbSN.Clear();
+
+ #endregion
+
+ private void tbSN_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (SN_KeyDown != null)
+ {
+ SN_KeyDown.Invoke(this, e);
+ return;
+ }
+ }
+
+ private void tbSN_KeyPress(object sender, KeyPressEventArgs e)
+ {
+ if (SN_KeyPress != null)
+ {
+ SN_KeyPress.Invoke(this, e);
+ return;
+ }
+ }
+
+ private void tbSN_KeyUp(object sender, KeyEventArgs e)
+ {
+ if (SN_KeyUp != null)
+ {
+ SN_KeyUp.Invoke(this, e);
+ return;
+ }
+
+ // 外部没有注册,使用默认逻辑
+ var key = e.KeyCode;
+ if (key == Keys.Enter)
+ {
+ Key_Click?.Invoke(this, e);
+ e.Handled = true;
+ }
+ else if (key == Keys.Escape)
+ {
+ tbSN.Clear();
+ e.Handled = true;
+ }
+ }
+
+ private void btKey_Click(object sender, EventArgs e)
+ {
+ if (Key_Click != null)
+ {
+ Key_Click.Invoke(this, e);
+ return;
+ }
+ }
+ }
+}
diff --git a/A2Lib/FormComponent/SN_Input.Designer.cs b/A2Lib/FormComponent/SN_Input.Designer.cs
new file mode 100644
index 0000000..4107478
--- /dev/null
+++ b/A2Lib/FormComponent/SN_Input.Designer.cs
@@ -0,0 +1,88 @@
+namespace ALib.Forms
+{
+ partial class SN_Input
+ {
+ /// <summary>
+ /// 必需的设计器变量。
+ /// </summary>
+ private System.ComponentModel.IContainer components = null;
+
+ /// <summary>
+ /// 清理所有正在使用的资源。
+ /// </summary>
+ /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region 组件设计器生成的代码
+
+ /// <summary>
+ /// 设计器支持所需的方法 - 不要修改
+ /// 使用代码编辑器修改此方法的内容。
+ /// </summary>
+ private void InitializeComponent()
+ {
+ tbSN = new TextBox();
+ lbSN = new Label();
+ btKey = new Button();
+ SuspendLayout();
+ //
+ // tbSN
+ //
+ tbSN.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
+ tbSN.Font = new Font("Microsoft YaHei UI", 14.25F, FontStyle.Regular, GraphicsUnit.Point, 134);
+ tbSN.ImeMode = ImeMode.Disable;
+ tbSN.Location = new Point(34, 3);
+ tbSN.Name = "tbSN";
+ tbSN.Size = new Size(304, 32);
+ tbSN.TabIndex = 0;
+ tbSN.KeyDown += tbSN_KeyDown;
+ tbSN.KeyPress += tbSN_KeyPress;
+ tbSN.KeyUp += tbSN_KeyUp;
+ //
+ // lbSN
+ //
+ lbSN.AutoSize = true;
+ lbSN.Location = new Point(3, 10);
+ lbSN.Name = "lbSN";
+ lbSN.Size = new Size(25, 17);
+ lbSN.TabIndex = 1;
+ lbSN.Text = "SN";
+ //
+ // btKey
+ //
+ btKey.Anchor = AnchorStyles.Top | AnchorStyles.Right;
+ btKey.Location = new Point(344, 3);
+ btKey.Name = "btKey";
+ btKey.Size = new Size(88, 31);
+ btKey.TabIndex = 2;
+ btKey.Text = "keyName";
+ btKey.UseVisualStyleBackColor = true;
+ btKey.Click += btKey_Click;
+ //
+ // SN_Input
+ //
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
+ Controls.Add(btKey);
+ Controls.Add(lbSN);
+ Controls.Add(tbSN);
+ Name = "SN_Input";
+ Size = new Size(435, 37);
+ ResumeLayout(false);
+ PerformLayout();
+ }
+
+ #endregion
+
+ private TextBox tbSN;
+ private Label lbSN;
+ private Button btKey;
+ }
+}
diff --git a/A2Lib/FormComponent/SN_Input.resx b/A2Lib/FormComponent/SN_Input.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/A2Lib/FormComponent/SN_Input.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/XCoder/Windows/playTest.wav b/A2Lib/Properties/playTest.wav
similarity index 100%
rename from XCoder/Windows/playTest.wav
rename to A2Lib/Properties/playTest.wav
Binary files a/XCoder/Windows/playTest.wav and b/A2Lib/Properties/playTest.wav differ
diff --git a/A2Lib/Properties/Resources.Designer.cs b/A2Lib/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..7a73cb7
--- /dev/null
+++ b/A2Lib/Properties/Resources.Designer.cs
@@ -0,0 +1,73 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// 此代码由工具生成。
+// 运行时版本:4.0.30319.42000
+//
+// 对此文件的更改可能会导致不正确的行为,并且如果
+// 重新生成代码,这些更改将会丢失。
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace ALib.Properties {
+ using System;
+
+
+ /// <summary>
+ /// 一个强类型的资源类,用于查找本地化的字符串等。
+ /// </summary>
+ // 此类是由 StronglyTypedResourceBuilder
+ // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
+ // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
+ // (以 /str 作为命令选项),或重新生成 VS 项目。
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ /// <summary>
+ /// 返回此类使用的缓存的 ResourceManager 实例。
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ALib.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// 重写当前线程的 CurrentUICulture 属性,对
+ /// 使用此强类型资源类的所有资源查找执行重写。
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// 查找 System.Byte[] 类型的本地化资源。
+ /// </summary>
+ public static byte[] playTest {
+ get {
+ object obj = ResourceManager.GetObject("playTest", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+ }
+}
diff --git a/A2Lib/Properties/Resources.resx b/A2Lib/Properties/Resources.resx
new file mode 100644
index 0000000..c097d37
--- /dev/null
+++ b/A2Lib/Properties/Resources.resx
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
+ <data name="playTest" type="System.Resources.ResXFileRef, System.Windows.Forms">
+ <value>playTest.wav;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </data>
+</root>
\ No newline at end of file
diff --git a/XCoder/Windows/UsbHubSelect.cs b/A2Lib/UsbHub/UsbHubSelect.cs
similarity index 83%
rename from XCoder/Windows/UsbHubSelect.cs
rename to A2Lib/UsbHub/UsbHubSelect.cs
index c69fe7c..e5fc05b 100644
--- a/XCoder/Windows/UsbHubSelect.cs
+++ b/A2Lib/UsbHub/UsbHubSelect.cs
@@ -1,4 +1,4 @@
-
+using MathNet.Numerics.Distributions;
using NewLife;
using System;
using System.Collections.Generic;
@@ -9,12 +9,11 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
-using static XCoder.UsbHelper;
+using static ALib.UsbHub.UsbHelper;
-namespace XCoder
+namespace ALib.UsbHub
{
- [DisplayName("USB HUB 设备")]
- public partial class UsbHubSelect : Form, IXForm
+ public partial class UsbHubSelect : Form
{
public UsbHubSelect()
{
@@ -24,6 +23,9 @@ namespace XCoder
/// <summary>HUB号 & HUB下所有信息</summary>
public Dictionary<int, List<UsbDevice>> Hubs;
+ /// <summary>用于外部筛选 Hubs 范围的钩子,返回 true 表示保留该 Hub</summary>
+ public event Func<List<UsbDevice>, bool> ShouldKeepHub;
+
/// <summary>选中的 Hub 编号。 负数无效</summary>
public int SelectedHub { get; private set; } = -1;
@@ -31,22 +33,38 @@ namespace XCoder
{
rtb_Show.MouseWheel += Rtb_Show_MouseWheel;
-
Hubs = UsbHelper.GetAllUsbInfo();
+ // 如果外部注册了筛选器,则逐个判断筛选(所有筛选器都返回 true 才保留)
+ if (ShouldKeepHub != null)
+ {
+ Hubs = Hubs.Where(kv =>
+ {
+ var keep = true;
+ foreach (var filter in ShouldKeepHub.GetInvocationList())
+ {
+ var func = (Func<List<UsbDevice>, bool>)filter;
+ if (!func(kv.Value))
+ {
+ keep = false;
+ break;
+ }
+ }
+ return keep;
+ }).ToDictionary(kv => kv.Key, kv => kv.Value);
+ }
+
+ // 序号添加到下拉框
foreach (var kv in Hubs)
{
var hub = kv.Key;
- var devs = kv.Value;
+ // var devs = kv.Value;
cb_Hub.Items.Add($"{hub}");
}
cb_Hub.SelectedValueChanged += Cb_Hub_SelectedValueChanged;
if (cb_Hub.Items.Count > 0) cb_Hub.SelectedIndex = 0;
-
- bt_Canel.Hide();
- bt_Comf.Hide();
}
private void Rtb_Show_MouseWheel(object sender, MouseEventArgs e)
@@ -144,22 +162,5 @@ namespace XCoder
base.WndProc(ref m);
}
-
- private void bt_flash_Click(object sender, EventArgs e)
- {
- cb_Hub.Items.Clear();
- Hubs = UsbHelper.GetAllUsbInfo();
-
- foreach (var kv in Hubs)
- {
- var hub = kv.Key;
- var devs = kv.Value;
-
- cb_Hub.Items.Add($"{hub}");
- }
-
- cb_Hub.SelectedValueChanged += Cb_Hub_SelectedValueChanged;
- if (cb_Hub.Items.Count > 0) cb_Hub.SelectedIndex = 0;
- }
}
}
diff --git a/XCoder/Windows/UsbHubSelect.Designer.cs b/A2Lib/UsbHub/UsbHubSelect.Designer.cs
similarity index 90%
rename from XCoder/Windows/UsbHubSelect.Designer.cs
rename to A2Lib/UsbHub/UsbHubSelect.Designer.cs
index e790750..f461885 100644
--- a/XCoder/Windows/UsbHubSelect.Designer.cs
+++ b/A2Lib/UsbHub/UsbHubSelect.Designer.cs
@@ -1,4 +1,4 @@
-namespace XCoder
+namespace ALib.UsbHub
{
partial class UsbHubSelect
{
@@ -33,7 +33,6 @@
bt_Comf = new Button();
bt_Canel = new Button();
label1 = new Label();
- bt_flash = new Button();
SuspendLayout();
//
// cb_Hub
@@ -48,7 +47,7 @@
// rtb_Show
//
rtb_Show.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
- rtb_Show.Font = new Font("Microsoft YaHei UI", 10.5F, FontStyle.Regular, GraphicsUnit.Point);
+ rtb_Show.Font = new Font("Consolas", 9F, FontStyle.Regular, GraphicsUnit.Point, 0);
rtb_Show.Location = new Point(12, 43);
rtb_Show.Name = "rtb_Show";
rtb_Show.Size = new Size(889, 327);
@@ -84,21 +83,11 @@
label1.TabIndex = 4;
label1.Text = "HUB";
//
- // bt_flash
- //
- bt_flash.Location = new Point(413, 11);
- bt_flash.Name = "bt_flash";
- bt_flash.Size = new Size(75, 25);
- bt_flash.TabIndex = 5;
- bt_flash.Text = "刷新";
- bt_flash.UseVisualStyleBackColor = true;
- bt_flash.Click += bt_flash_Click;
- //
// UsbHubSelect
//
- AutoScaleMode = AutoScaleMode.Dpi;
+ AutoScaleDimensions = new SizeF(7F, 17F);
+ AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(913, 382);
- Controls.Add(bt_flash);
Controls.Add(label1);
Controls.Add(bt_Canel);
Controls.Add(bt_Comf);
@@ -118,6 +107,5 @@
private Button bt_Comf;
private Button bt_Canel;
private Label label1;
- private Button bt_flash;
}
}
\ No newline at end of file
diff --git a/A2Lib/UsbHub/UsbHubSelect.resx b/A2Lib/UsbHub/UsbHubSelect.resx
new file mode 100644
index 0000000..8b2ff64
--- /dev/null
+++ b/A2Lib/UsbHub/UsbHubSelect.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/XCoder/CrazyCoder.csproj b/XCoder/CrazyCoder.csproj
index 99e04ee..c83a3fe 100644
--- a/XCoder/CrazyCoder.csproj
+++ b/XCoder/CrazyCoder.csproj
@@ -67,8 +67,6 @@
<Content Include="数据库命名规范.txt" />
</ItemGroup>
<ItemGroup>
- <PackageReference Include="MathNet.Numerics" Version="5.0.0" />
- <PackageReference Include="NAudio" Version="2.3.0" />
<PackageReference Include="NewLife.Map" Version="2.6.2026.102" />
<PackageReference Include="NewLife.ModbusRTU" Version="2.0.2026.610" />
<PackageReference Include="NewLife.Remoting" Version="3.8.2026.706" />
@@ -124,6 +122,9 @@
<EmbeddedResource Remove="XMessage\FrmMain.resx" />
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\A2Lib\A2Lib.csproj" />
+ </ItemGroup>
+ <ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
diff --git a/XCoder/Properties/Resources.Designer.cs b/XCoder/Properties/Resources.Designer.cs
index 57cb355..162102e 100644
--- a/XCoder/Properties/Resources.Designer.cs
+++ b/XCoder/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace CrazyCoder.Properties {
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
@@ -59,15 +59,5 @@ namespace CrazyCoder.Properties {
resourceCulture = value;
}
}
-
- /// <summary>
- /// 查找 System.Byte[] 类型的本地化资源。
- /// </summary>
- internal static byte[] playTest {
- get {
- object obj = ResourceManager.GetObject("playTest", resourceCulture);
- return ((byte[])(obj));
- }
- }
}
}
diff --git a/XCoder/Properties/Resources.resx b/XCoder/Properties/Resources.resx
index 687b470..1af7de1 100644
--- a/XCoder/Properties/Resources.resx
+++ b/XCoder/Properties/Resources.resx
@@ -117,8 +117,4 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
- <assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
- <data name="playTest" type="System.Resources.ResXFileRef, System.Windows.Forms">
- <value>..\Windows\playTest.wav;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
- </data>
</root>
\ No newline at end of file
diff --git a/XCoder/Tools/FrmUsbHubs.cs b/XCoder/Tools/FrmUsbHubs.cs
new file mode 100644
index 0000000..75335e6
--- /dev/null
+++ b/XCoder/Tools/FrmUsbHubs.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ALib.UsbHub;
+using XCoder;
+
+namespace CrazyCoder.Tools
+{
+ [Category("工具")]
+ [DisplayName("USB HUB 查看器")]
+ public class FrmUsbHubs : UsbHubSelect, IXForm
+ {
+ // 隐藏 确认(bt_Comf) / 取消(bt_Canel) 按钮,本窗体仅作 HUB 信息查看器使用
+ protected override void OnLoad(EventArgs e)
+ {
+ base.OnLoad(e);
+
+ Controls.Find("bt_Comf", true).FirstOrDefault()?.Hide();
+ Controls.Find("bt_Canel", true).FirstOrDefault()?.Hide();
+ }
+ }
+}
diff --git a/XCoder/Tools/FrmUsbHubs.resx b/XCoder/Tools/FrmUsbHubs.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/XCoder/Tools/FrmUsbHubs.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/XCoder/XCan/FrmCan.cs b/XCoder/XCan/FrmCan.cs
new file mode 100644
index 0000000..ab1bbde
--- /dev/null
+++ b/XCoder/XCan/FrmCan.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ALib.CAN;
+using XCoder;
+
+namespace CrazyCoder.XCan
+{
+ [Category("工业电子")]
+ [DisplayName("CAN调试工具")]
+ public class FrmCan : CanMonitor, IXForm
+ {
+ }
+}
diff --git a/XCoder/XMedia/FrmAudioCard.cs b/XCoder/XMedia/FrmAudioCard.cs
new file mode 100644
index 0000000..bb5e021
--- /dev/null
+++ b/XCoder/XMedia/FrmAudioCard.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ALib.Audio;
+using XCoder;
+
+namespace CrazyCoder.XAudio
+{
+ [Category("工具")]
+ [DisplayName("声卡测试")]
+ public class FrmAudioCard : AudioRecordForm, IXForm
+ {
+ }
+}
diff --git a/XCoder/XMedia/FrmAudioCard.resx b/XCoder/XMedia/FrmAudioCard.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/XCoder/XMedia/FrmAudioCard.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git a/XCoder/XMedia/FrmMel.cs b/XCoder/XMedia/FrmMel.cs
new file mode 100644
index 0000000..91c7086
--- /dev/null
+++ b/XCoder/XMedia/FrmMel.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ALib.Mel;
+using XCoder;
+
+namespace CrazyCoder.XAudio
+{
+ [Category("工具")]
+ [DisplayName("声音梅尔频谱")]
+ public class FrmMel : MelForm, IXForm
+ {
+ }
+}
diff --git a/XCoder/XMedia/FrmMel.resx b/XCoder/XMedia/FrmMel.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/XCoder/XMedia/FrmMel.resx
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+</root>
\ No newline at end of file
diff --git "a/\347\240\201\347\245\236\345\267\245\345\205\267.sln" "b/\347\240\201\347\245\236\345\267\245\345\205\267.sln"
index f17e659..68067d7 100644
--- "a/\347\240\201\347\245\236\345\267\245\345\205\267.sln"
+++ "b/\347\240\201\347\245\236\345\267\245\345\205\267.sln"
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
-VisualStudioVersion = 18.7.11925.98 stable
+VisualStudioVersion = 18.7.11925.98
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{32891F65-FBA1-42B2-918E-61F3A9623F2A}"
ProjectSection(SolutionItems) = preProject
@@ -16,6 +16,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XCoderLinux", "XCoderLinux\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XCoderAv", "XCoderAv\XCoderAv.csproj", "{6CDAB042-3987-9E0D-8475-62B756428DF6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "A1Lib", "A1Lib\A1Lib.csproj", "{01D91140-F6AC-E604-002A-765C6AFB039D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "A2Lib", "A2Lib\A2Lib.csproj", "{DEC434B4-DE86-752B-5A4D-734D2A0659E5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -104,6 +108,54 @@ Global
{6CDAB042-3987-9E0D-8475-62B756428DF6}.Release|x64.Build.0 = Release|Any CPU
{6CDAB042-3987-9E0D-8475-62B756428DF6}.Release|x86.ActiveCfg = Release|Any CPU
{6CDAB042-3987-9E0D-8475-62B756428DF6}.Release|x86.Build.0 = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|ARM.Build.0 = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|x64.Build.0 = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Debug|x86.Build.0 = Debug|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|ARM.ActiveCfg = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|ARM.Build.0 = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|iPhone.Build.0 = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|x64.ActiveCfg = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|x64.Build.0 = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|x86.ActiveCfg = Release|Any CPU
+ {01D91140-F6AC-E604-002A-765C6AFB039D}.Release|x86.Build.0 = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|ARM.Build.0 = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|x64.Build.0 = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Debug|x86.Build.0 = Debug|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|ARM.ActiveCfg = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|ARM.Build.0 = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|iPhone.Build.0 = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|x64.ActiveCfg = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|x64.Build.0 = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|x86.ActiveCfg = Release|Any CPU
+ {DEC434B4-DE86-752B-5A4D-734D2A0659E5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE