42个文件已修改
110个文件已添加
2 文件已重命名
1个文件已删除
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>Library</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <Reference Include="HslCommunication"> |
| | | <HintPath>..\Libs\HslCommunication.dll</HintPath> |
| | | </Reference> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" /> |
| | | <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" /> |
| | | <ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj" /> |
| | | <ProjectReference Include="..\PLCHelper\PLCHelper.csproj" /> |
| | | <ProjectReference Include="..\S7Driver\S7Driver.csproj" /> |
| | | <ProjectReference Include="..\ModBusTcpController\ModBusTcpController.csproj" /> |
| | | <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| New file |
| | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 设备能力枚举,标识硬件设备支持的功能类型 |
| | | /// </summary> |
| | | public enum DeviceCapability |
| | | { |
| | | /// <summary> |
| | | /// 电源设备 |
| | | /// </summary> |
| | | PowerSupply, |
| | | |
| | | /// <summary> |
| | | /// CAN总线设备 |
| | | /// </summary> |
| | | CAN, |
| | | |
| | | /// <summary> |
| | | /// LIN总线设备 |
| | | /// </summary> |
| | | LIN, |
| | | |
| | | /// <summary> |
| | | /// PLC数字IO设备 |
| | | /// </summary> |
| | | PLC_IO, |
| | | |
| | | /// <summary> |
| | | /// 温箱设备 |
| | | /// </summary> |
| | | Chamber, |
| | | |
| | | /// <summary> |
| | | /// 数据采集设备 |
| | | /// </summary> |
| | | DAQ, |
| | | |
| | | /// <summary> |
| | | /// 串口设备 |
| | | /// </summary> |
| | | Serial, |
| | | |
| | | /// <summary> |
| | | /// 通用设备 |
| | | /// </summary> |
| | | Generic |
| | | } |
| | | } |
| New file |
| | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 设备驱动状态信息 |
| | | /// </summary> |
| | | public sealed class DeviceDriverStatus |
| | | { |
| | | /// <summary> |
| | | /// 设备是否已连接 |
| | | /// </summary> |
| | | public bool IsConnected { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 设备是否健康(无故障) |
| | | /// </summary> |
| | | public bool IsHealthy { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 最后一次错误信息 |
| | | /// </summary> |
| | | public string LastError { get; set; } = ""; |
| | | |
| | | /// <summary> |
| | | /// 最后一次错误发生的时间 |
| | | /// </summary> |
| | | public DateTime? LastErrorTime { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 通信延迟(毫秒) |
| | | /// </summary> |
| | | public double LatencyMs { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 重连次数 |
| | | /// </summary> |
| | | public int ReconnectCount { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设备连接状态变化事件参数 |
| | | /// </summary> |
| | | public sealed class DeviceConnectionEventArgs : EventArgs |
| | | { |
| | | /// <summary> |
| | | /// 设备标识符 |
| | | /// </summary> |
| | | public string DeviceId { get; set; } = ""; |
| | | |
| | | /// <summary> |
| | | /// 是否已连接 |
| | | /// </summary> |
| | | public bool IsConnected { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 状态变化原因(可选) |
| | | /// </summary> |
| | | public string? Reason { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设备驱动统一接口。 |
| | | /// 所有硬件设备驱动必须实现此接口,提供连接、断开、命令发送和查询功能。 |
| | | /// </summary> |
| | | public interface IDeviceDriver : IDisposable |
| | | { |
| | | /// <summary> |
| | | /// 设备唯一标识符 |
| | | /// </summary> |
| | | string DeviceId { get; } |
| | | |
| | | /// <summary> |
| | | /// 设备当前是否已连接 |
| | | /// </summary> |
| | | bool IsConnected { get; } |
| | | |
| | | /// <summary> |
| | | /// 异步连接设备 |
| | | /// </summary> |
| | | /// <param name="ct">取消令牌</param> |
| | | Task ConnectAsync(CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 异步断开设备连接 |
| | | /// </summary> |
| | | Task DisconnectAsync(); |
| | | |
| | | /// <summary> |
| | | /// 发送命令到设备 |
| | | /// </summary> |
| | | /// <param name="command">命令字符串</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>设备响应字符串</returns> |
| | | Task<string> SendCommandAsync(string command, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 向设备发送查询并获取响应 |
| | | /// </summary> |
| | | /// <param name="query">查询字符串</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>设备查询响应字符串</returns> |
| | | Task<string> QueryAsync(string query, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 获取设备当前状态信息 |
| | | /// </summary> |
| | | /// <returns>设备驱动状态</returns> |
| | | DeviceDriverStatus GetStatus(); |
| | | |
| | | /// <summary> |
| | | /// 设备连接状态变化事件 |
| | | /// </summary> |
| | | event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged; |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 设备管理接口。 |
| | | /// 提供设备驱动的注册、连接管理和状态查询功能。 |
| | | /// </summary> |
| | | public interface IDeviceManager |
| | | { |
| | | /// <summary> |
| | | /// 根据设备ID获取设备驱动实例 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <returns>设备驱动实例,不存在时返回 null</returns> |
| | | IDeviceDriver? GetDriver(string deviceId); |
| | | |
| | | /// <summary> |
| | | /// 获取设备当前状态 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <returns>设备驱动状态</returns> |
| | | DeviceDriverStatus GetStatus(string deviceId); |
| | | |
| | | /// <summary> |
| | | /// 注册设备驱动 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="driver">设备驱动实例</param> |
| | | void RegisterDriver(string deviceId, IDeviceDriver driver); |
| | | |
| | | /// <summary> |
| | | /// 注销设备驱动 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | void UnregisterDriver(string deviceId); |
| | | |
| | | /// <summary> |
| | | /// 异步连接设备 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | Task ConnectAsync(string deviceId, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 异步断开设备连接 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | Task DisconnectAsync(string deviceId); |
| | | |
| | | /// <summary> |
| | | /// 获取所有已注册设备的ID列表 |
| | | /// </summary> |
| | | /// <returns>设备ID只读列表</returns> |
| | | IReadOnlyList<string> GetRegisteredDevices(); |
| | | |
| | | /// <summary> |
| | | /// 检查设备是否已注册 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <returns>设备是否已注册</returns> |
| | | bool IsDeviceRegistered(string deviceId); |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 设备命令执行结果 |
| | | /// </summary> |
| | | public sealed class DeviceCommandResult |
| | | { |
| | | /// <summary> |
| | | /// 命令是否执行成功 |
| | | /// </summary> |
| | | public bool Success { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 设备原始响应字符串 |
| | | /// </summary> |
| | | public string? RawResponse { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 回读值(解析后的有效数据) |
| | | /// </summary> |
| | | public string? Readback { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 错误代码 |
| | | /// </summary> |
| | | public string? ErrorCode { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 错误信息 |
| | | /// </summary> |
| | | public string? ErrorMessage { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 命令执行延迟(毫秒) |
| | | /// </summary> |
| | | public double LatencyMs { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设备服务接口。 |
| | | /// 提供基于资源预约的命令执行和安全关机功能,协调资源管理和设备驱动。 |
| | | /// </summary> |
| | | public interface IDeviceService |
| | | { |
| | | /// <summary> |
| | | /// 异步执行设备命令。 |
| | | /// 需要有效的资源预约才能执行。 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="command">命令字符串</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>命令执行结果</returns> |
| | | Task<DeviceCommandResult> ExecuteCommandAsync( |
| | | string deviceId, string command, ResourceOwnerToken owner, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 异步执行设备查询。 |
| | | /// 需要有效的资源预约才能执行。 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="query">查询字符串</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>命令执行结果</returns> |
| | | Task<DeviceCommandResult> ExecuteQueryAsync( |
| | | string deviceId, string query, ResourceOwnerToken owner, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 异步安全关闭设备。 |
| | | /// 执行设备特定的关机序列,确保设备处于安全状态。 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | Task SafeShutdownAsync(string deviceId, ResourceOwnerToken owner, CancellationToken ct); |
| | | } |
| | | } |
| New file |
| | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 短期操作租约接口。 |
| | | /// 在执行具体硬件操作前获取,确保操作期间资源不被抢占。 |
| | | /// 支持同步和异步释放。 |
| | | /// </summary> |
| | | public interface IOperationLease : IDisposable, IAsyncDisposable |
| | | { |
| | | /// <summary> |
| | | /// 租约唯一标识 |
| | | /// </summary> |
| | | string LeaseId { get; } |
| | | |
| | | /// <summary> |
| | | /// 租约关联的资源键 |
| | | /// </summary> |
| | | ResourceKey Resource { get; } |
| | | |
| | | /// <summary> |
| | | /// 租约所有者令牌 |
| | | /// </summary> |
| | | ResourceOwnerToken Owner { get; } |
| | | |
| | | /// <summary> |
| | | /// 租约获取时间 |
| | | /// </summary> |
| | | DateTime AcquiredAt { get; } |
| | | |
| | | /// <summary> |
| | | /// 租约过期时间(可选) |
| | | /// </summary> |
| | | DateTime? ExpiresAt { get; } |
| | | |
| | | /// <summary> |
| | | /// 租约是否仍然有效 |
| | | /// </summary> |
| | | bool IsValid { get; } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 资源冲突异常。 |
| | | /// 当尝试预约已被其他所有者占用的资源时抛出。 |
| | | /// </summary> |
| | | public sealed class ResourceConflictException : Exception |
| | | { |
| | | /// <summary> |
| | | /// 初始化资源冲突异常实例 |
| | | /// </summary> |
| | | /// <param name="message">冲突描述信息</param> |
| | | public ResourceConflictException(string message) : base(message) |
| | | { |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化资源冲突异常实例 |
| | | /// </summary> |
| | | /// <param name="message">冲突描述信息</param> |
| | | /// <param name="innerException">内部异常</param> |
| | | public ResourceConflictException(string message, Exception innerException) : base(message, innerException) |
| | | { |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 资源管理接口。 |
| | | /// 提供硬件资源的预约、租约获取和释放功能,确保资源不被多个会话同时占用。 |
| | | /// </summary> |
| | | public interface IResourceManager |
| | | { |
| | | /// <summary> |
| | | /// 异步预约资源。 |
| | | /// 创建长期预约,阻止其他所有者使用该资源。 |
| | | /// </summary> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="resource">资源键</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>资源预约实体</returns> |
| | | /// <exception cref="ResourceConflictException">资源已被其他所有者预约时抛出</exception> |
| | | Task<ResourceReservation> ReserveAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 异步获取操作租约。 |
| | | /// 在执行具体硬件操作前调用,确保操作期间资源不被抢占。 |
| | | /// </summary> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="resource">资源键</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>操作租约</returns> |
| | | /// <exception cref="InvalidOperationException">资源未预约或所有者不匹配时抛出</exception> |
| | | Task<IOperationLease> AcquireOperationAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 异步释放资源预约。 |
| | | /// 取消对资源的占用,允许其他所有者预约。 |
| | | /// </summary> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="resource">资源键</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | Task ReleaseReservationAsync(ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct); |
| | | |
| | | /// <summary> |
| | | /// 检查资源是否已被预约 |
| | | /// </summary> |
| | | /// <param name="resource">资源键</param> |
| | | /// <param name="reservation">输出预约实体(如果已预约)</param> |
| | | /// <returns>资源是否已被预约</returns> |
| | | bool IsReserved(ResourceKey resource, out ResourceReservation? reservation); |
| | | |
| | | /// <summary> |
| | | /// 验证所有者令牌是否匹配指定资源的预约 |
| | | /// </summary> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="resource">资源键</param> |
| | | /// <returns>所有者是否匹配</returns> |
| | | bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource); |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 资源键值对象,唯一标识一个可管理的硬件资源。 |
| | | /// 格式: "DeviceId:Channel",如 "PSU024:CH1"、"CAN_CARD01:CH2"、"PLC01:OUTPUT_GROUP_A" |
| | | /// </summary> |
| | | public sealed class ResourceKey : IEquatable<ResourceKey> |
| | | { |
| | | /// <summary> |
| | | /// 设备标识符 |
| | | /// </summary> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <summary> |
| | | /// 通道标识(可选) |
| | | /// </summary> |
| | | public string? Channel { get; } |
| | | |
| | | /// <summary> |
| | | /// 资源类型(PowerSupply/CAN/PLC/Chamber等) |
| | | /// </summary> |
| | | public string ResourceType { get; } |
| | | |
| | | /// <summary> |
| | | /// 初始化资源键实例 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="resourceType">资源类型</param> |
| | | /// <param name="channel">通道标识(可选)</param> |
| | | public ResourceKey(string deviceId, string resourceType, string? channel = null) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | ResourceType = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); |
| | | Channel = channel; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从字符串解析资源键,格式为 "DeviceId:Channel" |
| | | /// </summary> |
| | | /// <param name="key">资源键字符串</param> |
| | | /// <returns>解析后的资源键实例</returns> |
| | | /// <exception cref="ArgumentException">格式无效时抛出</exception> |
| | | public static ResourceKey FromString(string key) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(key)) |
| | | throw new ArgumentException("资源键不能为空", nameof(key)); |
| | | |
| | | var parts = key.Split(':'); |
| | | if (parts.Length < 2) |
| | | throw new ArgumentException($"无效的资源键格式: '{key}',期望格式为 'DeviceId:Channel'"); |
| | | |
| | | var deviceId = parts[0].Trim(); |
| | | var channel = string.Join(":", parts.Skip(1)).Trim(); |
| | | |
| | | if (string.IsNullOrEmpty(deviceId)) |
| | | throw new ArgumentException($"无效的资源键格式: '{key}',DeviceId 不能为空"); |
| | | |
| | | // 根据设备ID前缀推断资源类型 |
| | | var resourceType = InferResourceType(deviceId); |
| | | |
| | | return new ResourceKey(deviceId, resourceType, string.IsNullOrEmpty(channel) ? null : channel); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取操作资源键,用于物理会话锁 |
| | | /// </summary> |
| | | public string OperationResourceKey => $"{DeviceId}:SESSION"; |
| | | |
| | | /// <summary> |
| | | /// 根据设备ID前缀推断资源类型 |
| | | /// </summary> |
| | | private static string InferResourceType(string deviceId) |
| | | { |
| | | var upper = deviceId.ToUpperInvariant(); |
| | | if (upper.StartsWith("PSU") || upper.StartsWith("POWER")) |
| | | return "PowerSupply"; |
| | | if (upper.StartsWith("CAN")) |
| | | return "CAN"; |
| | | if (upper.StartsWith("PLC")) |
| | | return "PLC"; |
| | | if (upper.StartsWith("CHAMBER") || upper.StartsWith("TEMP")) |
| | | return "Chamber"; |
| | | if (upper.StartsWith("DAQ")) |
| | | return "DAQ"; |
| | | if (upper.StartsWith("SERIAL") || upper.StartsWith("COM")) |
| | | return "Serial"; |
| | | return "Generic"; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool Equals(ResourceKey? other) |
| | | { |
| | | if (other is null) return false; |
| | | if (ReferenceEquals(this, other)) return true; |
| | | return string.Equals(DeviceId, other.DeviceId, StringComparison.OrdinalIgnoreCase) && |
| | | string.Equals(Channel, other.Channel, StringComparison.OrdinalIgnoreCase) && |
| | | string.Equals(ResourceType, other.ResourceType, StringComparison.OrdinalIgnoreCase); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public override bool Equals(object? obj) => Equals(obj as ResourceKey); |
| | | |
| | | /// <inheritdoc/> |
| | | public override int GetHashCode() |
| | | { |
| | | unchecked |
| | | { |
| | | int hash = 17; |
| | | hash = hash * 31 + (DeviceId?.ToUpperInvariant().GetHashCode() ?? 0); |
| | | hash = hash * 31 + (Channel?.ToUpperInvariant().GetHashCode() ?? 0); |
| | | hash = hash * 31 + (ResourceType?.ToUpperInvariant().GetHashCode() ?? 0); |
| | | return hash; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public override string ToString() => |
| | | string.IsNullOrEmpty(Channel) ? DeviceId : $"{DeviceId}:{Channel}"; |
| | | |
| | | /// <summary> |
| | | /// 相等运算符重载 |
| | | /// </summary> |
| | | public static bool operator ==(ResourceKey? left, ResourceKey? right) => |
| | | Equals(left, right); |
| | | |
| | | /// <summary> |
| | | /// 不等运算符重载 |
| | | /// </summary> |
| | | public static bool operator !=(ResourceKey? left, ResourceKey? right) => |
| | | !Equals(left, right); |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 所有者类型枚举 |
| | | /// </summary> |
| | | public enum OwnerType |
| | | { |
| | | /// <summary> |
| | | /// 生产模式(正常老化测试流程) |
| | | /// </summary> |
| | | Production, |
| | | |
| | | /// <summary> |
| | | /// 维护模式(调试/维护会话) |
| | | /// </summary> |
| | | Maintenance |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 所有者令牌,标识资源预约的归属。 |
| | | /// 用于区分不同会话对硬件资源的所有权。 |
| | | /// </summary> |
| | | public sealed class ResourceOwnerToken |
| | | { |
| | | /// <summary> |
| | | /// 令牌唯一标识 |
| | | /// </summary> |
| | | public string TokenId { get; } |
| | | |
| | | /// <summary> |
| | | /// 所有者类型(生产/维护) |
| | | /// </summary> |
| | | public OwnerType OwnerType { get; } |
| | | |
| | | /// <summary> |
| | | /// 所有者标识(RunID 或 DebugSessionID) |
| | | /// </summary> |
| | | public string OwnerId { get; } |
| | | |
| | | /// <summary> |
| | | /// 世代号,每次新预约递增,防止重放攻击 |
| | | /// </summary> |
| | | public long Generation { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 令牌签发时间 |
| | | /// </summary> |
| | | public DateTime IssuedAt { get; } |
| | | |
| | | /// <summary> |
| | | /// 令牌过期时间(可选) |
| | | /// </summary> |
| | | public DateTime? ExpiresAt { get; } |
| | | |
| | | /// <summary> |
| | | /// 初始化所有者令牌实例 |
| | | /// </summary> |
| | | /// <param name="ownerType">所有者类型</param> |
| | | /// <param name="ownerId">所有者标识</param> |
| | | /// <param name="expiresAt">过期时间(可选)</param> |
| | | public ResourceOwnerToken(OwnerType ownerType, string ownerId, DateTime? expiresAt = null) |
| | | { |
| | | TokenId = Guid.NewGuid().ToString(); |
| | | OwnerType = ownerType; |
| | | OwnerId = ownerId ?? throw new ArgumentNullException(nameof(ownerId)); |
| | | Generation = 0; |
| | | IssuedAt = DateTime.UtcNow; |
| | | ExpiresAt = expiresAt; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查令牌是否已过期 |
| | | /// </summary> |
| | | /// <returns>过期返回 true;未设置过期时间或尚未过期返回 false</returns> |
| | | public bool IsExpired() |
| | | { |
| | | if (!ExpiresAt.HasValue) |
| | | return false; |
| | | return DateTime.UtcNow > ExpiresAt.Value; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | |
| | | namespace Aging.Infrastructure.Contracts |
| | | { |
| | | /// <summary> |
| | | /// 预约状态枚举 |
| | | /// </summary> |
| | | public enum ReservationState |
| | | { |
| | | /// <summary> |
| | | /// 活跃状态 |
| | | /// </summary> |
| | | Active, |
| | | |
| | | /// <summary> |
| | | /// 已释放 |
| | | /// </summary> |
| | | Released, |
| | | |
| | | /// <summary> |
| | | /// 冲突状态 |
| | | /// </summary> |
| | | Conflicted |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 长期预约实体,表示对某个硬件资源的持久占用。 |
| | | /// 预约期间,其他所有者无法获取该资源。 |
| | | /// </summary> |
| | | public sealed class ResourceReservation |
| | | { |
| | | /// <summary> |
| | | /// 预约唯一标识 |
| | | /// </summary> |
| | | public string ReservationId { get; } |
| | | |
| | | /// <summary> |
| | | /// 被预约的资源键 |
| | | /// </summary> |
| | | public ResourceKey Resource { get; } |
| | | |
| | | /// <summary> |
| | | /// 预约所有者令牌 |
| | | /// </summary> |
| | | public ResourceOwnerToken Owner { get; } |
| | | |
| | | /// <summary> |
| | | /// 预约状态 |
| | | /// </summary> |
| | | public ReservationState State { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 预约创建时间 |
| | | /// </summary> |
| | | public DateTime ReservedAt { get; } |
| | | |
| | | /// <summary> |
| | | /// 预约描述(可选) |
| | | /// </summary> |
| | | public string? Description { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 初始化资源预约实例 |
| | | /// </summary> |
| | | /// <param name="resource">资源键</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="description">预约描述(可选)</param> |
| | | public ResourceReservation(ResourceKey resource, ResourceOwnerToken owner, string? description = null) |
| | | { |
| | | ReservationId = Guid.NewGuid().ToString(); |
| | | Resource = resource ?? throw new ArgumentNullException(nameof(resource)); |
| | | Owner = owner ?? throw new ArgumentNullException(nameof(owner)); |
| | | State = ReservationState.Active; |
| | | ReservedAt = DateTime.UtcNow; |
| | | Description = description; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Collections.Concurrent; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Core |
| | | { |
| | | /// <summary> |
| | | /// 设备管理实现。 |
| | | /// 提供设备驱动的注册、连接管理和状态查询功能。 |
| | | /// 使用 ConcurrentDictionary 管理驱动实例和缓存状态。 |
| | | /// </summary> |
| | | public sealed class DeviceManager : IDeviceManager |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(DeviceManager)); |
| | | |
| | | /// <summary> |
| | | /// 设备驱动字典(key = deviceId) |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, IDeviceDriver> _drivers = new(); |
| | | |
| | | /// <summary> |
| | | /// 设备状态缓存字典(key = deviceId) |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, DeviceDriverStatus> _statusCache = new(); |
| | | |
| | | /// <inheritdoc/> |
| | | public void RegisterDriver(string deviceId, IDeviceDriver driver) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | if (driver == null) throw new ArgumentNullException(nameof(driver)); |
| | | |
| | | if (_drivers.ContainsKey(deviceId)) |
| | | { |
| | | Log.Warn($"[DeviceManager] 设备 {deviceId} 已注册,将覆盖现有驱动"); |
| | | UnregisterDriver(deviceId); |
| | | } |
| | | |
| | | _drivers[deviceId] = driver; |
| | | |
| | | // 初始化状态缓存 |
| | | var initialStatus = driver.GetStatus(); |
| | | _statusCache[deviceId] = initialStatus; |
| | | |
| | | // 订阅连接状态变化事件 |
| | | driver.ConnectionChanged += OnConnectionChanged; |
| | | |
| | | Log.Info($"[DeviceManager] 设备 {deviceId} 驱动注册成功,当前连接状态: {initialStatus.IsConnected}"); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void UnregisterDriver(string deviceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | |
| | | if (_drivers.TryRemove(deviceId, out var driver)) |
| | | { |
| | | // 取消事件订阅 |
| | | driver.ConnectionChanged -= OnConnectionChanged; |
| | | |
| | | // 清理状态缓存 |
| | | _statusCache.TryRemove(deviceId, out _); |
| | | |
| | | Log.Info($"[DeviceManager] 设备 {deviceId} 驱动已注销"); |
| | | } |
| | | else |
| | | { |
| | | Log.Warn($"[DeviceManager] 尝试注销不存在的设备: {deviceId}"); |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public IDeviceDriver? GetDriver(string deviceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | |
| | | _drivers.TryGetValue(deviceId, out var driver); |
| | | return driver; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public DeviceDriverStatus GetStatus(string deviceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | |
| | | if (_statusCache.TryGetValue(deviceId, out var status)) |
| | | { |
| | | return status; |
| | | } |
| | | |
| | | // 如果缓存中没有,返回默认状态 |
| | | Log.Warn($"[DeviceManager] 设备 {deviceId} 状态缓存不存在,返回默认状态"); |
| | | return new DeviceDriverStatus |
| | | { |
| | | IsConnected = false, |
| | | IsHealthy = false, |
| | | LastError = "设备未注册或状态未初始化" |
| | | }; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task ConnectAsync(string deviceId, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | |
| | | if (!_drivers.TryGetValue(deviceId, out var driver)) |
| | | { |
| | | throw new InvalidOperationException($"设备 {deviceId} 未注册驱动"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[DeviceManager] 正在连接设备: {deviceId}"); |
| | | await driver.ConnectAsync(ct).ConfigureAwait(false); |
| | | |
| | | // 更新状态缓存 |
| | | _statusCache[deviceId] = driver.GetStatus(); |
| | | |
| | | Log.Info($"[DeviceManager] 设备 {deviceId} 连接成功"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | // 更新错误状态 |
| | | if (_statusCache.TryGetValue(deviceId, out var status)) |
| | | { |
| | | status.IsConnected = false; |
| | | status.IsHealthy = false; |
| | | status.LastError = ex.Message; |
| | | status.LastErrorTime = DateTime.UtcNow; |
| | | _statusCache[deviceId] = status; |
| | | } |
| | | |
| | | Log.Error($"[DeviceManager] 设备 {deviceId} 连接失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task DisconnectAsync(string deviceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | |
| | | if (!_drivers.TryGetValue(deviceId, out var driver)) |
| | | { |
| | | Log.Warn($"[DeviceManager] 尝试断开未注册的设备: {deviceId}"); |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[DeviceManager] 正在断开设备: {deviceId}"); |
| | | await driver.DisconnectAsync().ConfigureAwait(false); |
| | | |
| | | // 更新状态缓存 |
| | | _statusCache[deviceId] = driver.GetStatus(); |
| | | |
| | | Log.Info($"[DeviceManager] 设备 {deviceId} 已断开连接"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[DeviceManager] 设备 {deviceId} 断开连接失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public IReadOnlyList<string> GetRegisteredDevices() |
| | | { |
| | | return _drivers.Keys.ToList().AsReadOnly(); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsDeviceRegistered(string deviceId) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | return false; |
| | | |
| | | return _drivers.ContainsKey(deviceId); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理设备连接状态变化事件 |
| | | /// </summary> |
| | | /// <param name="sender">事件发送者</param> |
| | | /// <param name="e">事件参数</param> |
| | | private void OnConnectionChanged(object? sender, DeviceConnectionEventArgs e) |
| | | { |
| | | var deviceId = e.DeviceId; |
| | | |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | { |
| | | Log.Warn("[DeviceManager] 收到连接变化事件但 DeviceId 为空"); |
| | | return; |
| | | } |
| | | |
| | | if (_statusCache.TryGetValue(deviceId, out var status)) |
| | | { |
| | | status.IsConnected = e.IsConnected; |
| | | if (!e.IsConnected) |
| | | { |
| | | status.IsHealthy = false; |
| | | if (!string.IsNullOrEmpty(e.Reason)) |
| | | { |
| | | status.LastError = e.Reason; |
| | | status.LastErrorTime = DateTime.UtcNow; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | status.IsHealthy = true; |
| | | status.LastError = ""; |
| | | } |
| | | |
| | | _statusCache[deviceId] = status; |
| | | |
| | | Log.Info($"[DeviceManager] 设备 {deviceId} 连接状态变化: {(e.IsConnected ? "已连接" : "已断开")}" + |
| | | (string.IsNullOrEmpty(e.Reason) ? "" : $", 原因: {e.Reason}")); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using Aging.Infrastructure.Contracts; |
| | | |
| | | namespace Aging.Infrastructure.Core |
| | | { |
| | | /// <summary> |
| | | /// 操作租约实现。 |
| | | /// 提供短期资源占用保证,支持同步和异步释放。 |
| | | /// 释放时通过回调通知 ResourceManager。 |
| | | /// </summary> |
| | | public sealed class OperationLease : IOperationLease |
| | | { |
| | | private readonly Action? _onRelease; |
| | | private bool _disposed; |
| | | |
| | | /// <inheritdoc/> |
| | | public string LeaseId { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public ResourceKey Resource { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public ResourceOwnerToken Owner { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public DateTime AcquiredAt { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public DateTime? ExpiresAt { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsValid => !_disposed && !IsExpired(); |
| | | |
| | | /// <summary> |
| | | /// 初始化操作租约实例 |
| | | /// </summary> |
| | | /// <param name="resource">资源键</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="onRelease">释放时的回调通知</param> |
| | | /// <param name="leaseDurationSeconds">租约有效期(秒),默认30秒</param> |
| | | public OperationLease( |
| | | ResourceKey resource, |
| | | ResourceOwnerToken owner, |
| | | Action? onRelease = null, |
| | | int leaseDurationSeconds = 30) |
| | | { |
| | | LeaseId = Guid.NewGuid().ToString(); |
| | | Resource = resource ?? throw new ArgumentNullException(nameof(resource)); |
| | | Owner = owner ?? throw new ArgumentNullException(nameof(owner)); |
| | | AcquiredAt = DateTime.UtcNow; |
| | | ExpiresAt = leaseDurationSeconds > 0 |
| | | ? AcquiredAt.AddSeconds(leaseDurationSeconds) |
| | | : null; |
| | | _onRelease = onRelease; |
| | | _disposed = false; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查租约是否已过期 |
| | | /// </summary> |
| | | private bool IsExpired() |
| | | { |
| | | if (!ExpiresAt.HasValue) |
| | | return false; |
| | | return DateTime.UtcNow > ExpiresAt.Value; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) return; |
| | | _disposed = true; |
| | | _onRelease?.Invoke(); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public ValueTask DisposeAsync() |
| | | { |
| | | Dispose(); |
| | | return default; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Collections.Concurrent; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Core |
| | | { |
| | | /// <summary> |
| | | /// 资源管理实现。 |
| | | /// 提供线程安全的资源预约、租约获取和释放功能。 |
| | | /// 使用 ConcurrentDictionary 存储活跃预约,使用 SemaphoreSlim 实现 per-resource 锁。 |
| | | /// </summary> |
| | | public sealed class ResourceManager : IResourceManager |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(ResourceManager)); |
| | | |
| | | /// <summary> |
| | | /// 活跃预约字典(key = ResourceKey.ToString()) |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, ResourceReservation> _reservations = new(); |
| | | |
| | | /// <summary> |
| | | /// 每资源信号量字典,用于实现细粒度锁 |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, SemaphoreSlim> _resourceLocks = new(); |
| | | |
| | | /// <summary> |
| | | /// 全局世代号,每次新预约递增 |
| | | /// </summary> |
| | | private long _generation; |
| | | |
| | | /// <summary> |
| | | /// 默认租约有效期(秒) |
| | | /// </summary> |
| | | private const int DefaultLeaseDurationSeconds = 30; |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<ResourceReservation> ReserveAsync( |
| | | ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct) |
| | | { |
| | | if (owner == null) throw new ArgumentNullException(nameof(owner)); |
| | | if (resource == null) throw new ArgumentNullException(nameof(resource)); |
| | | |
| | | var resourceKey = resource.ToString(); |
| | | var semaphore = _resourceLocks.GetOrAdd(resourceKey, _ => new SemaphoreSlim(1, 1)); |
| | | |
| | | try |
| | | { |
| | | await semaphore.WaitAsync(ct).ConfigureAwait(false); |
| | | |
| | | try |
| | | { |
| | | // 检查是否已有 Active 预约 |
| | | if (_reservations.TryGetValue(resourceKey, out var existingReservation)) |
| | | { |
| | | if (existingReservation.State == ReservationState.Active) |
| | | { |
| | | // 检查是否为同一 Owner(续期场景) |
| | | if (existingReservation.Owner.TokenId == owner.TokenId) |
| | | { |
| | | // 同一所有者续期:更新世代号 |
| | | var renewedGeneration = Interlocked.Increment(ref _generation); |
| | | owner.Generation = renewedGeneration; |
| | | existingReservation.Owner.Generation = renewedGeneration; |
| | | |
| | | Log.Info($"[ResourceManager] 资源 {resourceKey} 续期成功,新世代号: {renewedGeneration}"); |
| | | return existingReservation; |
| | | } |
| | | |
| | | // 不同所有者冲突 |
| | | var conflictMsg = $"资源 {resourceKey} 已被所有者 {existingReservation.Owner.OwnerId} 预约," + |
| | | $"当前请求者: {owner.OwnerId}"; |
| | | Log.Warn($"[ResourceManager] 资源冲突: {conflictMsg}"); |
| | | throw new ResourceConflictException(conflictMsg); |
| | | } |
| | | } |
| | | |
| | | // 创建新预约 |
| | | var newGeneration = Interlocked.Increment(ref _generation); |
| | | owner.Generation = newGeneration; |
| | | |
| | | var reservation = new ResourceReservation(resource, owner) |
| | | { |
| | | State = ReservationState.Active |
| | | }; |
| | | |
| | | _reservations[resourceKey] = reservation; |
| | | |
| | | Log.Info($"[ResourceManager] 资源 {resourceKey} 预约成功,所有者: {owner.OwnerId},世代号: {newGeneration}"); |
| | | return reservation; |
| | | } |
| | | finally |
| | | { |
| | | semaphore.Release(); |
| | | } |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | Log.Warn($"[ResourceManager] 资源 {resourceKey} 预约被取消"); |
| | | throw; |
| | | } |
| | | catch (ResourceConflictException) |
| | | { |
| | | throw; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[ResourceManager] 资源 {resourceKey} 预约失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<IOperationLease> AcquireOperationAsync( |
| | | ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct) |
| | | { |
| | | if (owner == null) throw new ArgumentNullException(nameof(owner)); |
| | | if (resource == null) throw new ArgumentNullException(nameof(resource)); |
| | | |
| | | var resourceKey = resource.ToString(); |
| | | |
| | | try |
| | | { |
| | | // 检查资源是否有有效预约 |
| | | if (!_reservations.TryGetValue(resourceKey, out var reservation)) |
| | | { |
| | | throw new InvalidOperationException($"资源 {resourceKey} 未被预约,无法获取操作租约"); |
| | | } |
| | | |
| | | if (reservation.State != ReservationState.Active) |
| | | { |
| | | throw new InvalidOperationException($"资源 {resourceKey} 预约状态为 {reservation.State},无法获取操作租约"); |
| | | } |
| | | |
| | | // 验证 Owner 匹配(TokenId + Generation) |
| | | if (reservation.Owner.TokenId != owner.TokenId || reservation.Owner.Generation != owner.Generation) |
| | | { |
| | | throw new InvalidOperationException( |
| | | $"资源 {resourceKey} 所有者不匹配,期望 TokenId: {reservation.Owner.TokenId},Generation: {reservation.Owner.Generation}"); |
| | | } |
| | | |
| | | // 创建 OperationLease(短期,默认30秒过期) |
| | | var lease = new OperationLease( |
| | | resource, |
| | | owner, |
| | | onRelease: () => Log.Debug($"[ResourceManager] 租约释放: {resourceKey}"), |
| | | leaseDurationSeconds: DefaultLeaseDurationSeconds); |
| | | |
| | | Log.Info($"[ResourceManager] 获取操作租约: {resourceKey},所有者: {owner.OwnerId},有效期: {DefaultLeaseDurationSeconds}秒"); |
| | | |
| | | await Task.CompletedTask; // 保持异步签名 |
| | | return lease; |
| | | } |
| | | catch (InvalidOperationException) |
| | | { |
| | | throw; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[ResourceManager] 获取操作租约失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task ReleaseReservationAsync( |
| | | ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct) |
| | | { |
| | | if (owner == null) throw new ArgumentNullException(nameof(owner)); |
| | | if (resource == null) throw new ArgumentNullException(nameof(resource)); |
| | | |
| | | var resourceKey = resource.ToString(); |
| | | var semaphore = _resourceLocks.GetOrAdd(resourceKey, _ => new SemaphoreSlim(1, 1)); |
| | | |
| | | try |
| | | { |
| | | await semaphore.WaitAsync(ct).ConfigureAwait(false); |
| | | |
| | | try |
| | | { |
| | | if (!_reservations.TryGetValue(resourceKey, out var reservation)) |
| | | { |
| | | Log.Warn($"[ResourceManager] 尝试释放不存在的资源预约: {resourceKey}"); |
| | | return; |
| | | } |
| | | |
| | | // 验证 Owner 匹配 |
| | | if (reservation.Owner.TokenId != owner.TokenId) |
| | | { |
| | | throw new InvalidOperationException( |
| | | $"资源 {resourceKey} 释放失败:所有者不匹配,期望 TokenId: {reservation.Owner.TokenId}"); |
| | | } |
| | | |
| | | // 标记预约为 Released |
| | | reservation.State = ReservationState.Released; |
| | | _reservations.TryRemove(resourceKey, out _); |
| | | |
| | | Log.Info($"[ResourceManager] 资源 {resourceKey} 预约已释放,所有者: {owner.OwnerId}"); |
| | | } |
| | | finally |
| | | { |
| | | semaphore.Release(); |
| | | } |
| | | |
| | | await Task.CompletedTask; // 保持异步签名 |
| | | } |
| | | catch (InvalidOperationException) |
| | | { |
| | | throw; |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | Log.Warn($"[ResourceManager] 资源 {resourceKey} 释放操作被取消"); |
| | | throw; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[ResourceManager] 释放资源 {resourceKey} 失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsReserved(ResourceKey resource, out ResourceReservation? reservation) |
| | | { |
| | | if (resource == null) throw new ArgumentNullException(nameof(resource)); |
| | | |
| | | var resourceKey = resource.ToString(); |
| | | var result = _reservations.TryGetValue(resourceKey, out var res); |
| | | |
| | | if (result && res != null && res.State == ReservationState.Active) |
| | | { |
| | | reservation = res; |
| | | return true; |
| | | } |
| | | |
| | | reservation = null; |
| | | return false; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource) |
| | | { |
| | | if (owner == null) throw new ArgumentNullException(nameof(owner)); |
| | | if (resource == null) throw new ArgumentNullException(nameof(resource)); |
| | | |
| | | var resourceKey = resource.ToString(); |
| | | |
| | | if (!_reservations.TryGetValue(resourceKey, out var reservation)) |
| | | return false; |
| | | |
| | | if (reservation.State != ReservationState.Active) |
| | | return false; |
| | | |
| | | return reservation.Owner.TokenId == owner.TokenId && |
| | | reservation.Owner.Generation == owner.Generation; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Diagnostics; |
| | | using System.Linq; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using Aging.Infrastructure.Contracts; |
| | | using CanSimulator; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Drivers |
| | | { |
| | | /// <summary> |
| | | /// CAN 通讯驱动适配器。 |
| | | /// 封装 CanSimulator,实现统一的 IDeviceDriver 接口。 |
| | | /// 命令格式约定: |
| | | /// 发送: "SEND:frameId:data" 如 "SEND:0x100:01 02 03 04" |
| | | /// 查询: |
| | | /// "RECV" - 获取接收邮箱中的下一帧 |
| | | /// "RECV_ALL" - 获取接收邮箱中的所有帧 |
| | | /// "STATUS" - 获取节点/总线状态 |
| | | /// "FILTER:id:mask" - 设置接收滤波器 |
| | | /// "FILTER_CLEAR" - 清除所有滤波器 |
| | | /// "PROMISCUOUS" - 设置混杂模式(接收所有帧) |
| | | /// "LOG" - 获取总线日志 |
| | | /// "BUS_STATUS" - 获取总线状态摘要 |
| | | /// </summary> |
| | | public sealed class CanDriverAdapter : IDeviceDriver |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(CanDriverAdapter)); |
| | | |
| | | /// <summary> |
| | | /// 全局 CAN 总线模拟器实例(所有 CAN 驱动共享) |
| | | /// </summary> |
| | | private static readonly Dictionary<string, CanBusSimulator> _sharedBuses = new(); |
| | | private static readonly object _busLock = new(); |
| | | |
| | | private readonly string _ip; // 总线名称(用 ip 字段映射) |
| | | private readonly int _port; // 节点编号(用 port 字段映射) |
| | | private CanBusSimulator? _bus; |
| | | private CanNode? _node; |
| | | private bool _disposed; |
| | | private readonly Stopwatch _latencyStopwatch = new(); |
| | | private double _lastLatencyMs; |
| | | private string _lastError = string.Empty; |
| | | private DateTime? _lastErrorTime; |
| | | private int _reconnectCount; |
| | | |
| | | /// <summary> |
| | | /// 接收到的帧缓存(用于 RECV 查询) |
| | | /// </summary> |
| | | private readonly Queue<CanFrame> _recvBuffer = new(); |
| | | |
| | | /// <inheritdoc/> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsConnected => _node != null && _node.IsActive; |
| | | |
| | | /// <inheritdoc/> |
| | | public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged; |
| | | |
| | | /// <summary> |
| | | /// 初始化 CAN 驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="busName">总线名称(如 CAN_BUS_1)</param> |
| | | /// <param name="nodeIndex">节点编号(用于区分同一总线上的不同设备)</param> |
| | | public CanDriverAdapter(string deviceId, string busName, int nodeIndex) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | _ip = busName ?? "CAN_BUS_1"; |
| | | _port = nodeIndex; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取或创建共享总线 |
| | | /// </summary> |
| | | private CanBusSimulator GetOrCreateBus(string busName) |
| | | { |
| | | lock (_busLock) |
| | | { |
| | | if (!_sharedBuses.TryGetValue(busName, out var bus)) |
| | | { |
| | | bus = new CanBusSimulator(busName); |
| | | _sharedBuses[busName] = bus; |
| | | Log.Info($"[CAN] 创建新的虚拟总线: {busName}"); |
| | | } |
| | | return bus; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task ConnectAsync(CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[CAN] 设备 {DeviceId} 正在连接到总线 {_ip} (节点: {_port})..."); |
| | | |
| | | try |
| | | { |
| | | // 获取或创建共享总线 |
| | | _bus = GetOrCreateBus(_ip); |
| | | |
| | | // 如果总线未启动,自动启动 |
| | | if (_bus.State != CanBusState.Running) |
| | | { |
| | | _bus.Start(); |
| | | } |
| | | |
| | | // 注册节点 |
| | | string nodeId = $"{DeviceId}_node{_port}"; |
| | | _node = _bus.RegisterNode(nodeId, DeviceId); |
| | | |
| | | // 设置默认滤波器(接收所有帧) |
| | | _node.SetPromiscuousMode(); |
| | | |
| | | // 激活节点 |
| | | _node.Activate(); |
| | | |
| | | // 订阅接收事件 |
| | | _node.FrameReceived += OnFrameReceived; |
| | | |
| | | _lastError = string.Empty; |
| | | Log.Info($"[CAN] 设备 {DeviceId} 已成功连接到总线 {_ip} (节点: {_node.NodeId})"); |
| | | OnConnectionChanged(true, "连接成功"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[CAN] 设备 {DeviceId} 连接异常: {ex.Message}", ex); |
| | | _node?.Deactivate(); |
| | | _node = null; |
| | | throw; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task DisconnectAsync() |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[CAN] 设备 {DeviceId} 正在断开连接..."); |
| | | |
| | | try |
| | | { |
| | | if (_node != null) |
| | | { |
| | | _node.FrameReceived -= OnFrameReceived; |
| | | _node.Deactivate(); |
| | | _bus?.UnregisterNode(_node.NodeId); |
| | | _node = null; |
| | | } |
| | | |
| | | Log.Info($"[CAN] 设备 {DeviceId} 已断开连接"); |
| | | OnConnectionChanged(false, "主动断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[CAN] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex); |
| | | _node = null; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> SendCommandAsync(string command, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[CAN] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[CAN] 设备 {DeviceId} 发送命令: {command}"); |
| | | |
| | | // 解析命令格式: "SEND:frameId:data" |
| | | string[] parts = command.Split(':'); |
| | | if (parts.Length < 3) |
| | | { |
| | | throw new ArgumentException($"[CAN] 命令格式错误,应为 SEND:frameId:data,实际: {command}"); |
| | | } |
| | | |
| | | string operation = parts[0].ToUpper(); |
| | | if (operation != "SEND") |
| | | { |
| | | throw new ArgumentException($"[CAN] 不支持的操作类型: {operation},应为 SEND"); |
| | | } |
| | | |
| | | // 解析帧 ID |
| | | string frameIdStr = parts[1]; |
| | | uint frameId = CanFrame.ParseFrameId(frameIdStr); |
| | | |
| | | // 解析数据 |
| | | string dataStr = parts[2]; |
| | | byte[] data = CanFrame.ParseData(dataStr); |
| | | |
| | | // 验证数据长度 |
| | | if (data.Length > 8) |
| | | { |
| | | throw new ArgumentException($"[CAN] 数据长度超限: {data.Length} 字节(最大 8 字节)"); |
| | | } |
| | | |
| | | // 创建并发送帧 |
| | | var frame = new CanFrame |
| | | { |
| | | FrameId = frameId, |
| | | Format = frameId > 0x7FF ? CanFrameFormat.Extended : CanFrameFormat.Standard, |
| | | Type = CanFrameType.Data, |
| | | Data = data |
| | | }; |
| | | |
| | | _node!.SendFrame(frame); |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | |
| | | string response = $"OK:TX:{frame.FrameIdHex}:{frame.Dlc}"; |
| | | Log.Debug($"[CAN] 设备 {DeviceId} 发送成功 (延迟 {_lastLatencyMs:F2}ms): {response}"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[CAN] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> QueryAsync(string query, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[CAN] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[CAN] 设备 {DeviceId} 查询: {query}"); |
| | | |
| | | string[] parts = query.Split(':'); |
| | | string operation = parts[0].ToUpper(); |
| | | |
| | | string response; |
| | | switch (operation) |
| | | { |
| | | case "RECV": |
| | | response = HandleRecv(); |
| | | break; |
| | | |
| | | case "RECV_ALL": |
| | | response = HandleRecvAll(); |
| | | break; |
| | | |
| | | case "STATUS": |
| | | response = HandleStatus(); |
| | | break; |
| | | |
| | | case "FILTER": |
| | | response = HandleFilter(parts); |
| | | break; |
| | | |
| | | case "FILTER_CLEAR": |
| | | _node!.ClearFilters(); |
| | | response = "OK:滤波器已清除"; |
| | | break; |
| | | |
| | | case "PROMISCUOUS": |
| | | _node!.SetPromiscuousMode(); |
| | | response = "OK:混杂模式已启用"; |
| | | break; |
| | | |
| | | case "AUTO_RESPONSE": |
| | | response = HandleAutoResponse(parts); |
| | | break; |
| | | |
| | | case "RESPONSE_DELAY": |
| | | response = HandleResponseDelay(parts); |
| | | break; |
| | | |
| | | case "LOG": |
| | | response = HandleLog(); |
| | | break; |
| | | |
| | | case "BUS_STATUS": |
| | | response = _bus?.GetStatusSummary() ?? "总线未初始化"; |
| | | break; |
| | | |
| | | default: |
| | | throw new ArgumentException($"[CAN] 不支持的查询类型: {operation}"); |
| | | } |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | Log.Debug($"[CAN] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[CAN] 设备 {DeviceId} 查询失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 RECV 查询 - 获取下一帧 |
| | | /// </summary> |
| | | private string HandleRecv() |
| | | { |
| | | // 先从缓冲区取 |
| | | if (_recvBuffer.Count > 0) |
| | | { |
| | | var frame = _recvBuffer.Dequeue(); |
| | | return $"RECV:{frame.FrameIdHex}:{frame.DataHex}:{frame.Timestamp:HH:mm:ss.fff}"; |
| | | } |
| | | |
| | | // 从节点邮箱取 |
| | | if (_node!.TryDequeueReceived(out var frame2)) |
| | | { |
| | | return $"RECV:{frame2!.FrameIdHex}:{frame2.DataHex}:{frame2.Timestamp:HH:mm:ss.fff}"; |
| | | } |
| | | |
| | | return "RECV:EMPTY"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 RECV_ALL 查询 - 获取所有帧 |
| | | /// </summary> |
| | | private string HandleRecvAll() |
| | | { |
| | | var frames = new List<string>(); |
| | | |
| | | // 先取缓冲区 |
| | | while (_recvBuffer.Count > 0) |
| | | { |
| | | var f = _recvBuffer.Dequeue(); |
| | | frames.Add($"{f.FrameIdHex}:{f.DataHex}"); |
| | | } |
| | | |
| | | // 再取节点邮箱 |
| | | var mailbox = _node!.PeekReceiveMailbox(); |
| | | _node.ClearReceiveMailbox(); |
| | | foreach (var f in mailbox) |
| | | { |
| | | frames.Add($"{f.FrameIdHex}:{f.DataHex}:{f.Timestamp:HH:mm:ss.fff}"); |
| | | } |
| | | |
| | | return frames.Count > 0 ? $"RECV_ALL:{frames.Count}:{string.Join("|", frames)}" : "RECV_ALL:0"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 STATUS 查询 |
| | | /// </summary> |
| | | private string HandleStatus() |
| | | { |
| | | var nodeStatus = _node?.GetStatusSummary() ?? "节点未初始化"; |
| | | var busStatus = _bus?.GetStatusSummary() ?? "总线未初始化"; |
| | | return $"STATUS:{nodeStatus}|{busStatus}"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 FILTER 设置 |
| | | /// </summary> |
| | | private string HandleFilter(string[] parts) |
| | | { |
| | | if (parts.Length < 3) |
| | | { |
| | | throw new ArgumentException("[CAN] FILTER 命令格式错误,应为 FILTER:id:mask"); |
| | | } |
| | | |
| | | uint filterId = CanFrame.ParseFrameId(parts[1]); |
| | | uint mask = CanFrame.ParseFrameId(parts[2]); |
| | | |
| | | _node!.AddFilter(new AcceptanceFilter |
| | | { |
| | | FilterId = filterId, |
| | | Mask = mask, |
| | | IsExtended = filterId > 0x7FF |
| | | }); |
| | | |
| | | return $"OK:滤波器已添加 ID=0x{filterId:X3}, Mask=0x{mask:X3}"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 AUTO_RESPONSE 命令 |
| | | /// 格式: AUTO_RESPONSE:on|off |
| | | /// </summary> |
| | | private string HandleAutoResponse(string[] parts) |
| | | { |
| | | if (parts.Length < 2) |
| | | { |
| | | // 查询当前状态 |
| | | bool current = _node!.AutoResponseEnabled; |
| | | return $"AUTO_RESPONSE:{(current ? "ON" : "OFF")}"; |
| | | } |
| | | |
| | | string mode = parts[1].Trim().ToUpper(); |
| | | switch (mode) |
| | | { |
| | | case "ON": |
| | | case "1": |
| | | case "TRUE": |
| | | case "ENABLE": |
| | | _node!.AutoResponseEnabled = true; |
| | | Log.Info($"[CAN] 设备 {DeviceId} 启用自动响应模式"); |
| | | return "OK:自动响应模式已启用"; |
| | | |
| | | case "OFF": |
| | | case "0": |
| | | case "FALSE": |
| | | case "DISABLE": |
| | | _node!.AutoResponseEnabled = false; |
| | | Log.Info($"[CAN] 设备 {DeviceId} 禁用自动响应模式"); |
| | | return "OK:自动响应模式已禁用"; |
| | | |
| | | default: |
| | | throw new ArgumentException($"[CAN] AUTO_RESPONSE 参数错误: {mode},应为 on/off"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 RESPONSE_DELAY 命令 |
| | | /// 格式: RESPONSE_DELAY:毫秒数 |
| | | /// </summary> |
| | | private string HandleResponseDelay(string[] parts) |
| | | { |
| | | if (parts.Length < 2) |
| | | { |
| | | // 查询当前延迟 |
| | | return $"RESPONSE_DELAY:{_node!.AutoResponseDelayMs}ms"; |
| | | } |
| | | |
| | | if (!int.TryParse(parts[1].Trim(), out int delayMs) || delayMs < 0) |
| | | { |
| | | throw new ArgumentException($"[CAN] RESPONSE_DELAY 参数错误: {parts[1]},应为非负整数(毫秒)"); |
| | | } |
| | | |
| | | _node!.AutoResponseDelayMs = delayMs; |
| | | Log.Info($"[CAN] 设备 {DeviceId} 设置响应延迟: {delayMs}ms"); |
| | | return $"OK:响应延迟已设置为 {delayMs}ms"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 处理 LOG 查询 |
| | | /// </summary> |
| | | private string HandleLog() |
| | | { |
| | | var log = _bus?.GetBusLog(50) ?? new List<CanFrame>(); |
| | | if (log.Count == 0) return "LOG:EMPTY"; |
| | | |
| | | var entries = log.Select(f => |
| | | $"[{f.Timestamp:HH:mm:ss.fff}]{f.SourceNodeId}>{f.FrameIdHex}:{f.DataHex}"); |
| | | return $"LOG:{log.Count}:{string.Join("|", entries)}"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 帧接收事件(公开给外部订阅) |
| | | /// </summary> |
| | | public event EventHandler<CanFrame>? DriverFrameReceived; |
| | | |
| | | /// <summary> |
| | | /// 帧接收事件处理 |
| | | /// </summary> |
| | | private void OnFrameReceived(object? sender, CanFrame frame) |
| | | { |
| | | // 放入接收缓冲区(用于 RECV/RECV_ALL 查询) |
| | | while (_recvBuffer.Count >= 1000) |
| | | { |
| | | _recvBuffer.Dequeue(); |
| | | } |
| | | _recvBuffer.Enqueue(frame); |
| | | |
| | | // 触发公开事件(用于即时显示) |
| | | DriverFrameReceived?.Invoke(this, frame); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public DeviceDriverStatus GetStatus() |
| | | { |
| | | return new DeviceDriverStatus |
| | | { |
| | | IsConnected = IsConnected, |
| | | IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError), |
| | | LastError = _lastError, |
| | | LastErrorTime = _lastErrorTime, |
| | | LatencyMs = _lastLatencyMs, |
| | | ReconnectCount = _reconnectCount |
| | | }; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) return; |
| | | _disposed = true; |
| | | |
| | | try |
| | | { |
| | | if (_node != null) |
| | | { |
| | | _node.FrameReceived -= OnFrameReceived; |
| | | _node.Deactivate(); |
| | | _bus?.UnregisterNode(_node.NodeId); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CAN] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发所有总线和节点(用于测试清理) |
| | | /// </summary> |
| | | public static void ResetAllBuses() |
| | | { |
| | | lock (_busLock) |
| | | { |
| | | foreach (var bus in _sharedBuses.Values) |
| | | { |
| | | bus.Dispose(); |
| | | } |
| | | _sharedBuses.Clear(); |
| | | Log.Info("[CAN] 所有虚拟总线已重置"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发所有总线实例(供外部查询) |
| | | /// </summary> |
| | | public static IReadOnlyDictionary<string, CanBusSimulator> GetAllBuses() |
| | | { |
| | | lock (_busLock) |
| | | { |
| | | return new Dictionary<string, CanBusSimulator>(_sharedBuses); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发连接状态变化事件 |
| | | /// </summary> |
| | | private void OnConnectionChanged(bool isConnected, string reason) |
| | | { |
| | | ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs |
| | | { |
| | | DeviceId = DeviceId, |
| | | IsConnected = isConnected, |
| | | Reason = reason |
| | | }); |
| | | |
| | | if (!isConnected) |
| | | { |
| | | _reconnectCount++; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查是否已释放 |
| | | /// </summary> |
| | | private void ThrowIfDisposed() |
| | | { |
| | | if (_disposed) throw new ObjectDisposedException(nameof(CanDriverAdapter)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Drivers |
| | | { |
| | | /// <summary> |
| | | /// 设备驱动工厂。 |
| | | /// 根据传输协议类型和端点信息创建对应的驱动适配器实例。 |
| | | /// </summary> |
| | | public static class DriverFactory |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(DriverFactory)); |
| | | |
| | | /// <summary> |
| | | /// 根据传输协议类型和端点创建驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="transport">传输协议类型: "Tcp", "SCPI", "ModBus", "S7", "Serial"</param> |
| | | /// <param name="endpoint">端点信息,格式: "ip:port" 或 "ip:port:extra"</param> |
| | | /// <returns>IDeviceDriver 实例</returns> |
| | | /// <exception cref="NotSupportedException">不支持的传输协议类型</exception> |
| | | /// <exception cref="ArgumentException">端点格式错误</exception> |
| | | public static IDeviceDriver CreateDriver(string deviceId, string transport, string endpoint) |
| | | { |
| | | return CreateDriver(deviceId, transport, endpoint, null); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据传输协议类型、端点和额外参数创建驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="transport">传输协议类型: "Tcp", "SCPI", "ModBus", "S7", "Serial"</param> |
| | | /// <param name="endpoint">端点信息,格式: "ip:port" 或 "ip:port:extra"</param> |
| | | /// <param name="extraParams">额外参数(可选)</param> |
| | | /// <returns>IDeviceDriver 实例</returns> |
| | | /// <exception cref="NotSupportedException">不支持的传输协议类型</exception> |
| | | /// <exception cref="ArgumentException">端点格式错误</exception> |
| | | public static IDeviceDriver CreateDriver(string deviceId, string transport, string endpoint, Dictionary<string, string>? extraParams) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentNullException(nameof(deviceId)); |
| | | if (string.IsNullOrWhiteSpace(transport)) |
| | | throw new ArgumentNullException(nameof(transport)); |
| | | if (string.IsNullOrWhiteSpace(endpoint)) |
| | | throw new ArgumentNullException(nameof(endpoint)); |
| | | |
| | | Log.Info($"[DriverFactory] 创建设备驱动: DeviceId={deviceId}, Transport={transport}, Endpoint={endpoint}"); |
| | | |
| | | string[] parts = endpoint.Split(':'); |
| | | if (parts.Length < 2) |
| | | { |
| | | throw new ArgumentException($"端点格式错误,应为 'ip:port' 或 'ip:port:extra',实际: {endpoint}"); |
| | | } |
| | | |
| | | string ip = parts[0]; |
| | | if (!int.TryParse(parts[1], out int port)) |
| | | { |
| | | throw new ArgumentException($"端口号格式错误: {parts[1]}"); |
| | | } |
| | | |
| | | switch (transport.ToUpper()) |
| | | { |
| | | case "TCP": |
| | | Log.Info($"[DriverFactory] 创建 TCP 原始通信驱动: {ip}:{port}"); |
| | | return new TcpRawDriverAdapter(deviceId, ip, port); |
| | | |
| | | case "SCPI": |
| | | Log.Info($"[DriverFactory] 创建 SCPI 协议驱动: {ip}:{port}"); |
| | | return new ScpiDriverAdapter(deviceId, ip, port); |
| | | |
| | | case "MODBUS": |
| | | byte slaveId = 1; |
| | | if (extraParams != null && extraParams.TryGetValue("slaveId", out string slaveIdStr)) |
| | | { |
| | | if (!byte.TryParse(slaveIdStr, out slaveId)) |
| | | { |
| | | slaveId = 1; |
| | | } |
| | | } |
| | | // 如果 endpoint 有第三部分,作为 slaveId |
| | | else if (parts.Length >= 3 && byte.TryParse(parts[2], out byte parsedSlaveId)) |
| | | { |
| | | slaveId = parsedSlaveId; |
| | | } |
| | | Log.Info($"[DriverFactory] 创建 ModBus TCP 驱动: {ip}:{port} (从站: {slaveId})"); |
| | | return new ModBusDriverAdapter(deviceId, ip, port, slaveId); |
| | | |
| | | case "S7": |
| | | Log.Info($"[DriverFactory] 创建 S7 PLC 驱动: {ip}:{port}"); |
| | | return new S7DriverAdapter(deviceId, ip, port); |
| | | |
| | | case "SERIAL": |
| | | // TODO: 实现串口驱动适配器 |
| | | throw new NotSupportedException("串口驱动适配器尚未实现"); |
| | | |
| | | case "CAN": |
| | | // CAN 驱动: endpoint 格式为 "busName:nodeIndex" 如 "CAN_BUS_1:0" |
| | | string busName = ip; // ip 字段用作总线名称 |
| | | int nodeIndex = port; // port 字段用作节点编号 |
| | | Log.Info($"[DriverFactory] 创建 CAN 虚拟驱动: Bus={busName}, Node={nodeIndex}"); |
| | | return new CanDriverAdapter(deviceId, busName, nodeIndex); |
| | | |
| | | default: |
| | | throw new NotSupportedException($"不支持的传输协议类型: {transport}。支持的类型: Tcp, SCPI, ModBus, S7, Serial, CAN"); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Diagnostics; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using Aging.Infrastructure.Contracts; |
| | | using HslCommunication; |
| | | using HslCommunication.ModBus; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Drivers |
| | | { |
| | | /// <summary> |
| | | /// ModBus TCP 驱动适配器。 |
| | | /// 封装 HslCommunication.ModBusTcpNet,实现统一的 IDeviceDriver 接口。 |
| | | /// 命令格式约定: |
| | | /// 写入: "WRITE:address:value" 如 "WRITE:40001:100" 或 "WRITE:00001:TRUE" |
| | | /// 读取: "READ:address" 如 "READ:00001" 或 "READ:40001" |
| | | /// </summary> |
| | | public sealed class ModBusDriverAdapter : IDeviceDriver |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(ModBusDriverAdapter)); |
| | | |
| | | private readonly string _ip; |
| | | private readonly int _port; |
| | | private readonly byte _slaveId; |
| | | private ModbusTcpNet? _client; |
| | | private bool _disposed; |
| | | private readonly Stopwatch _latencyStopwatch = new Stopwatch(); |
| | | private double _lastLatencyMs; |
| | | private string _lastError = string.Empty; |
| | | private DateTime? _lastErrorTime; |
| | | private int _reconnectCount; |
| | | |
| | | /// <inheritdoc/> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsConnected => _client != null; |
| | | |
| | | /// <inheritdoc/> |
| | | public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged; |
| | | |
| | | /// <summary> |
| | | /// 初始化 ModBus TCP 驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="ip">目标 IP 地址</param> |
| | | /// <param name="port">目标端口号</param> |
| | | /// <param name="slaveId">从站地址(默认为 1)</param> |
| | | public ModBusDriverAdapter(string deviceId, string ip, int port, byte slaveId = 1) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | _ip = ip ?? throw new ArgumentNullException(nameof(ip)); |
| | | _port = port; |
| | | _slaveId = slaveId; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task ConnectAsync(CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[ModBus] 设备 {DeviceId} 正在连接 {_ip}:{_port} (从站: {_slaveId})..."); |
| | | |
| | | try |
| | | { |
| | | _client = new ModbusTcpNet(_ip, _port, _slaveId); |
| | | OperateResult result = _client.ConnectServer(); |
| | | |
| | | if (!result.IsSuccess) |
| | | { |
| | | _lastError = result.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new InvalidOperationException($"[ModBus] 设备 {DeviceId} 连接失败: {result.Message}"); |
| | | } |
| | | |
| | | _lastError = string.Empty; |
| | | Log.Info($"[ModBus] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}"); |
| | | OnConnectionChanged(true, "连接成功"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[ModBus] 设备 {DeviceId} 连接异常: {ex.Message}", ex); |
| | | _client?.ConnectClose(); |
| | | _client?.Dispose(); |
| | | _client = null; |
| | | throw; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task DisconnectAsync() |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[ModBus] 设备 {DeviceId} 正在断开连接..."); |
| | | |
| | | try |
| | | { |
| | | if (_client != null) |
| | | { |
| | | _client.ConnectClose(); |
| | | _client.Dispose(); |
| | | _client = null; |
| | | } |
| | | |
| | | Log.Info($"[ModBus] 设备 {DeviceId} 已断开连接"); |
| | | OnConnectionChanged(false, "主动断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[ModBus] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex); |
| | | _client = null; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> SendCommandAsync(string command, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[ModBus] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[ModBus] 设备 {DeviceId} 发送命令: {command}"); |
| | | |
| | | // 解析命令格式: "WRITE:address:value" |
| | | string[] parts = command.Split(':'); |
| | | if (parts.Length < 3) |
| | | { |
| | | throw new ArgumentException($"[ModBus] 命令格式错误,应为 WRITE:address:value,实际: {command}"); |
| | | } |
| | | |
| | | string operation = parts[0].ToUpper(); |
| | | string address = parts[1]; |
| | | string value = parts[2]; |
| | | |
| | | if (operation != "WRITE") |
| | | { |
| | | throw new ArgumentException($"[ModBus] 不支持的操作类型: {operation},应为 WRITE"); |
| | | } |
| | | |
| | | // 根据值判断数据类型并写入 |
| | | OperateResult result; |
| | | if (bool.TryParse(value, out bool boolVal)) |
| | | { |
| | | result = _client.Write(address, boolVal); |
| | | } |
| | | else if (short.TryParse(value, out short shortVal)) |
| | | { |
| | | result = _client.Write(address, shortVal); |
| | | } |
| | | else if (int.TryParse(value, out int intVal)) |
| | | { |
| | | result = _client.Write(address, intVal); |
| | | } |
| | | else if (float.TryParse(value, out float floatVal)) |
| | | { |
| | | result = _client.Write(address, floatVal); |
| | | } |
| | | else |
| | | { |
| | | throw new ArgumentException($"[ModBus] 无法解析的值: {value}"); |
| | | } |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | |
| | | if (!result.IsSuccess) |
| | | { |
| | | _lastError = result.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new InvalidOperationException($"[ModBus] 写入失败: {result.Message}"); |
| | | } |
| | | |
| | | string response = "OK"; |
| | | Log.Debug($"[ModBus] 设备 {DeviceId} 写入成功 (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[ModBus] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> QueryAsync(string query, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[ModBus] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[ModBus] 设备 {DeviceId} 发送查询: {query}"); |
| | | |
| | | // 解析查询格式: "READ:address" |
| | | string[] parts = query.Split(':'); |
| | | if (parts.Length < 2) |
| | | { |
| | | throw new ArgumentException($"[ModBus] 查询格式错误,应为 READ:address,实际: {query}"); |
| | | } |
| | | |
| | | string operation = parts[0].ToUpper(); |
| | | string address = parts[1]; |
| | | |
| | | if (operation != "READ") |
| | | { |
| | | throw new ArgumentException($"[ModBus] 不支持的操作类型: {operation},应为 READ"); |
| | | } |
| | | |
| | | // 尝试读取不同类型(根据地址范围自动判断) |
| | | // 00001-09999: 线圈(BOOL) |
| | | // 10001-19999: 离散输入(BOOL) |
| | | // 30001-39999: 输入寄存器(INT16) |
| | | // 40001-49999: 保持寄存器(INT16) |
| | | string response; |
| | | int addrNum; |
| | | if (int.TryParse(address, out addrNum)) |
| | | { |
| | | if (addrNum >= 1 && addrNum <= 9999) |
| | | { |
| | | // 线圈读取 |
| | | var result = _client.ReadBool(address); |
| | | response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}"; |
| | | } |
| | | else if (addrNum >= 10001 && addrNum <= 19999) |
| | | { |
| | | // 离散输入读取 |
| | | var result = _client.ReadBool(address); |
| | | response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}"; |
| | | } |
| | | else if (addrNum >= 30001 && addrNum <= 39999) |
| | | { |
| | | // 输入寄存器读取 |
| | | var result = _client.ReadInt16(address); |
| | | response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}"; |
| | | } |
| | | else if (addrNum >= 40001 && addrNum <= 49999) |
| | | { |
| | | // 保持寄存器读取 |
| | | var result = _client.ReadInt16(address); |
| | | response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}"; |
| | | } |
| | | else |
| | | { |
| | | // 默认读取 INT16 |
| | | var result = _client.ReadInt16(address); |
| | | response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}"; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 非数字地址,尝试读取 INT16 |
| | | var result = _client.ReadInt16(address); |
| | | response = result.IsSuccess ? result.Content.ToString() : $"ERROR: {result.Message}"; |
| | | } |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | Log.Debug($"[ModBus] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[ModBus] 设备 {DeviceId} 查询失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public DeviceDriverStatus GetStatus() |
| | | { |
| | | return new DeviceDriverStatus |
| | | { |
| | | IsConnected = IsConnected, |
| | | IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError), |
| | | LastError = _lastError, |
| | | LastErrorTime = _lastErrorTime, |
| | | LatencyMs = _lastLatencyMs, |
| | | ReconnectCount = _reconnectCount |
| | | }; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) return; |
| | | _disposed = true; |
| | | |
| | | try |
| | | { |
| | | if (_client != null) |
| | | { |
| | | _client.ConnectClose(); |
| | | _client.Dispose(); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[ModBus] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发连接状态变化事件 |
| | | /// </summary> |
| | | private void OnConnectionChanged(bool isConnected, string reason) |
| | | { |
| | | ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs |
| | | { |
| | | DeviceId = DeviceId, |
| | | IsConnected = isConnected, |
| | | Reason = reason |
| | | }); |
| | | |
| | | if (!isConnected) |
| | | { |
| | | _reconnectCount++; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查是否已释放 |
| | | /// </summary> |
| | | private void ThrowIfDisposed() |
| | | { |
| | | if (_disposed) throw new ObjectDisposedException(nameof(ModBusDriverAdapter)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Diagnostics; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | using S7Driver; |
| | | |
| | | namespace Aging.Infrastructure.Drivers |
| | | { |
| | | /// <summary> |
| | | /// 西门子 S7 PLC 驱动适配器。 |
| | | /// 封装 S7Driver.PLCS7Driver,实现统一的 IDeviceDriver 接口。 |
| | | /// 命令格式约定: |
| | | /// 写入: "WRITE:DBx.DBXy.z:value" 如 "WRITE:DB1.DBX0.0:TRUE" |
| | | /// 读取: "READ:DBx.DBWy" 如 "READ:DB1.DBD0" |
| | | /// 支持数据类型: BOOL, INT, REAL |
| | | /// </summary> |
| | | public sealed class S7DriverAdapter : IDeviceDriver |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(S7DriverAdapter)); |
| | | |
| | | private readonly string _ip; |
| | | private readonly int _port; |
| | | private PLCS7Driver? _driver; |
| | | private bool _disposed; |
| | | private readonly Stopwatch _latencyStopwatch = new Stopwatch(); |
| | | private double _lastLatencyMs; |
| | | private string _lastError = string.Empty; |
| | | private DateTime? _lastErrorTime; |
| | | private int _reconnectCount; |
| | | |
| | | /// <inheritdoc/> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsConnected => _driver != null; |
| | | |
| | | /// <inheritdoc/> |
| | | public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged; |
| | | |
| | | /// <summary> |
| | | /// 初始化 S7 PLC 驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="ip">PLC IP 地址</param> |
| | | /// <param name="port">PLC 端口号(通常为 102)</param> |
| | | public S7DriverAdapter(string deviceId, string ip, int port) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | _ip = ip ?? throw new ArgumentNullException(nameof(ip)); |
| | | _port = port; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task ConnectAsync(CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[S7] 设备 {DeviceId} 正在连接 {_ip}:{_port}..."); |
| | | |
| | | try |
| | | { |
| | | _driver = new PLCS7Driver(_ip, _port); |
| | | bool success = _driver.Connect(out string errMsg); |
| | | |
| | | if (!success) |
| | | { |
| | | _lastError = errMsg; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new InvalidOperationException($"[S7] 设备 {DeviceId} 连接失败: {errMsg}"); |
| | | } |
| | | |
| | | _lastError = string.Empty; |
| | | Log.Info($"[S7] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}"); |
| | | OnConnectionChanged(true, "连接成功"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[S7] 设备 {DeviceId} 连接异常: {ex.Message}", ex); |
| | | _driver?.Dispose(); |
| | | _driver = null; |
| | | throw; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task DisconnectAsync() |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[S7] 设备 {DeviceId} 正在断开连接..."); |
| | | |
| | | try |
| | | { |
| | | if (_driver != null) |
| | | { |
| | | _driver.DisConnect(out string errMsg); |
| | | _driver.Dispose(); |
| | | _driver = null; |
| | | } |
| | | |
| | | Log.Info($"[S7] 设备 {DeviceId} 已断开连接"); |
| | | OnConnectionChanged(false, "主动断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[S7] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex); |
| | | _driver?.Dispose(); |
| | | _driver = null; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> SendCommandAsync(string command, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[S7] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[S7] 设备 {DeviceId} 发送命令: {command}"); |
| | | |
| | | // 解析命令格式: "WRITE:DBx.DBXy.z:value" 或 "WRITE:DBx.DBWy:value" |
| | | string[] parts = command.Split(':'); |
| | | if (parts.Length < 3) |
| | | { |
| | | throw new ArgumentException($"[S7] 命令格式错误,应为 WRITE:address:value,实际: {command}"); |
| | | } |
| | | |
| | | string operation = parts[0].ToUpper(); |
| | | string address = parts[1]; |
| | | string value = parts[2]; |
| | | |
| | | if (operation != "WRITE") |
| | | { |
| | | throw new ArgumentException($"[S7] 不支持的操作类型: {operation},应为 WRITE"); |
| | | } |
| | | |
| | | // 解析地址和值,执行写入 |
| | | bool success = ParseAndWrite(address, value, out string msg); |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | |
| | | if (!success) |
| | | { |
| | | _lastError = msg; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new InvalidOperationException($"[S7] 写入失败: {msg}"); |
| | | } |
| | | |
| | | string response = "OK"; |
| | | Log.Debug($"[S7] 设备 {DeviceId} 写入成功 (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[S7] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> QueryAsync(string query, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[S7] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[S7] 设备 {DeviceId} 发送查询: {query}"); |
| | | |
| | | // 解析查询格式: "READ:DBx.DBWy" 或 "READ:DBx.DBXy.z" |
| | | string[] parts = query.Split(':'); |
| | | if (parts.Length < 2) |
| | | { |
| | | throw new ArgumentException($"[S7] 查询格式错误,应为 READ:address,实际: {query}"); |
| | | } |
| | | |
| | | string operation = parts[0].ToUpper(); |
| | | string address = parts[1]; |
| | | |
| | | if (operation != "READ") |
| | | { |
| | | throw new ArgumentException($"[S7] 不支持的操作类型: {operation},应为 READ"); |
| | | } |
| | | |
| | | // 解析地址并执行读取 |
| | | string response = ParseAndRead(address); |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | Log.Debug($"[S7] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[S7] 设备 {DeviceId} 查询失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public DeviceDriverStatus GetStatus() |
| | | { |
| | | return new DeviceDriverStatus |
| | | { |
| | | IsConnected = IsConnected, |
| | | IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError), |
| | | LastError = _lastError, |
| | | LastErrorTime = _lastErrorTime, |
| | | LatencyMs = _lastLatencyMs, |
| | | ReconnectCount = _reconnectCount |
| | | }; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) return; |
| | | _disposed = true; |
| | | |
| | | try |
| | | { |
| | | _driver?.Dispose(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[S7] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析地址并执行写入操作 |
| | | /// </summary> |
| | | /// <param name="address">S7 地址,如 "DB1.DBX0.0" 或 "DB1.DBW10"</param> |
| | | /// <param name="value">要写入的值</param> |
| | | /// <param name="msg">错误信息</param> |
| | | /// <returns>是否成功</returns> |
| | | private bool ParseAndWrite(string address, string value, out string msg) |
| | | { |
| | | msg = null; |
| | | |
| | | try |
| | | { |
| | | // 解析地址格式: DBx.DBXy.z (BOOL) 或 DBx.DBWy (INT/REAL) |
| | | if (address.Contains(".DBX")) |
| | | { |
| | | // BOOL 类型: DB1.DBX0.0 |
| | | if (bool.TryParse(value, out bool boolVal)) |
| | | { |
| | | var addrParts = ParseBoolAddress(address); |
| | | return _driver.Simens_S7_WriteBool(addrParts.db, addrParts.startAddr, addrParts.pos, boolVal, out msg); |
| | | } |
| | | throw new ArgumentException($"[S7] BOOL 值解析失败: {value}"); |
| | | } |
| | | else if (address.Contains(".DBW")) |
| | | { |
| | | // 尝试解析为不同类型 |
| | | if (float.TryParse(value, out float realVal)) |
| | | { |
| | | var addrParts = ParseWordAddress(address); |
| | | return _driver.Simens_S7_WriteReal(addrParts.db, addrParts.startAddr, realVal, out msg); |
| | | } |
| | | else if (short.TryParse(value, out short shortVal)) |
| | | { |
| | | var addrParts = ParseWordAddress(address); |
| | | return _driver.Simens_S7_WriteInt(addrParts.db, addrParts.startAddr, shortVal, out msg); |
| | | } |
| | | throw new ArgumentException($"[S7] 值解析失败: {value}"); |
| | | } |
| | | else |
| | | { |
| | | msg = $"[S7] 不支持的地址格式: {address}"; |
| | | return false; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | msg = ex.Message; |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析地址并执行读取操作 |
| | | /// </summary> |
| | | /// <param name="address">S7 地址,如 "DB1.DBX0.0" 或 "DB1.DBW10"</param> |
| | | /// <returns>读取的值字符串</returns> |
| | | private string ParseAndRead(string address) |
| | | { |
| | | if (address.Contains(".DBX")) |
| | | { |
| | | // BOOL 类型读取 |
| | | var addrParts = ParseBoolAddress(address); |
| | | if (_driver.Simens_S7_ReadBool(addrParts.db, addrParts.startAddr, addrParts.pos, out bool value, out string msg)) |
| | | { |
| | | return value.ToString(); |
| | | } |
| | | throw new InvalidOperationException($"[S7] 读取失败: {msg}"); |
| | | } |
| | | else if (address.Contains(".DBD")) |
| | | { |
| | | // REAL 类型读取 (DBD = Double Word) |
| | | var addrParts = ParseWordAddress(address); |
| | | if (_driver.Simens_S7_ReadReal(addrParts.db, addrParts.startAddr, out float realValue, out string msg)) |
| | | { |
| | | return realValue.ToString(); |
| | | } |
| | | throw new InvalidOperationException($"[S7] 读取失败: {msg}"); |
| | | } |
| | | else if (address.Contains(".DBW")) |
| | | { |
| | | // INT 类型读取 |
| | | var addrParts = ParseWordAddress(address); |
| | | if (_driver.Simens_S7_ReadInt(addrParts.db, addrParts.startAddr, out int value, out string msg)) |
| | | { |
| | | return value.ToString(); |
| | | } |
| | | throw new InvalidOperationException($"[S7] 读取失败: {msg}"); |
| | | } |
| | | else |
| | | { |
| | | throw new ArgumentException($"[S7] 不支持的地址格式: {address}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析 BOOL 地址格式: DBx.DBXy.z |
| | | /// </summary> |
| | | private (int db, int startAddr, int pos) ParseBoolAddress(string address) |
| | | { |
| | | // 格式: DB1.DBX0.0 |
| | | string upper = address.ToUpper(); |
| | | int dbStart = upper.IndexOf("DB") + 2; |
| | | int dbxStart = upper.IndexOf(".DBX"); |
| | | int db = int.Parse(upper.Substring(dbStart, dbxStart - dbStart)); |
| | | |
| | | int posStart = dbxStart + 5; // .DBX 后 |
| | | int dotPos = address.IndexOf('.', posStart); |
| | | int startAddr = int.Parse(address.Substring(posStart, dotPos - posStart)); |
| | | int pos = int.Parse(address.Substring(dotPos + 1)); |
| | | |
| | | return (db, startAddr, pos); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析 Word 地址格式: DBx.DBWy 或 DBx.DBDy |
| | | /// </summary> |
| | | private (int db, int startAddr) ParseWordAddress(string address) |
| | | { |
| | | // 格式: DB1.DBW10 或 DB1.DBD0 |
| | | string upper = address.ToUpper(); |
| | | int dbStart = upper.IndexOf("DB") + 2; |
| | | int dbwStart = upper.Contains(".DBW") ? upper.IndexOf(".DBW") : upper.IndexOf(".DBD"); |
| | | int db = int.Parse(upper.Substring(dbStart, dbwStart - dbStart)); |
| | | |
| | | int addrStart = dbwStart + 5; // .DBW 或 .DBD 后 |
| | | int startAddr = int.Parse(address.Substring(addrStart)); |
| | | |
| | | return (db, startAddr); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发连接状态变化事件 |
| | | /// </summary> |
| | | private void OnConnectionChanged(bool isConnected, string reason) |
| | | { |
| | | ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs |
| | | { |
| | | DeviceId = DeviceId, |
| | | IsConnected = isConnected, |
| | | Reason = reason |
| | | }); |
| | | |
| | | if (!isConnected) |
| | | { |
| | | _reconnectCount++; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查是否已释放 |
| | | /// </summary> |
| | | private void ThrowIfDisposed() |
| | | { |
| | | if (_disposed) throw new ObjectDisposedException(nameof(S7DriverAdapter)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Diagnostics; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | using PLCHelper; |
| | | |
| | | namespace Aging.Infrastructure.Drivers |
| | | { |
| | | /// <summary> |
| | | /// SCPI 协议驱动适配器。 |
| | | /// 封装 PLCHelper.RestTcpClient,实现统一的 IDeviceDriver 接口。 |
| | | /// 用于控制支持 SCPI 命令的仪器(如电源、电子负载等)。 |
| | | /// </summary> |
| | | public sealed class ScpiDriverAdapter : IDeviceDriver |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(ScpiDriverAdapter)); |
| | | |
| | | private readonly string _ip; |
| | | private readonly int _port; |
| | | private RestTcpClient? _client; |
| | | private bool _disposed; |
| | | private readonly Stopwatch _latencyStopwatch = new Stopwatch(); |
| | | private double _lastLatencyMs; |
| | | private string _lastError = string.Empty; |
| | | private DateTime? _lastErrorTime; |
| | | private int _reconnectCount; |
| | | |
| | | /// <inheritdoc/> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsConnected => _client != null; |
| | | |
| | | /// <inheritdoc/> |
| | | public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged; |
| | | |
| | | /// <summary> |
| | | /// 初始化 SCPI 协议驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="ip">目标 IP 地址</param> |
| | | /// <param name="port">目标端口号</param> |
| | | public ScpiDriverAdapter(string deviceId, string ip, int port) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | _ip = ip ?? throw new ArgumentNullException(nameof(ip)); |
| | | _port = port; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task ConnectAsync(CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[SCPI] 设备 {DeviceId} 正在连接 {_ip}:{_port}..."); |
| | | |
| | | try |
| | | { |
| | | _client = new RestTcpClient(_ip, _port); |
| | | _client.ReceiveTimeout = 5000; |
| | | _client.ConnectServer(); |
| | | |
| | | // 发送复位命令验证连接 |
| | | if (_client.Request("*RST", out string response, out string errMsg)) |
| | | { |
| | | Log.Info($"[SCPI] 设备 {DeviceId} 复位响应: {response}"); |
| | | } |
| | | else |
| | | { |
| | | Log.Warn($"[SCPI] 设备 {DeviceId} 复位失败: {errMsg}"); |
| | | } |
| | | |
| | | _lastError = string.Empty; |
| | | Log.Info($"[SCPI] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}"); |
| | | OnConnectionChanged(true, "连接成功"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[SCPI] 设备 {DeviceId} 连接异常: {ex.Message}", ex); |
| | | _client?.Dispose(); |
| | | _client = null; |
| | | throw; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task DisconnectAsync() |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[SCPI] 设备 {DeviceId} 正在断开连接..."); |
| | | |
| | | try |
| | | { |
| | | _client?.Dispose(); |
| | | _client = null; |
| | | |
| | | Log.Info($"[SCPI] 设备 {DeviceId} 已断开连接"); |
| | | OnConnectionChanged(false, "主动断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[SCPI] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex); |
| | | _client = null; |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> SendCommandAsync(string command, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[SCPI] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[SCPI] 设备 {DeviceId} 发送命令: {command}"); |
| | | |
| | | // SCPI 命令以 \r 结尾(RestTcpClient 协议自动处理) |
| | | bool success = _client.Request(command, out string response, out string errMsg); |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | |
| | | if (!success) |
| | | { |
| | | _lastError = errMsg; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new InvalidOperationException($"[SCPI] 命令执行失败: {errMsg}"); |
| | | } |
| | | |
| | | Log.Debug($"[SCPI] 设备 {DeviceId} 命令响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[SCPI] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task<string> QueryAsync(string query, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[SCPI] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[SCPI] 设备 {DeviceId} 发送查询: {query}"); |
| | | |
| | | // 发送查询命令并等待回读(如 "MEAS:CURR?") |
| | | bool success = _client.Request(query, out string response, out string errMsg); |
| | | |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | |
| | | if (!success) |
| | | { |
| | | _lastError = errMsg; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new TimeoutException($"[SCPI] 查询超时或失败: {errMsg}"); |
| | | } |
| | | |
| | | Log.Debug($"[SCPI] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return Task.FromResult(response); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[SCPI] 设备 {DeviceId} 查询失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public DeviceDriverStatus GetStatus() |
| | | { |
| | | return new DeviceDriverStatus |
| | | { |
| | | IsConnected = IsConnected, |
| | | IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError), |
| | | LastError = _lastError, |
| | | LastErrorTime = _lastErrorTime, |
| | | LatencyMs = _lastLatencyMs, |
| | | ReconnectCount = _reconnectCount |
| | | }; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) return; |
| | | _disposed = true; |
| | | |
| | | try |
| | | { |
| | | _client?.Dispose(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[SCPI] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发连接状态变化事件 |
| | | /// </summary> |
| | | private void OnConnectionChanged(bool isConnected, string reason) |
| | | { |
| | | ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs |
| | | { |
| | | DeviceId = DeviceId, |
| | | IsConnected = isConnected, |
| | | Reason = reason |
| | | }); |
| | | |
| | | if (!isConnected) |
| | | { |
| | | _reconnectCount++; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查是否已释放 |
| | | /// </summary> |
| | | private void ThrowIfDisposed() |
| | | { |
| | | if (_disposed) throw new ObjectDisposedException(nameof(ScpiDriverAdapter)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Diagnostics; |
| | | using System.Net.Sockets; |
| | | using System.Text; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using Aging.Infrastructure.Contracts; |
| | | using ConnectionUtil; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Drivers |
| | | { |
| | | /// <summary> |
| | | /// TCP 原始通信驱动适配器。 |
| | | /// 封装 ConnectionUtil.TcpClientWrapper,实现统一的 IDeviceDriver 接口。 |
| | | /// </summary> |
| | | public sealed class TcpRawDriverAdapter : IDeviceDriver |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(TcpRawDriverAdapter)); |
| | | |
| | | private readonly string _ip; |
| | | private readonly int _port; |
| | | private TcpClientWrapper? _client; |
| | | private bool _disposed; |
| | | private readonly Stopwatch _latencyStopwatch = new Stopwatch(); |
| | | private double _lastLatencyMs; |
| | | private string _lastError = string.Empty; |
| | | private DateTime? _lastErrorTime; |
| | | private int _reconnectCount; |
| | | |
| | | /// <inheritdoc/> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <inheritdoc/> |
| | | public bool IsConnected => _client?.IsConnected ?? false; |
| | | |
| | | /// <inheritdoc/> |
| | | public event EventHandler<DeviceConnectionEventArgs>? ConnectionChanged; |
| | | |
| | | /// <summary> |
| | | /// 初始化 TCP 原始通信驱动适配器 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备唯一标识符</param> |
| | | /// <param name="ip">目标 IP 地址</param> |
| | | /// <param name="port">目标端口号</param> |
| | | public TcpRawDriverAdapter(string deviceId, string ip, int port) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | _ip = ip ?? throw new ArgumentNullException(nameof(ip)); |
| | | _port = port; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task ConnectAsync(CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[TCP] 设备 {DeviceId} 正在连接 {_ip}:{_port}..."); |
| | | |
| | | try |
| | | { |
| | | _client = new TcpClientWrapper(_ip, _port); |
| | | bool success = await _client.ConnectAsync(); |
| | | |
| | | if (!success) |
| | | { |
| | | _lastError = "连接失败"; |
| | | _lastErrorTime = DateTime.Now; |
| | | throw new InvalidOperationException($"[TCP] 设备 {DeviceId} 连接 {_ip}:{_port} 失败"); |
| | | } |
| | | |
| | | _lastError = string.Empty; |
| | | Log.Info($"[TCP] 设备 {DeviceId} 已成功连接到 {_ip}:{_port}"); |
| | | OnConnectionChanged(true, "连接成功"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[TCP] 设备 {DeviceId} 连接异常: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public Task DisconnectAsync() |
| | | { |
| | | ThrowIfDisposed(); |
| | | Log.Info($"[TCP] 设备 {DeviceId} 正在断开连接..."); |
| | | |
| | | try |
| | | { |
| | | if (_client != null) |
| | | { |
| | | _client?.Close(); |
| | | _client = null; |
| | | } |
| | | |
| | | Log.Info($"[TCP] 设备 {DeviceId} 已断开连接"); |
| | | OnConnectionChanged(false, "主动断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[TCP] 设备 {DeviceId} 断开连接异常: {ex.Message}", ex); |
| | | } |
| | | |
| | | return Task.CompletedTask; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<string> SendCommandAsync(string command, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[TCP] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[TCP] 设备 {DeviceId} 发送命令: {command}"); |
| | | string response = await _client.SendMessageAsync(command); |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | Log.Debug($"[TCP] 设备 {DeviceId} 收到响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return response; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[TCP] 设备 {DeviceId} 发送命令失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<string> QueryAsync(string query, CancellationToken ct) |
| | | { |
| | | ThrowIfDisposed(); |
| | | if (!IsConnected) throw new InvalidOperationException("[TCP] 设备未连接"); |
| | | |
| | | _latencyStopwatch.Restart(); |
| | | try |
| | | { |
| | | Log.Debug($"[TCP] 设备 {DeviceId} 发送查询: {query}"); |
| | | // 使用较短超时进行查询操作 |
| | | string response = await _client.SendMessageAsync(query, TimeSpan.FromSeconds(5)); |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | Log.Debug($"[TCP] 设备 {DeviceId} 查询响应: {response} (延迟 {_lastLatencyMs:F2}ms)"); |
| | | return response; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _latencyStopwatch.Stop(); |
| | | _lastLatencyMs = _latencyStopwatch.Elapsed.TotalMilliseconds; |
| | | _lastError = ex.Message; |
| | | _lastErrorTime = DateTime.Now; |
| | | Log.Error($"[TCP] 设备 {DeviceId} 查询失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public DeviceDriverStatus GetStatus() |
| | | { |
| | | return new DeviceDriverStatus |
| | | { |
| | | IsConnected = IsConnected, |
| | | IsHealthy = IsConnected && string.IsNullOrEmpty(_lastError), |
| | | LastError = _lastError, |
| | | LastErrorTime = _lastErrorTime, |
| | | LatencyMs = _lastLatencyMs, |
| | | ReconnectCount = _reconnectCount |
| | | }; |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public void Dispose() |
| | | { |
| | | if (_disposed) return; |
| | | _disposed = true; |
| | | |
| | | try |
| | | { |
| | | _client?.Dispose(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[TCP] 设备 {DeviceId} 释放资源异常: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 触发连接状态变化事件 |
| | | /// </summary> |
| | | private void OnConnectionChanged(bool isConnected, string reason) |
| | | { |
| | | ConnectionChanged?.Invoke(this, new DeviceConnectionEventArgs |
| | | { |
| | | DeviceId = DeviceId, |
| | | IsConnected = isConnected, |
| | | Reason = reason |
| | | }); |
| | | |
| | | if (!isConnected) |
| | | { |
| | | _reconnectCount++; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查是否已释放 |
| | | /// </summary> |
| | | private void ThrowIfDisposed() |
| | | { |
| | | if (_disposed) throw new ObjectDisposedException(nameof(TcpRawDriverAdapter)); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using log4net; |
| | | using HMI.Core.Util; |
| | | using Aging.Infrastructure.Contracts; |
| | | using Aging.Infrastructure.Core; |
| | | using Aging.Infrastructure.Services; |
| | | using Aging.Infrastructure.Drivers; |
| | | |
| | | namespace Aging.Infrastructure |
| | | { |
| | | /// <summary> |
| | | /// 基础设施启动引导 - 初始化并注册所有核心服务到 UtilContainer |
| | | /// </summary> |
| | | public static class InfrastructureBootstrapper |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(InfrastructureBootstrapper)); |
| | | private static bool _initialized = false; |
| | | |
| | | /// <summary> |
| | | /// 初始化基础设施服务 |
| | | /// 应在 WPF 主程序启动时或插件 InitFinish 中调用 |
| | | /// </summary> |
| | | /// <param name="deviceConfigDbPath">DeviceConfig 插件的 SQLite 数据库路径</param> |
| | | public static void Initialize(string deviceConfigDbPath) |
| | | { |
| | | if (_initialized) |
| | | { |
| | | log.Warn("基础设施已初始化,跳过重复初始化"); |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | log.Info("开始初始化基础设施服务..."); |
| | | |
| | | // 1. 创建 ResourceManager 单例 |
| | | var resourceManager = new ResourceManager(); |
| | | UtilContainer.RegisterUtil("ResourceManager", resourceManager); |
| | | log.Info("ResourceManager 已注册"); |
| | | |
| | | // 2. 创建 DeviceManager 单例 |
| | | var deviceManager = new DeviceManager(); |
| | | UtilContainer.RegisterUtil("DeviceManager", deviceManager); |
| | | log.Info("DeviceManager 已注册"); |
| | | |
| | | // 3. 创建 SlotDeviceMappingResolver(需要 DeviceConfig DB 路径) |
| | | var mappingResolver = new SlotDeviceMappingResolver(deviceConfigDbPath); |
| | | UtilContainer.RegisterUtil("SlotDeviceMappingResolver", mappingResolver); |
| | | log.Info("SlotDeviceMappingResolver 已注册"); |
| | | |
| | | // 4. 创建 DeviceService |
| | | var deviceService = new DeviceService(resourceManager, deviceManager, mappingResolver); |
| | | UtilContainer.RegisterUtil("DeviceService", deviceService); |
| | | log.Info("DeviceService 已注册"); |
| | | |
| | | // 5. 创建 PowerSupplyService |
| | | var powerSupplyService = new PowerSupplyService(resourceManager, deviceManager, mappingResolver); |
| | | UtilContainer.RegisterUtil("PowerSupplyService", powerSupplyService); |
| | | log.Info("PowerSupplyService 已注册"); |
| | | |
| | | // 6. 从数据库加载设备定义并注册驱动 |
| | | LoadDevicesFromConfig(deviceManager, mappingResolver); |
| | | |
| | | _initialized = true; |
| | | log.Info("基础设施服务初始化完成"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("基础设施初始化失败", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从 DeviceConfig 数据库加载设备定义并注册驱动 |
| | | /// </summary> |
| | | private static void LoadDevicesFromConfig(IDeviceManager deviceManager, SlotDeviceMappingResolver mappingResolver) |
| | | { |
| | | try |
| | | { |
| | | // 读取所有设备映射 |
| | | var mappings = mappingResolver.GetAllMappings(); |
| | | // 从映射中提取唯一的设备ID |
| | | var deviceIds = mappings |
| | | .Select(m => m.DeviceId) |
| | | .Distinct() |
| | | .ToList(); |
| | | |
| | | int loaded = 0; |
| | | foreach (var deviceId in deviceIds) |
| | | { |
| | | try |
| | | { |
| | | // 这里暂时使用通用 TCP 驱动作为占位 |
| | | // 实际应该从 device_definition 表读取 transport 和 endpoint |
| | | // 然后通过 DriverFactory.CreateDriver 创建对应驱动 |
| | | // |
| | | // TODO: 从数据库读取设备详细信息(transport, endpoint) |
| | | // var device = GetDeviceDefinition(deviceId); |
| | | // var driver = DriverFactory.CreateDriver(deviceId, device.Transport, device.Endpoint); |
| | | |
| | | log.Debug($"设备 {deviceId} 驱动注册待配置详细信息"); |
| | | loaded++; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"注册设备 {deviceId} 驱动失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | log.Info($"从设备配置加载了 {loaded} 个设备驱动"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("加载设备配置失败", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 注册单个设备驱动(供外部调用,用于动态添加设备) |
| | | /// </summary> |
| | | public static void RegisterDevice(string deviceId, string transport, string endpoint) |
| | | { |
| | | var container = new UtilContainer(); |
| | | var deviceManager = container.GetUtil<IDeviceManager>("DeviceManager"); |
| | | if (deviceManager == null) |
| | | { |
| | | log.Error("DeviceManager 未初始化"); |
| | | return; |
| | | } |
| | | |
| | | try |
| | | { |
| | | var driver = DriverFactory.CreateDriver(deviceId, transport, endpoint); |
| | | deviceManager.RegisterDriver(deviceId, driver); |
| | | log.Info($"设备 {deviceId} ({transport}) 驱动已注册"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"注册设备 {deviceId} 失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 重置基础设施(用于测试或重新初始化) |
| | | /// </summary> |
| | | public static void Reset() |
| | | { |
| | | _initialized = false; |
| | | log.Info("基础设施已重置"); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Diagnostics; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Services |
| | | { |
| | | /// <summary> |
| | | /// 设备服务实现。 |
| | | /// 协调 ResourceManager、DeviceManager 和 SlotMapping, |
| | | /// 提供面向 DeviceId 的命令执行、查询和安全关机功能。 |
| | | /// 所有操作均需通过资源预约验证和租约管理,确保硬件资源的安全访问。 |
| | | /// </summary> |
| | | public sealed class DeviceService : IDeviceService |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(DeviceService)); |
| | | |
| | | /// <summary> |
| | | /// 资源管理器 |
| | | /// </summary> |
| | | private readonly IResourceManager _resourceManager; |
| | | |
| | | /// <summary> |
| | | /// 设备管理器 |
| | | /// </summary> |
| | | private readonly IDeviceManager _deviceManager; |
| | | |
| | | /// <summary> |
| | | /// Slot-设备-通道映射解析器 |
| | | /// </summary> |
| | | private readonly SlotDeviceMappingResolver _slotMappingResolver; |
| | | |
| | | /// <summary> |
| | | /// 初始化设备服务实例 |
| | | /// </summary> |
| | | /// <param name="resourceManager">资源管理器</param> |
| | | /// <param name="deviceManager">设备管理器</param> |
| | | /// <param name="slotMappingResolver">Slot 映射解析器</param> |
| | | public DeviceService( |
| | | IResourceManager resourceManager, |
| | | IDeviceManager deviceManager, |
| | | SlotDeviceMappingResolver slotMappingResolver) |
| | | { |
| | | _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager)); |
| | | _deviceManager = deviceManager ?? throw new ArgumentNullException(nameof(deviceManager)); |
| | | _slotMappingResolver = slotMappingResolver ?? throw new ArgumentNullException(nameof(slotMappingResolver)); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<DeviceCommandResult> ExecuteCommandAsync( |
| | | string deviceId, string command, ResourceOwnerToken owner, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | if (string.IsNullOrWhiteSpace(command)) |
| | | throw new ArgumentException("命令不能为空", nameof(command)); |
| | | if (owner == null) |
| | | throw new ArgumentNullException(nameof(owner)); |
| | | |
| | | var stopwatch = Stopwatch.StartNew(); |
| | | var resourceKey = new ResourceKey(deviceId, "Generic"); |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[DeviceService] 执行命令: Device={deviceId}, Command={command}, Owner={owner.OwnerId}"); |
| | | |
| | | // 1. 获取设备驱动 |
| | | var driver = _deviceManager.GetDriver(deviceId); |
| | | if (driver == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[DeviceService] 设备 {deviceId} 驱动不存在"); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "DRIVER_NOT_FOUND", |
| | | ErrorMessage = $"设备 {deviceId} 的驱动未注册", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | |
| | | // 2. 验证预约 |
| | | if (!_resourceManager.ValidateOwner(owner, resourceKey)) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[DeviceService] 权限验证失败: Device={deviceId}, Owner={owner.OwnerId}"); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "PERMISSION_DENIED", |
| | | ErrorMessage = $"所有者 {owner.OwnerId} 未预约设备 {deviceId} 或令牌已过期", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | |
| | | // 3. 获取租约并执行命令 |
| | | using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false); |
| | | |
| | | var response = await driver.SendCommandAsync(command, ct).ConfigureAwait(false); |
| | | |
| | | stopwatch.Stop(); |
| | | Log.Info($"[DeviceService] 命令执行成功: Device={deviceId}, Command={command}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms"); |
| | | |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = true, |
| | | RawResponse = response, |
| | | Readback = response, |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (ResourceConflictException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[DeviceService] 资源冲突: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "RESOURCE_CONFLICT", |
| | | ErrorMessage = ex.Message, |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[DeviceService] 命令执行被取消: Device={deviceId}"); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "OPERATION_CANCELLED", |
| | | ErrorMessage = "操作已被取消", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (TimeoutException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[DeviceService] 命令执行超时: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "TIMEOUT", |
| | | ErrorMessage = $"设备 {deviceId} 命令执行超时: {ex.Message}", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[DeviceService] 命令执行异常: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "UNKNOWN_ERROR", |
| | | ErrorMessage = $"设备 {deviceId} 命令执行失败: {ex.Message}", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task<DeviceCommandResult> ExecuteQueryAsync( |
| | | string deviceId, string query, ResourceOwnerToken owner, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | if (string.IsNullOrWhiteSpace(query)) |
| | | throw new ArgumentException("查询命令不能为空", nameof(query)); |
| | | if (owner == null) |
| | | throw new ArgumentNullException(nameof(owner)); |
| | | |
| | | var stopwatch = Stopwatch.StartNew(); |
| | | var resourceKey = new ResourceKey(deviceId, "Generic"); |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[DeviceService] 执行查询: Device={deviceId}, Query={query}, Owner={owner.OwnerId}"); |
| | | |
| | | // 1. 获取设备驱动 |
| | | var driver = _deviceManager.GetDriver(deviceId); |
| | | if (driver == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[DeviceService] 设备 {deviceId} 驱动不存在"); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "DRIVER_NOT_FOUND", |
| | | ErrorMessage = $"设备 {deviceId} 的驱动未注册", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | |
| | | // 2. 验证预约 |
| | | if (!_resourceManager.ValidateOwner(owner, resourceKey)) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[DeviceService] 权限验证失败: Device={deviceId}, Owner={owner.OwnerId}"); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "PERMISSION_DENIED", |
| | | ErrorMessage = $"所有者 {owner.OwnerId} 未预约设备 {deviceId} 或令牌已过期", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | |
| | | // 3. 获取租约并执行查询 |
| | | using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false); |
| | | |
| | | var response = await driver.QueryAsync(query, ct).ConfigureAwait(false); |
| | | |
| | | stopwatch.Stop(); |
| | | Log.Info($"[DeviceService] 查询执行成功: Device={deviceId}, Query={query}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms"); |
| | | |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = true, |
| | | RawResponse = response, |
| | | Readback = response, |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (ResourceConflictException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[DeviceService] 资源冲突: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "RESOURCE_CONFLICT", |
| | | ErrorMessage = ex.Message, |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[DeviceService] 查询执行被取消: Device={deviceId}"); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "OPERATION_CANCELLED", |
| | | ErrorMessage = "操作已被取消", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (TimeoutException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[DeviceService] 查询执行超时: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "TIMEOUT", |
| | | ErrorMessage = $"设备 {deviceId} 查询执行超时: {ex.Message}", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[DeviceService] 查询执行异常: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | return new DeviceCommandResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = "UNKNOWN_ERROR", |
| | | ErrorMessage = $"设备 {deviceId} 查询执行失败: {ex.Message}", |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public async Task SafeShutdownAsync(string deviceId, ResourceOwnerToken owner, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(deviceId)) |
| | | throw new ArgumentException("设备ID不能为空", nameof(deviceId)); |
| | | if (owner == null) |
| | | throw new ArgumentNullException(nameof(owner)); |
| | | |
| | | var resourceKey = new ResourceKey(deviceId, "Generic"); |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[DeviceService] 开始安全关机: Device={deviceId}, Owner={owner.OwnerId}"); |
| | | |
| | | // 1. 获取驱动 |
| | | var driver = _deviceManager.GetDriver(deviceId); |
| | | if (driver == null) |
| | | { |
| | | Log.Warn($"[DeviceService] 安全关机失败: 设备 {deviceId} 驱动不存在"); |
| | | return; |
| | | } |
| | | |
| | | // 2. 验证预约 |
| | | if (!_resourceManager.ValidateOwner(owner, resourceKey)) |
| | | { |
| | | Log.Warn($"[DeviceService] 安全关机权限验证失败: Device={deviceId}, Owner={owner.OwnerId}"); |
| | | return; |
| | | } |
| | | |
| | | // 3. 获取租约 |
| | | using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false); |
| | | |
| | | // 4. 发送安全关闭命令 |
| | | Log.Info($"[DeviceService] 发送安全关闭命令: Device={deviceId}"); |
| | | await driver.SendCommandAsync("OUTP OFF", ct).ConfigureAwait(false); |
| | | |
| | | // 5. 等待确认(短暂延迟确保命令执行完成) |
| | | await Task.Delay(500, ct).ConfigureAwait(false); |
| | | |
| | | // 6. 验证关闭状态 |
| | | var statusResponse = await driver.QueryAsync("OUTP?", ct).ConfigureAwait(false); |
| | | Log.Info($"[DeviceService] 设备 {deviceId} 关闭状态确认: {statusResponse}"); |
| | | |
| | | Log.Info($"[DeviceService] 安全关机完成: Device={deviceId}"); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | Log.Warn($"[DeviceService] 安全关机操作被取消: Device={deviceId}"); |
| | | throw; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[DeviceService] 安全关机异常: Device={deviceId}, 错误: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Diagnostics; |
| | | using Aging.Infrastructure.Contracts; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Services |
| | | { |
| | | /// <summary> |
| | | /// 电源配置命令 |
| | | /// </summary> |
| | | public sealed class PowerCommand |
| | | { |
| | | /// <summary> |
| | | /// 目标电压值(可选,单位: V) |
| | | /// </summary> |
| | | public double? Voltage { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 目标电流限制值(可选,单位: A) |
| | | /// </summary> |
| | | public double? CurrentLimit { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 输出开关状态(true=ON, false=OFF, null=不改变) |
| | | /// </summary> |
| | | public bool? Output { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 电源操作结果 |
| | | /// </summary> |
| | | public sealed class PowerOperationResult |
| | | { |
| | | /// <summary> |
| | | /// 操作是否成功 |
| | | /// </summary> |
| | | public bool Success { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 实测电压值(单位: V) |
| | | /// </summary> |
| | | public double? MeasuredVoltage { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 实测电流值(单位: A) |
| | | /// </summary> |
| | | public double? MeasuredCurrent { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 设备原始响应字符串 |
| | | /// </summary> |
| | | public string? RawResponse { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 错误代码 |
| | | /// </summary> |
| | | public string? ErrorCode { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 错误信息 |
| | | /// </summary> |
| | | public string? ErrorMessage { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 操作延迟(毫秒) |
| | | /// </summary> |
| | | public double LatencyMs { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 创建一个成功的结果 |
| | | /// </summary> |
| | | public static PowerOperationResult Succeed(double? voltage, double? current, string? rawResponse, double latencyMs) |
| | | { |
| | | return new PowerOperationResult |
| | | { |
| | | Success = true, |
| | | MeasuredVoltage = voltage, |
| | | MeasuredCurrent = current, |
| | | RawResponse = rawResponse, |
| | | LatencyMs = latencyMs |
| | | }; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 创建一个失败的结果 |
| | | /// </summary> |
| | | public static PowerOperationResult Failed(string errorCode, string errorMessage, double latencyMs) |
| | | { |
| | | return new PowerOperationResult |
| | | { |
| | | Success = false, |
| | | ErrorCode = errorCode, |
| | | ErrorMessage = errorMessage, |
| | | LatencyMs = latencyMs |
| | | }; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 电源配置服务 - 面向 Slot 的高层电源操作。 |
| | | /// 通过 SlotDeviceMappingResolver 解析 Slot 到具体设备端点的映射, |
| | | /// 协调 ResourceManager 和 DeviceManager 完成电源配置、测量和状态查询。 |
| | | /// </summary> |
| | | public sealed class PowerSupplyService |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(PowerSupplyService)); |
| | | |
| | | /// <summary> |
| | | /// 资源管理器 |
| | | /// </summary> |
| | | private readonly IResourceManager _resourceManager; |
| | | |
| | | /// <summary> |
| | | /// 设备管理器 |
| | | /// </summary> |
| | | private readonly IDeviceManager _deviceManager; |
| | | |
| | | /// <summary> |
| | | /// Slot-设备-通道映射解析器 |
| | | /// </summary> |
| | | private readonly SlotDeviceMappingResolver _slotMappingResolver; |
| | | |
| | | /// <summary> |
| | | /// 初始化电源配置服务实例 |
| | | /// </summary> |
| | | /// <param name="resourceManager">资源管理器</param> |
| | | /// <param name="deviceManager">设备管理器</param> |
| | | /// <param name="slotMappingResolver">Slot 映射解析器</param> |
| | | public PowerSupplyService( |
| | | IResourceManager resourceManager, |
| | | IDeviceManager deviceManager, |
| | | SlotDeviceMappingResolver slotMappingResolver) |
| | | { |
| | | _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager)); |
| | | _deviceManager = deviceManager ?? throw new ArgumentNullException(nameof(deviceManager)); |
| | | _slotMappingResolver = slotMappingResolver ?? throw new ArgumentNullException(nameof(slotMappingResolver)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 异步配置 Slot 对应的电源参数。 |
| | | /// 根据 SlotId 解析映射获取设备端点,验证资源预约后发送 SCPI 命令配置电源。 |
| | | /// </summary> |
| | | /// <param name="slotId">Slot 标识符</param> |
| | | /// <param name="command">电源配置命令</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>电源操作结果,包含测量值</returns> |
| | | public async Task<PowerOperationResult> ConfigureAsync( |
| | | string slotId, PowerCommand command, ResourceOwnerToken owner, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(slotId)) |
| | | throw new ArgumentException("SlotId 不能为空", nameof(slotId)); |
| | | if (command == null) |
| | | throw new ArgumentNullException(nameof(command)); |
| | | if (owner == null) |
| | | throw new ArgumentNullException(nameof(owner)); |
| | | |
| | | var stopwatch = Stopwatch.StartNew(); |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[PowerSupplyService] 配置电源: Slot={slotId}, Owner={owner.OwnerId}"); |
| | | |
| | | // 1. 解析 Slot 映射 |
| | | var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply"); |
| | | if (endpoint == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] Slot {slotId} 未找到 PowerSupply 映射"); |
| | | return PowerOperationResult.Failed("MAPPING_NOT_FOUND", |
| | | $"Slot {slotId} 未找到 PowerSupply 类型的设备映射", stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | var resourceKey = endpoint.ToResourceKey(); |
| | | |
| | | // 2. 验证预约 |
| | | if (!_resourceManager.ValidateOwner(owner, resourceKey)) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 权限验证失败: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}"); |
| | | return PowerOperationResult.Failed("PERMISSION_DENIED", |
| | | $"所有者 {owner.OwnerId} 未预约 Slot {slotId} 对应的设备 {endpoint.DeviceId} 或令牌已过期", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | // 3. 获取租约 |
| | | using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false); |
| | | |
| | | // 4. 获取设备驱动 |
| | | var driver = _deviceManager.GetDriver(endpoint.DeviceId); |
| | | if (driver == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 设备 {endpoint.DeviceId} 驱动不存在"); |
| | | return PowerOperationResult.Failed("DRIVER_NOT_FOUND", |
| | | $"设备 {endpoint.DeviceId} 的驱动未注册", stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | // 5. 构建并发送 SCPI 命令 |
| | | var scpiCommands = BuildScpiCommands(command); |
| | | foreach (var scpiCommand in scpiCommands) |
| | | { |
| | | Log.Info($"[PowerSupplyService] 发送 SCPI 命令: Device={endpoint.DeviceId}, Command={scpiCommand}"); |
| | | await driver.SendCommandAsync(scpiCommand, ct).ConfigureAwait(false); |
| | | } |
| | | |
| | | // 6. 回读测量值 |
| | | string rawResponse; |
| | | try |
| | | { |
| | | // 优先尝试 MEAS:ALL? 一次性获取所有测量值 |
| | | rawResponse = await driver.QueryAsync("MEAS:ALL?", ct).ConfigureAwait(false); |
| | | Log.Info($"[PowerSupplyService] MEAS:ALL? 响应: {rawResponse}"); |
| | | } |
| | | catch |
| | | { |
| | | // 如果 MEAS:ALL? 失败,分别查询电压和电流 |
| | | Log.Info($"[PowerSupplyService] MEAS:ALL? 失败,尝试分别查询 VOLT? 和 CURR?"); |
| | | var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false); |
| | | var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false); |
| | | rawResponse = $"{voltageResponse},{currentResponse}"; |
| | | } |
| | | |
| | | // 7. 解析回读值 |
| | | ParseMeasurementResponse(rawResponse, out var measuredVoltage, out var measuredCurrent); |
| | | |
| | | stopwatch.Stop(); |
| | | Log.Info($"[PowerSupplyService] 电源配置完成: Slot={slotId}, Device={endpoint.DeviceId}, " + |
| | | $"V={measuredVoltage}, I={measuredCurrent}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms"); |
| | | |
| | | return PowerOperationResult.Succeed(measuredVoltage, measuredCurrent, rawResponse, |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (ResourceConflictException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 资源冲突: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 电源配置被取消: Slot={slotId}"); |
| | | return PowerOperationResult.Failed("OPERATION_CANCELLED", "操作已被取消", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (TimeoutException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 电源配置超时: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("TIMEOUT", $"电源配置超时: {ex.Message}", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 电源配置异常: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("UNKNOWN_ERROR", $"电源配置失败: {ex.Message}", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 异步测量 Slot 对应电源的输出值(电压、电流)。 |
| | | /// 根据 SlotId 解析映射获取设备端点,查询实时测量值。 |
| | | /// </summary> |
| | | /// <param name="slotId">Slot 标识符</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>电源操作结果,包含测量值</returns> |
| | | public async Task<PowerOperationResult> MeasureAsync( |
| | | string slotId, ResourceOwnerToken owner, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(slotId)) |
| | | throw new ArgumentException("SlotId 不能为空", nameof(slotId)); |
| | | if (owner == null) |
| | | throw new ArgumentNullException(nameof(owner)); |
| | | |
| | | var stopwatch = Stopwatch.StartNew(); |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[PowerSupplyService] 测量电源: Slot={slotId}, Owner={owner.OwnerId}"); |
| | | |
| | | // 1. 解析 Slot 映射 |
| | | var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply"); |
| | | if (endpoint == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] Slot {slotId} 未找到 PowerSupply 映射"); |
| | | return PowerOperationResult.Failed("MAPPING_NOT_FOUND", |
| | | $"Slot {slotId} 未找到 PowerSupply 类型的设备映射", stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | var resourceKey = endpoint.ToResourceKey(); |
| | | |
| | | // 2. 验证预约 |
| | | if (!_resourceManager.ValidateOwner(owner, resourceKey)) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 权限验证失败: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}"); |
| | | return PowerOperationResult.Failed("PERMISSION_DENIED", |
| | | $"所有者 {owner.OwnerId} 未预约 Slot {slotId} 对应的设备 {endpoint.DeviceId} 或令牌已过期", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | // 3. 获取租约 |
| | | using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false); |
| | | |
| | | // 4. 获取设备驱动 |
| | | var driver = _deviceManager.GetDriver(endpoint.DeviceId); |
| | | if (driver == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 设备 {endpoint.DeviceId} 驱动不存在"); |
| | | return PowerOperationResult.Failed("DRIVER_NOT_FOUND", |
| | | $"设备 {endpoint.DeviceId} 的驱动未注册", stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | // 5. 查询测量值 |
| | | string rawResponse; |
| | | try |
| | | { |
| | | rawResponse = await driver.QueryAsync("MEAS:ALL?", ct).ConfigureAwait(false); |
| | | } |
| | | catch |
| | | { |
| | | var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false); |
| | | var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false); |
| | | rawResponse = $"{voltageResponse},{currentResponse}"; |
| | | } |
| | | |
| | | // 6. 解析回读值 |
| | | ParseMeasurementResponse(rawResponse, out var measuredVoltage, out var measuredCurrent); |
| | | |
| | | stopwatch.Stop(); |
| | | Log.Info($"[PowerSupplyService] 测量完成: Slot={slotId}, Device={endpoint.DeviceId}, " + |
| | | $"V={measuredVoltage}, I={measuredCurrent}, 延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms"); |
| | | |
| | | return PowerOperationResult.Succeed(measuredVoltage, measuredCurrent, rawResponse, |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (ResourceConflictException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 资源冲突: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 测量操作被取消: Slot={slotId}"); |
| | | return PowerOperationResult.Failed("OPERATION_CANCELLED", "操作已被取消", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (TimeoutException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 测量操作超时: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("TIMEOUT", $"测量操作超时: {ex.Message}", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 测量操作异常: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("UNKNOWN_ERROR", $"测量操作失败: {ex.Message}", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 异步获取 Slot 对应电源的当前状态(电压、电流、输出开关状态)。 |
| | | /// </summary> |
| | | /// <param name="slotId">Slot 标识符</param> |
| | | /// <param name="owner">所有者令牌</param> |
| | | /// <param name="ct">取消令牌</param> |
| | | /// <returns>电源操作结果,包含状态信息</returns> |
| | | public async Task<PowerOperationResult> GetPowerStatusAsync( |
| | | string slotId, ResourceOwnerToken owner, CancellationToken ct) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(slotId)) |
| | | throw new ArgumentException("SlotId 不能为空", nameof(slotId)); |
| | | if (owner == null) |
| | | throw new ArgumentNullException(nameof(owner)); |
| | | |
| | | var stopwatch = Stopwatch.StartNew(); |
| | | |
| | | try |
| | | { |
| | | Log.Info($"[PowerSupplyService] 查询电源状态: Slot={slotId}, Owner={owner.OwnerId}"); |
| | | |
| | | // 1. 解析 Slot 映射 |
| | | var endpoint = _slotMappingResolver.ResolveEndpoint(slotId, "PowerSupply"); |
| | | if (endpoint == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] Slot {slotId} 未找到 PowerSupply 映射"); |
| | | return PowerOperationResult.Failed("MAPPING_NOT_FOUND", |
| | | $"Slot {slotId} 未找到 PowerSupply 类型的设备映射", stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | var resourceKey = endpoint.ToResourceKey(); |
| | | |
| | | // 2. 验证预约 |
| | | if (!_resourceManager.ValidateOwner(owner, resourceKey)) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 权限验证失败: Slot={slotId}, Device={endpoint.DeviceId}, Owner={owner.OwnerId}"); |
| | | return PowerOperationResult.Failed("PERMISSION_DENIED", |
| | | $"所有者 {owner.OwnerId} 未预约 Slot {slotId} 对应的设备 {endpoint.DeviceId} 或令牌已过期", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | // 3. 获取租约 |
| | | using var lease = await _resourceManager.AcquireOperationAsync(owner, resourceKey, ct).ConfigureAwait(false); |
| | | |
| | | // 4. 获取设备驱动 |
| | | var driver = _deviceManager.GetDriver(endpoint.DeviceId); |
| | | if (driver == null) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 设备 {endpoint.DeviceId} 驱动不存在"); |
| | | return PowerOperationResult.Failed("DRIVER_NOT_FOUND", |
| | | $"设备 {endpoint.DeviceId} 的驱动未注册", stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | |
| | | // 5. 查询电压、电流和输出状态 |
| | | var voltageResponse = await driver.QueryAsync("VOLT?", ct).ConfigureAwait(false); |
| | | var currentResponse = await driver.QueryAsync("CURR?", ct).ConfigureAwait(false); |
| | | var outputResponse = await driver.QueryAsync("OUTP?", ct).ConfigureAwait(false); |
| | | |
| | | var rawResponse = $"VOLT={voltageResponse},CURR={currentResponse},OUTP={outputResponse}"; |
| | | |
| | | // 6. 解析测量值 |
| | | ParseMeasurementResponse($"{voltageResponse},{currentResponse}", |
| | | out var measuredVoltage, out var measuredCurrent); |
| | | |
| | | stopwatch.Stop(); |
| | | Log.Info($"[PowerSupplyService] 电源状态查询完成: Slot={slotId}, Device={endpoint.DeviceId}, " + |
| | | $"V={measuredVoltage}, I={measuredCurrent}, OUTP={outputResponse}, " + |
| | | $"延迟={stopwatch.Elapsed.TotalMilliseconds:F2}ms"); |
| | | |
| | | return new PowerOperationResult |
| | | { |
| | | Success = true, |
| | | MeasuredVoltage = measuredVoltage, |
| | | MeasuredCurrent = measuredCurrent, |
| | | RawResponse = rawResponse, |
| | | LatencyMs = stopwatch.Elapsed.TotalMilliseconds |
| | | }; |
| | | } |
| | | catch (ResourceConflictException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 资源冲突: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("RESOURCE_CONFLICT", ex.Message, stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (OperationCanceledException) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Warn($"[PowerSupplyService] 状态查询被取消: Slot={slotId}"); |
| | | return PowerOperationResult.Failed("OPERATION_CANCELLED", "操作已被取消", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (TimeoutException ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 状态查询超时: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("TIMEOUT", $"状态查询超时: {ex.Message}", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | stopwatch.Stop(); |
| | | Log.Error($"[PowerSupplyService] 状态查询异常: Slot={slotId}, 错误: {ex.Message}", ex); |
| | | return PowerOperationResult.Failed("UNKNOWN_ERROR", $"状态查询失败: {ex.Message}", |
| | | stopwatch.Elapsed.TotalMilliseconds); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据电源配置命令构建 SCPI 命令列表 |
| | | /// </summary> |
| | | /// <param name="command">电源配置命令</param> |
| | | /// <returns>SCPI 命令字符串列表</returns> |
| | | private static List<string> BuildScpiCommands(PowerCommand command) |
| | | { |
| | | var commands = new List<string>(); |
| | | |
| | | if (command.Voltage.HasValue) |
| | | { |
| | | commands.Add($"VOLT {command.Voltage.Value:G}"); |
| | | } |
| | | |
| | | if (command.CurrentLimit.HasValue) |
| | | { |
| | | commands.Add($"CURR {command.CurrentLimit.Value:G}"); |
| | | } |
| | | |
| | | if (command.Output.HasValue) |
| | | { |
| | | commands.Add(command.Output.Value ? "OUTP ON" : "OUTP OFF"); |
| | | } |
| | | |
| | | return commands; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析设备返回的测量值响应字符串。 |
| | | /// 支持格式: "voltage,current" 或单个数值。 |
| | | /// </summary> |
| | | /// <param name="rawResponse">原始响应字符串</param> |
| | | /// <param name="voltage">解析后的电压值</param> |
| | | /// <param name="current">解析后的电流值</param> |
| | | private static void ParseMeasurementResponse(string rawResponse, out double? voltage, out double? current) |
| | | { |
| | | voltage = null; |
| | | current = null; |
| | | |
| | | if (string.IsNullOrWhiteSpace(rawResponse)) |
| | | return; |
| | | |
| | | try |
| | | { |
| | | var parts = rawResponse.Split(','); |
| | | |
| | | if (parts.Length >= 1 && double.TryParse(parts[0].Trim(), out var v)) |
| | | { |
| | | voltage = v; |
| | | } |
| | | |
| | | if (parts.Length >= 2 && double.TryParse(parts[1].Trim(), out var c)) |
| | | { |
| | | current = c; |
| | | } |
| | | } |
| | | catch (Exception) |
| | | { |
| | | // 解析失败时保持 null 值 |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System.Data; |
| | | using Aging.Infrastructure.Contracts; |
| | | using HMI.Core.Util; |
| | | using log4net; |
| | | |
| | | namespace Aging.Infrastructure.Services |
| | | { |
| | | /// <summary> |
| | | /// 电源端点信息,描述 Slot 映射到的具体设备通道 |
| | | /// </summary> |
| | | public sealed class PowerEndpoint |
| | | { |
| | | /// <summary> |
| | | /// 设备标识符 |
| | | /// </summary> |
| | | public string DeviceId { get; } |
| | | |
| | | /// <summary> |
| | | /// 通道标识 |
| | | /// </summary> |
| | | public int ChannelId { get; } |
| | | |
| | | /// <summary> |
| | | /// 生效版本号 |
| | | /// </summary> |
| | | public string EffectiveVersion { get; } |
| | | |
| | | /// <summary> |
| | | /// 初始化电源端点实例 |
| | | /// </summary> |
| | | /// <param name="deviceId">设备标识符</param> |
| | | /// <param name="channelId">通道标识</param> |
| | | /// <param name="effectiveVersion">生效版本号</param> |
| | | public PowerEndpoint(string deviceId, int channelId, string effectiveVersion) |
| | | { |
| | | DeviceId = deviceId ?? throw new ArgumentNullException(nameof(deviceId)); |
| | | ChannelId = channelId; |
| | | EffectiveVersion = effectiveVersion ?? string.Empty; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 转换为资源键,用于资源管理器的预约和租约操作 |
| | | /// </summary> |
| | | /// <returns>对应的资源键实例</returns> |
| | | public ResourceKey ToResourceKey() |
| | | { |
| | | return new ResourceKey(DeviceId, "PowerSupply", ChannelId.ToString()); |
| | | } |
| | | |
| | | /// <inheritdoc/> |
| | | public override string ToString() => $"{DeviceId}:CH{ChannelId}(v{EffectiveVersion})"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// Slot-设备-通道映射解析器。 |
| | | /// 从 Plugin.DeviceConfig 的 SQLite 数据库读取 Slot-Device-Channel 映射关系, |
| | | /// 提供根据 SlotId 和能力类型解析具体设备端点的功能。 |
| | | /// </summary> |
| | | public sealed class SlotDeviceMappingResolver |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(SlotDeviceMappingResolver)); |
| | | |
| | | /// <summary> |
| | | /// DeviceConfig 的 SQLite 数据库文件路径 |
| | | /// </summary> |
| | | private readonly string _deviceConfigDbPath; |
| | | |
| | | /// <summary> |
| | | /// 初始化映射解析器实例 |
| | | /// </summary> |
| | | /// <param name="deviceConfigDbPath">DeviceConfig 的 SQLite 数据库文件路径</param> |
| | | public SlotDeviceMappingResolver(string deviceConfigDbPath) |
| | | { |
| | | _deviceConfigDbPath = deviceConfigDbPath ?? throw new ArgumentNullException(nameof(deviceConfigDbPath)); |
| | | Log.Info($"[SlotDeviceMappingResolver] 初始化完成,数据库路径: {deviceConfigDbPath}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据 SlotId 和能力类型解析具体的电源端点。 |
| | | /// 从 slot_device_mapping 表查询匹配的最新版本映射记录。 |
| | | /// </summary> |
| | | /// <param name="slotId">Slot 标识符</param> |
| | | /// <param name="capability">能力类型(如 "PowerSupply")</param> |
| | | /// <returns>解析后的电源端点;未找到匹配映射时返回 null</returns> |
| | | public PowerEndpoint? ResolveEndpoint(string slotId, string capability) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(slotId)) |
| | | throw new ArgumentException("SlotId 不能为空", nameof(slotId)); |
| | | if (string.IsNullOrWhiteSpace(capability)) |
| | | throw new ArgumentException("能力类型不能为空", nameof(capability)); |
| | | |
| | | SqliteHelper? sqliteHelper = null; |
| | | try |
| | | { |
| | | sqliteHelper = new SqliteHelper(_deviceConfigDbPath); |
| | | |
| | | var sql = $"SELECT slot_id, capability, version, device_id, channel_id, mapping_version " + |
| | | $"FROM slot_device_mapping " + |
| | | $"WHERE slot_id = '{slotId}' AND capability = '{capability}' " + |
| | | $"ORDER BY mapping_version DESC LIMIT 1"; |
| | | |
| | | var dataTable = sqliteHelper.GetDataTable(sql); |
| | | |
| | | if (dataTable.Rows.Count == 0) |
| | | { |
| | | Log.Warn($"[SlotDeviceMappingResolver] 未找到 Slot={slotId}, Capability={capability} 的映射记录"); |
| | | return null; |
| | | } |
| | | |
| | | var row = dataTable.Rows[0]; |
| | | var deviceId = row["device_id"]?.ToString() ?? string.Empty; |
| | | var channelId = Convert.ToInt32(row["channel_id"]); |
| | | var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty; |
| | | |
| | | var endpoint = new PowerEndpoint(deviceId, channelId, effectiveVersion); |
| | | |
| | | Log.Info($"[SlotDeviceMappingResolver] 解析映射: Slot={slotId}, Capability={capability} → {endpoint}"); |
| | | return endpoint; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[SlotDeviceMappingResolver] 解析映射失败: Slot={slotId}, Capability={capability}, 错误: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | sqliteHelper?.Close(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取数据库中所有的 Slot-设备-通道映射记录 |
| | | /// </summary> |
| | | /// <returns>所有映射记录的列表</returns> |
| | | public List<PowerEndpoint> GetAllMappings() |
| | | { |
| | | SqliteHelper? sqliteHelper = null; |
| | | try |
| | | { |
| | | sqliteHelper = new SqliteHelper(_deviceConfigDbPath); |
| | | |
| | | var sql = "SELECT slot_id, capability, version, device_id, channel_id, mapping_version " + |
| | | "FROM slot_device_mapping " + |
| | | "ORDER BY slot_id, capability, mapping_version DESC"; |
| | | |
| | | var dataTable = sqliteHelper.GetDataTable(sql); |
| | | var results = new List<PowerEndpoint>(); |
| | | |
| | | foreach (DataRow row in dataTable.Rows) |
| | | { |
| | | var deviceId = row["device_id"]?.ToString() ?? string.Empty; |
| | | var channelId = Convert.ToInt32(row["channel_id"]); |
| | | var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty; |
| | | |
| | | results.Add(new PowerEndpoint(deviceId, channelId, effectiveVersion)); |
| | | } |
| | | |
| | | Log.Info($"[SlotDeviceMappingResolver] 获取全部映射记录,共 {results.Count} 条"); |
| | | return results; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[SlotDeviceMappingResolver] 获取全部映射记录失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | sqliteHelper?.Close(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 按能力类型筛选映射记录 |
| | | /// </summary> |
| | | /// <param name="capability">能力类型(如 "PowerSupply"、"CAN" 等)</param> |
| | | /// <returns>匹配指定能力类型的映射记录列表</returns> |
| | | public List<PowerEndpoint> GetMappingsByCapability(string capability) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(capability)) |
| | | throw new ArgumentException("能力类型不能为空", nameof(capability)); |
| | | |
| | | SqliteHelper? sqliteHelper = null; |
| | | try |
| | | { |
| | | sqliteHelper = new SqliteHelper(_deviceConfigDbPath); |
| | | |
| | | var sql = $"SELECT slot_id, capability, version, device_id, channel_id, mapping_version " + |
| | | $"FROM slot_device_mapping " + |
| | | $"WHERE capability = '{capability}' " + |
| | | $"ORDER BY slot_id, mapping_version DESC"; |
| | | |
| | | var dataTable = sqliteHelper.GetDataTable(sql); |
| | | var results = new List<PowerEndpoint>(); |
| | | |
| | | foreach (DataRow row in dataTable.Rows) |
| | | { |
| | | var deviceId = row["device_id"]?.ToString() ?? string.Empty; |
| | | var channelId = Convert.ToInt32(row["channel_id"]); |
| | | var effectiveVersion = row["mapping_version"]?.ToString() ?? string.Empty; |
| | | |
| | | results.Add(new PowerEndpoint(deviceId, channelId, effectiveVersion)); |
| | | } |
| | | |
| | | Log.Info($"[SlotDeviceMappingResolver] 按能力类型 {capability} 筛选映射记录,共 {results.Count} 条"); |
| | | return results; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[SlotDeviceMappingResolver] 按能力类型筛选映射记录失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | finally |
| | | { |
| | | sqliteHelper?.Close(); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Concurrent; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using System.Threading; |
| | | using log4net; |
| | | |
| | | namespace CanSimulator |
| | | { |
| | | /// <summary> |
| | | /// CAN 总线状态 |
| | | /// </summary> |
| | | public enum CanBusState |
| | | { |
| | | /// <summary>停止状态</summary> |
| | | Stopped, |
| | | /// <summary>运行中</summary> |
| | | Running, |
| | | /// <summary>暂停</summary> |
| | | Paused, |
| | | /// <summary>错误</summary> |
| | | Error |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CAN 总线模拟器事件参数 |
| | | /// </summary> |
| | | public sealed class CanBusFrameEventArgs : EventArgs |
| | | { |
| | | public CanFrame Frame { get; } |
| | | public string SourceNodeId { get; } |
| | | |
| | | public CanBusFrameEventArgs(CanFrame frame, string sourceNodeId) |
| | | { |
| | | Frame = frame; |
| | | SourceNodeId = sourceNodeId; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CAN 总线模拟器 |
| | | /// 模拟一条虚拟 CAN 总线,多个虚拟节点可以挂载到总线上进行通讯 |
| | | /// </summary> |
| | | public sealed class CanBusSimulator : IDisposable |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(CanBusSimulator)); |
| | | |
| | | /// <summary> |
| | | /// 总线名称 |
| | | /// </summary> |
| | | public string BusName { get; } |
| | | |
| | | /// <summary> |
| | | /// 总线状态 |
| | | /// </summary> |
| | | public CanBusState State { get; private set; } = CanBusState.Stopped; |
| | | |
| | | /// <summary> |
| | | /// 波特率(仅用于模拟显示,不影响实际逻辑) |
| | | /// </summary> |
| | | public int BaudRate { get; set; } = 500000; // 默认 500kbps |
| | | |
| | | /// <summary> |
| | | /// 模拟延迟(毫秒)- 模拟真实总线传输延迟 |
| | | /// </summary> |
| | | public int SimulatedLatencyMs { get; set; } = 0; |
| | | |
| | | /// <summary> |
| | | /// 已注册的节点 |
| | | /// </summary> |
| | | private readonly ConcurrentDictionary<string, CanNode> _nodes = new(); |
| | | |
| | | /// <summary> |
| | | /// 总线日志(最近 N 条帧) |
| | | /// </summary> |
| | | private readonly ConcurrentQueue<CanFrame> _busLog = new(); |
| | | private int _maxLogSize = 5000; |
| | | |
| | | /// <summary> |
| | | /// 总线统计 |
| | | /// </summary> |
| | | public long TotalFramesTransmitted { get; private set; } |
| | | public long TotalFramesReceived { get; private set; } |
| | | public DateTime? LastActivityTime { get; private set; } |
| | | |
| | | /// <summary> |
| | | /// 帧在总线上传输事件(可用于监控/日志) |
| | | /// </summary> |
| | | public event EventHandler<CanBusFrameEventArgs>? FrameTransmitted; |
| | | |
| | | /// <summary> |
| | | /// 总线状态变化事件 |
| | | /// </summary> |
| | | public event EventHandler<CanBusState>? StateChanged; |
| | | |
| | | public CanBusSimulator(string busName = "CAN_BUS_1") |
| | | { |
| | | BusName = busName ?? throw new ArgumentNullException(nameof(busName)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 启动总线 |
| | | /// </summary> |
| | | public void Start() |
| | | { |
| | | if (State == CanBusState.Running) |
| | | { |
| | | Log.Warn($"[CanBus] 总线 {BusName} 已在运行中"); |
| | | return; |
| | | } |
| | | |
| | | State = CanBusState.Running; |
| | | Log.Info($"[CanBus] 总线 {BusName} 已启动 (波特率: {BaudRate}bps)"); |
| | | StateChanged?.Invoke(this, State); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 停止总线 |
| | | /// </summary> |
| | | public void Stop() |
| | | { |
| | | if (State == CanBusState.Stopped) return; |
| | | |
| | | State = CanBusState.Stopped; |
| | | Log.Info($"[CanBus] 总线 {BusName} 已停止"); |
| | | StateChanged?.Invoke(this, State); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 暂停总线(帧仍在保留但不路由) |
| | | /// </summary> |
| | | public void Pause() |
| | | { |
| | | if (State != CanBusState.Running) return; |
| | | |
| | | State = CanBusState.Paused; |
| | | Log.Info($"[CanBus] 总线 {BusName} 已暂停"); |
| | | StateChanged?.Invoke(this, State); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 恢复总线 |
| | | /// </summary> |
| | | public void Resume() |
| | | { |
| | | if (State != CanBusState.Paused) return; |
| | | |
| | | State = CanBusState.Running; |
| | | Log.Info($"[CanBus] 总线 {BusName} 已恢复"); |
| | | StateChanged?.Invoke(this, State); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 注册节点到总线 |
| | | /// </summary> |
| | | public CanNode RegisterNode(string nodeId, string? name = null) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(nodeId)) |
| | | throw new ArgumentException("节点 ID 不能为空", nameof(nodeId)); |
| | | |
| | | var node = new CanNode(nodeId, name); |
| | | node.FrameSent += OnNodeFrameSent; |
| | | |
| | | if (!_nodes.TryAdd(nodeId, node)) |
| | | { |
| | | Log.Warn($"[CanBus] 节点 {nodeId} 已存在,将被替换"); |
| | | _nodes.TryRemove(nodeId, out var oldNode); |
| | | oldNode?.Deactivate(); |
| | | _nodes.TryAdd(nodeId, node); |
| | | } |
| | | |
| | | // 如果总线已运行,自动激活节点 |
| | | if (State == CanBusState.Running) |
| | | { |
| | | node.Activate(); |
| | | } |
| | | |
| | | Log.Info($"[CanBus] 节点 {nodeId} ({name ?? nodeId}) 已注册到总线 {BusName}"); |
| | | return node; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 注销节点 |
| | | /// </summary> |
| | | public bool UnregisterNode(string nodeId) |
| | | { |
| | | if (_nodes.TryRemove(nodeId, out var node)) |
| | | { |
| | | node.FrameSent -= OnNodeFrameSent; |
| | | node.Deactivate(); |
| | | Log.Info($"[CanBus] 节点 {nodeId} 已从总线 {BusName} 注销"); |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取节点 |
| | | /// </summary> |
| | | public CanNode? GetNode(string nodeId) |
| | | { |
| | | _nodes.TryGetValue(nodeId, out var node); |
| | | return node; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取所有已注册节点 |
| | | /// </summary> |
| | | public IReadOnlyList<CanNode> GetAllNodes() |
| | | { |
| | | return _nodes.Values.ToList(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 向总线发送帧(由节点发送,总线负责路由) |
| | | /// </summary> |
| | | private void OnNodeFrameSent(object? sender, CanFrame frame) |
| | | { |
| | | if (State != CanBusState.Running) return; |
| | | |
| | | // 模拟延迟 |
| | | if (SimulatedLatencyMs > 0) |
| | | { |
| | | Thread.Sleep(SimulatedLatencyMs); |
| | | } |
| | | |
| | | // 记录到总线日志 |
| | | while (_busLog.Count >= _maxLogSize) |
| | | { |
| | | _busLog.TryDequeue(out _); |
| | | } |
| | | _busLog.Enqueue(frame); |
| | | TotalFramesTransmitted++; |
| | | LastActivityTime = DateTime.Now; |
| | | |
| | | // 路由到所有其他节点 |
| | | int routed = 0; |
| | | foreach (var kvp in _nodes) |
| | | { |
| | | if (kvp.Key == frame.SourceNodeId) continue; // 不路由回源节点 |
| | | |
| | | try |
| | | { |
| | | kvp.Value.ReceiveFrame(frame); |
| | | routed++; |
| | | TotalFramesReceived++; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CanBus] 路由帧到节点 {kvp.Key} 失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | // 检查源节点是否启用自动响应模式 |
| | | if (_nodes.TryGetValue(frame.SourceNodeId, out var sourceNode) && sourceNode.AutoResponseEnabled) |
| | | { |
| | | try |
| | | { |
| | | // 模拟响应延迟 |
| | | if (sourceNode.AutoResponseDelayMs > 0) |
| | | { |
| | | Thread.Sleep(sourceNode.AutoResponseDelayMs); |
| | | } |
| | | |
| | | // 创建响应帧(复制原始帧数据,来源为源节点) |
| | | var responseFrame = new CanFrame |
| | | { |
| | | FrameId = frame.FrameId, |
| | | Format = frame.Format, |
| | | Type = frame.Type, |
| | | Data = (byte[])frame.Data.Clone(), |
| | | SourceNodeId = sourceNode.NodeId, |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | |
| | | // 将响应帧路由到所有其他节点(不包括响应源自身) |
| | | foreach (var kvp in _nodes) |
| | | { |
| | | if (kvp.Key == sourceNode.NodeId) continue; |
| | | try |
| | | { |
| | | kvp.Value.ReceiveFrame(responseFrame); |
| | | TotalFramesReceived++; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CanBus] 路由响应帧到节点 {kvp.Key} 失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | // 将响应帧送入源节点的接收邮箱(让源节点能看到自己的响应) |
| | | sourceNode.ReceiveFrame(responseFrame); |
| | | TotalFramesReceived++; |
| | | |
| | | // 记录响应帧到总线日志 |
| | | _busLog.Enqueue(responseFrame); |
| | | TotalFramesTransmitted++; |
| | | |
| | | Log.Debug($"[CanBus] 节点 {sourceNode.NodeId} 自动响应帧 {responseFrame.FrameIdHex}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CanBus] 源节点自动响应失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | Log.Debug($"[CanBus] 帧 {frame.FrameIdHex} 从 {frame.SourceNodeId} 路由到 {routed} 个节点"); |
| | | FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId)); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 直接注入帧到总线(用于模拟外部设备发送) |
| | | /// </summary> |
| | | public void InjectFrame(CanFrame frame) |
| | | { |
| | | if (State != CanBusState.Running) |
| | | { |
| | | Log.Warn($"[CanBus] 总线未运行,无法注入帧"); |
| | | return; |
| | | } |
| | | |
| | | frame.Timestamp = DateTime.Now; |
| | | |
| | | // 记录到日志 |
| | | while (_busLog.Count >= _maxLogSize) |
| | | { |
| | | _busLog.TryDequeue(out _); |
| | | } |
| | | _busLog.Enqueue(frame); |
| | | TotalFramesTransmitted++; |
| | | LastActivityTime = DateTime.Now; |
| | | |
| | | // 路由到所有节点 |
| | | foreach (var kvp in _nodes) |
| | | { |
| | | try |
| | | { |
| | | kvp.Value.ReceiveFrame(frame); |
| | | TotalFramesReceived++; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log.Error($"[CanBus] 注入帧路由到节点 {kvp.Key} 失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | FrameTransmitted?.Invoke(this, new CanBusFrameEventArgs(frame, frame.SourceNodeId)); |
| | | Log.Debug($"[CanBus] 注入帧 {frame.FrameIdHex} 到总线"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取总线日志 |
| | | /// </summary> |
| | | public List<CanFrame> GetBusLog(int maxCount = 100) |
| | | { |
| | | var all = _busLog.ToList(); |
| | | if (all.Count <= maxCount) return all; |
| | | return all.Skip(all.Count - maxCount).ToList(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清空总线日志 |
| | | /// </summary> |
| | | public void ClearBusLog() |
| | | { |
| | | while (_busLog.TryDequeue(out _)) { } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 重置所有统计 |
| | | /// </summary> |
| | | public void ResetStatistics() |
| | | { |
| | | TotalFramesTransmitted = 0; |
| | | TotalFramesReceived = 0; |
| | | LastActivityTime = null; |
| | | foreach (var node in _nodes.Values) |
| | | { |
| | | node.ResetCounters(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取总线状态摘要 |
| | | /// </summary> |
| | | public string GetStatusSummary() |
| | | { |
| | | return $"Bus={BusName}, State={State}, BaudRate={BaudRate}, Nodes={_nodes.Count}, " + |
| | | $"Tx={TotalFramesTransmitted}, Rx={TotalFramesReceived}, " + |
| | | $"LastActivity={LastActivityTime?.ToString("HH:mm:ss") ?? "N/A"}"; |
| | | } |
| | | |
| | | public void Dispose() |
| | | { |
| | | Stop(); |
| | | foreach (var node in _nodes.Values) |
| | | { |
| | | node.Deactivate(); |
| | | } |
| | | _nodes.Clear(); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | |
| | | namespace CanSimulator |
| | | { |
| | | /// <summary> |
| | | /// CAN 帧格式类型 |
| | | /// </summary> |
| | | public enum CanFrameFormat |
| | | { |
| | | /// <summary>标准帧 (11-bit ID)</summary> |
| | | Standard, |
| | | /// <summary>扩展帧 (29-bit ID)</summary> |
| | | Extended |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CAN 帧类型 |
| | | /// </summary> |
| | | public enum CanFrameType |
| | | { |
| | | /// <summary>数据帧</summary> |
| | | Data, |
| | | /// <summary>远程帧</summary> |
| | | Remote |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CAN 总线帧数据结构 |
| | | /// </summary> |
| | | public sealed class CanFrame |
| | | { |
| | | /// <summary> |
| | | /// 帧 ID(标准帧 11-bit,扩展帧 29-bit) |
| | | /// </summary> |
| | | public uint FrameId { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 帧格式(标准/扩展) |
| | | /// </summary> |
| | | public CanFrameFormat Format { get; set; } = CanFrameFormat.Standard; |
| | | |
| | | /// <summary> |
| | | /// 帧类型(数据帧/远程帧) |
| | | /// </summary> |
| | | public CanFrameType Type { get; set; } = CanFrameType.Data; |
| | | |
| | | /// <summary> |
| | | /// 数据负载(最大 8 字节,CAN FD 可达 64 字节) |
| | | /// </summary> |
| | | public byte[] Data { get; set; } = Array.Empty<byte>(); |
| | | |
| | | /// <summary> |
| | | /// 时间戳 |
| | | /// </summary> |
| | | public DateTime Timestamp { get; set; } = DateTime.Now; |
| | | |
| | | /// <summary> |
| | | /// 发送节点标识 |
| | | /// </summary> |
| | | public string SourceNodeId { get; set; } = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// DLC (Data Length Code) |
| | | /// </summary> |
| | | public int Dlc => Data.Length; |
| | | |
| | | /// <summary> |
| | | /// 将帧 ID 格式化为十六进制字符串 |
| | | /// </summary> |
| | | public string FrameIdHex => Format == CanFrameFormat.Extended |
| | | ? $"0x{FrameId:X8}" |
| | | : $"0x{FrameId:X3}"; |
| | | |
| | | /// <summary> |
| | | /// 将数据格式化为十六进制字符串 |
| | | /// </summary> |
| | | public string DataHex => BitConverter.ToString(Data).Replace("-", " "); |
| | | |
| | | /// <summary> |
| | | /// 从十六进制字符串解析帧 ID |
| | | /// </summary> |
| | | public static uint ParseFrameId(string frameIdStr) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(frameIdStr)) |
| | | throw new ArgumentException("帧 ID 不能为空", nameof(frameIdStr)); |
| | | |
| | | if (frameIdStr.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) |
| | | { |
| | | if (!uint.TryParse(frameIdStr.Substring(2), System.Globalization.NumberStyles.HexNumber, null, out uint id)) |
| | | throw new ArgumentException($"无法解析帧 ID: {frameIdStr}"); |
| | | return id; |
| | | } |
| | | else |
| | | { |
| | | if (!uint.TryParse(frameIdStr, out uint id)) |
| | | throw new ArgumentException($"无法解析帧 ID: {frameIdStr}"); |
| | | return id; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从十六进制字符串解析数据 |
| | | /// </summary> |
| | | public static byte[] ParseData(string dataStr) |
| | | { |
| | | if (string.IsNullOrWhiteSpace(dataStr)) |
| | | return Array.Empty<byte>(); |
| | | |
| | | try |
| | | { |
| | | var parts = dataStr.Split(new[] { ' ', '-', ',' }, StringSplitOptions.RemoveEmptyEntries); |
| | | var bytes = new byte[parts.Length]; |
| | | for (int i = 0; i < parts.Length; i++) |
| | | { |
| | | bytes[i] = byte.Parse(parts[i], System.Globalization.NumberStyles.HexNumber); |
| | | } |
| | | return bytes; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new ArgumentException($"无法解析数据字符串: {dataStr}, 错误: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | public override string ToString() |
| | | { |
| | | return $"[{Timestamp:HH:mm:ss.fff}] {FrameIdHex} [{Dlc}] {DataHex} (from {SourceNodeId})"; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Concurrent; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using log4net; |
| | | |
| | | namespace CanSimulator |
| | | { |
| | | /// <summary> |
| | | /// 接收滤波器配置 |
| | | /// </summary> |
| | | public sealed class AcceptanceFilter |
| | | { |
| | | /// <summary> |
| | | /// 滤波器 ID(匹配值) |
| | | /// </summary> |
| | | public uint FilterId { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 掩码(为 1 的位参与比较) |
| | | /// </summary> |
| | | public uint Mask { get; set; } = 0x7FF; // 默认标准帧全匹配 |
| | | |
| | | /// <summary> |
| | | /// 是否为扩展帧滤波器 |
| | | /// </summary> |
| | | public bool IsExtended { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 检查给定帧 ID 是否通过此滤波器 |
| | | /// </summary> |
| | | public bool Accepts(uint frameId, bool isExtended) |
| | | { |
| | | if (IsExtended != isExtended) return false; |
| | | return (frameId & Mask) == (FilterId & Mask); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 虚拟 CAN 节点 |
| | | /// 模拟一个 CAN 通讯卡的端点,具有发送和接收功能 |
| | | /// </summary> |
| | | public sealed class CanNode |
| | | { |
| | | private static readonly ILog Log = LogManager.GetLogger(typeof(CanNode)); |
| | | |
| | | /// <summary> |
| | | /// 节点唯一标识 |
| | | /// </summary> |
| | | public string NodeId { get; } |
| | | |
| | | /// <summary> |
| | | /// 节点名称(可选描述) |
| | | /// </summary> |
| | | public string Name { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 节点是否已激活(连接到总线) |
| | | /// </summary> |
| | | public bool IsActive { get; private set; } |
| | | |
| | | /// <summary> |
| | | /// 接收滤波器列表 |
| | | /// </summary> |
| | | private readonly List<AcceptanceFilter> _filters = new(); |
| | | |
| | | /// <summary> |
| | | /// 接收邮箱(FIFO 队列) |
| | | /// </summary> |
| | | private readonly ConcurrentQueue<CanFrame> _receiveMailbox = new(); |
| | | |
| | | /// <summary> |
| | | /// 发送历史记录 |
| | | /// </summary> |
| | | private readonly ConcurrentQueue<CanFrame> _sendHistory = new(); |
| | | |
| | | /// <summary> |
| | | /// 最大邮箱容量 |
| | | /// </summary> |
| | | public int MaxMailboxSize { get; set; } = 1000; |
| | | |
| | | /// <summary> |
| | | /// 最大历史记录容量 |
| | | /// </summary> |
| | | public int MaxHistorySize { get; set; } = 500; |
| | | |
| | | /// <summary> |
| | | /// 接收帧计数器 |
| | | /// </summary> |
| | | public long RxCount { get; private set; } |
| | | |
| | | /// <summary> |
| | | /// 发送帧计数器 |
| | | /// </summary> |
| | | public long TxCount { get; private set; } |
| | | |
| | | /// <summary> |
| | | /// 错误计数器 |
| | | /// </summary> |
| | | public long ErrorCount { get; private set; } |
| | | |
| | | /// <summary> |
| | | /// 是否启用自动响应模式(收到帧后原封不动回复) |
| | | /// </summary> |
| | | public bool AutoResponseEnabled { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 自动响应延迟(毫秒)- 模拟真实设备响应时间 |
| | | /// </summary> |
| | | public int AutoResponseDelayMs { get; set; } = 0; |
| | | |
| | | /// <summary> |
| | | /// 自动响应事件(用于日志/监控) |
| | | /// </summary> |
| | | public event EventHandler<CanFrame>? AutoResponseSent; |
| | | |
| | | /// <summary> |
| | | /// 帧接收事件 |
| | | /// </summary> |
| | | public event EventHandler<CanFrame>? FrameReceived; |
| | | |
| | | /// <summary> |
| | | /// 帧发送事件 |
| | | /// </summary> |
| | | public event EventHandler<CanFrame>? FrameSent; |
| | | |
| | | public CanNode(string nodeId, string? name = null) |
| | | { |
| | | NodeId = nodeId ?? throw new ArgumentNullException(nameof(nodeId)); |
| | | Name = name ?? nodeId; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 激活节点(连接到总线) |
| | | /// </summary> |
| | | public void Activate() |
| | | { |
| | | IsActive = true; |
| | | Log.Info($"[CanNode] 节点 {NodeId} ({Name}) 已激活"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 停用节点(从总线断开) |
| | | /// </summary> |
| | | public void Deactivate() |
| | | { |
| | | IsActive = false; |
| | | Log.Info($"[CanNode] 节点 {NodeId} ({Name}) 已停用"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 添加接收滤波器 |
| | | /// </summary> |
| | | public void AddFilter(AcceptanceFilter filter) |
| | | { |
| | | lock (_filters) |
| | | { |
| | | _filters.Add(filter); |
| | | } |
| | | Log.Debug($"[CanNode] 节点 {NodeId} 添加滤波器: ID=0x{filter.FilterId:X3}, Mask=0x{filter.Mask:X3}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清除所有滤波器 |
| | | /// </summary> |
| | | public void ClearFilters() |
| | | { |
| | | lock (_filters) |
| | | { |
| | | _filters.Clear(); |
| | | } |
| | | Log.Debug($"[CanNode] 节点 {NodeId} 清除所有滤波器"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设置通配符滤波器(接收所有帧) |
| | | /// </summary> |
| | | public void SetPromiscuousMode() |
| | | { |
| | | lock (_filters) |
| | | { |
| | | _filters.Clear(); |
| | | _filters.Add(new AcceptanceFilter { FilterId = 0, Mask = 0, IsExtended = false }); |
| | | _filters.Add(new AcceptanceFilter { FilterId = 0, Mask = 0, IsExtended = true }); |
| | | } |
| | | Log.Debug($"[CanNode] 节点 {NodeId} 设置为混杂模式(接收所有帧)"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查帧是否通过滤波器 |
| | | /// </summary> |
| | | internal bool AcceptsFrame(CanFrame frame) |
| | | { |
| | | lock (_filters) |
| | | { |
| | | // 如果没有设置滤波器,默认拒绝 |
| | | if (_filters.Count == 0) return false; |
| | | |
| | | // 任一滤波器匹配即接受 |
| | | return _filters.Any(f => f.Accepts(frame.FrameId, frame.Format == CanFrameFormat.Extended)); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 接收帧(由总线调用) |
| | | /// </summary> |
| | | internal void ReceiveFrame(CanFrame frame) |
| | | { |
| | | if (!IsActive) return; |
| | | if (!AcceptsFrame(frame)) return; |
| | | |
| | | // 入队接收邮箱 |
| | | while (_receiveMailbox.Count >= MaxMailboxSize) |
| | | { |
| | | _receiveMailbox.TryDequeue(out _); // 丢弃最旧的帧 |
| | | } |
| | | _receiveMailbox.Enqueue(frame); |
| | | RxCount++; |
| | | |
| | | // 触发事件 |
| | | FrameReceived?.Invoke(this, frame); |
| | | Log.Debug($"[CanNode] 节点 {NodeId} 接收帧: {frame}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 发送帧 |
| | | /// </summary> |
| | | public void SendFrame(CanFrame frame) |
| | | { |
| | | if (!IsActive) |
| | | { |
| | | ErrorCount++; |
| | | throw new InvalidOperationException($"节点 {NodeId} 未激活,无法发送帧"); |
| | | } |
| | | |
| | | frame.SourceNodeId = NodeId; |
| | | frame.Timestamp = DateTime.Now; |
| | | |
| | | // 记录发送历史 |
| | | while (_sendHistory.Count >= MaxHistorySize) |
| | | { |
| | | _sendHistory.TryDequeue(out _); |
| | | } |
| | | _sendHistory.Enqueue(frame); |
| | | TxCount++; |
| | | |
| | | // 触发事件(由总线处理路由) |
| | | FrameSent?.Invoke(this, frame); |
| | | Log.Debug($"[CanNode] 节点 {NodeId} 发送帧: {frame}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从接收邮箱中取出一帧 |
| | | /// </summary> |
| | | public bool TryDequeueReceived(out CanFrame? frame) |
| | | { |
| | | return _receiveMailbox.TryDequeue(out frame); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 查看接收邮箱中的所有帧(不移除) |
| | | /// </summary> |
| | | public List<CanFrame> PeekReceiveMailbox() |
| | | { |
| | | return _receiveMailbox.ToList(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清空接收邮箱 |
| | | /// </summary> |
| | | public void ClearReceiveMailbox() |
| | | { |
| | | while (_receiveMailbox.TryDequeue(out _)) { } |
| | | Log.Debug($"[CanNode] 节点 {NodeId} 清空接收邮箱"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取发送历史 |
| | | /// </summary> |
| | | public List<CanFrame> GetSendHistory() |
| | | { |
| | | return _sendHistory.ToList(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清空发送历史 |
| | | /// </summary> |
| | | public void ClearSendHistory() |
| | | { |
| | | while (_sendHistory.TryDequeue(out _)) { } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 重置节点统计 |
| | | /// </summary> |
| | | public void ResetCounters() |
| | | { |
| | | RxCount = 0; |
| | | TxCount = 0; |
| | | ErrorCount = 0; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取节点状态摘要 |
| | | /// </summary> |
| | | public string GetStatusSummary() |
| | | { |
| | | return $"Node={NodeId}, Active={IsActive}, Rx={RxCount}, Tx={TxCount}, Err={ErrorCount}, Mailbox={_receiveMailbox.Count}"; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>Library</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| | |
| | | public static bool ShowBurnInAbort { get; private set; } = true; |
| | | public static string SocketNames { get; private set; } = string.Empty; |
| | | public static string StationName { get; private set; } = string.Empty; |
| | | public static string MainSeqPath { get; private set; } = string.Empty; |
| | | |
| | | |
| | | public static string EngineType |
| | |
| | | { |
| | | |
| | | } |
| | | |
| | | try |
| | | { |
| | | |
| | | MainSeqPath = ParameterManager.GetCommonParameterByName<string>("BurnInMainSeq", string.Empty); |
| | | } |
| | | catch |
| | | { |
| | | |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | { |
| | | public class LangUtil |
| | | { |
| | | public const string DefaultLang = "zh-CN"; |
| | | public static List<LangInfo> langs = new List<LangInfo>() { |
| | | new LangInfo("zh-CN","中文"), |
| | | new LangInfo(DefaultLang,"中文"), |
| | | new LangInfo("en-US","English") |
| | | }; |
| | | |
| | |
| | | private static string GetLang() |
| | | { |
| | | var lang = SystemSetting.Instance.CurrentLanguage; |
| | | if (string.IsNullOrEmpty(lang)) lang = CultureInfo.CurrentUICulture.ToString(); |
| | | if (string.IsNullOrEmpty(lang)) lang = "zh-CN"; |
| | | return lang; |
| | | } |
| | | |
| | |
| | | { |
| | | try |
| | | { |
| | | /// 按Json加载 |
| | | //LoadAllDataByJson(); |
| | | /// 下面是按xaml加载 |
| | | LoadAllDataByXaml(); |
| | | LoadAllData(); |
| | | LoadLang(); |
| | | } |
| | | finally |
| | |
| | | ResourceMap[lang] = resource; |
| | | } |
| | | |
| | | |
| | | private static void LoadDataByXaml(string lang) |
| | | private static void LoadXamlFromJson2(string lang) |
| | | { |
| | | string langFolder = PathHelper.LangFolder; |
| | | string folder = Path.Combine(langFolder, lang); |
| | | if (!Directory.Exists(folder)) |
| | | LoadDataByJson(lang); |
| | | |
| | | if (!JsonMap.ContainsKey(lang)) return; |
| | | JObject json = JsonMap[lang]; |
| | | var resource = new ResourceDictionary(); |
| | | foreach (var item in json) |
| | | { |
| | | return; |
| | | resource.Add(item.Key, item.Value.ToString()); |
| | | } |
| | | |
| | | string[] files = Directory.GetFiles(folder, "*.xaml", SearchOption.AllDirectories); |
| | | |
| | | foreach (var path in files) |
| | | ResourceMap[lang] = resource; |
| | | if (lang != DefaultLang) |
| | | { |
| | | try |
| | | var defaultMap = new ResourceDictionary(); |
| | | foreach (var item in json) |
| | | { |
| | | using (FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read)) |
| | | { |
| | | var resource = (ResourceDictionary)XamlReader.Load(stream); |
| | | if (!ResourceMap.ContainsKey(lang)) |
| | | { |
| | | ResourceMap[lang] = resource; |
| | | } |
| | | else |
| | | { |
| | | ResourceMap[lang].MergedDictionaries.Add(resource); |
| | | } |
| | | } |
| | | defaultMap.Add(item.Key, item.Key); |
| | | } |
| | | catch |
| | | { |
| | | } |
| | | ResourceMap[DefaultLang] = defaultMap; |
| | | } |
| | | } |
| | | |
| | | private static void LoadAllDataByJson() |
| | | { |
| | | foreach (var lang in langs) |
| | | { |
| | | LoadDataByJson(lang.Code); |
| | | } |
| | | } |
| | | |
| | | private static void LoadAllDataByXaml() |
| | | private static void LoadAllData() |
| | | { |
| | | foreach (var lang in langs) |
| | | { |
| | | LoadXamlFromJson(lang.Code); |
| | | } |
| | | } |
| | | |
| | | public static string Format(string key, params object[] datas) |
| | | { |
| | | return string.Format(Get(key), datas); |
| | | } |
| | | } |
| | | |
| | | public static class LangReader |
| | |
| | | xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI"> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <pu:StyleDictionary Includes="PasswordBox,TextBox,ComboBox,Button,TreeView,Menu,ListBox,ContextMenu,TabControl" /> |
| | | <pu:StyleDictionary Includes="PasswordBox,TextBox,ComboBox,Button,TreeView,ListBox" /> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonDataGrid.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/ScrollBarResource.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Resources/Font/Font.xaml"/> |
| | |
| | | </Style> |
| | | |
| | | <Style x:Key="CommonMenu" TargetType="Menu" BasedOn="{StaticResource {x:Static pu:StyleKeys.MenuStyle}}"> |
| | | <Style.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <pu:StyleDictionary Includes="Menu" /> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | </ResourceDictionary> |
| | | </Style.Resources> |
| | | <Setter Property="Background" Value="{DynamicResource PrimaryColor}"/> |
| | | <Setter Property="Foreground" Value="#FFF"/> |
| | | <Setter Property="pu:IconHelper.FontFamily" Value="{StaticResource iconfont}"/> |
| | |
| | | <Prefer32Bit>true</Prefer32Bit> |
| | | </PropertyGroup> |
| | | <ItemGroup> |
| | | <Reference Include="HslCommunication"> |
| | | <HintPath>..\Libs\HslCommunication.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="log4net, Version=3.0.4.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL"> |
| | | <HintPath>..\WPFProject\packages\log4net.3.0.4\lib\net462\log4net.dll</HintPath> |
| | | </Reference> |
| | |
| | | <None Include="App.config" /> |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\..\UILib\UILib.csproj"> |
| | | <Project>{27325fe0-3ce2-4cd5-8eb5-7e45cc441479}</Project> |
| | | <Name>UILib</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\ConnectionUtil\ConnectionUtil.csproj"> |
| | | <Project>{d0806fa6-3f6d-4304-81b8-ead9da7cb5b2}</Project> |
| | | <Name>ConnectionUtil</Name> |
| | |
| | | <Project>{f17de6b5-af1d-48c6-94a0-6b763a6546a6}</Project> |
| | | <Name>HMI.UI</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\PLCHelper\PLCHelper.csproj"> |
| | | <Project>{C3F17E03-EF05-4203-8D29-3AD984F3BD9C}</Project> |
| | | <Name>PLCHelper</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\Plugin.DataBoard\Plugin.DataBoard.csproj"> |
| | | <Project>{be42ab26-3270-43e1-84b8-6929d862ff8d}</Project> |
| | | <Name>Plugin.DataBoard</Name> |
| | |
| | | <Project>{22dc8d91-50b2-47ca-86a8-29a6da48f388}</Project> |
| | | <Name>Plugin.UICollection</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\S7Driver\S7Driver.csproj"> |
| | | <Project>{628ef12b-9ed2-4b20-950f-dc3ecc3fd15f}</Project> |
| | | <Name>S7Driver</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\SimpleUtil\HMI.Simple.Util\HMI.Simple.Util.csproj"> |
| | | <Project>{cf5bebe9-2803-49c3-b773-73d082e0984a}</Project> |
| | | <Name>HMI.Simple.Util</Name> |
| | |
| | | using HMI.Core.Api; |
| | | using HMI.Core.Util; |
| | | using HMI.Simple.Util; |
| | | using HslCommunication.Core.Pipe; |
| | | using HslCommunication.Profinet.Siemens; |
| | | using log4net; |
| | | using Panuon.WPF.UI; |
| | | using PLCHelper; |
| | | using Plugin.DataBoard; |
| | | using Plugin.TestResult_WPF; |
| | | using Plugin.TsExecution; |
| | |
| | | using System; |
| | | using System.IO; |
| | | using System.Windows; |
| | | using Util.ManualDebug; |
| | | using System.Xml.Linq; |
| | | namespace UnitTest |
| | | { |
| | | /// <summary> |
| | |
| | | private ILog log = LogManager.GetLogger("UnitTest"); |
| | | public MainWindow() |
| | | { |
| | | ProductModelHelper.GetModelBySNR("cc123", out string model, out string msg); |
| | | LangUtil.Load(); |
| | | InitializeComponent(); |
| | | //RebuildDb(); |
| | | border2.Child = new TcpDebugger(); |
| | | //ProductModelHelper.GetModelBySNR("cc123", out string model, out string msg); |
| | | //LangUtil.Load(); |
| | | //InitializeComponent(); |
| | | ////RebuildDb(); |
| | | //border2.Child = new TcpDebugger(); |
| | | |
| | | |
| | | var client = new SiemensS7Net(SiemensPLCS.S1500); |
| | | PipeTcpNet pipeTcpNet = new PipeTcpNet("127.0.0.1", 102); |
| | | pipeTcpNet.ConnectTimeOut = 200; |
| | | pipeTcpNet.ReceiveTimeOut = 200; |
| | | client.CommunicationPipe = pipeTcpNet; |
| | | client.ConnectServer(); |
| | | |
| | | var ss = new IntReader(client, "DB10.DBW100", 0); |
| | | ss.DataChanged += Ss_DataChanged; |
| | | ss.Run(); |
| | | |
| | | |
| | | |
| | | |
| | | //page.Children.Add(new UserControl1()); |
| | | } |
| | | |
| | | private void Ss_DataChanged(object sender, DataChangedArg<int> e) |
| | | { |
| | | MessageBox.Show($"{e.oldValue} => {e.newValue}"); |
| | | } |
| | | |
| | | TcpClientWrapper tcpClient; |
| | |
| | | |
| | | private void Button_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | var win = new ManualDebug() |
| | | { |
| | | seqPath = "C:\\Users\\chrry\\Desktop\\手动调试示例.seq" |
| | | }; |
| | | //var win = new ManualDebug() |
| | | //{ |
| | | // seqPath = "C:\\Users\\chrry\\Desktop\\手动调试示例.seq" |
| | | //}; |
| | | |
| | | win.Show(); |
| | | //win.Show(); |
| | | |
| | | //Task.Run(() => |
| | | //{ |
| | | // for (int j = 0; j < 1000; j++) |
| | |
| | | return temp.ToString(); |
| | | } |
| | | else |
| | | return String.Empty; |
| | | return NoText; |
| | | } |
| | | |
| | | #endregion |
| New file |
| | |
| | | using HslCommunication.Profinet.Siemens; |
| | | using System; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | |
| | | namespace PLCHelper |
| | | { |
| | | |
| | | public class BoolReader |
| | | { |
| | | private bool lastVallue = false; |
| | | |
| | | private string Address = string.Empty; |
| | | private int Timeout = 1000; |
| | | |
| | | private SiemensS7Net client; |
| | | |
| | | private volatile bool isRunning = false; |
| | | |
| | | public event EventHandler<DataChangedArg<bool>> DataChanged; |
| | | |
| | | public BoolReader(SiemensS7Net client, string address, bool initValue = false) |
| | | { |
| | | this.client = client; |
| | | this.Address = address; |
| | | this.lastVallue = initValue; |
| | | } |
| | | |
| | | public void Stop() |
| | | { |
| | | isRunning = false; |
| | | } |
| | | |
| | | public void Run() |
| | | { |
| | | isRunning = true; |
| | | Task.Run(() => |
| | | { |
| | | while (isRunning) |
| | | { |
| | | var res = client.ReadBool(Address); |
| | | if (res.IsSuccess) |
| | | { |
| | | bool val = res.Content; |
| | | if (val != lastVallue) |
| | | { |
| | | DataChanged?.Invoke(this, new DataChangedArg<bool> |
| | | { |
| | | oldValue = lastVallue, |
| | | newValue = val |
| | | }); |
| | | lastVallue = val; |
| | | } |
| | | } |
| | | Thread.Sleep(Timeout); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using HslCommunication.Profinet.Siemens; |
| | | using System; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | |
| | | namespace PLCHelper |
| | | { |
| | | public class DataChangedArg<T> |
| | | { |
| | | public T oldValue { get; set; } |
| | | public T newValue { get; set; } |
| | | } |
| | | |
| | | public class IntReader |
| | | { |
| | | private int lastVallue = 0; |
| | | |
| | | private string Address = string.Empty; |
| | | private int Timeout = 1000; |
| | | |
| | | private SiemensS7Net client; |
| | | |
| | | private volatile bool isRunning = false; |
| | | |
| | | public event EventHandler<DataChangedArg<int>> DataChanged; |
| | | |
| | | public IntReader(SiemensS7Net client, string address, int initValue = 0) |
| | | { |
| | | this.client = client; |
| | | this.Address = address; |
| | | this.lastVallue = initValue; |
| | | } |
| | | |
| | | public void Stop() |
| | | { |
| | | isRunning = false; |
| | | } |
| | | |
| | | public void Run() |
| | | { |
| | | isRunning = true; |
| | | Task.Run(() => |
| | | { |
| | | while (isRunning) |
| | | { |
| | | var res = client.ReadInt16(Address); |
| | | if (res.IsSuccess) |
| | | { |
| | | int val = res.Content; |
| | | if (val != lastVallue) |
| | | { |
| | | DataChanged?.Invoke(this, new DataChangedArg<int> |
| | | { |
| | | oldValue = lastVallue, |
| | | newValue = val |
| | | }); |
| | | lastVallue = val; |
| | | } |
| | | } |
| | | Thread.Sleep(Timeout); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | } |
| | |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <Compile Include="Entity\PLCTagData.cs" /> |
| | | <Compile Include="BoolReader.cs" /> |
| | | <Compile Include="IntReader.cs" /> |
| | | <Compile Include="ModbusHelper.cs" /> |
| | | <Compile Include="NXI_5010Helper.cs" /> |
| | | <Compile Include="Properties\AssemblyInfo.cs" /> |
| | |
| | | using Plugin.BurnIn.Model; |
| | | using Plugin.TsSupport; |
| | | using SuperSocket.SocketBase.Config; |
| | | using System.Collections; |
| | | using System.Collections.ObjectModel; |
| | | using System.ComponentModel; |
| | | using System.Configuration; |
| | | using System.Globalization; |
| | | using System.Runtime.CompilerServices; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Data; |
| | | using System.Windows.Documents; |
| | | using System.Windows.Media; |
| | | using System.Xml.Linq; |
| | | |
| | | namespace Plugin.BurnIn |
| | | { |
| | |
| | | }); |
| | | } |
| | | |
| | | private int GetConfigPort() |
| | | { |
| | | try |
| | | { |
| | | var customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig"); |
| | | if (customConfig.ContainsKey("PortStart")) |
| | | { |
| | | int port = int.Parse(customConfig["PortStart"].ToString()) - 1; |
| | | return port; |
| | | } |
| | | else |
| | | { |
| | | return 6659; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | AppendLog(LogLevel.Error, $"读取起始端口错误: {ex.Message} {ex.StackTrace} 使用默认端口6659"); |
| | | return 6659; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 启动Tcp服务器 |
| | | /// </summary> |
| | | [RelayCommand] |
| | | private void StartTcpServer() |
| | | { |
| | | int port = GetConfigPort(); |
| | | var serverConfig = new ServerConfig() |
| | | { |
| | | MaxRequestLength = 1024 * 1024, |
| | | Port = 7777 |
| | | Port = port |
| | | }; |
| | | try |
| | | { |
| | |
| | | return; |
| | | } |
| | | ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running")); |
| | | AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}"); |
| | | } |
| | | else if (tcpServer.State != SuperSocket.SocketBase.ServerState.Running) |
| | | { |
| | |
| | | return; |
| | | } |
| | | ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running")); |
| | | AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", $"{ex.Message}"); |
| | | AppendLog(LogLevel.Error, $"数据接收服务器启动错误: {ex.Message} {ex.StackTrace}"); |
| | | tcpServer?.Stop(); |
| | | tcpServer = null; |
| | | } |
| | |
| | | { |
| | | executionWrapper?.SetFileGlobalValue("Running.StopTest", 1, true); |
| | | executionWrapper?.WaitForExit(); |
| | | executionWrapper?.Dispose(); |
| | | tcpServer?.Stop(); |
| | | } |
| | | catch (Exception ex) |
| | |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | <Platforms>AnyCPU;x64</Platforms> |
| | | <AssemblyVersion>1.0.0.2</AssemblyVersion> |
| | | <FileVersion>1.0.0.2</FileVersion> |
| | | <AssemblyVersion>1.0.0.3</AssemblyVersion> |
| | | <FileVersion>1.0.0.3</FileVersion> |
| | | <IncludeSymbols>False</IncludeSymbols> |
| | | </PropertyGroup> |
| | | |
| | |
| | | } |
| | | else |
| | | { |
| | | executionWrapper = new TsExecutionWrapper(); |
| | | executionWrapper = new TeststandWrapper(); |
| | | } |
| | | executionWrapper.UIMessageEvent += ExecutionWrapper_UIMessageEvent; |
| | | |
| | | if (executionWrapper is TsExecutionWrapper tsWrapper) |
| | | if (executionWrapper is TeststandWrapper tsWrapper) |
| | | { |
| | | ((System.ComponentModel.ISupportInitialize)(tsWrapper.AxApplicationMgr)).BeginInit(); |
| | | host.Child = tsWrapper.AxApplicationMgr; |
| | |
| | | public void ReleaseResource() |
| | | { |
| | | executionWrapper.UIMessageEvent -= ExecutionWrapper_UIMessageEvent; |
| | | |
| | | executionWrapper.Dispose(); |
| | | foreach (var table in tables) |
| | | { |
| | | table.RelaseResource(); |
| | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("BurnInOvenPort"); |
| | | config.PortStart = int.Parse(value.Value); |
| | | config.PortStart = value == null ? config.PortStart : int.Parse(value.Value); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| New file |
| | |
| | | <pu:WindowX x:Class="Plugin.DeviceConfig.BatchGenerateModal" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:local="clr-namespace:Plugin.DeviceConfig" |
| | | mc:Ignorable="d" |
| | | Title="批量生成设备" |
| | | SizeToContent="Height" |
| | | Width="780" |
| | | WindowStartupLocation="CenterOwner" |
| | | Style="{StaticResource ModalWindow}"> |
| | | <pu:WindowX.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | </ResourceDictionary> |
| | | </pu:WindowX.Resources> |
| | | |
| | | <Grid Margin="16"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 参数输入区 --> |
| | | <GroupBox Grid.Row="0" Header="生成参数" FontWeight="SemiBold" Margin="0,0,0,12"> |
| | | <Grid Margin="8"> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="120"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="120"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="设备类型 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <ComboBox Grid.Row="0" Grid.Column="1" x:Name="cbDeviceType" VerticalAlignment="Center" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="2" Text="驱动类型" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="0" Grid.Column="3" x:Name="tbDriverType" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="传输方式 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <ComboBox Grid.Row="1" Grid.Column="1" x:Name="cbTransport" VerticalAlignment="Center" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="2" Text="设备数量 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <pu:NumberInput Grid.Row="1" Grid.Column="3" x:Name="niDeviceCount" VerticalAlignment="Center" |
| | | Style="{StaticResource FormNumberInput}" Margin="4,0" Minimum="1" Maximum="100" Value="1"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="每设备通道数" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <pu:NumberInput Grid.Row="2" Grid.Column="1" x:Name="niChannelsPerDevice" VerticalAlignment="Center" |
| | | Style="{StaticResource FormNumberInput}" Margin="4,0" Minimum="0" Maximum="64" Value="1"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="2" Text="起始端口号" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <pu:NumberInput Grid.Row="2" Grid.Column="3" x:Name="niStartPort" VerticalAlignment="Center" |
| | | Style="{StaticResource FormNumberInput}" Margin="4,0" Minimum="1" Maximum="65535" Value="30000"/> |
| | | |
| | | <TextBlock Grid.Row="3" Grid.Column="0" Text="地址生成规则" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" x:Name="tbAddressRule" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0" |
| | | pu:TextBoxHelper.Watermark="例: 192.168.10.{0},{0}为序号占位符"/> |
| | | |
| | | <TextBlock Grid.Row="4" Grid.Column="0" Text="Slot映射起点" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="4" Grid.Column="1" x:Name="tbSlotStart" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0" |
| | | pu:TextBoxHelper.Watermark="例: Slot001"/> |
| | | |
| | | <TextBlock Grid.Row="4" Grid.Column="2" Text="设备ID前缀" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="4" Grid.Column="3" x:Name="tbDeviceIdPrefix" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0" |
| | | pu:TextBoxHelper.Watermark="例: PSU"/> |
| | | </Grid> |
| | | </GroupBox> |
| | | |
| | | <!-- 预览区 --> |
| | | <GroupBox Grid.Row="1" Header="生成预览" FontWeight="SemiBold" Margin="0,0,0,12"> |
| | | <Grid Margin="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="200"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" x:Name="tbConflictMsg" Foreground="Red" Margin="0,0,0,4" |
| | | TextWrapping="Wrap" Visibility="Collapsed"/> |
| | | |
| | | <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="previewListView" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="50" Header="#"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Converter={StaticResource lvRowNumber}, |
| | | RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="140" Header="DeviceId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding DeviceId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="Kind"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Kind}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="180" Header="Endpoint"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Endpoint}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="通道数"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelCount}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="Slot起点"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding SlotStart}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | </Grid> |
| | | </GroupBox> |
| | | |
| | | <!-- 底部按钮 --> |
| | | <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" dp:StackPanelHelper.ChildSpace="10" Margin="0,12,0,0"> |
| | | <Button x:Name="previewBtn" Click="PreviewBtn_Click" Content="生成预览" |
| | | Style="{StaticResource RfhBtn}" Width="90"/> |
| | | <Button x:Name="saveBtn" Click="SaveBtn_Click" Content="确认保存" |
| | | Style="{StaticResource AddBtn}" Width="90"/> |
| | | <Button x:Name="cancelBtn" Click="CancelBtn_Click" Content="取消" |
| | | Style="{StaticResource RfhBtn}" Width="90"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using HMI.UI.Util; |
| | | using Panuon.WPF.UI; |
| | | using Plugin.DeviceConfig.Entity; |
| | | using Plugin.DeviceConfig.Model; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Collections.ObjectModel; |
| | | using System.Linq; |
| | | using System.Windows; |
| | | |
| | | namespace Plugin.DeviceConfig |
| | | { |
| | | /// <summary> |
| | | /// BatchGenerateModal.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class BatchGenerateModal : WindowX |
| | | { |
| | | private DeviceConfigModel _model; |
| | | private ObservableCollection<BatchPreviewItem> _previewItems = new(); |
| | | |
| | | public BatchGenerateModal() |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 打开弹窗 |
| | | /// </summary> |
| | | public bool? Open(DeviceConfigModel model) |
| | | { |
| | | _model = model; |
| | | |
| | | // 初始化枚举下拉 |
| | | cbDeviceType.ItemsSource = Enum.GetValues(typeof(DeviceKind)).Cast<DeviceKind>().ToList(); |
| | | cbTransport.ItemsSource = Enum.GetValues(typeof(TransportType)).Cast<TransportType>().ToList(); |
| | | |
| | | // 默认值 |
| | | cbDeviceType.SelectedIndex = 0; |
| | | cbTransport.SelectedIndex = 0; |
| | | tbAddressRule.Text = "192.168.10.{0}"; |
| | | tbSlotStart.Text = "Slot001"; |
| | | tbDeviceIdPrefix.Text = "DEV"; |
| | | |
| | | previewListView.ItemsSource = _previewItems; |
| | | |
| | | return ShowDialog(); |
| | | } |
| | | |
| | | #region 预览 |
| | | |
| | | private void PreviewBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | GeneratePreview(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | tbConflictMsg.Text = $"生成预览失败: {ex.Message}"; |
| | | tbConflictMsg.Visibility = Visibility.Visible; |
| | | } |
| | | } |
| | | |
| | | private void GeneratePreview() |
| | | { |
| | | _previewItems.Clear(); |
| | | tbConflictMsg.Visibility = Visibility.Collapsed; |
| | | |
| | | if (cbDeviceType.SelectedItem is not DeviceKind kind) |
| | | { |
| | | ShowConflict("请选择设备类型"); |
| | | return; |
| | | } |
| | | if (cbTransport.SelectedItem is not TransportType transport) |
| | | { |
| | | ShowConflict("请选择传输方式"); |
| | | return; |
| | | } |
| | | |
| | | int count = (int)(niDeviceCount.Value ?? 1); |
| | | int channelsPerDevice = (int)(niChannelsPerDevice.Value ?? 1); |
| | | int startPort = (int)(niStartPort.Value ?? 30000); |
| | | string addressRule = tbAddressRule.Text?.Trim() ?? string.Empty; |
| | | string slotStart = tbSlotStart.Text?.Trim() ?? string.Empty; |
| | | string prefix = tbDeviceIdPrefix.Text?.Trim() ?? "DEV"; |
| | | string driverType = tbDriverType.Text?.Trim() ?? string.Empty; |
| | | |
| | | // 检测已有设备ID |
| | | var existingDevices = _model.GetAllDevices(); |
| | | var existingIds = new HashSet<string>(existingDevices.Select(d => d.DeviceId)); |
| | | var existingEndpoints = new HashSet<string>(existingDevices.Select(d => d.Endpoint)); |
| | | |
| | | var conflicts = new List<string>(); |
| | | var generatedIds = new HashSet<string>(); |
| | | var generatedEndpoints = new HashSet<string>(); |
| | | |
| | | for (int i = 1; i <= count; i++) |
| | | { |
| | | var deviceId = $"{prefix}{i:D3}"; |
| | | var endpoint = string.Format(addressRule, i); |
| | | if (transport == TransportType.Tcp && !string.IsNullOrEmpty(addressRule)) |
| | | { |
| | | endpoint = $"{endpoint}:{startPort + i - 1}"; |
| | | } |
| | | else if (transport == TransportType.Serial) |
| | | { |
| | | endpoint = $"COM{i}"; |
| | | } |
| | | |
| | | // 解析 SlotStart 的序号部分并递增 |
| | | var slotPrefix = new string(slotStart.TakeWhile(c => !char.IsDigit(c)).ToArray()); |
| | | var slotNumStr = new string(slotStart.SkipWhile(c => !char.IsDigit(c)).ToArray()); |
| | | int.TryParse(slotNumStr, out int startNum); |
| | | var slotId = $"{slotPrefix}{(startNum + i - 1):D3}"; |
| | | |
| | | // 检测冲突 |
| | | if (existingIds.Contains(deviceId) || generatedIds.Contains(deviceId)) |
| | | conflicts.Add($"设备ID冲突: {deviceId}"); |
| | | if (existingEndpoints.Contains(endpoint) || generatedEndpoints.Contains(endpoint)) |
| | | conflicts.Add($"Endpoint冲突: {endpoint}"); |
| | | |
| | | generatedIds.Add(deviceId); |
| | | generatedEndpoints.Add(endpoint); |
| | | |
| | | _previewItems.Add(new BatchPreviewItem |
| | | { |
| | | DeviceId = deviceId, |
| | | Kind = kind, |
| | | DriverType = driverType, |
| | | Transport = transport, |
| | | Endpoint = endpoint, |
| | | ChannelCount = channelsPerDevice, |
| | | SlotStart = slotId, |
| | | }); |
| | | } |
| | | |
| | | if (conflicts.Count > 0) |
| | | { |
| | | ShowConflict(string.Join("\n", conflicts)); |
| | | } |
| | | } |
| | | |
| | | private void ShowConflict(string msg) |
| | | { |
| | | tbConflictMsg.Text = msg; |
| | | tbConflictMsg.Visibility = Visibility.Visible; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 保存 |
| | | |
| | | private void SaveBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (_previewItems.Count == 0) |
| | | { |
| | | ToastHelper.Warning("请先生成预览"); |
| | | return; |
| | | } |
| | | |
| | | if (tbConflictMsg.Visibility == Visibility.Visible) |
| | | { |
| | | if (MsgBoxHelper.WarnMessage(this, "存在冲突项,是否仍要继续保存?") != MessageBoxResult.OK) |
| | | { |
| | | return; |
| | | } |
| | | } |
| | | |
| | | int channelsPerDevice = (int)(niChannelsPerDevice.Value ?? 1); |
| | | string slotStart = tbSlotStart.Text?.Trim() ?? string.Empty; |
| | | |
| | | foreach (var item in _previewItems) |
| | | { |
| | | // 保存设备 |
| | | var device = new DeviceDefinition |
| | | { |
| | | DeviceId = item.DeviceId, |
| | | Kind = item.Kind, |
| | | DriverType = item.DriverType, |
| | | Transport = item.Transport, |
| | | Endpoint = item.Endpoint, |
| | | CreatedAt = DateTime.Now, |
| | | UpdatedAt = DateTime.Now, |
| | | }; |
| | | _model.SaveDevice(device); |
| | | |
| | | // 保存通道 |
| | | for (int ch = 0; ch < channelsPerDevice; ch++) |
| | | { |
| | | var channel = new DeviceChannel |
| | | { |
| | | DeviceId = item.DeviceId, |
| | | ChannelId = ch, |
| | | Capability = $"CH{ch}", |
| | | MaxVoltage = 0, |
| | | MaxCurrent = 0, |
| | | }; |
| | | _model.SaveChannel(channel); |
| | | } |
| | | |
| | | // 保存 Slot 映射 |
| | | if (!string.IsNullOrEmpty(item.SlotStart)) |
| | | { |
| | | var mapping = new SlotDeviceMapping |
| | | { |
| | | SlotId = item.SlotStart, |
| | | Capability = "Default", |
| | | Version = "1.0", |
| | | DeviceId = item.DeviceId, |
| | | ChannelId = 0, |
| | | MappingVersion = "1.0", |
| | | }; |
| | | _model.SaveSlotMapping(mapping); |
| | | } |
| | | } |
| | | |
| | | ToastHelper.Success($"成功生成 {_previewItems.Count} 个设备"); |
| | | DialogResult = true; |
| | | Close(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"保存失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void CancelBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | DialogResult = false; |
| | | Close(); |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 批量生成预览项 |
| | | /// </summary> |
| | | public class BatchPreviewItem |
| | | { |
| | | public string DeviceId { get; set; } = string.Empty; |
| | | public DeviceKind Kind { get; set; } |
| | | public string DriverType { get; set; } = string.Empty; |
| | | public TransportType Transport { get; set; } |
| | | public string Endpoint { get; set; } = string.Empty; |
| | | public int ChannelCount { get; set; } |
| | | public string SlotStart { get; set; } = string.Empty; |
| | | } |
| | | } |
| New file |
| | |
| | | <hb:SimpleMaskControl x:Class="Plugin.DeviceConfig.DeviceConfig" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:local="clr-namespace:Plugin.DeviceConfig" |
| | | mc:Ignorable="d" |
| | | Loaded="SimpleMaskControl_Loaded" |
| | | d:DesignHeight="700" d:DesignWidth="1200"> |
| | | <hb:SimpleMaskControl.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | <Style x:Key="TitleBox" TargetType="TextBlock"> |
| | | <Setter Property="FontSize" Value="14"/> |
| | | <Setter Property="FontWeight" Value="SemiBold"/> |
| | | <Setter Property="Padding" Value="8,4"/> |
| | | <Setter Property="VerticalAlignment" Value="Center"/> |
| | | </Style> |
| | | </ResourceDictionary> |
| | | </hb:SimpleMaskControl.Resources> |
| | | |
| | | <Grid dp:GridHelper.Space="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 顶部工具栏 --> |
| | | <DockPanel Grid.Row="0"> |
| | | <StackPanel dp:StackPanelHelper.ChildSpace="10" DockPanel.Dock="Right" Orientation="Horizontal"> |
| | | <Button Style="{StaticResource AddBtn}" x:Name="addBtn" Click="Button_Click"/> |
| | | <Button Style="{StaticResource EditBtn}" x:Name="editBtn" Content="编辑" Click="Button_Click"/> |
| | | <Button Style="{StaticResource DelBtn}" x:Name="delBtn" Content="删除" Click="Button_Click"/> |
| | | <Button x:Name="batchBtn" Click="Button_Click" Content="批量生成" |
| | | Style="{StaticResource AddBtn}"/> |
| | | <Button Style="{StaticResource ImportBtn}" x:Name="importBtn" Click="Button_Click"/> |
| | | <Button Style="{StaticResource ExportBtn}" x:Name="exportBtn" Click="Button_Click"/> |
| | | <Button x:Name="versionBtn" Click="Button_Click" Content="版本管理" |
| | | Style="{StaticResource RfhBtn}"/> |
| | | <Button Style="{StaticResource RfhBtn}" x:Name="rfhBtn" Click="Button_Click"/> |
| | | </StackPanel> |
| | | <TextBlock Style="{StaticResource TitleBox}" Text="仪器仪表注册"/> |
| | | </DockPanel> |
| | | |
| | | <!-- 主体三栏区域 --> |
| | | <Grid Grid.Row="1"> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="250"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="300"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 左栏:设备分类树 --> |
| | | <Border Grid.Column="0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" Margin="0,0,4,0"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <TextBox x:Name="searchBox" Grid.Row="0" Margin="6" |
| | | pu:TextBoxHelper.Watermark="搜索设备..." |
| | | Style="{StaticResource EditorTextBoxStyle}" |
| | | TextChanged="SearchBox_TextChanged"/> |
| | | <TreeView x:Name="deviceTree" Grid.Row="1" Margin="4" |
| | | SelectedItemChanged="DeviceTree_SelectedItemChanged"> |
| | | <TreeView.ItemTemplate> |
| | | <HierarchicalDataTemplate ItemsSource="{Binding Children}"> |
| | | <TextBlock Text="{Binding Name}" FontWeight="SemiBold"/> |
| | | </HierarchicalDataTemplate> |
| | | </TreeView.ItemTemplate> |
| | | </TreeView> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 中栏:设备详情 --> |
| | | <Border Grid.Column="1" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" Margin="2,0,2,0"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 设备基本信息表单 --> |
| | | <GroupBox Grid.Row="0" Header="设备信息" Margin="6" FontWeight="SemiBold"> |
| | | <Grid Margin="8"> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="120"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="120"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="设备ID" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbDeviceId" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="2" Text="设备类型" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="0" Grid.Column="3" x:Name="tbKind" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="驱动类型" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbDriverType" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="2" Text="传输方式" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="1" Grid.Column="3" x:Name="tbTransport" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="连接端点" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="2" Grid.Column="1" x:Name="tbEndpoint" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="2" Text="生效版本" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="2" Grid.Column="3" x:Name="tbEffectiveVersion" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" Margin="4,0"/> |
| | | </Grid> |
| | | </GroupBox> |
| | | |
| | | <!-- 通道映射表 --> |
| | | <Grid Grid.Row="1" Margin="6"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <TextBlock Grid.Row="0" Text="通道列表" FontWeight="SemiBold" Margin="0,0,0,4"/> |
| | | <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="channelListView" |
| | | ItemsSource="{Binding Channels}" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="50" Header="#"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Converter={StaticResource lvRowNumber}, |
| | | RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="150" Header="DeviceId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding DeviceId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="ChannelId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="200" Header="Capability"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Capability}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="MaxVoltage"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxVoltage, StringFormat=F2}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="MaxCurrent"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxCurrent, StringFormat=F2}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | </Grid> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 右栏:Slot映射表 --> |
| | | <Border Grid.Column="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" Margin="4,0,0,0"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <TextBlock Grid.Row="0" Text="Slot 映射" FontWeight="SemiBold" Margin="6"/> |
| | | <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="slotListView" |
| | | ItemsSource="{Binding SlotMappings}" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard" |
| | | Margin="4"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="80" Header="SlotId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding SlotId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="Capability"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Capability}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="DeviceId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding DeviceId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="60" Header="ChId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="MapVer"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MappingVersion}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | </Grid> |
| | | </Border> |
| | | </Grid> |
| | | |
| | | <!-- 底部状态栏 --> |
| | | <DockPanel Grid.Row="2" Margin="0,4,0,0"> |
| | | <StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="6"> |
| | | <TextBlock Text="当前版本状态:" FontWeight="SemiBold" VerticalAlignment="Center"/> |
| | | <Border Background="{DynamicResource SecondPrimaryColor}" CornerRadius="3" Padding="8,2"> |
| | | <TextBlock x:Name="tbVersionStatus" Text="{Binding CurrentVersionStatus}" Foreground="White" FontWeight="Bold"/> |
| | | </Border> |
| | | </StackPanel> |
| | | <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right" dp:StackPanelHelper.ChildSpace="10"> |
| | | <Button x:Name="saveDraftBtn" Click="Button_Click" Content="保存草稿" |
| | | Style="{StaticResource RfhBtn}" Width="90"/> |
| | | <Button x:Name="publishBtn" Click="Button_Click" Content="发布" |
| | | Style="{StaticResource AddBtn}" Width="70"/> |
| | | <Button x:Name="applyBtn" Click="Button_Click" Content="应用" |
| | | Style="{StaticResource AddBtn}" Width="70"/> |
| | | </StackPanel> |
| | | </DockPanel> |
| | | </Grid> |
| | | </hb:SimpleMaskControl> |
| New file |
| | |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | using HMI.Base; |
| | | using HMI.Core.Util; |
| | | using HMI.UI.Util; |
| | | using Plugin.DeviceConfig.Entity; |
| | | using Plugin.DeviceConfig.Model; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Collections.ObjectModel; |
| | | using System.IO; |
| | | using System.Linq; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Data; |
| | | |
| | | namespace Plugin.DeviceConfig |
| | | { |
| | | /// <summary> |
| | | /// DeviceConfig.xaml 的交互逻辑 |
| | | /// </summary> |
| | | [ObservableObject] |
| | | public partial class DeviceConfig : SimpleMaskControl |
| | | { |
| | | private DeviceConfigModel _model; |
| | | |
| | | [ObservableProperty] |
| | | private ObservableCollection<DeviceDefinition> devices = new(); |
| | | |
| | | [ObservableProperty] |
| | | private ObservableCollection<DeviceChannel> channels = new(); |
| | | |
| | | [ObservableProperty] |
| | | private ObservableCollection<SlotDeviceMapping> slotMappings = new(); |
| | | |
| | | [ObservableProperty] |
| | | private DeviceDefinition selectedDevice; |
| | | |
| | | [ObservableProperty] |
| | | private string currentVersionStatus = "Draft"; |
| | | |
| | | [ObservableProperty] |
| | | private ObservableCollection<TreeNode> treeCategories = new(); |
| | | |
| | | public DeviceConfig() |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 强制刷新数据(供集成测试预置设备后调用) |
| | | /// </summary> |
| | | public void ForceRefreshForTest() |
| | | { |
| | | try { RefreshAllData(); } |
| | | catch (Exception ex) { log.Error("ForceRefreshForTest Error.", ex); } |
| | | } |
| | | |
| | | public override void InitFinish() |
| | | { |
| | | base.InitFinish(); |
| | | try |
| | | { |
| | | _model = new DeviceConfigModel(); |
| | | var dbPath = Path.Combine(BaseFolder, "data.db"); |
| | | _model.InitDatabase(dbPath); |
| | | LoadTreeCategories(); |
| | | RefreshAllData(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("InitFinish Error.", ex); |
| | | ToastHelper.Error($"初始化失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #region 树分类加载 |
| | | |
| | | private void LoadTreeCategories() |
| | | { |
| | | TreeCategories = new ObservableCollection<TreeNode> |
| | | { |
| | | new TreeNode { Name = "全部设备", Kind = null, Children = new ObservableCollection<TreeNode>() }, |
| | | new TreeNode { Name = "电源系统", Kind = DeviceKind.PowerSupply, Children = new ObservableCollection<TreeNode>() }, |
| | | new TreeNode { Name = "CAN/LIN", Kind = DeviceKind.CanDevice, Children = new ObservableCollection<TreeNode>() }, |
| | | new TreeNode { Name = "PLC/IO", Kind = DeviceKind.PlcDevice, Children = new ObservableCollection<TreeNode>() }, |
| | | new TreeNode { Name = "温箱/DAQ", Kind = null, Children = new ObservableCollection<TreeNode>(), |
| | | FilterKinds = new[] { DeviceKind.Chamber, DeviceKind.DAQ } }, |
| | | }; |
| | | deviceTree.ItemsSource = TreeCategories; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 数据加载 |
| | | |
| | | private void RefreshAllData() |
| | | { |
| | | try |
| | | { |
| | | var deviceList = _model.GetAllDevices(); |
| | | Devices = new ObservableCollection<DeviceDefinition>(deviceList); |
| | | SlotMappings = new ObservableCollection<SlotDeviceMapping>(_model.GetSlotMappings()); |
| | | RefreshTreeDevices(); |
| | | LoadLatestVersionStatus(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("RefreshAllData Error.", ex); |
| | | ToastHelper.Error($"加载数据失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void RefreshTreeDevices() |
| | | { |
| | | foreach (var cat in TreeCategories) |
| | | { |
| | | cat.Children.Clear(); |
| | | IEnumerable<DeviceDefinition> filtered = Devices; |
| | | if (cat.Kind.HasValue) |
| | | { |
| | | filtered = Devices.Where(d => d.Kind == cat.Kind.Value); |
| | | } |
| | | else if (cat.FilterKinds != null) |
| | | { |
| | | filtered = Devices.Where(d => cat.FilterKinds.Contains(d.Kind)); |
| | | } |
| | | foreach (var dev in filtered) |
| | | { |
| | | cat.Children.Add(new TreeNode { Name = $"{dev.DeviceId} ({dev.Endpoint})", Tag = dev }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private void LoadLatestVersionStatus() |
| | | { |
| | | try |
| | | { |
| | | var versions = _model.GetConfigVersions(); |
| | | var effective = versions.FirstOrDefault(v => v.Status == VersionStatus.Effective); |
| | | if (effective != null) |
| | | { |
| | | CurrentVersionStatus = "Effective"; |
| | | } |
| | | else if (versions.Any(v => v.Status == VersionStatus.Published)) |
| | | { |
| | | CurrentVersionStatus = "Published"; |
| | | } |
| | | else |
| | | { |
| | | CurrentVersionStatus = "Draft"; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("LoadLatestVersionStatus Error.", ex); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 事件处理 |
| | | |
| | | private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | // InitFinish 已加载数据,此处仅做一次性 UI 初始化 |
| | | Loaded -= SimpleMaskControl_Loaded; |
| | | } |
| | | |
| | | private void DeviceTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) |
| | | { |
| | | if (e.NewValue is TreeNode node && node.Tag is DeviceDefinition dev) |
| | | { |
| | | SelectDevice(dev); |
| | | } |
| | | } |
| | | |
| | | private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | var keyword = searchBox.Text?.Trim() ?? string.Empty; |
| | | if (string.IsNullOrEmpty(keyword)) |
| | | { |
| | | RefreshTreeDevices(); |
| | | return; |
| | | } |
| | | foreach (var cat in TreeCategories) |
| | | { |
| | | cat.Children.Clear(); |
| | | var matched = Devices.Where(d => |
| | | d.DeviceId.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0 || |
| | | d.Endpoint.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0 || |
| | | d.DriverType.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0); |
| | | if (cat.Kind.HasValue) |
| | | matched = matched.Where(d => d.Kind == cat.Kind.Value); |
| | | else if (cat.FilterKinds != null) |
| | | matched = matched.Where(d => cat.FilterKinds.Contains(d.Kind)); |
| | | |
| | | foreach (var dev in matched) |
| | | cat.Children.Add(new TreeNode { Name = $"{dev.DeviceId} ({dev.Endpoint})", Tag = dev }); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("SearchBox_TextChanged Error.", ex); |
| | | } |
| | | } |
| | | |
| | | private void SelectDevice(DeviceDefinition dev) |
| | | { |
| | | try |
| | | { |
| | | SelectedDevice = dev; |
| | | tbDeviceId.Text = dev.DeviceId; |
| | | tbKind.Text = dev.Kind.ToString(); |
| | | tbDriverType.Text = dev.DriverType; |
| | | tbTransport.Text = dev.Transport.ToString(); |
| | | tbEndpoint.Text = dev.Endpoint; |
| | | tbEffectiveVersion.Text = dev.EffectiveVersion; |
| | | |
| | | var chList = _model.GetChannelsByDevice(dev.DeviceId); |
| | | Channels = new ObservableCollection<DeviceChannel>(chList); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("SelectDevice Error.", ex); |
| | | ToastHelper.Error($"加载设备详情失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void Button_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | Button btn = sender as Button; |
| | | try |
| | | { |
| | | switch (btn?.Name) |
| | | { |
| | | case "addBtn": |
| | | OpenDeviceEditModal(null); |
| | | break; |
| | | case "editBtn": |
| | | EditSelectedDevice(); |
| | | break; |
| | | case "delBtn": |
| | | DeleteSelectedDevice(); |
| | | break; |
| | | case "batchBtn": |
| | | OpenBatchGenerateModal(); |
| | | break; |
| | | case "importBtn": |
| | | ImportDevices(); |
| | | break; |
| | | case "exportBtn": |
| | | ExportDevices(); |
| | | break; |
| | | case "versionBtn": |
| | | OpenVersionManageModal(); |
| | | break; |
| | | case "rfhBtn": |
| | | RefreshAllData(); |
| | | ToastHelper.Success("刷新成功"); |
| | | break; |
| | | case "saveDraftBtn": |
| | | SaveDraft(); |
| | | break; |
| | | case "publishBtn": |
| | | PublishVersion(); |
| | | break; |
| | | case "applyBtn": |
| | | ApplyVersion(); |
| | | break; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("Button_Click Error.", ex); |
| | | ToastHelper.Error($"操作失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 弹窗 |
| | | |
| | | private void OpenDeviceEditModal(DeviceDefinition device) |
| | | { |
| | | try |
| | | { |
| | | device ??= new DeviceDefinition(); |
| | | var win = new DeviceEditModal |
| | | { |
| | | Title = string.IsNullOrEmpty(device.DeviceId) ? "新增设备" : "编辑设备", |
| | | Owner = Window.GetWindow(this) |
| | | }; |
| | | if (win.Open(device, _model) == true) |
| | | { |
| | | RefreshAllData(); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("OpenDeviceEditModal Error.", ex); |
| | | ToastHelper.Error($"打开编辑弹窗失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 编辑当前选中的设备 |
| | | /// </summary> |
| | | private void EditSelectedDevice() |
| | | { |
| | | try |
| | | { |
| | | if (SelectedDevice == null) |
| | | { |
| | | ToastHelper.Warning("请先在左侧设备树中选择一个设备"); |
| | | return; |
| | | } |
| | | |
| | | // 使用当前设备信息的副本避免直接修改内部数据 |
| | | var editingDevice = new DeviceDefinition |
| | | { |
| | | DeviceId = SelectedDevice.DeviceId, |
| | | Kind = SelectedDevice.Kind, |
| | | DriverType = SelectedDevice.DriverType, |
| | | Transport = SelectedDevice.Transport, |
| | | Endpoint = SelectedDevice.Endpoint, |
| | | EffectiveVersion = SelectedDevice.EffectiveVersion, |
| | | CreatedAt = SelectedDevice.CreatedAt, |
| | | UpdatedAt = SelectedDevice.UpdatedAt |
| | | }; |
| | | |
| | | var win = new DeviceEditModal |
| | | { |
| | | Title = $"编辑设备 - {SelectedDevice.DeviceId}", |
| | | Owner = Window.GetWindow(this) |
| | | }; |
| | | if (win.Open(editingDevice, _model) == true) |
| | | { |
| | | RefreshAllData(); |
| | | ToastHelper.Success($"设备 {editingDevice.DeviceId} 已更新"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("EditSelectedDevice Error.", ex); |
| | | ToastHelper.Error($"编辑设备失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除当前选中的设备(同时清理相关通道与 Slot 映射) |
| | | /// </summary> |
| | | private void DeleteSelectedDevice() |
| | | { |
| | | try |
| | | { |
| | | if (SelectedDevice == null) |
| | | { |
| | | ToastHelper.Warning("请先在左侧设备树中选择一个设备"); |
| | | return; |
| | | } |
| | | |
| | | var deviceId = SelectedDevice.DeviceId; |
| | | if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), |
| | | $"确定要删除设备 [{deviceId}] 吗?\n该操作将同时删除相关的通道与 Slot 映射,且不可恢复。") != MessageBoxResult.OK) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 先删除关联的通道与 Slot 映射 |
| | | var channels = _model.GetChannelsByDevice(deviceId); |
| | | foreach (var ch in channels) |
| | | { |
| | | _model.DeleteChannel(deviceId, ch.ChannelId); |
| | | } |
| | | |
| | | var mappings = _model.GetSlotMappings(); |
| | | foreach (var m in mappings.Where(x => x.DeviceId == deviceId)) |
| | | { |
| | | _model.DeleteSlotMapping(m.SlotId, m.Capability, m.Version); |
| | | } |
| | | |
| | | // 删除设备本身 |
| | | _model.DeleteDevice(deviceId); |
| | | |
| | | SelectedDevice = null; |
| | | ClearDeviceDetails(); |
| | | RefreshAllData(); |
| | | ToastHelper.Success($"设备 {deviceId} 已删除"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("DeleteSelectedDevice Error.", ex); |
| | | ToastHelper.Error($"删除设备失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清空中栏设备详情展示 |
| | | /// </summary> |
| | | private void ClearDeviceDetails() |
| | | { |
| | | tbDeviceId.Text = string.Empty; |
| | | tbKind.Text = string.Empty; |
| | | tbDriverType.Text = string.Empty; |
| | | tbTransport.Text = string.Empty; |
| | | tbEndpoint.Text = string.Empty; |
| | | tbEffectiveVersion.Text = string.Empty; |
| | | Channels = new ObservableCollection<DeviceChannel>(); |
| | | } |
| | | |
| | | private void OpenBatchGenerateModal() |
| | | { |
| | | try |
| | | { |
| | | var win = new BatchGenerateModal |
| | | { |
| | | Title = "批量生成设备", |
| | | Owner = Window.GetWindow(this) |
| | | }; |
| | | if (win.Open(_model) == true) |
| | | { |
| | | RefreshAllData(); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("OpenBatchGenerateModal Error.", ex); |
| | | ToastHelper.Error($"打开批量生成弹窗失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void OpenVersionManageModal() |
| | | { |
| | | try |
| | | { |
| | | var win = new VersionManageModal |
| | | { |
| | | Title = "版本管理", |
| | | Owner = Window.GetWindow(this) |
| | | }; |
| | | if (win.Open(_model) == true) |
| | | { |
| | | RefreshAllData(); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("OpenVersionManageModal Error.", ex); |
| | | ToastHelper.Error($"打开版本管理弹窗失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 版本操作 |
| | | |
| | | private void SaveDraft() |
| | | { |
| | | try |
| | | { |
| | | var versionId = $"V{DateTime.Now:yyyyMMddHHmmss}"; |
| | | var hash = _model.ComputeConfigHash(); |
| | | var ver = new DeviceConfigVersion |
| | | { |
| | | VersionId = versionId, |
| | | Status = VersionStatus.Draft, |
| | | Hash = hash, |
| | | Author = Environment.UserName, |
| | | Timestamp = DateTime.Now, |
| | | DiffDescription = "保存草稿" |
| | | }; |
| | | _model.SaveConfigVersion(ver); |
| | | CurrentVersionStatus = "Draft"; |
| | | ToastHelper.Success($"草稿已保存: {versionId}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("SaveDraft Error.", ex); |
| | | ToastHelper.Error($"保存草稿失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void PublishVersion() |
| | | { |
| | | try |
| | | { |
| | | var versions = _model.GetConfigVersions(); |
| | | var draft = versions.FirstOrDefault(v => v.Status == VersionStatus.Draft); |
| | | if (draft == null) |
| | | { |
| | | ToastHelper.Warning("没有可发布的草稿版本"); |
| | | return; |
| | | } |
| | | var hash = _model.ComputeConfigHash(); |
| | | draft.Hash = hash; |
| | | draft.Status = VersionStatus.Published; |
| | | draft.Timestamp = DateTime.Now; |
| | | _model.SaveConfigVersion(draft); |
| | | CurrentVersionStatus = "Published"; |
| | | ToastHelper.Success($"版本 {draft.VersionId} 已发布"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("PublishVersion Error.", ex); |
| | | ToastHelper.Error($"发布失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void ApplyVersion() |
| | | { |
| | | try |
| | | { |
| | | var versions = _model.GetConfigVersions(); |
| | | var published = versions.FirstOrDefault(v => v.Status == VersionStatus.Published); |
| | | if (published == null) |
| | | { |
| | | ToastHelper.Warning("没有可应用的已发布版本"); |
| | | return; |
| | | } |
| | | |
| | | var effective = versions.FirstOrDefault(v => v.Status == VersionStatus.Effective); |
| | | if (effective != null) |
| | | { |
| | | if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), |
| | | $"当前已有生效版本 [{effective.VersionId}],应用新版本将替换它,是否继续?") != MessageBoxResult.OK) |
| | | { |
| | | return; |
| | | } |
| | | effective.Status = VersionStatus.Published; |
| | | _model.SaveConfigVersion(effective); |
| | | } |
| | | |
| | | published.Status = VersionStatus.Effective; |
| | | published.Timestamp = DateTime.Now; |
| | | _model.SaveConfigVersion(published); |
| | | CurrentVersionStatus = "Effective"; |
| | | ToastHelper.Success($"版本 {published.VersionId} 已应用为生效版本"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("ApplyVersion Error.", ex); |
| | | ToastHelper.Error($"应用版本失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 导入导出 |
| | | |
| | | private void ImportDevices() |
| | | { |
| | | try |
| | | { |
| | | var ofd = new Microsoft.Win32.OpenFileDialog |
| | | { |
| | | Filter = "XML文件|*.xml|JSON文件|*.json" |
| | | }; |
| | | if (ofd.ShowDialog() == true) |
| | | { |
| | | // TODO: 实现导入逻辑 |
| | | ToastHelper.Success($"已从 {Path.GetFileName(ofd.FileName)} 导入"); |
| | | RefreshAllData(); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("ImportDevices Error.", ex); |
| | | ToastHelper.Error($"导入失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void ExportDevices() |
| | | { |
| | | try |
| | | { |
| | | var sfd = new Microsoft.Win32.SaveFileDialog |
| | | { |
| | | FileName = "DeviceConfig", |
| | | Filter = "XML文件|*.xml" |
| | | }; |
| | | if (sfd.ShowDialog() == true) |
| | | { |
| | | // TODO: 实现导出逻辑 |
| | | ToastHelper.Success("导出成功"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("ExportDevices Error.", ex); |
| | | ToastHelper.Error($"导出失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 树节点模型 |
| | | /// </summary> |
| | | public class TreeNode |
| | | { |
| | | public string Name { get; set; } = string.Empty; |
| | | public DeviceKind? Kind { get; set; } |
| | | public DeviceKind[] FilterKinds { get; set; } |
| | | public object Tag { get; set; } |
| | | public ObservableCollection<TreeNode> Children { get; set; } = new(); |
| | | } |
| | | } |
| New file |
| | |
| | | <pu:WindowX x:Class="Plugin.DeviceConfig.DeviceEditModal" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:local="clr-namespace:Plugin.DeviceConfig" |
| | | mc:Ignorable="d" |
| | | Title="设备编辑" |
| | | SizeToContent="Height" |
| | | Width="720" |
| | | WindowStartupLocation="CenterOwner" |
| | | Style="{StaticResource ModalWindow}"> |
| | | <pu:WindowX.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | </ResourceDictionary> |
| | | </pu:WindowX.Resources> |
| | | |
| | | <Grid Margin="16"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 设备基本信息 --> |
| | | <GroupBox Grid.Row="0" Header="设备信息" FontWeight="SemiBold" Margin="0,0,0,12"> |
| | | <Grid Margin="8"> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | <RowDefinition Height="36"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="设备ID *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="0" Grid.Column="1" x:Name="tbDeviceId" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="2" Text="设备类型 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <ComboBox Grid.Row="0" Grid.Column="3" x:Name="cbKind" VerticalAlignment="Center" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="驱动类型" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="1" Grid.Column="1" x:Name="tbDriverType" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="2" Text="传输方式 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <ComboBox Grid.Row="1" Grid.Column="3" x:Name="cbTransport" VerticalAlignment="Center" Margin="4,0"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="连接端点 *" VerticalAlignment="Center" Style="{StaticResource ListTableTextBlock}"/> |
| | | <TextBox Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" x:Name="tbEndpoint" VerticalAlignment="Center" |
| | | Style="{StaticResource EditorTextBoxStyle}" Margin="4,0" |
| | | pu:TextBoxHelper.Watermark="例: 192.168.10.124:30000 或 COM3"/> |
| | | </Grid> |
| | | </GroupBox> |
| | | |
| | | <!-- 通道列表 --> |
| | | <GroupBox Grid.Row="1" Header="通道列表" FontWeight="SemiBold" Margin="0,0,0,12"> |
| | | <Grid Margin="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="180"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,4" |
| | | dp:StackPanelHelper.ChildSpace="8"> |
| | | <Button x:Name="addChannelBtn" Click="AddChannel_Click" Content="+ 新增通道" |
| | | Style="{StaticResource AddBtn}" Width="100"/> |
| | | <Button x:Name="delChannelBtn" Click="DelChannel_Click" Content="删除通道" |
| | | Style="{StaticResource DelBtn}" Width="100"/> |
| | | </StackPanel> |
| | | |
| | | <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="channelListView" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="50" Header="#"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Converter={StaticResource lvRowNumber}, |
| | | RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="ChannelId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding ChannelId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="200" Header="Capability"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Capability}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="MaxVoltage"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxVoltage, StringFormat=F2}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="MaxCurrent"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding MaxCurrent, StringFormat=F2}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="操作"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="6"> |
| | | <Button x:Name="editChBtn" Style="{StaticResource EditLink}" Click="EditChannel_Click" Tag="{Binding}"/> |
| | | <Button x:Name="delChBtn" Style="{StaticResource DeleteLink}" Click="DeleteChannel_Click" Tag="{Binding}"/> |
| | | </StackPanel> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | |
| | | <!-- 通道编辑行 --> |
| | | <Grid Grid.Row="2" Margin="0,6,0,0"> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="80"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="80"/> |
| | | </Grid.ColumnDefinitions> |
| | | <TextBox Grid.Column="0" x:Name="tbChId" Style="{StaticResource EditorTextBoxStyle}" |
| | | pu:TextBoxHelper.Watermark="编号" Margin="0,0,4,0"/> |
| | | <TextBox Grid.Column="1" x:Name="tbChCap" Style="{StaticResource EditorTextBoxStyle}" |
| | | pu:TextBoxHelper.Watermark="能力描述" Margin="0,0,4,0"/> |
| | | <TextBox Grid.Column="2" x:Name="tbChVolt" Style="{StaticResource EditorTextBoxStyle}" |
| | | pu:TextBoxHelper.Watermark="最大电压" Margin="0,0,4,0"/> |
| | | <TextBox Grid.Column="3" x:Name="tbChCurr" Style="{StaticResource EditorTextBoxStyle}" |
| | | pu:TextBoxHelper.Watermark="最大电流" Margin="0,0,4,0"/> |
| | | <Button Grid.Column="4" x:Name="saveChBtn" Click="SaveChannel_Click" Content="保存" |
| | | Style="{StaticResource AddBtn}" Width="70"/> |
| | | </Grid> |
| | | </Grid> |
| | | </GroupBox> |
| | | |
| | | <!-- 底部按钮 --> |
| | | <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" dp:StackPanelHelper.ChildSpace="10" Margin="0,12,0,0"> |
| | | <Button x:Name="saveBtn" Click="SaveBtn_Click" Content="保存" |
| | | Style="{StaticResource AddBtn}" Width="90"/> |
| | | <Button x:Name="cancelBtn" Click="CancelBtn_Click" Content="取消" |
| | | Style="{StaticResource RfhBtn}" Width="90"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using HMI.UI.Util; |
| | | using Panuon.WPF.UI; |
| | | using Plugin.DeviceConfig.Entity; |
| | | using Plugin.DeviceConfig.Model; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.Linq; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Plugin.DeviceConfig |
| | | { |
| | | /// <summary> |
| | | /// DeviceEditModal.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class DeviceEditModal : WindowX |
| | | { |
| | | private DeviceConfigModel _model; |
| | | private DeviceDefinition _device; |
| | | private ObservableCollection<DeviceChannel> _channels = new(); |
| | | private bool _isNew; |
| | | |
| | | public DeviceEditModal() |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 打开弹窗 |
| | | /// </summary> |
| | | public bool? Open(DeviceDefinition device, DeviceConfigModel model) |
| | | { |
| | | _model = model; |
| | | _device = device; |
| | | _isNew = string.IsNullOrEmpty(device.DeviceId); |
| | | |
| | | // 初始化枚举下拉 |
| | | cbKind.ItemsSource = Enum.GetValues(typeof(DeviceKind)).Cast<DeviceKind>().ToList(); |
| | | cbTransport.ItemsSource = Enum.GetValues(typeof(TransportType)).Cast<TransportType>().ToList(); |
| | | cbTransport.SelectionChanged += CbTransport_SelectionChanged; |
| | | |
| | | // 填充数据 |
| | | tbDeviceId.Text = device.DeviceId; |
| | | tbDeviceId.IsReadOnly = !_isNew; |
| | | cbKind.SelectedItem = device.Kind; |
| | | tbDriverType.Text = device.DriverType; |
| | | cbTransport.SelectedItem = device.Transport; |
| | | tbEndpoint.Text = device.Endpoint; |
| | | |
| | | // 默认驱动类型建议 & 端点提示 |
| | | UpdateDriverHint(); |
| | | UpdateEndpointHint(); |
| | | |
| | | // 加载通道 |
| | | if (!_isNew && !string.IsNullOrEmpty(device.DeviceId)) |
| | | { |
| | | var chList = _model.GetChannelsByDevice(device.DeviceId); |
| | | _channels = new ObservableCollection<DeviceChannel>(chList); |
| | | } |
| | | else |
| | | { |
| | | _channels = new ObservableCollection<DeviceChannel>(); |
| | | } |
| | | channelListView.ItemsSource = _channels; |
| | | |
| | | return ShowDialog(); |
| | | } |
| | | |
| | | #region 通道操作 |
| | | |
| | | private void AddChannel_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (!int.TryParse(tbChId.Text, out int chId) || chId < 0) |
| | | { |
| | | ToastHelper.Warning("请输入有效的通道编号(非负整数)"); |
| | | return; |
| | | } |
| | | if (_channels.Any(c => c.ChannelId == chId)) |
| | | { |
| | | ToastHelper.Warning($"通道 {chId} 已存在"); |
| | | return; |
| | | } |
| | | |
| | | double.TryParse(tbChVolt.Text, out double volt); |
| | | double.TryParse(tbChCurr.Text, out double curr); |
| | | |
| | | var ch = new DeviceChannel |
| | | { |
| | | DeviceId = _device.DeviceId ?? tbDeviceId.Text, |
| | | ChannelId = chId, |
| | | Capability = tbChCap.Text ?? string.Empty, |
| | | MaxVoltage = volt, |
| | | MaxCurrent = curr, |
| | | }; |
| | | _channels.Add(ch); |
| | | ClearChannelInputs(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"新增通道失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void EditChannel_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (sender is Button btn && btn.Tag is DeviceChannel ch) |
| | | { |
| | | tbChId.Text = ch.ChannelId.ToString(); |
| | | tbChCap.Text = ch.Capability; |
| | | tbChVolt.Text = ch.MaxVoltage.ToString("F2"); |
| | | tbChCurr.Text = ch.MaxCurrent.ToString("F2"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"加载通道数据失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void DeleteChannel_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (sender is Button btn && btn.Tag is DeviceChannel ch) |
| | | { |
| | | if (MsgBoxHelper.WarnMessage(this, $"确定要删除通道 {ch.ChannelId} 吗?") == MessageBoxResult.OK) |
| | | { |
| | | _channels.Remove(ch); |
| | | } |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"删除通道失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void SaveChannel_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (!int.TryParse(tbChId.Text, out int chId) || chId < 0) |
| | | { |
| | | ToastHelper.Warning("请输入有效的通道编号"); |
| | | return; |
| | | } |
| | | |
| | | double.TryParse(tbChVolt.Text, out double volt); |
| | | double.TryParse(tbChCurr.Text, out double curr); |
| | | |
| | | var existing = _channels.FirstOrDefault(c => c.ChannelId == chId); |
| | | if (existing != null) |
| | | { |
| | | existing.Capability = tbChCap.Text ?? string.Empty; |
| | | existing.MaxVoltage = volt; |
| | | existing.MaxCurrent = curr; |
| | | } |
| | | else |
| | | { |
| | | _channels.Add(new DeviceChannel |
| | | { |
| | | DeviceId = _device.DeviceId ?? tbDeviceId.Text, |
| | | ChannelId = chId, |
| | | Capability = tbChCap.Text ?? string.Empty, |
| | | MaxVoltage = volt, |
| | | MaxCurrent = curr, |
| | | }); |
| | | } |
| | | ClearChannelInputs(); |
| | | ToastHelper.Success("通道已保存"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"保存通道失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void DelChannel_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | var selected = channelListView.SelectedItem as DeviceChannel; |
| | | if (selected == null) |
| | | { |
| | | ToastHelper.Warning("请先选中要删除的通道"); |
| | | return; |
| | | } |
| | | if (MsgBoxHelper.WarnMessage(this, $"确定要删除通道 {selected.ChannelId} 吗?") == MessageBoxResult.OK) |
| | | { |
| | | _channels.Remove(selected); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"删除通道失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void ClearChannelInputs() |
| | | { |
| | | tbChId.Text = string.Empty; |
| | | tbChCap.Text = string.Empty; |
| | | tbChVolt.Text = string.Empty; |
| | | tbChCurr.Text = string.Empty; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 保存/取消 |
| | | |
| | | private void SaveBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | // 验证 |
| | | var deviceId = tbDeviceId.Text?.Trim(); |
| | | if (string.IsNullOrEmpty(deviceId)) |
| | | { |
| | | ToastHelper.Warning("设备ID不能为空"); |
| | | return; |
| | | } |
| | | |
| | | if (cbKind.SelectedItem is not DeviceKind kind) |
| | | { |
| | | ToastHelper.Warning("请选择设备类型"); |
| | | return; |
| | | } |
| | | |
| | | if (cbTransport.SelectedItem is not TransportType transport) |
| | | { |
| | | ToastHelper.Warning("请选择传输方式"); |
| | | return; |
| | | } |
| | | |
| | | var endpoint = tbEndpoint.Text?.Trim(); |
| | | if (string.IsNullOrEmpty(endpoint)) |
| | | { |
| | | ToastHelper.Warning("连接端点不能为空"); |
| | | return; |
| | | } |
| | | |
| | | // 验证 Endpoint 格式 |
| | | if (transport == TransportType.Tcp) |
| | | { |
| | | if (!endpoint.Contains(":") && !endpoint.Contains(".")) |
| | | { |
| | | ToastHelper.Warning("TCP端点格式不正确,应为 IP:Port 格式"); |
| | | return; |
| | | } |
| | | } |
| | | else if (transport == TransportType.Can) |
| | | { |
| | | // CAN 端点格式:总线名称:节点编号 如 "CAN_BUS_1:0" |
| | | var parts = endpoint.Split(':'); |
| | | if (parts.Length != 2 || string.IsNullOrWhiteSpace(parts[0]) || !int.TryParse(parts[1], out int nodeIdx) || nodeIdx < 0) |
| | | { |
| | | ToastHelper.Warning("CAN端点格式不正确,应为 总线名称:节点编号(如 CAN_BUS_1:0)"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 检查 DeviceId 是否重复(新增时) |
| | | if (_isNew) |
| | | { |
| | | var existing = _model.GetDeviceById(deviceId); |
| | | if (existing != null) |
| | | { |
| | | ToastHelper.Warning($"设备ID [{deviceId}] 已存在"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // 保存设备 |
| | | _device.DeviceId = deviceId; |
| | | _device.Kind = kind; |
| | | _device.DriverType = tbDriverType.Text?.Trim() ?? string.Empty; |
| | | _device.Transport = transport; |
| | | _device.Endpoint = endpoint; |
| | | |
| | | _model.SaveDevice(_device); |
| | | |
| | | // 保存通道 |
| | | foreach (var ch in _channels) |
| | | { |
| | | ch.DeviceId = deviceId; |
| | | _model.SaveChannel(ch); |
| | | } |
| | | |
| | | DialogResult = true; |
| | | Close(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"保存失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void CancelBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | DialogResult = false; |
| | | Close(); |
| | | } |
| | | |
| | | private void CbTransport_SelectionChanged(object sender, SelectionChangedEventArgs e) |
| | | { |
| | | UpdateEndpointHint(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据传输方式更新端点输入框提示 |
| | | /// </summary> |
| | | private void UpdateEndpointHint() |
| | | { |
| | | if (cbTransport.SelectedItem is TransportType tt) |
| | | { |
| | | switch (tt) |
| | | { |
| | | case TransportType.Can: |
| | | Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "CAN总线端点,格式: 总线名称:节点编号(如 CAN_BUS_1:0)"); |
| | | break; |
| | | case TransportType.Serial: |
| | | Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "串口号,如 COM3:115200:8:N:1"); |
| | | break; |
| | | case TransportType.Visa: |
| | | Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "VISG 地址,如 TCPIP0::192.168.1.100::inst0::INSTR"); |
| | | break; |
| | | case TransportType.SDK: |
| | | Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "SDK 设备标识,如 SDK://DeviceName/Instance"); |
| | | break; |
| | | default: |
| | | Panuon.WPF.UI.TextBoxHelper.SetWatermark(tbEndpoint, "例: 192.168.10.124:30000 或 COM3"); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据设备类型建议默认驱动类型 |
| | | /// </summary> |
| | | private void UpdateDriverHint() |
| | | { |
| | | // 保留现有驱动类型,不自动修改。仅当为空时提示默认值 |
| | | if (string.IsNullOrWhiteSpace(tbDriverType.Text) && cbKind.SelectedItem is DeviceKind kind) |
| | | { |
| | | string hint = kind switch |
| | | { |
| | | DeviceKind.CanDevice => "CAN", |
| | | DeviceKind.PowerSupply => "SCPI", |
| | | DeviceKind.PlcDevice => "S7", |
| | | DeviceKind.Chamber => "Tcp", |
| | | DeviceKind.DAQ => "Tcp", |
| | | _ => "" |
| | | }; |
| | | if (!string.IsNullOrEmpty(hint)) |
| | | { |
| | | tbDriverType.Text = hint; |
| | | } |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | } |
| New file |
| | |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceConfig.Entity |
| | | { |
| | | /// <summary> |
| | | /// 设备通道实体 |
| | | /// </summary> |
| | | public partial class DeviceChannel : ObservableObject |
| | | { |
| | | /// <summary> |
| | | /// 无参构造函数(用于数据绑定) |
| | | /// </summary> |
| | | public DeviceChannel() { } |
| | | |
| | | /// <summary> |
| | | /// 所属设备ID(联合主键之一) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string deviceId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 通道编号(联合主键之一) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private int channelId; |
| | | |
| | | /// <summary> |
| | | /// 通道能力描述 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string capability = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 最大电压 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private double maxVoltage; |
| | | |
| | | /// <summary> |
| | | /// 最大电流 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private double maxCurrent; |
| | | |
| | | /// <summary> |
| | | /// 限制条件(JSON格式) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string limits = "{}"; |
| | | |
| | | /// <summary> |
| | | /// 会话密钥 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string sessionKey = string.Empty; |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceConfig.Entity |
| | | { |
| | | /// <summary> |
| | | /// 配置版本状态枚举 |
| | | /// </summary> |
| | | public enum VersionStatus |
| | | { |
| | | Draft, |
| | | Published, |
| | | Effective |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 配置版本实体 |
| | | /// </summary> |
| | | public partial class DeviceConfigVersion : ObservableObject |
| | | { |
| | | /// <summary> |
| | | /// 无参构造函数(用于数据绑定) |
| | | /// </summary> |
| | | public DeviceConfigVersion() { } |
| | | |
| | | /// <summary> |
| | | /// 版本唯一标识(主键) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string versionId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 版本状态: Draft / Published / Effective |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private VersionStatus status; |
| | | |
| | | /// <summary> |
| | | /// 配置哈希(SHA256) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string hash = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 作者 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string author = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 时间戳 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private DateTime timestamp = DateTime.Now; |
| | | |
| | | /// <summary> |
| | | /// 差异描述 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string diffDescription = string.Empty; |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceConfig.Entity |
| | | { |
| | | /// <summary> |
| | | /// 设备种类枚举 |
| | | /// </summary> |
| | | public enum DeviceKind |
| | | { |
| | | PowerSupply, |
| | | CanDevice, |
| | | PlcDevice, |
| | | Chamber, |
| | | DAQ |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 传输方式枚举 |
| | | /// </summary> |
| | | public enum TransportType |
| | | { |
| | | Tcp, |
| | | Serial, |
| | | Visa, |
| | | SDK, |
| | | Can |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设备定义实体 |
| | | /// </summary> |
| | | public partial class DeviceDefinition : ObservableObject |
| | | { |
| | | /// <summary> |
| | | /// 无参构造函数(用于数据绑定) |
| | | /// </summary> |
| | | public DeviceDefinition() { } |
| | | |
| | | /// <summary> |
| | | /// 设备唯一标识(主键) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string deviceId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 设备种类: PowerSupply / CanDevice / PlcDevice / Chamber / DAQ |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private DeviceKind kind; |
| | | |
| | | /// <summary> |
| | | /// 驱动类型名称 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string driverType = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 传输方式: Tcp / Serial / Visa / SDK |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private TransportType transport; |
| | | |
| | | /// <summary> |
| | | /// 连接端点(IP:Port 或 COM口) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string endpoint = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 生效版本号 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string effectiveVersion = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 创建时间 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private DateTime createdAt = DateTime.Now; |
| | | |
| | | /// <summary> |
| | | /// 更新时间 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private DateTime updatedAt = DateTime.Now; |
| | | } |
| | | } |
| New file |
| | |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceConfig.Entity |
| | | { |
| | | /// <summary> |
| | | /// 设备组模板实体 |
| | | /// </summary> |
| | | public partial class DeviceGroupTemplate : ObservableObject |
| | | { |
| | | /// <summary> |
| | | /// 无参构造函数(用于数据绑定) |
| | | /// </summary> |
| | | public DeviceGroupTemplate() { } |
| | | |
| | | /// <summary> |
| | | /// 模板唯一标识(主键) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string templateId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 设备类型 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string deviceType = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 驱动类型 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string driverType = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 传输方式 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string transport = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 设备数量 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private int deviceCount; |
| | | |
| | | /// <summary> |
| | | /// 每设备通道数 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private int channelsPerDevice; |
| | | |
| | | /// <summary> |
| | | /// 地址规则(如IP模板 192.168.10.{0}) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string addressRule = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 起始Slot |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private int slotStart; |
| | | } |
| | | } |
| New file |
| | |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceConfig.Entity |
| | | { |
| | | /// <summary> |
| | | /// Slot映射实体 |
| | | /// </summary> |
| | | public partial class SlotDeviceMapping : ObservableObject |
| | | { |
| | | /// <summary> |
| | | /// 无参构造函数(用于数据绑定) |
| | | /// </summary> |
| | | public SlotDeviceMapping() { } |
| | | |
| | | /// <summary> |
| | | /// Slot标识(联合主键之一) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string slotId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 能力描述(联合主键之一) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string capability = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 版本(联合主键之一) |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string version = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 关联设备ID |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string deviceId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 关联通道ID |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private int channelId; |
| | | |
| | | /// <summary> |
| | | /// 映射版本 |
| | | /// </summary> |
| | | [ObservableProperty] |
| | | private string mappingVersion = string.Empty; |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Data; |
| | | using System.Linq; |
| | | using System.Security.Cryptography; |
| | | using System.Text; |
| | | using HMI.Core.Util; |
| | | using Plugin.DeviceConfig.Entity; |
| | | |
| | | namespace Plugin.DeviceConfig.Model |
| | | { |
| | | /// <summary> |
| | | /// 设备配置数据访问层,基于 SQLite 实现 |
| | | /// </summary> |
| | | public class DeviceConfigModel |
| | | { |
| | | private string _dbPath = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 初始化数据库,创建所有表 |
| | | /// </summary> |
| | | /// <param name="dbPath">数据库文件路径</param> |
| | | public void InitDatabase(string dbPath) |
| | | { |
| | | _dbPath = dbPath; |
| | | try |
| | | { |
| | | // 确保目录存在 |
| | | string? dir = System.IO.Path.GetDirectoryName(dbPath); |
| | | if (!string.IsNullOrEmpty(dir) && !System.IO.Directory.Exists(dir)) |
| | | { |
| | | System.IO.Directory.CreateDirectory(dir); |
| | | } |
| | | |
| | | // 仅在数据库文件不存在时创建(避免重启后抛异常或丢失已有数据) |
| | | if (!System.IO.File.Exists(dbPath)) |
| | | { |
| | | SqliteHelper.CreateDatabase(dbPath); |
| | | } |
| | | |
| | | using var helper = new SqliteHelper(dbPath); |
| | | |
| | | var sqls = new List<string> |
| | | { |
| | | // 设备定义表 |
| | | @"CREATE TABLE IF NOT EXISTS DeviceDefinition ( |
| | | DeviceId TEXT PRIMARY KEY, |
| | | Kind TEXT NOT NULL, |
| | | DriverType TEXT NOT NULL, |
| | | Transport TEXT NOT NULL, |
| | | Endpoint TEXT NOT NULL, |
| | | EffectiveVersion TEXT NOT NULL DEFAULT '', |
| | | CreatedAt TEXT NOT NULL, |
| | | UpdatedAt TEXT NOT NULL |
| | | );", |
| | | |
| | | // 设备通道表 |
| | | @"CREATE TABLE IF NOT EXISTS DeviceChannel ( |
| | | DeviceId TEXT NOT NULL, |
| | | ChannelId INTEGER NOT NULL, |
| | | Capability TEXT NOT NULL DEFAULT '', |
| | | MaxVoltage REAL NOT NULL DEFAULT 0, |
| | | MaxCurrent REAL NOT NULL DEFAULT 0, |
| | | Limits TEXT NOT NULL DEFAULT '{}', |
| | | SessionKey TEXT NOT NULL DEFAULT '', |
| | | PRIMARY KEY (DeviceId, ChannelId) |
| | | );", |
| | | |
| | | // Slot映射表 |
| | | @"CREATE TABLE IF NOT EXISTS SlotDeviceMapping ( |
| | | SlotId TEXT NOT NULL, |
| | | Capability TEXT NOT NULL, |
| | | Version TEXT NOT NULL, |
| | | DeviceId TEXT NOT NULL, |
| | | ChannelId INTEGER NOT NULL DEFAULT 0, |
| | | MappingVersion TEXT NOT NULL DEFAULT '', |
| | | PRIMARY KEY (SlotId, Capability, Version) |
| | | );", |
| | | |
| | | // 配置版本表 |
| | | @"CREATE TABLE IF NOT EXISTS DeviceConfigVersion ( |
| | | VersionId TEXT PRIMARY KEY, |
| | | Status TEXT NOT NULL, |
| | | Hash TEXT NOT NULL DEFAULT '', |
| | | Author TEXT NOT NULL DEFAULT '', |
| | | Timestamp TEXT NOT NULL, |
| | | DiffDescription TEXT NOT NULL DEFAULT '' |
| | | );", |
| | | |
| | | // 设备组模板表 |
| | | @"CREATE TABLE IF NOT EXISTS DeviceGroupTemplate ( |
| | | TemplateId TEXT PRIMARY KEY, |
| | | DeviceType TEXT NOT NULL DEFAULT '', |
| | | DriverType TEXT NOT NULL DEFAULT '', |
| | | Transport TEXT NOT NULL DEFAULT '', |
| | | DeviceCount INTEGER NOT NULL DEFAULT 0, |
| | | ChannelsPerDevice INTEGER NOT NULL DEFAULT 0, |
| | | AddressRule TEXT NOT NULL DEFAULT '', |
| | | SlotStart INTEGER NOT NULL DEFAULT 0 |
| | | );" |
| | | }; |
| | | |
| | | helper.ExecuteBatchSql(sqls); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"初始化设备配置数据库失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | #region DeviceDefinition CRUD |
| | | |
| | | /// <summary> |
| | | /// 获取所有设备定义 |
| | | /// </summary> |
| | | public List<DeviceDefinition> GetAllDevices() |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var dt = helper.GetDataTable("SELECT * FROM DeviceDefinition ORDER BY CreatedAt DESC;"); |
| | | return MapToDeviceList(dt); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"获取所有设备定义失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据ID获取设备定义 |
| | | /// </summary> |
| | | public DeviceDefinition? GetDeviceById(string deviceId) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var dt = helper.GetDataTable($"SELECT * FROM DeviceDefinition WHERE DeviceId = '{Escape(deviceId)}';"); |
| | | var list = MapToDeviceList(dt); |
| | | return list.Count > 0 ? list[0] : null; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"获取设备定义[{deviceId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存设备定义(新增或更新) |
| | | /// </summary> |
| | | public void SaveDevice(DeviceDefinition device) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | device.UpdatedAt = DateTime.Now; |
| | | |
| | | var sql = $@"INSERT OR REPLACE INTO DeviceDefinition |
| | | (DeviceId, Kind, DriverType, Transport, Endpoint, EffectiveVersion, CreatedAt, UpdatedAt) |
| | | VALUES ( |
| | | '{Escape(device.DeviceId)}', |
| | | '{device.Kind}', |
| | | '{Escape(device.DriverType)}', |
| | | '{device.Transport}', |
| | | '{Escape(device.Endpoint)}', |
| | | '{Escape(device.EffectiveVersion)}', |
| | | '{device.CreatedAt:yyyy-MM-dd HH:mm:ss}', |
| | | '{device.UpdatedAt:yyyy-MM-dd HH:mm:ss}' |
| | | );"; |
| | | helper.ExecuteSql(sql); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"保存设备定义[{device.DeviceId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除设备定义 |
| | | /// </summary> |
| | | public void DeleteDevice(string deviceId) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | helper.ExecuteSql($"DELETE FROM DeviceDefinition WHERE DeviceId = '{Escape(deviceId)}';"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"删除设备定义[{deviceId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | private List<DeviceDefinition> MapToDeviceList(DataTable dt) |
| | | { |
| | | var list = new List<DeviceDefinition>(); |
| | | foreach (DataRow row in dt.Rows) |
| | | { |
| | | list.Add(new DeviceDefinition |
| | | { |
| | | DeviceId = row["DeviceId"].ToString() ?? string.Empty, |
| | | Kind = Enum.TryParse<DeviceKind>(row["Kind"].ToString(), out var kind) ? kind : DeviceKind.PowerSupply, |
| | | DriverType = row["DriverType"].ToString() ?? string.Empty, |
| | | Transport = Enum.TryParse<TransportType>(row["Transport"].ToString(), out var tp) ? tp : TransportType.Tcp, |
| | | Endpoint = row["Endpoint"].ToString() ?? string.Empty, |
| | | EffectiveVersion = row["EffectiveVersion"].ToString() ?? string.Empty, |
| | | CreatedAt = DateTime.TryParse(row["CreatedAt"].ToString(), out var ca) ? ca : DateTime.Now, |
| | | UpdatedAt = DateTime.TryParse(row["UpdatedAt"].ToString(), out var ua) ? ua : DateTime.Now, |
| | | }); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region DeviceChannel CRUD |
| | | |
| | | /// <summary> |
| | | /// 获取指定设备的所有通道 |
| | | /// </summary> |
| | | public List<DeviceChannel> GetChannelsByDevice(string deviceId) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var dt = helper.GetDataTable($"SELECT * FROM DeviceChannel WHERE DeviceId = '{Escape(deviceId)}' ORDER BY ChannelId;"); |
| | | return MapToChannelList(dt); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"获取设备[{deviceId}]通道失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存设备通道(新增或更新) |
| | | /// </summary> |
| | | public void SaveChannel(DeviceChannel channel) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var sql = $@"INSERT OR REPLACE INTO DeviceChannel |
| | | (DeviceId, ChannelId, Capability, MaxVoltage, MaxCurrent, Limits, SessionKey) |
| | | VALUES ( |
| | | '{Escape(channel.DeviceId)}', |
| | | {channel.ChannelId}, |
| | | '{Escape(channel.Capability)}', |
| | | {channel.MaxVoltage}, |
| | | {channel.MaxCurrent}, |
| | | '{Escape(channel.Limits)}', |
| | | '{Escape(channel.SessionKey)}' |
| | | );"; |
| | | helper.ExecuteSql(sql); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"保存通道[{channel.DeviceId}:{channel.ChannelId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除设备通道 |
| | | /// </summary> |
| | | public void DeleteChannel(string deviceId, int channelId) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | helper.ExecuteSql($"DELETE FROM DeviceChannel WHERE DeviceId = '{Escape(deviceId)}' AND ChannelId = {channelId};"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"删除通道[{deviceId}:{channelId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | private List<DeviceChannel> MapToChannelList(DataTable dt) |
| | | { |
| | | var list = new List<DeviceChannel>(); |
| | | foreach (DataRow row in dt.Rows) |
| | | { |
| | | list.Add(new DeviceChannel |
| | | { |
| | | DeviceId = row["DeviceId"].ToString() ?? string.Empty, |
| | | ChannelId = Convert.ToInt32(row["ChannelId"]), |
| | | Capability = row["Capability"].ToString() ?? string.Empty, |
| | | MaxVoltage = Convert.ToDouble(row["MaxVoltage"]), |
| | | MaxCurrent = Convert.ToDouble(row["MaxCurrent"]), |
| | | Limits = row["Limits"].ToString() ?? "{}", |
| | | SessionKey = row["SessionKey"].ToString() ?? string.Empty, |
| | | }); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region SlotDeviceMapping CRUD |
| | | |
| | | /// <summary> |
| | | /// 获取所有Slot映射 |
| | | /// </summary> |
| | | public List<SlotDeviceMapping> GetSlotMappings() |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var dt = helper.GetDataTable("SELECT * FROM SlotDeviceMapping ORDER BY SlotId;"); |
| | | return MapToSlotMappingList(dt); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"获取Slot映射失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存Slot映射(新增或更新) |
| | | /// </summary> |
| | | public void SaveSlotMapping(SlotDeviceMapping mapping) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var sql = $@"INSERT OR REPLACE INTO SlotDeviceMapping |
| | | (SlotId, Capability, Version, DeviceId, ChannelId, MappingVersion) |
| | | VALUES ( |
| | | '{Escape(mapping.SlotId)}', |
| | | '{Escape(mapping.Capability)}', |
| | | '{Escape(mapping.Version)}', |
| | | '{Escape(mapping.DeviceId)}', |
| | | {mapping.ChannelId}, |
| | | '{Escape(mapping.MappingVersion)}' |
| | | );"; |
| | | helper.ExecuteSql(sql); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"保存Slot映射[{mapping.SlotId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 删除Slot映射 |
| | | /// </summary> |
| | | public void DeleteSlotMapping(string slotId, string capability, string version) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | helper.ExecuteSql($@"DELETE FROM SlotDeviceMapping |
| | | WHERE SlotId = '{Escape(slotId)}' |
| | | AND Capability = '{Escape(capability)}' |
| | | AND Version = '{Escape(version)}';"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"删除Slot映射[{slotId}:{capability}:{version}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | private List<SlotDeviceMapping> MapToSlotMappingList(DataTable dt) |
| | | { |
| | | var list = new List<SlotDeviceMapping>(); |
| | | foreach (DataRow row in dt.Rows) |
| | | { |
| | | list.Add(new SlotDeviceMapping |
| | | { |
| | | SlotId = row["SlotId"].ToString() ?? string.Empty, |
| | | Capability = row["Capability"].ToString() ?? string.Empty, |
| | | Version = row["Version"].ToString() ?? string.Empty, |
| | | DeviceId = row["DeviceId"].ToString() ?? string.Empty, |
| | | ChannelId = Convert.ToInt32(row["ChannelId"]), |
| | | MappingVersion = row["MappingVersion"].ToString() ?? string.Empty, |
| | | }); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region DeviceConfigVersion CRUD |
| | | |
| | | /// <summary> |
| | | /// 获取所有配置版本 |
| | | /// </summary> |
| | | public List<DeviceConfigVersion> GetConfigVersions() |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var dt = helper.GetDataTable("SELECT * FROM DeviceConfigVersion ORDER BY Timestamp DESC;"); |
| | | return MapToVersionList(dt); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"获取配置版本失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存配置版本(新增或更新) |
| | | /// </summary> |
| | | public void SaveConfigVersion(DeviceConfigVersion version) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var sql = $@"INSERT OR REPLACE INTO DeviceConfigVersion |
| | | (VersionId, Status, Hash, Author, Timestamp, DiffDescription) |
| | | VALUES ( |
| | | '{Escape(version.VersionId)}', |
| | | '{version.Status}', |
| | | '{Escape(version.Hash)}', |
| | | '{Escape(version.Author)}', |
| | | '{version.Timestamp:yyyy-MM-dd HH:mm:ss}', |
| | | '{Escape(version.DiffDescription)}' |
| | | );"; |
| | | helper.ExecuteSql(sql); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"保存配置版本[{version.VersionId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新版本状态 |
| | | /// </summary> |
| | | public void UpdateVersionStatus(string versionId, string newStatus) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | helper.ExecuteSql($@"UPDATE DeviceConfigVersion |
| | | SET Status = '{Escape(newStatus)}' |
| | | WHERE VersionId = '{Escape(versionId)}';"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"更新版本状态[{versionId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | private List<DeviceConfigVersion> MapToVersionList(DataTable dt) |
| | | { |
| | | var list = new List<DeviceConfigVersion>(); |
| | | foreach (DataRow row in dt.Rows) |
| | | { |
| | | list.Add(new DeviceConfigVersion |
| | | { |
| | | VersionId = row["VersionId"].ToString() ?? string.Empty, |
| | | Status = Enum.TryParse<VersionStatus>(row["Status"].ToString(), out var st) ? st : VersionStatus.Draft, |
| | | Hash = row["Hash"].ToString() ?? string.Empty, |
| | | Author = row["Author"].ToString() ?? string.Empty, |
| | | Timestamp = DateTime.TryParse(row["Timestamp"].ToString(), out var ts) ? ts : DateTime.Now, |
| | | DiffDescription = row["DiffDescription"].ToString() ?? string.Empty, |
| | | }); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region DeviceGroupTemplate CRUD |
| | | |
| | | /// <summary> |
| | | /// 获取所有设备组模板 |
| | | /// </summary> |
| | | public List<DeviceGroupTemplate> GetGroupTemplates() |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var dt = helper.GetDataTable("SELECT * FROM DeviceGroupTemplate ORDER BY DeviceType;"); |
| | | return MapToTemplateList(dt); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"获取设备组模板失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 保存设备组模板(新增或更新) |
| | | /// </summary> |
| | | public void SaveGroupTemplate(DeviceGroupTemplate template) |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | var sql = $@"INSERT OR REPLACE INTO DeviceGroupTemplate |
| | | (TemplateId, DeviceType, DriverType, Transport, DeviceCount, ChannelsPerDevice, AddressRule, SlotStart) |
| | | VALUES ( |
| | | '{Escape(template.TemplateId)}', |
| | | '{Escape(template.DeviceType)}', |
| | | '{Escape(template.DriverType)}', |
| | | '{Escape(template.Transport)}', |
| | | {template.DeviceCount}, |
| | | {template.ChannelsPerDevice}, |
| | | '{Escape(template.AddressRule)}', |
| | | {template.SlotStart} |
| | | );"; |
| | | helper.ExecuteSql(sql); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"保存设备组模板[{template.TemplateId}]失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | private List<DeviceGroupTemplate> MapToTemplateList(DataTable dt) |
| | | { |
| | | var list = new List<DeviceGroupTemplate>(); |
| | | foreach (DataRow row in dt.Rows) |
| | | { |
| | | list.Add(new DeviceGroupTemplate |
| | | { |
| | | TemplateId = row["TemplateId"].ToString() ?? string.Empty, |
| | | DeviceType = row["DeviceType"].ToString() ?? string.Empty, |
| | | DriverType = row["DriverType"].ToString() ?? string.Empty, |
| | | Transport = row["Transport"].ToString() ?? string.Empty, |
| | | DeviceCount = Convert.ToInt32(row["DeviceCount"]), |
| | | ChannelsPerDevice = Convert.ToInt32(row["ChannelsPerDevice"]), |
| | | AddressRule = row["AddressRule"].ToString() ?? string.Empty, |
| | | SlotStart = Convert.ToInt32(row["SlotStart"]), |
| | | }); |
| | | } |
| | | return list; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 冲突检测与哈希计算 |
| | | |
| | | /// <summary> |
| | | /// 检测配置冲突:IP冲突、Slot映射重复、通道越界等 |
| | | /// </summary> |
| | | /// <returns>冲突描述列表,为空则表示无冲突</returns> |
| | | public List<string> DetectConflicts() |
| | | { |
| | | var conflicts = new List<string>(); |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | |
| | | // 1. 检测IP/Endpoint冲突(相同Endpoint的不同设备) |
| | | var ipSql = @"SELECT Endpoint, COUNT(*) as Cnt |
| | | FROM DeviceDefinition |
| | | WHERE Transport = 'Tcp' |
| | | GROUP BY Endpoint |
| | | HAVING Cnt > 1;"; |
| | | var ipDt = helper.GetDataTable(ipSql); |
| | | foreach (DataRow row in ipDt.Rows) |
| | | { |
| | | conflicts.Add($"IP/Endpoint冲突: {row["Endpoint"]} 被 {row["Cnt"]} 个设备使用"); |
| | | } |
| | | |
| | | // 2. 检测Slot映射重复(相同Slot+Capability+Version映射到不同设备) |
| | | var slotSql = @"SELECT SlotId, Capability, Version, COUNT(*) as Cnt |
| | | FROM SlotDeviceMapping |
| | | GROUP BY SlotId, Capability, Version |
| | | HAVING Cnt > 1;"; |
| | | var slotDt = helper.GetDataTable(slotSql); |
| | | foreach (DataRow row in slotDt.Rows) |
| | | { |
| | | conflicts.Add($"Slot映射重复: Slot={row["SlotId"]}, Capability={row["Capability"]}, Version={row["Version"]}"); |
| | | } |
| | | |
| | | // 3. 检测通道越界(MaxVoltage或MaxCurrent为负数) |
| | | var channelSql = @"SELECT DeviceId, ChannelId, MaxVoltage, MaxCurrent |
| | | FROM DeviceChannel |
| | | WHERE MaxVoltage < 0 OR MaxCurrent < 0;"; |
| | | var channelDt = helper.GetDataTable(channelSql); |
| | | foreach (DataRow row in channelDt.Rows) |
| | | { |
| | | conflicts.Add($"通道参数越界: 设备={row["DeviceId"]}, 通道={row["ChannelId"]}, " + |
| | | $"电压={row["MaxVoltage"]}, 电流={row["MaxCurrent"]}"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"冲突检测失败: {ex.Message}", ex); |
| | | } |
| | | |
| | | return conflicts; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 计算当前配置的SHA256哈希 |
| | | /// </summary> |
| | | /// <returns>SHA256哈希字符串(小写十六进制)</returns> |
| | | public string ComputeConfigHash() |
| | | { |
| | | try |
| | | { |
| | | using var helper = new SqliteHelper(_dbPath); |
| | | |
| | | // 拼接所有设备定义的关键信息作为哈希输入 |
| | | var sb = new StringBuilder(); |
| | | |
| | | var deviceDt = helper.GetDataTable("SELECT * FROM DeviceDefinition ORDER BY DeviceId;"); |
| | | foreach (DataRow row in deviceDt.Rows) |
| | | { |
| | | sb.Append($"DEVICE:{row["DeviceId"]}|{row["Kind"]}|{row["DriverType"]}|{row["Transport"]}|{row["Endpoint"]};"); |
| | | } |
| | | |
| | | var channelDt = helper.GetDataTable("SELECT * FROM DeviceChannel ORDER BY DeviceId, ChannelId;"); |
| | | foreach (DataRow row in channelDt.Rows) |
| | | { |
| | | sb.Append($"CHANNEL:{row["DeviceId"]}|{row["ChannelId"]}|{row["Capability"]}|{row["MaxVoltage"]}|{row["MaxCurrent"]};"); |
| | | } |
| | | |
| | | var slotDt = helper.GetDataTable("SELECT * FROM SlotDeviceMapping ORDER BY SlotId, Capability, Version;"); |
| | | foreach (DataRow row in slotDt.Rows) |
| | | { |
| | | sb.Append($"SLOT:{row["SlotId"]}|{row["Capability"]}|{row["Version"]}|{row["DeviceId"]}|{row["ChannelId"]};"); |
| | | } |
| | | |
| | | using var sha256 = SHA256.Create(); |
| | | var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(sb.ToString())); |
| | | return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception($"计算配置哈希失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 辅助方法 |
| | | |
| | | /// <summary> |
| | | /// SQL字符串转义(防止注入) |
| | | /// </summary> |
| | | private static string Escape(string value) |
| | | { |
| | | return value?.Replace("'", "''") ?? string.Empty; |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | } |
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>Library</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" /> |
| | | <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" /> |
| | | <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" /> |
| | | <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" /> |
| | | <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| New file |
| | |
| | | <pu:WindowX x:Class="Plugin.DeviceConfig.VersionManageModal" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:local="clr-namespace:Plugin.DeviceConfig" |
| | | mc:Ignorable="d" |
| | | Title="版本管理" |
| | | Height="520" |
| | | Width="800" |
| | | WindowStartupLocation="CenterOwner" |
| | | Style="{StaticResource ModalWindow}"> |
| | | <pu:WindowX.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | </ResourceDictionary> |
| | | </pu:WindowX.Resources> |
| | | |
| | | <Grid Margin="16"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 操作按钮栏 --> |
| | | <StackPanel Grid.Row="0" Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10" Margin="0,0,0,8"> |
| | | <Button x:Name="publishBtn" Click="PublishBtn_Click" Content="发布选中版本" |
| | | Style="{StaticResource AddBtn}" Width="110"/> |
| | | <Button x:Name="applyBtn" Click="ApplyBtn_Click" Content="应用选中版本" |
| | | Style="{StaticResource AddBtn}" Width="110"/> |
| | | <Button x:Name="detailBtn" Click="DetailBtn_Click" Content="版本详情" |
| | | Style="{StaticResource RfhBtn}" Width="90"/> |
| | | <Button x:Name="rfhBtn" Click="RfhBtn_Click" Content="刷新" |
| | | Style="{StaticResource RfhBtn}" Width="70"/> |
| | | </StackPanel> |
| | | |
| | | <!-- 版本列表 --> |
| | | <ListView Grid.Row="1" Style="{StaticResource DefaultListTable}" x:Name="versionListView" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="50" Header="#"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Converter={StaticResource lvRowNumber}, |
| | | RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ListViewItem,AncestorLevel=1}}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="180" Header="VersionId"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding VersionId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="Status"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <StackPanel Orientation="Horizontal"> |
| | | <Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0" |
| | | VerticalAlignment="Center"> |
| | | <Border.Style> |
| | | <Style TargetType="Border"> |
| | | <Setter Property="Background" Value="Gray"/> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Status}" Value="Draft"> |
| | | <Setter Property="Background" Value="Gray"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Published"> |
| | | <Setter Property="Background" Value="#2196F3"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Effective"> |
| | | <Setter Property="Background" Value="#4CAF50"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Border.Style> |
| | | </Border> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Status}" VerticalAlignment="Center"/> |
| | | </StackPanel> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="200" Header="Hash"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding HashShort}" |
| | | ToolTip="{Binding Hash}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="Author"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" Text="{Binding Author}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="160" Header="Timestamp"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Timestamp, StringFormat=yyyy-MM-dd HH:mm:ss}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | |
| | | <!-- 版本详情区 --> |
| | | <Border Grid.Row="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4" |
| | | Margin="0,8,0,0" Padding="10" x:Name="detailPanel" Visibility="Collapsed"> |
| | | <Grid> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="80"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="完整Hash:" FontWeight="SemiBold" VerticalAlignment="Center"/> |
| | | <TextBlock Grid.Row="0" Grid.Column="1" x:Name="tbDetailHash" TextWrapping="Wrap" |
| | | FontFamily="Consolas" Margin="4,2"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="差异描述:" FontWeight="SemiBold" VerticalAlignment="Center"/> |
| | | <TextBlock Grid.Row="1" Grid.Column="1" x:Name="tbDetailDiff" TextWrapping="Wrap" Margin="4,2"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="作者:" FontWeight="SemiBold" VerticalAlignment="Center"/> |
| | | <TextBlock Grid.Row="2" Grid.Column="1" x:Name="tbDetailAuthor" Margin="4,2"/> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 底部按钮 --> |
| | | <StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0"> |
| | | <Button x:Name="closeBtn" Click="CloseBtn_Click" Content="关闭" |
| | | Style="{StaticResource RfhBtn}" Width="90"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using HMI.UI.Util; |
| | | using Panuon.WPF.UI; |
| | | using Plugin.DeviceConfig.Entity; |
| | | using Plugin.DeviceConfig.Model; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.Linq; |
| | | using System.Windows; |
| | | |
| | | namespace Plugin.DeviceConfig |
| | | { |
| | | /// <summary> |
| | | /// VersionManageModal.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class VersionManageModal : WindowX |
| | | { |
| | | private DeviceConfigModel _model; |
| | | private ObservableCollection<VersionDisplayItem> _versions = new(); |
| | | private bool _changed; |
| | | |
| | | public VersionManageModal() |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 打开弹窗 |
| | | /// </summary> |
| | | public bool? Open(DeviceConfigModel model) |
| | | { |
| | | _model = model; |
| | | versionListView.ItemsSource = _versions; |
| | | LoadVersions(); |
| | | return ShowDialog(); |
| | | } |
| | | |
| | | private void LoadVersions() |
| | | { |
| | | try |
| | | { |
| | | var list = _model.GetConfigVersions(); |
| | | _versions.Clear(); |
| | | foreach (var v in list) |
| | | { |
| | | _versions.Add(new VersionDisplayItem |
| | | { |
| | | VersionId = v.VersionId, |
| | | Status = v.Status, |
| | | Hash = v.Hash, |
| | | HashShort = v.Hash.Length > 16 ? v.Hash.Substring(0, 16) + "..." : v.Hash, |
| | | Author = v.Author, |
| | | Timestamp = v.Timestamp, |
| | | DiffDescription = v.DiffDescription, |
| | | }); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"加载版本列表失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #region 操作 |
| | | |
| | | private void PublishBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | var selected = versionListView.SelectedItem as VersionDisplayItem; |
| | | if (selected == null) |
| | | { |
| | | ToastHelper.Warning("请先选中一个版本"); |
| | | return; |
| | | } |
| | | if (selected.Status != VersionStatus.Draft) |
| | | { |
| | | ToastHelper.Warning("只有 Draft 状态的版本可以发布"); |
| | | return; |
| | | } |
| | | |
| | | if (MsgBoxHelper.WarnMessage(this, $"确定要发布版本 [{selected.VersionId}] 吗?") != MessageBoxResult.OK) |
| | | return; |
| | | |
| | | // 计算最新 Hash |
| | | var hash = _model.ComputeConfigHash(); |
| | | selected.Hash = hash; |
| | | selected.HashShort = hash.Length > 16 ? hash.Substring(0, 16) + "..." : hash; |
| | | selected.Status = VersionStatus.Published; |
| | | selected.Timestamp = DateTime.Now; |
| | | |
| | | var ver = new DeviceConfigVersion |
| | | { |
| | | VersionId = selected.VersionId, |
| | | Status = VersionStatus.Published, |
| | | Hash = hash, |
| | | Author = selected.Author, |
| | | Timestamp = DateTime.Now, |
| | | DiffDescription = selected.DiffDescription, |
| | | }; |
| | | _model.SaveConfigVersion(ver); |
| | | |
| | | _changed = true; |
| | | LoadVersions(); |
| | | ToastHelper.Success($"版本 [{selected.VersionId}] 已发布"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"发布失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void ApplyBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | var selected = versionListView.SelectedItem as VersionDisplayItem; |
| | | if (selected == null) |
| | | { |
| | | ToastHelper.Warning("请先选中一个版本"); |
| | | return; |
| | | } |
| | | if (selected.Status != VersionStatus.Published) |
| | | { |
| | | ToastHelper.Warning("只有 Published 状态的版本可以应用"); |
| | | return; |
| | | } |
| | | |
| | | // 检查当前 Effective 版本 |
| | | var currentEffective = _versions.FirstOrDefault(v => v.Status == VersionStatus.Effective); |
| | | if (currentEffective != null) |
| | | { |
| | | if (MsgBoxHelper.WarnMessage(this, |
| | | $"当前已有生效版本 [{currentEffective.VersionId}],应用新版本将替换它。\n是否继续?") != MessageBoxResult.OK) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 将原 Effective 改回 Published |
| | | _model.UpdateVersionStatus(currentEffective.VersionId, VersionStatus.Published.ToString()); |
| | | } |
| | | |
| | | if (MsgBoxHelper.WarnMessage(this, $"确定要将 [{selected.VersionId}] 设为生效版本吗?") != MessageBoxResult.OK) |
| | | return; |
| | | |
| | | _model.UpdateVersionStatus(selected.VersionId, VersionStatus.Effective.ToString()); |
| | | _changed = true; |
| | | LoadVersions(); |
| | | ToastHelper.Success($"版本 [{selected.VersionId}] 已设为生效版本"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"应用版本失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void DetailBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | var selected = versionListView.SelectedItem as VersionDisplayItem; |
| | | if (selected == null) |
| | | { |
| | | ToastHelper.Warning("请先选中一个版本"); |
| | | return; |
| | | } |
| | | |
| | | tbDetailHash.Text = selected.Hash; |
| | | tbDetailDiff.Text = selected.DiffDescription; |
| | | tbDetailAuthor.Text = $"{selected.Author} @ {selected.Timestamp:yyyy-MM-dd HH:mm:ss}"; |
| | | detailPanel.Visibility = Visibility.Visible; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Error($"加载详情失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void RfhBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | LoadVersions(); |
| | | ToastHelper.Success("已刷新"); |
| | | } |
| | | |
| | | private void CloseBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | DialogResult = _changed; |
| | | Close(); |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 版本显示项 |
| | | /// </summary> |
| | | public class VersionDisplayItem |
| | | { |
| | | public string VersionId { get; set; } = string.Empty; |
| | | public VersionStatus Status { get; set; } |
| | | public string Hash { get; set; } = string.Empty; |
| | | public string HashShort { get; set; } = string.Empty; |
| | | public string Author { get; set; } = string.Empty; |
| | | public DateTime Timestamp { get; set; } |
| | | public string DiffDescription { get; set; } = string.Empty; |
| | | } |
| | | } |
| New file |
| | |
| | | <hb:SimpleMaskControl x:Class="Plugin.DeviceDebug.DeviceDebug" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI" |
| | | xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:local="clr-namespace:Plugin.DeviceDebug" |
| | | xmlns:panels="clr-namespace:Plugin.DeviceDebug.Panels" |
| | | mc:Ignorable="d" |
| | | Loaded="SimpleMaskControl_Loaded" |
| | | d:DesignHeight="600" d:DesignWidth="1000"> |
| | | <hb:SimpleMaskControl.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | <Style x:Key="TitleBox" TargetType="TextBlock"> |
| | | <Setter Property="FontSize" Value="14"/> |
| | | <Setter Property="FontWeight" Value="SemiBold"/> |
| | | <Setter Property="Padding" Value="8,4"/> |
| | | <Setter Property="VerticalAlignment" Value="Center"/> |
| | | </Style> |
| | | <BooleanToVisibilityConverter x:Key="BoolToVis"/> |
| | | </ResourceDictionary> |
| | | </hb:SimpleMaskControl.Resources> |
| | | |
| | | <Grid dp:GridHelper.Space="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 顶部标题栏 --> |
| | | <DockPanel Grid.Row="0"> |
| | | <StackPanel dp:StackPanelHelper.ChildSpace="10" DockPanel.Dock="Right" Orientation="Horizontal"> |
| | | <ComboBox x:Name="deviceSelector" Width="240" |
| | | ItemsSource="{Binding DeviceList}" |
| | | SelectedItem="{Binding SelectedDevice}" |
| | | SelectionChanged="DeviceSelector_SelectionChanged" |
| | | VerticalContentAlignment="Center" Padding="6,4"/> |
| | | <Button Style="{StaticResource RfhBtn}" x:Name="rfhBtn" Click="RfhBtn_Click"/> |
| | | </StackPanel> |
| | | <TextBlock Style="{StaticResource TitleBox}" Text="硬件调试"/> |
| | | </DockPanel> |
| | | |
| | | <!-- 设备信息栏 --> |
| | | <Border Grid.Row="1" BorderBrush="#CCCCCC" BorderThickness="1" Padding="10,6" Background="#F9F9F9" |
| | | Visibility="{Binding HasDevice, Converter={StaticResource BoolToVis}}"> |
| | | <WrapPanel dp:StackPanelHelper.ChildSpace="20"> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4"> |
| | | <TextBlock Text="设备ID:" FontWeight="Bold" Foreground="#555"/> |
| | | <TextBlock Text="{Binding CurrentDeviceId}" Style="{StaticResource ListTableTextBlock}"/> |
| | | </StackPanel> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4"> |
| | | <TextBlock Text="型号:" FontWeight="Bold" Foreground="#555"/> |
| | | <TextBlock Text="{Binding CurrentDeviceKind}" Style="{StaticResource ListTableTextBlock}"/> |
| | | </StackPanel> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4"> |
| | | <TextBlock Text="连接状态:" FontWeight="Bold" Foreground="#555"/> |
| | | <TextBlock Text="{Binding ConnectionStateText}" Foreground="{Binding ConnectionBrush}"/> |
| | | </StackPanel> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4"> |
| | | <TextBlock Text="版本:" FontWeight="Bold" Foreground="#555"/> |
| | | <TextBlock Text="{Binding CurrentEffectiveVersion}" Style="{StaticResource ListTableTextBlock}"/> |
| | | </StackPanel> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4"> |
| | | <TextBlock Text="预约状态:" FontWeight="Bold" Foreground="#555"/> |
| | | <TextBlock Text="{Binding ReservationStatusText}" Style="{StaticResource ListTableTextBlock}"/> |
| | | </StackPanel> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="4"> |
| | | <TextBlock Text="会话状态:" FontWeight="Bold" Foreground="#555"/> |
| | | <TextBlock Text="{Binding SessionStateText}" Foreground="{Binding SessionStateBrush}" FontWeight="Bold"/> |
| | | </StackPanel> |
| | | </WrapPanel> |
| | | </Border> |
| | | |
| | | <!-- 操作按钮栏 --> |
| | | <StackPanel Grid.Row="2" Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10" |
| | | Visibility="{Binding HasDevice, Converter={StaticResource BoolToVis}}"> |
| | | <Button x:Name="enterMaintenanceBtn" Content="进入维护控制" Padding="14,6" |
| | | IsEnabled="{Binding CanEnterMaintenance}" |
| | | Click="EnterMaintenanceBtn_Click" |
| | | Background="#2196F3" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | <Button x:Name="exitMaintenanceBtn" Content="退出维护" Padding="14,6" |
| | | IsEnabled="{Binding CanExitMaintenance}" |
| | | Click="ExitMaintenanceBtn_Click" |
| | | Background="#FF5722" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | </StackPanel> |
| | | |
| | | <!-- 主体区域 --> |
| | | <Grid Grid.Row="3"> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="350"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 左侧面板容器 --> |
| | | <Border Grid.Column="0" BorderBrush="#DDDDDD" BorderThickness="1" Margin="0,0,8,0"> |
| | | <ContentControl x:Name="panelContainer"> |
| | | <TextBlock Text="请选择设备" HorizontalAlignment="Center" VerticalAlignment="Center" |
| | | FontSize="16" Foreground="#999"/> |
| | | </ContentControl> |
| | | </Border> |
| | | |
| | | <!-- 右侧操作日志 --> |
| | | <Border Grid.Column="1" BorderBrush="#DDDDDD" BorderThickness="1"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <Border Grid.Row="0" Background="#F0F0F0" Padding="8,6"> |
| | | <TextBlock Text="操作日志" FontWeight="Bold" FontSize="14"/> |
| | | </Border> |
| | | <ListView Grid.Row="1" x:Name="operationLogList" |
| | | ItemsSource="{Binding OperationLogs}" |
| | | Style="{StaticResource DefaultListTable}" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="60" Header="类型"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding CommandType}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="120" Header="参数"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Parameters}" TextTrimming="CharacterEllipsis" |
| | | ToolTip="{Binding Parameters}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="60" Header="结果"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Outcome}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="时间"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Timestamp, StringFormat=HH:mm:ss}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | </Grid> |
| | | </Border> |
| | | </Grid> |
| | | </Grid> |
| | | </hb:SimpleMaskControl> |
| New file |
| | |
| | | using Aging.Infrastructure.Contracts; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | using HMI.Base; |
| | | using HMI.Core.Util; |
| | | using HMI.UI.Util; |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using Plugin.DeviceDebug.Model; |
| | | using Plugin.DeviceDebug.Panels; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.IO; |
| | | using System.Linq; |
| | | using System.Threading; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Media; |
| | | |
| | | namespace Plugin.DeviceDebug |
| | | { |
| | | /// <summary> |
| | | /// DeviceDebug.xaml 的交互逻辑 |
| | | /// </summary> |
| | | [ObservableObject] |
| | | public partial class DeviceDebug : SimpleMaskControl |
| | | { |
| | | private static readonly ILog _log = LogManager.GetLogger(typeof(DeviceDebug)); |
| | | private DeviceDebugModel _debugModel; |
| | | private DebugSessionManager _sessionManager; |
| | | |
| | | // 基础设施层服务 |
| | | private IDeviceManager? _deviceManager; |
| | | private IDeviceService? _deviceService; |
| | | private IResourceManager? _resourceManager; |
| | | |
| | | // 设备配置数据库路径(与 DeviceConfig 插件共享) |
| | | private string _deviceConfigDbPath; |
| | | |
| | | [ObservableProperty] private ObservableCollection<string> _deviceList = new(); |
| | | [ObservableProperty] private string _selectedDevice; |
| | | [ObservableProperty] private DebugSession _currentSession; |
| | | [ObservableProperty] private ObservableCollection<DebugOperation> _operationLogs = new(); |
| | | [ObservableProperty] private string _connectionStateText = "未连接"; |
| | | [ObservableProperty] private string _sessionStateText = ""; |
| | | [ObservableProperty] private string _reservationStatusText = ""; |
| | | [ObservableProperty] private string _currentDeviceId = ""; |
| | | [ObservableProperty] private string _currentDeviceKind = ""; |
| | | [ObservableProperty] private string _currentEffectiveVersion = ""; |
| | | [ObservableProperty] private string _currentEndpoint = ""; |
| | | [ObservableProperty] private string _currentDriverType = ""; |
| | | [ObservableProperty] private Brush _connectionBrush = new SolidColorBrush(Colors.Gray); |
| | | [ObservableProperty] private Brush _sessionStateBrush = new SolidColorBrush(Colors.Gray); |
| | | [ObservableProperty] private bool _canEnterMaintenance; |
| | | [ObservableProperty] private bool _canExitMaintenance; |
| | | [ObservableProperty] private bool _hasDevice; |
| | | |
| | | // 缓存当前选中的设备类型 |
| | | private string _currentDeviceKindStr; |
| | | |
| | | public DeviceDebug() |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 强制刷新设备列表(供集成测试预置设备后调用) |
| | | /// </summary> |
| | | public void ForceRefreshForTest() |
| | | { |
| | | try { LoadDeviceList(); } |
| | | catch (Exception ex) { _log.Error("ForceRefreshForTest Error.", ex); } |
| | | } |
| | | |
| | | public override void InitFinish() |
| | | { |
| | | base.InitFinish(); |
| | | try |
| | | { |
| | | // 初始化调试数据库 |
| | | _debugModel = new DeviceDebugModel(); |
| | | string dbPath = Path.Combine(BaseFolder, "debug.db"); |
| | | _debugModel.InitDatabase(dbPath); |
| | | |
| | | // 从 UtilContainer 获取基础设施服务 |
| | | var container = new UtilContainer(); |
| | | _deviceManager = container.GetUtil<IDeviceManager>("DeviceManager"); |
| | | _deviceService = container.GetUtil<IDeviceService>("DeviceService"); |
| | | _resourceManager = container.GetUtil<IResourceManager>("ResourceManager"); |
| | | |
| | | // 创建会话管理器(传入 ResourceManager) |
| | | _sessionManager = new DebugSessionManager(_debugModel, _resourceManager); |
| | | |
| | | // 设备配置数据库路径(与 DeviceConfig 共享) |
| | | _deviceConfigDbPath = Path.Combine(BaseFolder, "data.db"); |
| | | |
| | | log.Info("硬件调试模块初始化完成"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("硬件调试模块初始化失败", ex); |
| | | ToastHelper.Error($"硬件调试初始化失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载设备列表 |
| | | /// </summary> |
| | | private void LoadDeviceList() |
| | | { |
| | | try |
| | | { |
| | | if (!File.Exists(_deviceConfigDbPath)) |
| | | { |
| | | log.Warn($"设备配置数据库不存在: {_deviceConfigDbPath}"); |
| | | ToastHelper.Warning("设备配置数据库不存在,请先在设备配置模块中添加设备"); |
| | | return; |
| | | } |
| | | |
| | | var sqliteHelper = new SqliteHelper(_deviceConfigDbPath); |
| | | var dt = sqliteHelper.GetDataTable("SELECT DeviceId, Kind FROM DeviceDefinition ORDER BY DeviceId"); |
| | | |
| | | var list = new ObservableCollection<string>(); |
| | | foreach (System.Data.DataRow row in dt.Rows) |
| | | { |
| | | string deviceId = row["DeviceId"].ToString(); |
| | | string kind = row["Kind"].ToString(); |
| | | list.Add($"{deviceId} [{kind}]"); |
| | | } |
| | | |
| | | DeviceList = list; |
| | | log.Info($"加载设备列表完成,共 {list.Count} 个设备"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("加载设备列表失败", ex); |
| | | ToastHelper.Error($"加载设备列表失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析设备选择字符串,获取 DeviceId |
| | | /// </summary> |
| | | private string ParseDeviceId(string selection) |
| | | { |
| | | if (string.IsNullOrEmpty(selection)) return string.Empty; |
| | | int idx = selection.IndexOf(" ["); |
| | | return idx > 0 ? selection.Substring(0, idx) : selection; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 解析设备选择字符串,获取 Kind |
| | | /// </summary> |
| | | private string ParseDeviceKind(string selection) |
| | | { |
| | | if (string.IsNullOrEmpty(selection)) return string.Empty; |
| | | int start = selection.IndexOf(" ["); |
| | | int end = selection.IndexOf("]"); |
| | | if (start >= 0 && end > start) |
| | | return selection.Substring(start + 2, end - start - 2); |
| | | return string.Empty; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载设备详细信息 |
| | | /// </summary> |
| | | private void LoadDeviceInfo(string deviceId) |
| | | { |
| | | try |
| | | { |
| | | if (!File.Exists(_deviceConfigDbPath)) return; |
| | | |
| | | var sqliteHelper = new SqliteHelper(_deviceConfigDbPath); |
| | | var dt = sqliteHelper.GetDataTable( |
| | | $"SELECT DeviceId, Kind, EffectiveVersion, Endpoint, DriverType FROM DeviceDefinition WHERE DeviceId = '{deviceId}'"); |
| | | |
| | | if (dt.Rows.Count > 0) |
| | | { |
| | | var row = dt.Rows[0]; |
| | | CurrentDeviceId = row["DeviceId"].ToString(); |
| | | CurrentDeviceKind = row["Kind"].ToString(); |
| | | CurrentEffectiveVersion = row["EffectiveVersion"]?.ToString() ?? ""; |
| | | CurrentEndpoint = row["Endpoint"]?.ToString() ?? ""; |
| | | CurrentDriverType = row["DriverType"]?.ToString() ?? ""; |
| | | |
| | | // 缓存设备类型 |
| | | _currentDeviceKindStr = CurrentDeviceKind; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"加载设备信息失败: {deviceId}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 选择设备时:打开会话(ReadOnly),加载对应面板 |
| | | /// </summary> |
| | | private void OnDeviceSelected() |
| | | { |
| | | try |
| | | { |
| | | if (string.IsNullOrEmpty(SelectedDevice)) |
| | | { |
| | | HasDevice = false; |
| | | panelContainer.Content = new TextBlock |
| | | { |
| | | Text = "请选择设备", |
| | | HorizontalAlignment = HorizontalAlignment.Center, |
| | | VerticalAlignment = VerticalAlignment.Center, |
| | | FontSize = 16, |
| | | Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#999")) |
| | | }; |
| | | return; |
| | | } |
| | | |
| | | string deviceId = ParseDeviceId(SelectedDevice); |
| | | string kind = ParseDeviceKind(SelectedDevice); |
| | | |
| | | // 加载设备详细信息 |
| | | LoadDeviceInfo(deviceId); |
| | | |
| | | HasDevice = true; |
| | | |
| | | // 打开会话(ReadOnly) |
| | | CurrentSession = _sessionManager.OpenSession(deviceId, "", "operator", CurrentEffectiveVersion); |
| | | |
| | | // 通过 DeviceManager 连接设备 |
| | | if (_deviceManager != null) |
| | | { |
| | | try |
| | | { |
| | | _deviceManager.ConnectAsync(deviceId, CancellationToken.None) |
| | | .GetAwaiter().GetResult(); |
| | | ConnectionStateText = "已连接"; |
| | | ConnectionBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn($"通过 DeviceManager 连接设备失败: {ex.Message}"); |
| | | ConnectionStateText = "连接失败"; |
| | | ConnectionBrush = new SolidColorBrush(Colors.OrangeRed); |
| | | } |
| | | } |
| | | |
| | | // 加载操作日志 |
| | | LoadOperationLogs(); |
| | | |
| | | // 更新按钮状态 |
| | | UpdateButtonStates(); |
| | | |
| | | // 加载对应面板 |
| | | LoadPanel(kind); |
| | | |
| | | log.Info($"已选择设备: {deviceId}, 类型: {kind}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("选择设备失败", ex); |
| | | ToastHelper.Error($"选择设备失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据设备类型加载对应的调试面板 |
| | | /// </summary> |
| | | private void LoadPanel(string deviceKind) |
| | | { |
| | | try |
| | | { |
| | | UIElement panel = null; |
| | | |
| | | switch (deviceKind) |
| | | { |
| | | case "PowerSupply": |
| | | var powerPanel = new PowerSupplyDebugPanel(); |
| | | powerPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive; |
| | | powerPanel.OnCommandExecute += Panel_OnCommandExecute; |
| | | panel = powerPanel; |
| | | break; |
| | | case "CanDevice": |
| | | var canPanel = new CanDebugPanel |
| | | { |
| | | DeviceId = CurrentDeviceId, |
| | | InitialBusName = ParseCanBusName(CurrentEndpoint), |
| | | InitialNodeIndex = ParseCanNodeIndex(CurrentEndpoint), |
| | | AutoConnectOnLoad = true |
| | | }; |
| | | canPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive; |
| | | canPanel.OnCommandExecute += Panel_OnCommandExecute; |
| | | panel = canPanel; |
| | | break; |
| | | case "PlcDevice": |
| | | var plcPanel = new PlcDebugPanel(); |
| | | plcPanel.IsMaintenanceMode = CurrentSession?.State == DebugSessionState.MaintenanceActive; |
| | | plcPanel.OnCommandExecute += Panel_OnCommandExecute; |
| | | panel = plcPanel; |
| | | break; |
| | | default: |
| | | panel = new TextBlock |
| | | { |
| | | Text = $"不支持的设备类型: {deviceKind}", |
| | | HorizontalAlignment = HorizontalAlignment.Center, |
| | | VerticalAlignment = VerticalAlignment.Center, |
| | | FontSize = 14, |
| | | Foreground = new SolidColorBrush(Colors.OrangeRed) |
| | | }; |
| | | break; |
| | | } |
| | | |
| | | panelContainer.Content = panel; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"加载调试面板失败: {deviceKind}", ex); |
| | | ToastHelper.Error($"加载面板失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 面板命令执行事件处理 |
| | | /// </summary> |
| | | private void Panel_OnCommandExecute(object sender, DebugCommand command) |
| | | { |
| | | try |
| | | { |
| | | if (CurrentSession == null) |
| | | { |
| | | ToastHelper.Warning("请先选择设备"); |
| | | return; |
| | | } |
| | | |
| | | // 验证命令权限 |
| | | if (!_sessionManager.ValidateCommand(CurrentSession.SessionId, command)) |
| | | { |
| | | ToastHelper.Warning("当前会话状态不允许执行此操作,请先进入维护模式"); |
| | | return; |
| | | } |
| | | |
| | | // 通过 DeviceService 执行实际命令 |
| | | if (_deviceService != null) |
| | | { |
| | | ExecuteViaDeviceService(command); |
| | | } |
| | | else |
| | | { |
| | | // 降级模式:基础设施未初始化时保留原有行为 |
| | | log.Warn("DeviceService 未初始化,使用降级模式记录命令"); |
| | | ExecuteFallback(command); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"执行命令失败: {command.CommandType}", ex); |
| | | ToastHelper.Error($"命令执行失败: {ex.Message}"); |
| | | |
| | | // 记录失败操作 |
| | | var operation = new DebugOperation |
| | | { |
| | | SessionId = CurrentSession?.SessionId ?? "", |
| | | CommandType = command.CommandType, |
| | | Parameters = SerializeParameters(command), |
| | | Outcome = "Failed", |
| | | ErrorMessage = ex.Message, |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | _debugModel.RecordOperation(operation); |
| | | OperationLogs.Insert(0, operation); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 通过 DeviceService 执行设备命令 |
| | | /// </summary> |
| | | private void ExecuteViaDeviceService(DebugCommand command) |
| | | { |
| | | try |
| | | { |
| | | var ownerToken = new ResourceOwnerToken( |
| | | OwnerType.Maintenance, |
| | | CurrentSession.SessionId); |
| | | |
| | | var deviceCommand = BuildDeviceCommand(command); |
| | | |
| | | var result = _deviceService.ExecuteCommandAsync( |
| | | command.DeviceId, |
| | | deviceCommand, |
| | | ownerToken, |
| | | CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | var operation = new DebugOperation |
| | | { |
| | | SessionId = CurrentSession.SessionId, |
| | | CommandType = command.CommandType, |
| | | Parameters = SerializeParameters(command), |
| | | Outcome = result.Success ? "Success" : "Failed", |
| | | DeviceReadback = result.Readback ?? "", |
| | | ErrorCode = result.ErrorCode ?? "", |
| | | ErrorMessage = result.ErrorMessage ?? "", |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | |
| | | _debugModel.RecordOperation(operation); |
| | | OperationLogs.Insert(0, operation); |
| | | |
| | | if (result.Success) |
| | | ToastHelper.Success($"命令 {command.CommandType} 执行成功"); |
| | | else |
| | | ToastHelper.Warning($"命令执行失败: {result.ErrorMessage}"); |
| | | |
| | | log.Info($"执行命令: {command.CommandType}, 设备: {command.DeviceId}, 结果: {(result.Success ? "成功" : "失败")}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"DeviceService 执行命令失败: {ex.Message}", ex); |
| | | ToastHelper.Error($"设备通信失败: {ex.Message}"); |
| | | |
| | | var operation = new DebugOperation |
| | | { |
| | | SessionId = CurrentSession.SessionId, |
| | | CommandType = command.CommandType, |
| | | Parameters = SerializeParameters(command), |
| | | Outcome = "Failed", |
| | | ErrorMessage = ex.Message, |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | _debugModel.RecordOperation(operation); |
| | | OperationLogs.Insert(0, operation); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 降级模式:无 DeviceService 时的后备处理 |
| | | /// </summary> |
| | | private void ExecuteFallback(DebugCommand command) |
| | | { |
| | | var operation = new DebugOperation |
| | | { |
| | | SessionId = CurrentSession.SessionId, |
| | | CommandType = command.CommandType, |
| | | Parameters = SerializeParameters(command), |
| | | Outcome = "Success", |
| | | Timestamp = DateTime.Now |
| | | }; |
| | | |
| | | _debugModel.RecordOperation(operation); |
| | | OperationLogs.Insert(0, operation); |
| | | |
| | | ToastHelper.Success($"命令 {command.CommandType} 已发送(降级模式)"); |
| | | log.Info($"[降级模式] 执行命令: {command.CommandType}, 设备: {command.DeviceId}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 根据命令类型构建实际设备命令字符串 |
| | | /// </summary> |
| | | private string BuildDeviceCommand(DebugCommand command) |
| | | { |
| | | if (command is PowerSetCommand psc) |
| | | { |
| | | return $"VOLT {psc.Voltage};CURR {psc.CurrentLimit};OUTP {(psc.Output ? "ON" : "OFF")}"; |
| | | } |
| | | else if (command is PowerMeasureCommand) |
| | | { |
| | | return "MEAS:ALL?"; |
| | | } |
| | | else if (command is CanSendCommand csc) |
| | | { |
| | | return $"CAN:SEND {csc.Frame}"; |
| | | } |
| | | else if (command is PlcWriteCommand pwc) |
| | | { |
| | | return $"WRITE:{pwc.IoAddress}:{(pwc.Value ? "TRUE" : "FALSE")}"; |
| | | } |
| | | return command.CommandType; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 序列化命令参数为JSON字符串 |
| | | /// </summary> |
| | | private string SerializeParameters(DebugCommand command) |
| | | { |
| | | try |
| | | { |
| | | switch (command) |
| | | { |
| | | case PowerSetCommand psc: |
| | | return $"{{Voltage:{psc.Voltage},CurrentLimit:{psc.CurrentLimit},Output:{psc.Output}}}"; |
| | | case PowerMeasureCommand _: |
| | | return "{}"; |
| | | case CanSendCommand csc: |
| | | return $"{{Frame:{csc.Frame},Data:{BitConverter.ToString(csc.Data)}}}"; |
| | | case PlcWriteCommand pwc: |
| | | return $"{{IoAddress:{pwc.IoAddress},Value:{pwc.Value}}}"; |
| | | default: |
| | | return "{}"; |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | return "{}"; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 加载操作日志 |
| | | /// </summary> |
| | | private void LoadOperationLogs() |
| | | { |
| | | try |
| | | { |
| | | if (CurrentSession == null) return; |
| | | |
| | | var ops = _debugModel.GetOperationsBySession(CurrentSession.SessionId); |
| | | OperationLogs = new ObservableCollection<DebugOperation>(ops.Take(100)); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("加载操作日志失败", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新按钮启用状态 |
| | | /// </summary> |
| | | private void UpdateButtonStates() |
| | | { |
| | | if (CurrentSession == null) |
| | | { |
| | | CanEnterMaintenance = false; |
| | | CanExitMaintenance = false; |
| | | SessionStateText = ""; |
| | | return; |
| | | } |
| | | |
| | | var state = CurrentSession.State; |
| | | CanEnterMaintenance = state == DebugSessionState.ReadOnly; |
| | | CanExitMaintenance = state == DebugSessionState.MaintenanceActive; |
| | | |
| | | // 更新会话状态文本和颜色 |
| | | SessionStateText = state.ToString(); |
| | | SessionStateBrush = state switch |
| | | { |
| | | DebugSessionState.ReadOnly => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#2196F3")), |
| | | DebugSessionState.MaintenanceActive => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")), |
| | | DebugSessionState.Faulted => new SolidColorBrush(Colors.Red), |
| | | DebugSessionState.RecoveryRequired => new SolidColorBrush(Colors.Orange), |
| | | _ => new SolidColorBrush(Colors.Gray) |
| | | }; |
| | | |
| | | // 更新面板的维护模式 |
| | | if (panelContainer.Content is PowerSupplyDebugPanel powerPanel) |
| | | powerPanel.IsMaintenanceMode = state == DebugSessionState.MaintenanceActive; |
| | | else if (panelContainer.Content is CanDebugPanel canPanel) |
| | | canPanel.IsMaintenanceMode = state == DebugSessionState.MaintenanceActive; |
| | | else if (panelContainer.Content is PlcDebugPanel plcPanel) |
| | | plcPanel.IsMaintenanceMode = state == DebugSessionState.MaintenanceActive; |
| | | } |
| | | |
| | | #region 事件处理 |
| | | |
| | | private void SimpleMaskControl_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | LoadDeviceList(); |
| | | Loaded -= SimpleMaskControl_Loaded; |
| | | } |
| | | |
| | | private void DeviceSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) |
| | | { |
| | | OnDeviceSelected(); |
| | | } |
| | | |
| | | private void RfhBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | LoadDeviceList(); |
| | | } |
| | | |
| | | private void EnterMaintenanceBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (CurrentSession == null) return; |
| | | |
| | | // 请求维护模式 |
| | | if (_sessionManager.RequestMaintenance(CurrentSession.SessionId)) |
| | | { |
| | | // 激活维护模式 |
| | | _sessionManager.ActivateMaintenance(CurrentSession.SessionId); |
| | | UpdateButtonStates(); |
| | | |
| | | // 重新加载面板以更新IsMaintenanceMode |
| | | LoadPanel(CurrentDeviceKind); |
| | | |
| | | ToastHelper.Success("已进入维护控制模式"); |
| | | log.Info($"会话 {CurrentSession.SessionId} 进入维护模式"); |
| | | } |
| | | else |
| | | { |
| | | ToastHelper.Warning("无法进入维护模式,请检查设备状态"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("进入维护模式失败", ex); |
| | | ToastHelper.Error($"进入维护模式失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | private void ExitMaintenanceBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (CurrentSession == null) return; |
| | | |
| | | if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), "确定要退出维护模式吗?") == MessageBoxResult.OK) |
| | | { |
| | | _sessionManager.CloseSession(CurrentSession.SessionId); |
| | | CurrentSession = null; |
| | | UpdateButtonStates(); |
| | | |
| | | // 清空面板 |
| | | panelContainer.Content = new TextBlock |
| | | { |
| | | Text = "请选择设备", |
| | | HorizontalAlignment = HorizontalAlignment.Center, |
| | | VerticalAlignment = VerticalAlignment.Center, |
| | | FontSize = 16, |
| | | Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#999")) |
| | | }; |
| | | |
| | | HasDevice = false; |
| | | SelectedDevice = null; |
| | | |
| | | ToastHelper.Success("已退出维护模式"); |
| | | log.Info("已退出维护模式"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("退出维护模式失败", ex); |
| | | ToastHelper.Error($"退出维护模式失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从 CAN Endpoint 解析总线名称(格式: 总线名:节点编号) |
| | | /// </summary> |
| | | private string ParseCanBusName(string endpoint) |
| | | { |
| | | if (string.IsNullOrEmpty(endpoint)) return "CAN_BUS_1"; |
| | | int idx = endpoint.IndexOf(':'); |
| | | return idx > 0 ? endpoint.Substring(0, idx) : endpoint; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从 CAN Endpoint 解析节点编号 |
| | | /// </summary> |
| | | private int? ParseCanNodeIndex(string endpoint) |
| | | { |
| | | if (string.IsNullOrEmpty(endpoint)) return 0; |
| | | int idx = endpoint.IndexOf(':'); |
| | | if (idx > 0 && idx < endpoint.Length - 1 && int.TryParse(endpoint.Substring(idx + 1), out int n)) |
| | | return n; |
| | | return 0; |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceDebug.Entity |
| | | { |
| | | /// <summary> |
| | | /// 调试命令基类 |
| | | /// </summary> |
| | | public partial class DebugCommand : ObservableObject |
| | | { |
| | | public DebugCommand() |
| | | { |
| | | CommandId = Guid.NewGuid().ToString(); |
| | | CreatedAt = DateTime.Now; |
| | | Parameters = new Dictionary<string, object>(); |
| | | } |
| | | |
| | | [ObservableProperty] private string _commandId = string.Empty; |
| | | [ObservableProperty] private string _commandType = string.Empty; |
| | | [ObservableProperty] private string _deviceId = string.Empty; |
| | | [ObservableProperty] private int _channel; |
| | | [ObservableProperty] private Dictionary<string, object> _parameters = new(); |
| | | [ObservableProperty] private DateTime _createdAt = DateTime.Now; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 电源设置命令 |
| | | /// </summary> |
| | | public class PowerSetCommand : DebugCommand |
| | | { |
| | | public PowerSetCommand() { CommandType = "PowerSet"; } |
| | | public double Voltage { get; set; } |
| | | public double CurrentLimit { get; set; } |
| | | public bool Output { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 电源测量命令 |
| | | /// </summary> |
| | | public class PowerMeasureCommand : DebugCommand |
| | | { |
| | | public PowerMeasureCommand() { CommandType = "PowerMeasure"; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CAN发送命令 |
| | | /// </summary> |
| | | public class CanSendCommand : DebugCommand |
| | | { |
| | | public CanSendCommand() { CommandType = "CanSend"; } |
| | | public string Frame { get; set; } = string.Empty; |
| | | public byte[] Data { get; set; } = Array.Empty<byte>(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// PLC写入命令 |
| | | /// </summary> |
| | | public class PlcWriteCommand : DebugCommand |
| | | { |
| | | public PlcWriteCommand() { CommandType = "PlcWrite"; } |
| | | public string IoAddress { get; set; } = string.Empty; |
| | | public bool Value { get; set; } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceDebug.Entity |
| | | { |
| | | /// <summary> |
| | | /// 操作审计实体 - 记录每次调试操作 |
| | | /// </summary> |
| | | public partial class DebugOperation : ObservableObject |
| | | { |
| | | public DebugOperation() |
| | | { |
| | | OperationId = Guid.NewGuid().ToString(); |
| | | Timestamp = DateTime.Now; |
| | | } |
| | | |
| | | [ObservableProperty] private string _operationId = string.Empty; |
| | | [ObservableProperty] private string _sessionId = string.Empty; // FK → DebugSession |
| | | [ObservableProperty] private string _commandType = string.Empty; // PowerSet/PowerGet/OutputOn/OutputOff/CanSend/PlcWrite等 |
| | | [ObservableProperty] private string _parameters = string.Empty; // JSON字符串 |
| | | [ObservableProperty] private string _outcome = string.Empty; // Success/Failed/Rejected/Timeout |
| | | [ObservableProperty] private string _deviceReadback = string.Empty; |
| | | [ObservableProperty] private string _errorCode = string.Empty; |
| | | [ObservableProperty] private string _errorMessage = string.Empty; |
| | | [ObservableProperty] private DateTime _timestamp = DateTime.Now; |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceDebug.Entity |
| | | { |
| | | /// <summary> |
| | | /// 调试会话状态枚举 |
| | | /// </summary> |
| | | public enum DebugSessionState |
| | | { |
| | | /// <summary>只读模式(默认)</summary> |
| | | ReadOnly = 0, |
| | | /// <summary>正在申请维护权限</summary> |
| | | AcquiringMaintenance = 1, |
| | | /// <summary>维护模式激活</summary> |
| | | MaintenanceActive = 2, |
| | | /// <summary>正在关闭</summary> |
| | | Closing = 3, |
| | | /// <summary>已关闭</summary> |
| | | Closed = 4, |
| | | /// <summary>故障</summary> |
| | | Faulted = 5, |
| | | /// <summary>需要恢复</summary> |
| | | RecoveryRequired = 6 |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 调试会话实体 |
| | | /// </summary> |
| | | public partial class DebugSession : ObservableObject |
| | | { |
| | | public DebugSession() |
| | | { |
| | | SessionId = Guid.NewGuid().ToString(); |
| | | CreatedAt = DateTime.Now; |
| | | State = DebugSessionState.ReadOnly; |
| | | } |
| | | |
| | | /// <summary>会话ID(主键)</summary> |
| | | [ObservableProperty] |
| | | private string _sessionId = string.Empty; |
| | | |
| | | /// <summary>设备ID</summary> |
| | | [ObservableProperty] |
| | | private string _deviceId = string.Empty; |
| | | |
| | | /// <summary>操作员ID</summary> |
| | | [ObservableProperty] |
| | | private string _operatorId = string.Empty; |
| | | |
| | | /// <summary>操作员名称</summary> |
| | | [ObservableProperty] |
| | | private string _operatorName = string.Empty; |
| | | |
| | | /// <summary>生效版本</summary> |
| | | [ObservableProperty] |
| | | private string _effectiveVersion = string.Empty; |
| | | |
| | | /// <summary>会话状态</summary> |
| | | [ObservableProperty] |
| | | private DebugSessionState _state = DebugSessionState.ReadOnly; |
| | | |
| | | /// <summary>预约引用</summary> |
| | | [ObservableProperty] |
| | | private string _reservationRef = string.Empty; |
| | | |
| | | /// <summary>创建时间</summary> |
| | | [ObservableProperty] |
| | | private DateTime _createdAt = DateTime.Now; |
| | | |
| | | /// <summary>关闭时间</summary> |
| | | [ObservableProperty] |
| | | private DateTime? _closedAt; |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using CommunityToolkit.Mvvm.ComponentModel; |
| | | |
| | | namespace Plugin.DeviceDebug.Entity |
| | | { |
| | | public enum DeviceConnectionState |
| | | { |
| | | Connected, |
| | | Disconnected, |
| | | Connecting, |
| | | Error |
| | | } |
| | | |
| | | public enum DeviceHealth |
| | | { |
| | | Healthy, |
| | | Degraded, |
| | | Fault |
| | | } |
| | | |
| | | public enum ReservationStatus |
| | | { |
| | | Available, |
| | | ProductionReserved, |
| | | MaintenanceReserved, |
| | | RecoveryRequired |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设备运行状态(内存对象,不持久化) |
| | | /// </summary> |
| | | public partial class DeviceRuntimeStatus : ObservableObject |
| | | { |
| | | [ObservableProperty] private string _deviceId = string.Empty; |
| | | [ObservableProperty] private DeviceConnectionState _connectionState = DeviceConnectionState.Disconnected; |
| | | [ObservableProperty] private DeviceHealth _health = DeviceHealth.Healthy; |
| | | [ObservableProperty] private string _lastError = string.Empty; |
| | | [ObservableProperty] private DateTime? _lastErrorTime; |
| | | [ObservableProperty] private ReservationStatus _reservationStatus = ReservationStatus.Available; |
| | | [ObservableProperty] private string _effectiveVersion = string.Empty; |
| | | [ObservableProperty] private int _reconnectCount; |
| | | [ObservableProperty] private double _communicationLatencyMs; |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Linq; |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using Aging.Infrastructure.Contracts; |
| | | |
| | | namespace Plugin.DeviceDebug.Model |
| | | { |
| | | /// <summary> |
| | | /// 调试会话管理器 - 负责状态机转换和权限校验 |
| | | /// </summary> |
| | | public class DebugSessionManager |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(DebugSessionManager)); |
| | | private readonly DeviceDebugModel _model; |
| | | private readonly IResourceManager? _resourceManager; |
| | | |
| | | // 内存中的活跃会话缓存 |
| | | private readonly Dictionary<string, DebugSession> _activeSessions = new(); |
| | | |
| | | public DebugSessionManager(DeviceDebugModel model, IResourceManager? resourceManager = null) |
| | | { |
| | | _model = model; |
| | | _resourceManager = resourceManager; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 打开会话(ReadOnly模式) |
| | | /// </summary> |
| | | public DebugSession OpenSession(string deviceId, string operatorId, string operatorName, string effectiveVersion) |
| | | { |
| | | try |
| | | { |
| | | // 检查是否已有活跃会话 |
| | | var existingSession = GetActiveSessionByDevice(deviceId); |
| | | if (existingSession != null) |
| | | { |
| | | log.Warn($"设备 {deviceId} 已有活跃会话: {existingSession.SessionId}"); |
| | | return existingSession; |
| | | } |
| | | |
| | | var session = new DebugSession |
| | | { |
| | | DeviceId = deviceId, |
| | | OperatorId = operatorId, |
| | | OperatorName = operatorName, |
| | | EffectiveVersion = effectiveVersion, |
| | | State = DebugSessionState.ReadOnly |
| | | }; |
| | | |
| | | // 持久化到数据库 |
| | | _model.CreateSession(session); |
| | | |
| | | // 更新内存缓存 |
| | | _activeSessions[session.SessionId] = session; |
| | | |
| | | log.Info($"打开会话: {session.SessionId}, 设备: {deviceId}, 操作员: {operatorName}"); |
| | | return session; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"打开会话失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 请求进入维护模式(ReadOnly → AcquiringMaintenance) |
| | | /// 检查:设备是否有生产Reserved |
| | | /// </summary> |
| | | public bool RequestMaintenance(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (!_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | log.Warn($"会话不存在: {sessionId}"); |
| | | return false; |
| | | } |
| | | |
| | | // 验证状态转换 |
| | | if (session.State != DebugSessionState.ReadOnly) |
| | | { |
| | | log.Warn($"会话 {sessionId} 当前状态为 {session.State},无法请求维护模式"); |
| | | return false; |
| | | } |
| | | |
| | | // 检查设备是否有生产Reserved(与资源预约系统集成) |
| | | if (_resourceManager != null) |
| | | { |
| | | var resourceKey = new ResourceKey(session.DeviceId, "Generic"); |
| | | var ownerToken = new ResourceOwnerToken(OwnerType.Maintenance, session.SessionId); |
| | | |
| | | try |
| | | { |
| | | var reservation = _resourceManager.ReserveAsync( |
| | | ownerToken, resourceKey, CancellationToken.None).GetAwaiter().GetResult(); |
| | | session.ReservationRef = reservation.ReservationId; |
| | | } |
| | | catch (ResourceConflictException) |
| | | { |
| | | log.Warn($"设备 {session.DeviceId} 已被生产预约占用,拒绝维护请求"); |
| | | return false; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | // 降级模式:无 ResourceManager 时允许所有请求(向后兼容) |
| | | log.Warn("ResourceManager 未初始化,跳过资源预约检查"); |
| | | } |
| | | |
| | | // 状态转换 |
| | | session.State = DebugSessionState.AcquiringMaintenance; |
| | | |
| | | // 持久化 |
| | | _model.UpdateSession(session); |
| | | |
| | | log.Info($"会话 {sessionId} 请求维护模式"); |
| | | return true; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"请求维护模式失败: {ex.Message}", ex); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 激活维护模式(AcquiringMaintenance → MaintenanceActive) |
| | | /// </summary> |
| | | public void ActivateMaintenance(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (!_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | log.Warn($"会话不存在: {sessionId}"); |
| | | throw new InvalidOperationException($"会话不存在: {sessionId}"); |
| | | } |
| | | |
| | | // 验证状态转换 |
| | | if (session.State != DebugSessionState.AcquiringMaintenance) |
| | | { |
| | | log.Warn($"会话 {sessionId} 当前状态为 {session.State},无法激活维护模式"); |
| | | throw new InvalidOperationException($"会话 {sessionId} 当前状态为 {session.State},无法激活维护模式"); |
| | | } |
| | | |
| | | // 状态转换 |
| | | session.State = DebugSessionState.MaintenanceActive; |
| | | |
| | | // 持久化 |
| | | _model.UpdateSession(session); |
| | | |
| | | log.Info($"会话 {sessionId} 激活维护模式"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"激活维护模式失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 关闭会话(MaintenanceActive → Closing → Closed) |
| | | /// 执行安全状态检查 |
| | | /// </summary> |
| | | public void CloseSession(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (!_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | log.Warn($"会话不存在: {sessionId}"); |
| | | throw new InvalidOperationException($"会话不存在: {sessionId}"); |
| | | } |
| | | |
| | | // 验证状态转换(只有 MaintenanceActive 可以关闭) |
| | | if (session.State != DebugSessionState.MaintenanceActive) |
| | | { |
| | | log.Warn($"会话 {sessionId} 当前状态为 {session.State},无法关闭"); |
| | | throw new InvalidOperationException($"会话 {sessionId} 当前状态为 {session.State},无法关闭"); |
| | | } |
| | | |
| | | // 状态转换: MaintenanceActive → Closing |
| | | session.State = DebugSessionState.Closing; |
| | | _model.UpdateSession(session); |
| | | log.Debug($"会话 {sessionId} 进入关闭中状态"); |
| | | |
| | | // 执行安全状态检查:释放资源预约 |
| | | if (_resourceManager != null && !string.IsNullOrEmpty(session.ReservationRef)) |
| | | { |
| | | try |
| | | { |
| | | var resourceKey = new ResourceKey(session.DeviceId, "Generic"); |
| | | var ownerToken = new ResourceOwnerToken(OwnerType.Maintenance, session.SessionId); |
| | | |
| | | _resourceManager.ReleaseReservationAsync( |
| | | ownerToken, resourceKey, CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | log.Info($"已释放设备 {session.DeviceId} 的资源预约"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"释放资源预约失败: {ex.Message}", ex); |
| | | // 不阻止关闭流程,但记录警告 |
| | | } |
| | | } |
| | | |
| | | // 状态转换: Closing → Closed |
| | | session.State = DebugSessionState.Closed; |
| | | session.ClosedAt = DateTime.Now; |
| | | _model.UpdateSession(session); |
| | | |
| | | // 从缓存中移除 |
| | | _activeSessions.Remove(sessionId); |
| | | |
| | | log.Info($"会话 {sessionId} 已关闭"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"关闭会话失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 故障处理(任意 → Faulted) |
| | | /// </summary> |
| | | public void FaultSession(string sessionId, string errorMessage) |
| | | { |
| | | try |
| | | { |
| | | if (!_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | log.Warn($"会话不存在: {sessionId}"); |
| | | return; |
| | | } |
| | | |
| | | // 记录当前状态 |
| | | var previousState = session.State; |
| | | |
| | | // 状态转换 |
| | | session.State = DebugSessionState.Faulted; |
| | | |
| | | // 持久化 |
| | | _model.UpdateSession(session); |
| | | |
| | | log.Error($"会话 {sessionId} 发生故障,从 {previousState} 转为 Faulted: {errorMessage}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"标记故障状态失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 标记需要恢复(任意 → RecoveryRequired) |
| | | /// </summary> |
| | | public void MarkRecoveryRequired(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (!_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | log.Warn($"会话不存在: {sessionId}"); |
| | | return; |
| | | } |
| | | |
| | | // 记录当前状态 |
| | | var previousState = session.State; |
| | | |
| | | // 状态转换 |
| | | session.State = DebugSessionState.RecoveryRequired; |
| | | |
| | | // 持久化 |
| | | _model.UpdateSession(session); |
| | | |
| | | log.Warn($"会话 {sessionId} 标记为需要恢复,从 {previousState} 转为 RecoveryRequired"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"标记恢复状态失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 验证命令是否允许执行(只有 MaintenanceActive 状态允许写操作) |
| | | /// </summary> |
| | | public bool ValidateCommand(string sessionId, DebugCommand command) |
| | | { |
| | | try |
| | | { |
| | | if (!_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | log.Warn($"会话不存在: {sessionId}"); |
| | | return false; |
| | | } |
| | | |
| | | // 只有 MaintenanceActive 状态允许写操作 |
| | | bool allowed = session.State == DebugSessionState.MaintenanceActive; |
| | | |
| | | if (!allowed) |
| | | { |
| | | log.Warn($"会话 {sessionId} 状态为 {session.State},拒绝执行命令: {command.CommandType}"); |
| | | } |
| | | |
| | | // 增加资源预约验证 |
| | | if (allowed && _resourceManager != null) |
| | | { |
| | | var resourceKey = new ResourceKey(session.DeviceId, "Generic"); |
| | | allowed = _resourceManager.ValidateOwner( |
| | | new ResourceOwnerToken(OwnerType.Maintenance, session.SessionId), |
| | | resourceKey); |
| | | if (!allowed) |
| | | { |
| | | log.Warn($"会话 {sessionId} 资源预约验证失败,拒绝执行命令: {command.CommandType}"); |
| | | } |
| | | } |
| | | |
| | | return allowed; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"验证命令失败: {ex.Message}", ex); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取会话状态 |
| | | /// </summary> |
| | | public DebugSessionState? GetSessionState(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (_activeSessions.TryGetValue(sessionId, out var session)) |
| | | { |
| | | return session.State; |
| | | } |
| | | |
| | | // 从数据库查询 |
| | | var dbSession = _model.GetSession(sessionId); |
| | | return dbSession?.State; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取会话状态失败: {ex.Message}", ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取设备当前会话(如果有) |
| | | /// </summary> |
| | | public DebugSession? GetActiveSessionByDevice(string deviceId) |
| | | { |
| | | try |
| | | { |
| | | // 先从内存缓存查找 |
| | | var cachedSession = _activeSessions.Values |
| | | .FirstOrDefault(s => s.DeviceId == deviceId && s.State != DebugSessionState.Closed); |
| | | |
| | | if (cachedSession != null) |
| | | { |
| | | return cachedSession; |
| | | } |
| | | |
| | | // 从数据库查询 |
| | | var activeSessions = _model.GetActiveSessionsByDevice(deviceId); |
| | | return activeSessions.FirstOrDefault(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取设备活跃会话失败: {ex.Message}", ex); |
| | | return null; |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Data; |
| | | using System.IO; |
| | | using System.Linq; |
| | | using HMI.Core.Util; |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | |
| | | namespace Plugin.DeviceDebug.Model |
| | | { |
| | | /// <summary> |
| | | /// 数据访问层 - 管理调试会话和操作审计的持久化 |
| | | /// </summary> |
| | | public class DeviceDebugModel |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(DeviceDebugModel)); |
| | | private SqliteHelper? _sqliteHelper; |
| | | private string _dbPath = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 初始化数据库,创建必要的表结构 |
| | | /// </summary> |
| | | public void InitDatabase(string dbPath) |
| | | { |
| | | try |
| | | { |
| | | _dbPath = dbPath; |
| | | |
| | | // 确保目录存在 |
| | | string? dir = Path.GetDirectoryName(dbPath); |
| | | if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) |
| | | { |
| | | Directory.CreateDirectory(dir); |
| | | } |
| | | |
| | | // 如果数据库文件不存在,创建它 |
| | | if (!File.Exists(dbPath)) |
| | | { |
| | | SqliteHelper.CreateDatabase(dbPath); |
| | | log.Info($"创建调试数据库: {dbPath}"); |
| | | } |
| | | |
| | | _sqliteHelper = new SqliteHelper(dbPath); |
| | | |
| | | // 创建 debug_session 表 |
| | | string createSessionTable = @" |
| | | CREATE TABLE IF NOT EXISTS debug_session ( |
| | | session_id TEXT PRIMARY KEY, |
| | | device_id TEXT NOT NULL, |
| | | operator_id TEXT NOT NULL, |
| | | operator_name TEXT, |
| | | effective_version TEXT, |
| | | state INTEGER NOT NULL DEFAULT 0, |
| | | reservation_ref TEXT, |
| | | created_at TEXT NOT NULL, |
| | | closed_at TEXT |
| | | )"; |
| | | |
| | | // 创建 debug_operation 表 |
| | | string createOperationTable = @" |
| | | CREATE TABLE IF NOT EXISTS debug_operation ( |
| | | operation_id TEXT PRIMARY KEY, |
| | | session_id TEXT NOT NULL, |
| | | command_type TEXT NOT NULL, |
| | | parameters TEXT, |
| | | outcome TEXT NOT NULL, |
| | | device_readback TEXT, |
| | | error_code TEXT, |
| | | error_message TEXT, |
| | | timestamp TEXT NOT NULL, |
| | | FOREIGN KEY (session_id) REFERENCES debug_session(session_id) |
| | | )"; |
| | | |
| | | _sqliteHelper.ExecuteSql(createSessionTable); |
| | | _sqliteHelper.ExecuteSql(createOperationTable); |
| | | |
| | | log.Info("调试数据库表初始化完成"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"初始化调试数据库失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | #region 会话 CRUD |
| | | |
| | | /// <summary> |
| | | /// 创建会话 |
| | | /// </summary> |
| | | public void CreateSession(DebugSession session) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $@" |
| | | INSERT INTO debug_session ( |
| | | session_id, device_id, operator_id, operator_name, |
| | | effective_version, state, reservation_ref, created_at, closed_at |
| | | ) VALUES ( |
| | | '{EscapeSql(session.SessionId)}', |
| | | '{EscapeSql(session.DeviceId)}', |
| | | '{EscapeSql(session.OperatorId)}', |
| | | '{EscapeSql(session.OperatorName)}', |
| | | '{EscapeSql(session.EffectiveVersion)}', |
| | | {(int)session.State}, |
| | | '{EscapeSql(session.ReservationRef)}', |
| | | '{session.CreatedAt:yyyy-MM-dd HH:mm:ss}', |
| | | {(session.ClosedAt.HasValue ? $"'{session.ClosedAt.Value:yyyy-MM-dd HH:mm:ss}'" : "NULL")} |
| | | )"; |
| | | |
| | | _sqliteHelper.ExecuteSql(sql); |
| | | log.Debug($"创建会话: {session.SessionId}, 设备: {session.DeviceId}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"创建会话失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取会话 |
| | | /// </summary> |
| | | public DebugSession? GetSession(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $"SELECT * FROM debug_session WHERE session_id = '{EscapeSql(sessionId)}'"; |
| | | DataTable dt = _sqliteHelper.GetDataTable(sql); |
| | | |
| | | if (dt.Rows.Count == 0) |
| | | return null; |
| | | |
| | | return MapToDebugSession(dt.Rows[0]); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取会话失败: {ex.Message}", ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新会话 |
| | | /// </summary> |
| | | public void UpdateSession(DebugSession session) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $@" |
| | | UPDATE debug_session SET |
| | | device_id = '{EscapeSql(session.DeviceId)}', |
| | | operator_id = '{EscapeSql(session.OperatorId)}', |
| | | operator_name = '{EscapeSql(session.OperatorName)}', |
| | | effective_version = '{EscapeSql(session.EffectiveVersion)}', |
| | | state = {(int)session.State}, |
| | | reservation_ref = '{EscapeSql(session.ReservationRef)}', |
| | | closed_at = {(session.ClosedAt.HasValue ? $"'{session.ClosedAt.Value:yyyy-MM-dd HH:mm:ss}'" : "NULL")} |
| | | WHERE session_id = '{EscapeSql(session.SessionId)}'"; |
| | | |
| | | _sqliteHelper.ExecuteSql(sql); |
| | | log.Debug($"更新会话: {session.SessionId}, 状态: {session.State}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"更新会话失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取设备的所有活跃会话(未关闭的) |
| | | /// </summary> |
| | | public List<DebugSession> GetActiveSessionsByDevice(string deviceId) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $@" |
| | | SELECT * FROM debug_session |
| | | WHERE device_id = '{EscapeSql(deviceId)}' |
| | | AND state != {(int)DebugSessionState.Closed} |
| | | ORDER BY created_at DESC"; |
| | | |
| | | DataTable dt = _sqliteHelper.GetDataTable(sql); |
| | | return dt.Rows.Cast<DataRow>().Select(MapToDebugSession).ToList(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取设备活跃会话失败: {ex.Message}", ex); |
| | | return new List<DebugSession>(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取所有会话 |
| | | /// </summary> |
| | | public List<DebugSession> GetAllSessions() |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = "SELECT * FROM debug_session ORDER BY created_at DESC"; |
| | | DataTable dt = _sqliteHelper.GetDataTable(sql); |
| | | return dt.Rows.Cast<DataRow>().Select(MapToDebugSession).ToList(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取所有会话失败: {ex.Message}", ex); |
| | | return new List<DebugSession>(); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 审计 CRUD |
| | | |
| | | /// <summary> |
| | | /// 记录操作 |
| | | /// </summary> |
| | | public void RecordOperation(DebugOperation operation) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $@" |
| | | INSERT INTO debug_operation ( |
| | | operation_id, session_id, command_type, parameters, |
| | | outcome, device_readback, error_code, error_message, timestamp |
| | | ) VALUES ( |
| | | '{EscapeSql(operation.OperationId)}', |
| | | '{EscapeSql(operation.SessionId)}', |
| | | '{EscapeSql(operation.CommandType)}', |
| | | '{EscapeSql(operation.Parameters)}', |
| | | '{EscapeSql(operation.Outcome)}', |
| | | '{EscapeSql(operation.DeviceReadback)}', |
| | | '{EscapeSql(operation.ErrorCode)}', |
| | | '{EscapeSql(operation.ErrorMessage)}', |
| | | '{operation.Timestamp:yyyy-MM-dd HH:mm:ss}' |
| | | )"; |
| | | |
| | | _sqliteHelper.ExecuteSql(sql); |
| | | log.Debug($"记录操作: {operation.OperationId}, 类型: {operation.CommandType}, 结果: {operation.Outcome}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"记录操作失败: {ex.Message}", ex); |
| | | throw; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取会话的所有操作 |
| | | /// </summary> |
| | | public List<DebugOperation> GetOperationsBySession(string sessionId) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $@" |
| | | SELECT * FROM debug_operation |
| | | WHERE session_id = '{EscapeSql(sessionId)}' |
| | | ORDER BY timestamp DESC"; |
| | | |
| | | DataTable dt = _sqliteHelper.GetDataTable(sql); |
| | | return dt.Rows.Cast<DataRow>().Select(MapToDebugOperation).ToList(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取会话操作失败: {ex.Message}", ex); |
| | | return new List<DebugOperation>(); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取设备最近的操作 |
| | | /// </summary> |
| | | public List<DebugOperation> GetRecentOperations(string deviceId, int count) |
| | | { |
| | | try |
| | | { |
| | | if (_sqliteHelper == null) |
| | | throw new InvalidOperationException("数据库未初始化,请先调用 InitDatabase"); |
| | | |
| | | string sql = $@" |
| | | SELECT o.* FROM debug_operation o |
| | | INNER JOIN debug_session s ON o.session_id = s.session_id |
| | | WHERE s.device_id = '{EscapeSql(deviceId)}' |
| | | ORDER BY o.timestamp DESC |
| | | LIMIT {count}"; |
| | | |
| | | DataTable dt = _sqliteHelper.GetDataTable(sql); |
| | | return dt.Rows.Cast<DataRow>().Select(MapToDebugOperation).ToList(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"获取最近操作失败: {ex.Message}", ex); |
| | | return new List<DebugOperation>(); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 辅助方法 |
| | | |
| | | /// <summary> |
| | | /// 将 DataRow 映射为 DebugSession |
| | | /// </summary> |
| | | private DebugSession MapToDebugSession(DataRow row) |
| | | { |
| | | return new DebugSession |
| | | { |
| | | SessionId = row["session_id"].ToString() ?? string.Empty, |
| | | DeviceId = row["device_id"].ToString() ?? string.Empty, |
| | | OperatorId = row["operator_id"].ToString() ?? string.Empty, |
| | | OperatorName = row["operator_name"]?.ToString() ?? string.Empty, |
| | | EffectiveVersion = row["effective_version"]?.ToString() ?? string.Empty, |
| | | State = (DebugSessionState)Convert.ToInt32(row["state"]), |
| | | ReservationRef = row["reservation_ref"]?.ToString() ?? string.Empty, |
| | | CreatedAt = DateTime.Parse(row["created_at"].ToString() ?? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")), |
| | | ClosedAt = row["closed_at"] != DBNull.Value && !string.IsNullOrEmpty(row["closed_at"].ToString()) |
| | | ? DateTime.Parse(row["closed_at"].ToString()!) |
| | | : null |
| | | }; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 将 DataRow 映射为 DebugOperation |
| | | /// </summary> |
| | | private DebugOperation MapToDebugOperation(DataRow row) |
| | | { |
| | | return new DebugOperation |
| | | { |
| | | OperationId = row["operation_id"].ToString() ?? string.Empty, |
| | | SessionId = row["session_id"].ToString() ?? string.Empty, |
| | | CommandType = row["command_type"].ToString() ?? string.Empty, |
| | | Parameters = row["parameters"]?.ToString() ?? string.Empty, |
| | | Outcome = row["outcome"].ToString() ?? string.Empty, |
| | | DeviceReadback = row["device_readback"]?.ToString() ?? string.Empty, |
| | | ErrorCode = row["error_code"]?.ToString() ?? string.Empty, |
| | | ErrorMessage = row["error_message"]?.ToString() ?? string.Empty, |
| | | Timestamp = DateTime.Parse(row["timestamp"].ToString() ?? DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")) |
| | | }; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// SQL 字符串转义(防止注入) |
| | | /// </summary> |
| | | private string EscapeSql(string value) |
| | | { |
| | | return value.Replace("'", "''"); |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | } |
| New file |
| | |
| | | <UserControl x:Class="Plugin.DeviceDebug.Panels.CanDebugPanel" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | mc:Ignorable="d" |
| | | d:DesignHeight="700" d:DesignWidth="700"> |
| | | <UserControl.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | </ResourceDictionary> |
| | | </UserControl.Resources> |
| | | |
| | | <Grid dp:GridHelper.Space="10" Margin="10"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 标题 --> |
| | | <Border Grid.Row="0" Background="#FF9800" CornerRadius="4" Padding="12,8"> |
| | | <TextBlock Text="CAN 通讯调试面板" FontSize="16" FontWeight="Bold" Foreground="White"/> |
| | | </Border> |
| | | |
| | | <!-- 模拟器连接控制区 --> |
| | | <Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12" Background="#FAFAFA"> |
| | | <Grid dp:GridHelper.Space="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="80"/> |
| | | <ColumnDefinition Width="150"/> |
| | | <ColumnDefinition Width="80"/> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="Auto"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 第一行:总线名称和节点编号 --> |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="总线名称:" VerticalAlignment="Center" FontWeight="SemiBold"/> |
| | | <TextBox Grid.Row="0" Grid.Column="1" x:Name="busNameInput" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="CAN_BUS_1"/> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="2" Text="节点编号:" VerticalAlignment="Center" FontWeight="SemiBold" Margin="8,0,0,0"/> |
| | | <TextBox Grid.Row="0" Grid.Column="3" x:Name="nodeIndexInput" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="0"/> |
| | | |
| | | <Button Grid.Row="0" Grid.Column="5" x:Name="connectBtn" Content="连接" Width="80" Height="30" |
| | | Click="ConnectBtn_Click" |
| | | Background="#4CAF50" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/> |
| | | |
| | | <!-- 第二行:状态显示和控制按钮 --> |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="状态:" VerticalAlignment="Center" FontWeight="SemiBold"/> |
| | | <StackPanel Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Orientation="Horizontal" VerticalAlignment="Center"> |
| | | <Ellipse x:Name="statusIndicator" Width="12" Height="12" Fill="#9E9E9E" Margin="0,0,6,0"/> |
| | | <TextBlock x:Name="statusText" Text="未连接" VerticalAlignment="Center"/> |
| | | </StackPanel> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="3" Text="统计:" VerticalAlignment="Center" FontWeight="SemiBold" Margin="8,0,0,0"/> |
| | | <TextBlock Grid.Row="1" Grid.Column="4" x:Name="statsText" Text="TX: 0 RX: 0" |
| | | VerticalAlignment="Center" Foreground="#666"/> |
| | | |
| | | <StackPanel Grid.Row="1" Grid.Column="5" Orientation="Horizontal"> |
| | | <ToggleButton x:Name="autoResponseToggleBtn" Content="自动响应: 关" Width="100" Height="28" Margin="0,0,4,0" |
| | | Click="AutoResponseToggleBtn_Click" IsEnabled="False" IsChecked="False" |
| | | Background="#9E9E9E" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/> |
| | | <ToggleButton x:Name="maintenanceToggleBtn" Content="维护模式: 开" Width="100" Height="28" Margin="0,0,4,0" |
| | | Click="MaintenanceToggleBtn_Click" IsEnabled="False" IsChecked="True" |
| | | Background="#4CAF50" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/> |
| | | <Button x:Name="disconnectBtn" Content="断开" Width="60" Height="28" Margin="0,0,4,0" |
| | | Click="DisconnectBtn_Click" IsEnabled="False" |
| | | Background="#F44336" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | <Button x:Name="promiscuousBtn" Content="混杂模式" Width="80" Height="28" |
| | | Click="PromiscuousBtn_Click" IsEnabled="False" |
| | | Background="#2196F3" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 发送区 --> |
| | | <Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12"> |
| | | <Grid dp:GridHelper.Space="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="80"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="Auto"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 帧ID --> |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="帧 ID:" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <TextBox Grid.Row="0" Grid.Column="1" x:Name="frameIdInput" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="0x100"/> |
| | | |
| | | <!-- 数据 --> |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="数据(Hex):" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <TextBox Grid.Row="1" Grid.Column="1" x:Name="dataInput" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="00 00 00 00 00 00 00 00"/> |
| | | |
| | | <!-- 发送按钮 --> |
| | | <Button Grid.Row="1" Grid.Column="2" x:Name="sendBtn" Content="发送" Width="80" Height="30" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | Click="SendBtn_Click" |
| | | Background="#FF9800" Foreground="White" BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 接收区 --> |
| | | <Border Grid.Row="3" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <DockPanel Grid.Row="0" Margin="8,6"> |
| | | <Button x:Name="clearBtn" Content="清空" Padding="12,4" DockPanel.Dock="Right" |
| | | Click="ClearBtn_Click" |
| | | Background="#9E9E9E" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | <TextBlock Text="接收记录" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/> |
| | | </DockPanel> |
| | | |
| | | <ListView Grid.Row="1" x:Name="receiveList" |
| | | Style="{StaticResource DefaultListTable}" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="90" Header="时间"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Timestamp, StringFormat=HH:mm:ss.fff}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="帧ID"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding FrameId}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="50" Header="DLC"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Dlc}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="200" Header="数据"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Data}" FontFamily="Consolas"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="50" Header="方向"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Text="{Binding Direction}"> |
| | | <TextBlock.Style> |
| | | <Style TargetType="TextBlock" BasedOn="{StaticResource ListTableTextBlock}"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Direction}" Value="TX"> |
| | | <Setter Property="Foreground" Value="#2196F3"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Direction}" Value="RX"> |
| | | <Setter Property="Foreground" Value="#4CAF50"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </TextBlock.Style> |
| | | </TextBlock> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="100" Header="来源"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Source}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | </Grid> |
| | | </Border> |
| | | </Grid> |
| | | </UserControl> |
| New file |
| | |
| | | using Aging.Infrastructure.Drivers; |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.Globalization; |
| | | using System.Linq; |
| | | using System.Threading; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Media; |
| | | using System.Windows.Threading; |
| | | |
| | | namespace Plugin.DeviceDebug.Panels |
| | | { |
| | | /// <summary> |
| | | /// CAN 帧显示模型 |
| | | /// </summary> |
| | | public class CanFrameDisplay |
| | | { |
| | | public DateTime Timestamp { get; set; } |
| | | public string FrameId { get; set; } = string.Empty; |
| | | public int Dlc { get; set; } |
| | | public string Data { get; set; } = string.Empty; |
| | | public string Direction { get; set; } = string.Empty; // TX / RX |
| | | public string Source { get; set; } = string.Empty; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// CanDebugPanel.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class CanDebugPanel : UserControl |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(CanDebugPanel)); |
| | | |
| | | /// <summary> |
| | | /// 是否为维护模式(控制输入可用性) |
| | | /// </summary> |
| | | public static readonly DependencyProperty IsMaintenanceModeProperty = |
| | | DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(CanDebugPanel), |
| | | new PropertyMetadata(false)); |
| | | |
| | | public bool IsMaintenanceMode |
| | | { |
| | | get => (bool)GetValue(IsMaintenanceModeProperty); |
| | | set => SetValue(IsMaintenanceModeProperty, value); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 命令执行事件 |
| | | /// </summary> |
| | | public event EventHandler<DebugCommand>? OnCommandExecute; |
| | | |
| | | /// <summary> |
| | | /// 接收帧列表 |
| | | /// </summary> |
| | | public ObservableCollection<CanFrameDisplay> ReceivedFrames { get; } = new(); |
| | | |
| | | /// <summary> |
| | | /// CAN 驱动适配器 |
| | | /// </summary> |
| | | private CanDriverAdapter? _driver; |
| | | |
| | | /// <summary> |
| | | /// 轮询定时器(用于检查接收邮箱) |
| | | /// </summary> |
| | | private DispatcherTimer? _pollTimer; |
| | | |
| | | /// <summary> |
| | | /// 是否已连接 |
| | | /// </summary> |
| | | private bool _isConnected; |
| | | |
| | | /// <summary> |
| | | /// 设备ID(从 DeviceDebug 传入,面板自动拼接 deviceId) |
| | | /// </summary> |
| | | public string DeviceId |
| | | { |
| | | get => _deviceId; |
| | | set |
| | | { |
| | | _deviceId = value; |
| | | if (!string.IsNullOrEmpty(value)) |
| | | { |
| | | // 将设备ID前缀拼接到默认设备标识中,避免与模拟器面板重名 |
| | | UpdateDeviceIdDisplay(); |
| | | } |
| | | } |
| | | } |
| | | private string _deviceId = string.Empty; |
| | | |
| | | /// <summary> |
| | | /// 从设备 Endpoint 解析出的总线名称 |
| | | /// </summary> |
| | | public string InitialBusName { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 从设备 Endpoint 解析出的节点编号 |
| | | /// </summary> |
| | | public int? InitialNodeIndex { get; set; } |
| | | |
| | | /// <summary> |
| | | /// 是否自动连接(从 DeviceDebug 打开时=true) |
| | | /// </summary> |
| | | public bool AutoConnectOnLoad { get; set; } |
| | | |
| | | public CanDebugPanel() |
| | | { |
| | | InitializeComponent(); |
| | | receiveList.ItemsSource = ReceivedFrames; |
| | | InitializePollTimer(); |
| | | Loaded += CanDebugPanel_Loaded; |
| | | } |
| | | |
| | | private void CanDebugPanel_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | Loaded -= CanDebugPanel_Loaded; |
| | | |
| | | // 从设备信息初始化总线/节点输入 |
| | | if (!string.IsNullOrEmpty(InitialBusName)) |
| | | { |
| | | busNameInput.Text = InitialBusName; |
| | | } |
| | | if (InitialNodeIndex.HasValue) |
| | | { |
| | | nodeIndexInput.Text = InitialNodeIndex.Value.ToString(); |
| | | } |
| | | |
| | | // 自动连接 |
| | | if (AutoConnectOnLoad) |
| | | { |
| | | Dispatcher.BeginInvoke(new Action(() => |
| | | { |
| | | try { DoConnect(); } |
| | | catch (Exception ex) { log.Warn($"自动连接失败: {ex.Message}"); } |
| | | }), DispatcherPriority.Background); |
| | | } |
| | | } |
| | | |
| | | private void UpdateDeviceIdDisplay() |
| | | { |
| | | if (!string.IsNullOrEmpty(_deviceId) && busNameInput != null) |
| | | { |
| | | // 保留默认总线名,仅记录 deviceId 以供日志使用 |
| | | log.Info($"[CanDebugPanel] 关联设备: {_deviceId}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化轮询定时器 |
| | | /// </summary> |
| | | private void InitializePollTimer() |
| | | { |
| | | _pollTimer = new DispatcherTimer |
| | | { |
| | | Interval = TimeSpan.FromMilliseconds(100) // 100ms 轮询一次 |
| | | }; |
| | | _pollTimer.Tick += PollTimer_Tick; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 轮询定时器回调 - 仅更新统计信息(帧显示由事件驱动) |
| | | /// </summary> |
| | | private void PollTimer_Tick(object? sender, EventArgs e) |
| | | { |
| | | if (!_isConnected || _driver == null) return; |
| | | |
| | | try |
| | | { |
| | | // 更新统计 |
| | | UpdateStats(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"轮询统计更新失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 连接按钮点击 |
| | | /// </summary> |
| | | private void ConnectBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | DoConnect(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"CAN 连接失败: {ex.Message}", ex); |
| | | MessageBox.Show($"连接失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | UpdateConnectionStatus(false); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 实际连接逻辑(从 UI 或自动连接调用) |
| | | /// </summary> |
| | | private void DoConnect() |
| | | { |
| | | if (_isConnected) return; |
| | | |
| | | string busName = busNameInput.Text.Trim(); |
| | | if (!int.TryParse(nodeIndexInput.Text.Trim(), out int nodeIndex)) |
| | | { |
| | | throw new ArgumentException("节点编号格式错误"); |
| | | } |
| | | |
| | | // 优先使用 DeviceDebug 传入的设备 ID,否则自动生成一个 |
| | | string deviceId = string.IsNullOrEmpty(_deviceId) |
| | | ? $"CAN_Test_{nodeIndex}_{DateTime.Now:HHmmss}" |
| | | : _deviceId; |
| | | |
| | | _driver = new CanDriverAdapter(deviceId, busName, nodeIndex); |
| | | |
| | | // 订阅驱动的帧接收事件(即时显示接收到的帧) |
| | | _driver.DriverFrameReceived += OnDriverFrameReceived; |
| | | |
| | | // 连接 |
| | | _driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult(); |
| | | _isConnected = true; |
| | | |
| | | // 模拟器模式下,连接后自动启用维护模式(允许发送) |
| | | IsMaintenanceMode = true; |
| | | if (maintenanceToggleBtn != null) |
| | | { |
| | | maintenanceToggleBtn.IsChecked = true; |
| | | maintenanceToggleBtn.IsEnabled = true; |
| | | } |
| | | |
| | | // 更新 UI |
| | | UpdateConnectionStatus(true); |
| | | _pollTimer?.Start(); |
| | | |
| | | log.Info($"CAN 驱动已连接: Bus={busName}, Node={nodeIndex}, DeviceId={deviceId}"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 驱动帧接收事件处理 - 即时显示接收到的帧 |
| | | /// </summary> |
| | | private void OnDriverFrameReceived(object? sender, CanSimulator.CanFrame frame) |
| | | { |
| | | try |
| | | { |
| | | // 判断方向:如果帧的来源是当前节点,则为自动响应(显示为 RX) |
| | | string direction = "RX"; |
| | | string source = frame.SourceNodeId; |
| | | |
| | | AddReceivedFrame( |
| | | frame.FrameIdHex, |
| | | frame.DataHex, |
| | | direction, |
| | | source |
| | | ); |
| | | |
| | | log.Debug($"[即时显示] 接收帧: {frame}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"即时显示帧失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 断开按钮点击 |
| | | /// </summary> |
| | | private void DisconnectBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | _pollTimer?.Stop(); |
| | | if (_driver != null) |
| | | { |
| | | _driver.DriverFrameReceived -= OnDriverFrameReceived; |
| | | _driver.DisconnectAsync().GetAwaiter().GetResult(); |
| | | _driver.Dispose(); |
| | | } |
| | | _driver = null; |
| | | _isConnected = false; |
| | | IsMaintenanceMode = false; |
| | | |
| | | UpdateConnectionStatus(false); |
| | | log.Info("CAN 驱动已断开"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"CAN 断开失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 混杂模式按钮点击 |
| | | /// </summary> |
| | | private void PromiscuousBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | if (!_isConnected || _driver == null) return; |
| | | |
| | | try |
| | | { |
| | | _driver.QueryAsync("PROMISCUOUS", CancellationToken.None).GetAwaiter().GetResult(); |
| | | log.Info("已设置混杂模式(接收所有帧)"); |
| | | MessageBox.Show("已设置混杂模式,将接收所有 CAN 帧", "提示", MessageBoxButton.OK, MessageBoxImage.Information); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"设置混杂模式失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 维护模式切换按钮点击 |
| | | /// </summary> |
| | | private void MaintenanceToggleBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | bool isOn = maintenanceToggleBtn.IsChecked == true; |
| | | IsMaintenanceMode = isOn; |
| | | |
| | | if (isOn) |
| | | { |
| | | maintenanceToggleBtn.Content = "维护模式: 开"; |
| | | maintenanceToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")); |
| | | log.Info("维护模式已开启(可发送 CAN 帧)"); |
| | | } |
| | | else |
| | | { |
| | | maintenanceToggleBtn.Content = "维护模式: 关"; |
| | | maintenanceToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | log.Info("维护模式已关闭(只读监控)"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 自动响应模式切换按钮点击 |
| | | /// </summary> |
| | | private void AutoResponseToggleBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | if (!_isConnected || _driver == null) return; |
| | | |
| | | try |
| | | { |
| | | bool isOn = autoResponseToggleBtn.IsChecked == true; |
| | | |
| | | // 通过驱动发送命令启用/禁用自动响应 |
| | | string cmd = isOn ? "AUTO_RESPONSE:ON" : "AUTO_RESPONSE:OFF"; |
| | | _driver.QueryAsync(cmd, CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | if (isOn) |
| | | { |
| | | autoResponseToggleBtn.Content = "自动响应: 开"; |
| | | autoResponseToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF9800")); |
| | | log.Info("自动响应模式已开启(收到帧将自动回复)"); |
| | | } |
| | | else |
| | | { |
| | | autoResponseToggleBtn.Content = "自动响应: 关"; |
| | | autoResponseToggleBtn.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | log.Info("自动响应模式已关闭"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"切换自动响应模式失败: {ex.Message}"); |
| | | MessageBox.Show($"切换失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新连接状态显示 |
| | | /// </summary> |
| | | private void UpdateConnectionStatus(bool connected) |
| | | { |
| | | _isConnected = connected; |
| | | if (connected) |
| | | { |
| | | statusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")); |
| | | statusText.Text = "已连接"; |
| | | connectBtn.IsEnabled = false; |
| | | disconnectBtn.IsEnabled = true; |
| | | promiscuousBtn.IsEnabled = true; |
| | | maintenanceToggleBtn.IsEnabled = true; |
| | | autoResponseToggleBtn.IsEnabled = true; |
| | | busNameInput.IsEnabled = false; |
| | | nodeIndexInput.IsEnabled = false; |
| | | } |
| | | else |
| | | { |
| | | statusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | statusText.Text = "未连接"; |
| | | connectBtn.IsEnabled = true; |
| | | disconnectBtn.IsEnabled = false; |
| | | promiscuousBtn.IsEnabled = false; |
| | | maintenanceToggleBtn.IsEnabled = false; |
| | | autoResponseToggleBtn.IsEnabled = false; |
| | | busNameInput.IsEnabled = true; |
| | | nodeIndexInput.IsEnabled = true; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新统计信息 |
| | | /// </summary> |
| | | private void UpdateStats() |
| | | { |
| | | if (_driver == null) return; |
| | | |
| | | try |
| | | { |
| | | var status = _driver.GetStatus(); |
| | | var response = _driver.QueryAsync("STATUS", CancellationToken.None).GetAwaiter().GetResult(); |
| | | // 解析状态: "STATUS:Node=..., Active=True, Rx=0, Tx=0, Err=0, Mailbox=0|Bus=..." |
| | | statsText.Text = $"TX: {GetStatValue(response, "Tx")} RX: {GetStatValue(response, "Rx")} Err: {GetStatValue(response, "Err")}"; |
| | | } |
| | | catch |
| | | { |
| | | // 忽略统计更新失败 |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 从状态字符串中提取数值值 |
| | | /// </summary> |
| | | private string GetStatValue(string statusStr, string key) |
| | | { |
| | | try |
| | | { |
| | | var parts = statusStr.Split(','); |
| | | foreach (var part in parts) |
| | | { |
| | | var kv = part.Trim().Split('='); |
| | | if (kv.Length == 2 && kv[0].Trim() == key) |
| | | { |
| | | return kv[1].Trim(); |
| | | } |
| | | } |
| | | } |
| | | catch { } |
| | | return "0"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 添加接收帧 |
| | | /// </summary> |
| | | public void AddReceivedFrame(string frameId, string data, string direction = "RX", string source = "") |
| | | { |
| | | try |
| | | { |
| | | var frame = new CanFrameDisplay |
| | | { |
| | | Timestamp = DateTime.Now, |
| | | FrameId = frameId, |
| | | Dlc = data.Split(new[] { ' ', '-' }, StringSplitOptions.RemoveEmptyEntries).Length, |
| | | Data = data, |
| | | Direction = direction, |
| | | Source = source |
| | | }; |
| | | |
| | | Application.Current.Dispatcher.Invoke(() => |
| | | { |
| | | ReceivedFrames.Insert(0, frame); |
| | | |
| | | // 限制显示数量 |
| | | if (ReceivedFrames.Count > 500) |
| | | { |
| | | var toRemove = ReceivedFrames.Skip(500).ToList(); |
| | | foreach (var item in toRemove) |
| | | { |
| | | ReceivedFrames.Remove(item); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"添加接收帧失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 发送按钮点击 |
| | | /// </summary> |
| | | private void SendBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (!IsMaintenanceMode) |
| | | { |
| | | MessageBox.Show("当前为只读模式,无法发送数据", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | return; |
| | | } |
| | | |
| | | if (!_isConnected || _driver == null) |
| | | { |
| | | MessageBox.Show("请先连接 CAN 总线", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | return; |
| | | } |
| | | |
| | | string frameIdStr = frameIdInput.Text.Trim(); |
| | | string dataStr = dataInput.Text.Trim(); |
| | | |
| | | // 验证帧 ID |
| | | uint frameId = CanSimulator.CanFrame.ParseFrameId(frameIdStr); |
| | | |
| | | // 验证数据 |
| | | byte[] dataBytes = CanSimulator.CanFrame.ParseData(dataStr); |
| | | if (dataBytes.Length > 8) |
| | | { |
| | | MessageBox.Show("数据长度不能超过 8 字节", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | return; |
| | | } |
| | | |
| | | // 通过驱动发送 |
| | | string command = $"SEND:{frameIdStr}:{dataStr}"; |
| | | var response = _driver.SendCommandAsync(command, CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | // 添加到显示列表(TX) |
| | | AddReceivedFrame($"0x{frameId:X3}", BitConverter.ToString(dataBytes).Replace("-", " "), "TX", "LOCAL"); |
| | | |
| | | log.Info($"发送 CAN 帧: ID=0x{frameId:X3}, Data={BitConverter.ToString(dataBytes)}, Response={response}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"发送 CAN 帧失败: {ex.Message}", ex); |
| | | MessageBox.Show($"发送失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 清空按钮点击 |
| | | /// </summary> |
| | | private void ClearBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | ReceivedFrames.Clear(); |
| | | log.Info("清空 CAN 接收记录"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"清空接收记录失败: {ex.Message}", ex); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <UserControl x:Class="Plugin.DeviceDebug.Panels.PlcDebugPanel" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | mc:Ignorable="d" |
| | | d:DesignHeight="500" d:DesignWidth="600"> |
| | | <UserControl.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | </ResourceDictionary> |
| | | </UserControl.Resources> |
| | | |
| | | <Grid dp:GridHelper.Space="12" Margin="12"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 标题 --> |
| | | <Border Grid.Row="0" Background="#9C27B0" CornerRadius="4" Padding="12,8"> |
| | | <TextBlock Text="PLC 调试面板" FontSize="16" FontWeight="Bold" Foreground="White"/> |
| | | </Border> |
| | | |
| | | <!-- IO 控制区 --> |
| | | <Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12"> |
| | | <Grid dp:GridHelper.Space="10"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="80"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 地址 --> |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="地址:" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <TextBox Grid.Row="0" Grid.Column="1" x:Name="addressInput" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="M0.0"/> |
| | | |
| | | <!-- 值 --> |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="值:" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <ComboBox Grid.Row="1" Grid.Column="1" x:Name="valueSelect" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | VerticalContentAlignment="Center" Padding="6,4"> |
| | | <ComboBoxItem Content="True" IsSelected="True"/> |
| | | <ComboBoxItem Content="False"/> |
| | | </ComboBox> |
| | | |
| | | <!-- 写入按钮 --> |
| | | <Button Grid.Row="2" Grid.ColumnSpan="2" x:Name="writeBtn" Content="写入" Padding="20,6" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | Click="WriteBtn_Click" HorizontalAlignment="Left" |
| | | Background="#9C27B0" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- IO 状态显示区 --> |
| | | <Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <Border Grid.Row="0" Background="#F5F5F5" Padding="8,6"> |
| | | <TextBlock Text="IO 点位状态" FontWeight="Bold" FontSize="14"/> |
| | | </Border> |
| | | |
| | | <ListView Grid.Row="1" x:Name="ioStatusList" Height="180" |
| | | Style="{StaticResource DefaultListTable}" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Standard"> |
| | | <ListView.View> |
| | | <GridView AllowsColumnReorder="False"> |
| | | <GridViewColumn Width="120" Header="地址"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding Address}" FontFamily="Consolas"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="80" Header="值"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Text="{Binding Value}"> |
| | | <TextBlock.Style> |
| | | <Style TargetType="TextBlock" BasedOn="{StaticResource ListTableTextBlock}"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Value}" Value="True"> |
| | | <Setter Property="Foreground" Value="#4CAF50"/> |
| | | <Setter Property="FontWeight" Value="Bold"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Value}" Value="False"> |
| | | <Setter Property="Foreground" Value="#999"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </TextBlock.Style> |
| | | </TextBlock> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | <GridViewColumn Width="150" Header="更新时间"> |
| | | <GridViewColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Border Style="{StaticResource CellBorder}"> |
| | | <TextBlock Style="{StaticResource ListTableTextBlock}" |
| | | Text="{Binding LastUpdate, StringFormat=HH:mm:ss}"/> |
| | | </Border> |
| | | </DataTemplate> |
| | | </GridViewColumn.CellTemplate> |
| | | </GridViewColumn> |
| | | </GridView> |
| | | </ListView.View> |
| | | </ListView> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 互锁状态区 --> |
| | | <Border Grid.Row="3" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <Border Grid.Row="0" Background="#FFF3E0" Padding="8,6"> |
| | | <TextBlock Text="互锁状态" FontWeight="Bold" FontSize="14" Foreground="#E65100"/> |
| | | </Border> |
| | | |
| | | <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"> |
| | | <ItemsControl x:Name="interlockList" Margin="8"> |
| | | <ItemsControl.ItemTemplate> |
| | | <DataTemplate> |
| | | <Border BorderBrush="#EEE" BorderThickness="0,0,0,1" Padding="4,6"> |
| | | <StackPanel Orientation="Horizontal"> |
| | | <TextBlock Text="{Binding Name}" FontWeight="Bold" Width="150"/> |
| | | <TextBlock Text="{Binding Status}" Margin="10,0,0,0"> |
| | | <TextBlock.Style> |
| | | <Style TargetType="TextBlock"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Status}" Value="满足"> |
| | | <Setter Property="Foreground" Value="#4CAF50"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="不满足"> |
| | | <Setter Property="Foreground" Value="#F44336"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </TextBlock.Style> |
| | | </TextBlock> |
| | | </StackPanel> |
| | | </Border> |
| | | </DataTemplate> |
| | | </ItemsControl.ItemTemplate> |
| | | </ItemsControl> |
| | | </ScrollViewer> |
| | | </Grid> |
| | | </Border> |
| | | </Grid> |
| | | </UserControl> |
| New file |
| | |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Plugin.DeviceDebug.Panels |
| | | { |
| | | /// <summary> |
| | | /// IO 点位状态显示模型 |
| | | /// </summary> |
| | | public class IoPointStatus |
| | | { |
| | | public string Address { get; set; } |
| | | public bool Value { get; set; } |
| | | public DateTime LastUpdate { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 互锁状态显示模型 |
| | | /// </summary> |
| | | public class InterlockStatus |
| | | { |
| | | public string Name { get; set; } |
| | | public string Status { get; set; } // "满足" / "不满足" |
| | | } |
| | | |
| | | /// <summary> |
| | | /// PlcDebugPanel.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class PlcDebugPanel : UserControl |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(PlcDebugPanel)); |
| | | |
| | | /// <summary> |
| | | /// 是否为维护模式(控制输入可用性) |
| | | /// </summary> |
| | | public static readonly DependencyProperty IsMaintenanceModeProperty = |
| | | DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(PlcDebugPanel), |
| | | new PropertyMetadata(false)); |
| | | |
| | | public bool IsMaintenanceMode |
| | | { |
| | | get => (bool)GetValue(IsMaintenanceModeProperty); |
| | | set => SetValue(IsMaintenanceModeProperty, value); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 命令执行事件 |
| | | /// </summary> |
| | | public event EventHandler<DebugCommand> OnCommandExecute; |
| | | |
| | | /// <summary> |
| | | /// IO 点位状态列表 |
| | | /// </summary> |
| | | public ObservableCollection<IoPointStatus> IoStatuses { get; } = new(); |
| | | |
| | | /// <summary> |
| | | /// 互锁状态列表 |
| | | /// </summary> |
| | | public ObservableCollection<InterlockStatus> Interlocks { get; } = new(); |
| | | |
| | | public PlcDebugPanel() |
| | | { |
| | | InitializeComponent(); |
| | | ioStatusList.ItemsSource = IoStatuses; |
| | | interlockList.ItemsSource = Interlocks; |
| | | |
| | | // 初始化示例数据 |
| | | InitSampleData(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化示例数据(实际应从 PLC 读取) |
| | | /// </summary> |
| | | private void InitSampleData() |
| | | { |
| | | try |
| | | { |
| | | // 添加示例 IO 点位 |
| | | IoStatuses.Add(new IoPointStatus { Address = "M0.0", Value = false, LastUpdate = DateTime.Now }); |
| | | IoStatuses.Add(new IoPointStatus { Address = "M0.1", Value = true, LastUpdate = DateTime.Now }); |
| | | IoStatuses.Add(new IoPointStatus { Address = "M0.2", Value = false, LastUpdate = DateTime.Now }); |
| | | IoStatuses.Add(new IoPointStatus { Address = "I0.0", Value = true, LastUpdate = DateTime.Now }); |
| | | IoStatuses.Add(new IoPointStatus { Address = "Q0.0", Value = false, LastUpdate = DateTime.Now }); |
| | | |
| | | // 添加示例互锁 |
| | | Interlocks.Add(new InterlockStatus { Name = "安全门关闭", Status = "满足" }); |
| | | Interlocks.Add(new InterlockStatus { Name = "急停未触发", Status = "满足" }); |
| | | Interlocks.Add(new InterlockStatus { Name = "气压正常", Status = "不满足" }); |
| | | Interlocks.Add(new InterlockStatus { Name = "温度正常", Status = "满足" }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("初始化示例数据失败", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新 IO 点位状态 |
| | | /// </summary> |
| | | public void UpdateIoStatus(string address, bool value) |
| | | { |
| | | try |
| | | { |
| | | var existing = IoStatuses.FirstOrDefault(s => s.Address == address); |
| | | if (existing != null) |
| | | { |
| | | existing.Value = value; |
| | | existing.LastUpdate = DateTime.Now; |
| | | } |
| | | else |
| | | { |
| | | IoStatuses.Add(new IoPointStatus |
| | | { |
| | | Address = address, |
| | | Value = value, |
| | | LastUpdate = DateTime.Now |
| | | }); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"更新 IO 状态失败: {address}", ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新互锁状态 |
| | | /// </summary> |
| | | public void UpdateInterlock(string name, bool satisfied) |
| | | { |
| | | try |
| | | { |
| | | var existing = Interlocks.FirstOrDefault(i => i.Name == name); |
| | | if (existing != null) |
| | | { |
| | | existing.Status = satisfied ? "满足" : "不满足"; |
| | | } |
| | | else |
| | | { |
| | | Interlocks.Add(new InterlockStatus |
| | | { |
| | | Name = name, |
| | | Status = satisfied ? "满足" : "不满足" |
| | | }); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error($"更新互锁状态失败: {name}", ex); |
| | | } |
| | | } |
| | | |
| | | private void WriteBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (!IsMaintenanceMode) |
| | | { |
| | | MessageBox.Show("当前为只读模式,无法执行写入操作", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | return; |
| | | } |
| | | |
| | | string address = addressInput.Text.Trim(); |
| | | if (string.IsNullOrEmpty(address)) |
| | | { |
| | | MessageBox.Show("请输入 IO 地址", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | return; |
| | | } |
| | | |
| | | bool value = (valueSelect.SelectedItem as ComboBoxItem)?.Content?.ToString() == "True"; |
| | | |
| | | // 创建命令 |
| | | var command = new PlcWriteCommand |
| | | { |
| | | IoAddress = address, |
| | | Value = value |
| | | }; |
| | | |
| | | // 触发命令执行事件 |
| | | OnCommandExecute?.Invoke(this, command); |
| | | |
| | | // 更新本地显示 |
| | | UpdateIoStatus(address, value); |
| | | |
| | | log.Info($"写入 PLC IO: {address} = {value}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("写入 PLC IO 失败", ex); |
| | | MessageBox.Show($"写入失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <UserControl x:Class="Plugin.DeviceDebug.Panels.PowerSupplyDebugPanel" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | mc:Ignorable="d" |
| | | d:DesignHeight="500" d:DesignWidth="600"> |
| | | |
| | | <Grid dp:GridHelper.Space="12" Margin="12"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 标题 --> |
| | | <Border Grid.Row="0" Background="#2196F3" CornerRadius="4" Padding="12,8"> |
| | | <TextBlock Text="电源控制面板" FontSize="16" FontWeight="Bold" Foreground="White"/> |
| | | </Border> |
| | | |
| | | <!-- 设置区 --> |
| | | <Border Grid.Row="1" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12"> |
| | | <Grid dp:GridHelper.Space="10"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="*"/> |
| | | <ColumnDefinition Width="50"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 电压设定 --> |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="电压设定:" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <TextBox Grid.Row="0" Grid.Column="1" x:Name="voltageInput" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="0.00"/> |
| | | <TextBlock Grid.Row="0" Grid.Column="2" Text="V" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#666"/> |
| | | |
| | | <!-- 限流设定 --> |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="限流设定:" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <TextBox Grid.Row="1" Grid.Column="1" x:Name="currentInput" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | VerticalContentAlignment="Center" Padding="6,4" Text="0.000"/> |
| | | <TextBlock Grid.Row="1" Grid.Column="2" Text="A" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#666"/> |
| | | |
| | | <!-- 输出控制 --> |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="输出控制:" VerticalAlignment="Center" FontWeight="Bold"/> |
| | | <ToggleButton Grid.Row="2" Grid.Column="1" x:Name="outputToggle" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | Checked="OutputToggle_Changed" Unchecked="OutputToggle_Changed" |
| | | Height="30" Cursor="Hand"> |
| | | <ToggleButton.Style> |
| | | <Style TargetType="ToggleButton"> |
| | | <Setter Property="Content" Value="OFF"/> |
| | | <Setter Property="Background" Value="#F44336"/> |
| | | <Setter Property="Foreground" Value="White"/> |
| | | <Setter Property="FontWeight" Value="Bold"/> |
| | | <Style.Triggers> |
| | | <Trigger Property="IsChecked" Value="True"> |
| | | <Setter Property="Content" Value="ON"/> |
| | | <Setter Property="Background" Value="#4CAF50"/> |
| | | </Trigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </ToggleButton.Style> |
| | | </ToggleButton> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <StackPanel Grid.Row="3" Grid.ColumnSpan="3" Orientation="Horizontal" |
| | | dp:StackPanelHelper.ChildSpace="10" HorizontalAlignment="Left" Margin="0,6,0,0"> |
| | | <Button x:Name="applyBtn" Content="设置" Padding="16,6" |
| | | IsEnabled="{Binding IsMaintenanceMode, RelativeSource={RelativeSource AncestorType=UserControl}}" |
| | | Click="ApplyBtn_Click" |
| | | Background="#2196F3" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | <Button x:Name="measureBtn" Content="测量" Padding="16,6" |
| | | Click="MeasureBtn_Click" |
| | | Background="#4CAF50" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 实测值显示区 --> |
| | | <Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4" Padding="12"> |
| | | <Grid dp:GridHelper.Space="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="100"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="实测电压:" FontWeight="Bold" VerticalAlignment="Center"/> |
| | | <TextBlock Grid.Row="0" Grid.Column="1" x:Name="measuredVoltage" |
| | | Text="-- V" FontSize="18" Foreground="#2196F3" VerticalAlignment="Center"/> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="实测电流:" FontWeight="Bold" VerticalAlignment="Center"/> |
| | | <TextBlock Grid.Row="1" Grid.Column="1" x:Name="measuredCurrent" |
| | | Text="-- A" FontSize="18" Foreground="#FF9800" VerticalAlignment="Center"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="更新时间:" FontWeight="Bold" VerticalAlignment="Center"/> |
| | | <TextBlock Grid.Row="2" Grid.Column="1" x:Name="lastUpdateTime" |
| | | Text="--" Foreground="#999" VerticalAlignment="Center"/> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | |
| | | </Grid> |
| | | </UserControl> |
| New file |
| | |
| | | using log4net; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using System; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Plugin.DeviceDebug.Panels |
| | | { |
| | | /// <summary> |
| | | /// PowerSupplyDebugPanel.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class PowerSupplyDebugPanel : UserControl |
| | | { |
| | | private static readonly ILog log = LogManager.GetLogger(typeof(PowerSupplyDebugPanel)); |
| | | |
| | | /// <summary> |
| | | /// 是否为维护模式(控制输入可用性) |
| | | /// </summary> |
| | | public static readonly DependencyProperty IsMaintenanceModeProperty = |
| | | DependencyProperty.Register(nameof(IsMaintenanceMode), typeof(bool), typeof(PowerSupplyDebugPanel), |
| | | new PropertyMetadata(false)); |
| | | |
| | | public bool IsMaintenanceMode |
| | | { |
| | | get => (bool)GetValue(IsMaintenanceModeProperty); |
| | | set => SetValue(IsMaintenanceModeProperty, value); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 命令执行事件 |
| | | /// </summary> |
| | | public event EventHandler<DebugCommand> OnCommandExecute; |
| | | |
| | | public PowerSupplyDebugPanel() |
| | | { |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新实测值 |
| | | /// </summary> |
| | | public void UpdateReadback(string voltage, string current, DateTime time) |
| | | { |
| | | try |
| | | { |
| | | measuredVoltage.Text = $"{voltage} V"; |
| | | measuredCurrent.Text = $"{current} A"; |
| | | lastUpdateTime.Text = time.ToString("yyyy-MM-dd HH:mm:ss"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("更新实测值失败", ex); |
| | | } |
| | | } |
| | | |
| | | private void ApplyBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (!IsMaintenanceMode) |
| | | { |
| | | MessageBox.Show("当前为只读模式,无法执行设置操作", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | return; |
| | | } |
| | | |
| | | // 解析输入值 |
| | | if (!double.TryParse(voltageInput.Text, out double voltage)) |
| | | { |
| | | MessageBox.Show("电压值格式不正确", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | return; |
| | | } |
| | | |
| | | if (!double.TryParse(currentInput.Text, out double currentLimit)) |
| | | { |
| | | MessageBox.Show("电流值格式不正确", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | return; |
| | | } |
| | | |
| | | bool output = outputToggle.IsChecked == true; |
| | | |
| | | // 创建命令 |
| | | var command = new PowerSetCommand |
| | | { |
| | | Voltage = voltage, |
| | | CurrentLimit = currentLimit, |
| | | Output = output |
| | | }; |
| | | |
| | | // 触发命令执行事件 |
| | | OnCommandExecute?.Invoke(this, command); |
| | | |
| | | log.Info($"发送电源设置命令: V={voltage}, I={currentLimit}, Output={output}"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("发送电源设置命令失败", ex); |
| | | MessageBox.Show($"发送命令失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | private void MeasureBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | // 创建测量命令 |
| | | var command = new PowerMeasureCommand(); |
| | | |
| | | // 触发命令执行事件 |
| | | OnCommandExecute?.Invoke(this, command); |
| | | |
| | | log.Info("发送电源测量命令"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("发送电源测量命令失败", ex); |
| | | MessageBox.Show($"发送命令失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | private void OutputToggle_Changed(object sender, RoutedEventArgs e) |
| | | { |
| | | // ToggleButton 状态变化时的视觉反馈已在 XAML Style 中处理 |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>Library</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" /> |
| | | <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" /> |
| | | <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" /> |
| | | <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" /> |
| | | <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" /> |
| | | <ProjectReference Include="..\Aging.Infrastructure\Aging.Infrastructure.csproj" /> |
| | | <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| New file |
| | |
| | | <Application x:Class="Plugin.IntegrationTest.App" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI" |
| | | xmlns:hc="clr-namespace:HMI.Core;assembly=HMI.Core" |
| | | StartupUri="MainWindow.xaml" |
| | | xmlns:sys="clr-namespace:System;assembly=mscorlib"> |
| | | <Application.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/BaseWindow.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/FormStyle.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Resources/Font/Font.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/Colors.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonResource.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/MsgBoxStyle.xaml"/> |
| | | <ResourceDictionary Source="/HMI.Base;component/MaskControl.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | <FontFamily x:Key="iconfont"> |
| | | /HMI.UI;component/Resources/Font/#iconfont |
| | | </FontFamily> |
| | | <sys:Double x:Key="nanD">NaN</sys:Double> |
| | | <Style TargetType="{x:Type pu:FormGroup}" BasedOn="{StaticResource InputFormGroupStyle}"></Style> |
| | | <uic:PathImageConverter x:Key="path2Image"/> |
| | | <uic:DataGridRowNumberConverter x:Key="rowNumber"/> |
| | | <uic:VisibilityBoolConverter x:Key="vbConverter"/> |
| | | <uic:DataGridRowFlagConverter x:Key="rowFlag"/> |
| | | <uic:ReverseBoolConverter x:Key="reverseBool"/> |
| | | <hc:LangConverter x:Key="lc"/> |
| | | <hc:WidthSizeConverter x:Key="fc"/> |
| | | <hc:HeightSizeConverter x:Key="hfc"/> |
| | | </ResourceDictionary> |
| | | </Application.Resources> |
| | | </Application> |
| New file |
| | |
| | | using System.Windows; |
| | | |
| | | namespace Plugin.IntegrationTest |
| | | { |
| | | public partial class App : Application |
| | | { |
| | | } |
| | | } |
| New file |
| | |
| | | <Window x:Class="Plugin.IntegrationTest.MainWindow" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | Title="仪器仪表注册 & 硬件调试 - 集成测试环境" |
| | | Height="800" Width="1300" |
| | | WindowStartupLocation="CenterScreen" |
| | | WindowState="Maximized" |
| | | Background="#F5F5F5" |
| | | Loaded="Window_Loaded" Closing="Window_Closing"> |
| | | <Grid Margin="8"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 顶部状态栏 --> |
| | | <Border Grid.Row="0" Background="#E3F2FD" CornerRadius="4" Padding="12,6" Margin="0,0,0,6"> |
| | | <DockPanel> |
| | | <TextBlock Text="集成测试环境" FontSize="16" FontWeight="Bold" |
| | | VerticalAlignment="Center" Foreground="#1565C0"/> |
| | | <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right"> |
| | | <TextBlock x:Name="txtStatus" Text="就绪" FontSize="13" |
| | | VerticalAlignment="Center" Foreground="#666" Margin="0,0,12,0"/> |
| | | <Button x:Name="btnRun" Content="▶ 运行自动化测试" Width="150" Height="32" |
| | | FontSize="13" FontWeight="SemiBold" |
| | | Background="#4CAF50" Foreground="White" BorderThickness="0" |
| | | Cursor="Hand" Click="BtnRun_Click" Margin="0,0,6,0"/> |
| | | <Button x:Name="btnReset" Content="重置数据" Width="90" Height="32" |
| | | FontSize="13" Click="BtnReset_Click" Margin="0,0,6,0" |
| | | Background="#FF9800" Foreground="White" BorderThickness="0" Cursor="Hand"/> |
| | | <Button x:Name="btnClear" Content="清空日志" Width="80" Height="32" |
| | | FontSize="13" Click="BtnClear_Click"/> |
| | | </StackPanel> |
| | | </DockPanel> |
| | | </Border> |
| | | |
| | | <!-- 主 TabControl --> |
| | | <TabControl x:Name="mainTabs" Grid.Row="1" Margin="0,4"> |
| | | <!-- Tab 1: 自动化测试 --> |
| | | <TabItem Header=" 自动化测试 " FontSize="13"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <ProgressBar x:Name="progressBar" Height="6" Minimum="0" Maximum="100" Value="0" Margin="4"/> |
| | | <TextBox x:Name="txtLog" Grid.Row="1" |
| | | IsReadOnly="True" VerticalScrollBarVisibility="Auto" |
| | | HorizontalScrollBarVisibility="Auto" |
| | | FontFamily="Consolas" FontSize="12.5" |
| | | Background="#1E1E1E" Foreground="#D4D4D4" |
| | | BorderThickness="1" BorderBrush="#CCC" |
| | | Padding="8" TextWrapping="NoWrap" |
| | | AcceptsReturn="True" VerticalContentAlignment="Top"/> |
| | | </Grid> |
| | | </TabItem> |
| | | |
| | | <!-- Tab 2: 仪器仪表注册 --> |
| | | <TabItem Header=" 仪器仪表注册 " FontSize="13"> |
| | | <Border x:Name="deviceConfigHost" Background="White"/> |
| | | </TabItem> |
| | | |
| | | <!-- Tab 3: 硬件调试 --> |
| | | <TabItem Header=" 硬件调试 " FontSize="13"> |
| | | <Border x:Name="deviceDebugHost" Background="White"/> |
| | | </TabItem> |
| | | |
| | | <!-- Tab 4: CAN 模拟器 --> |
| | | <TabItem Header=" CAN 模拟器 " FontSize="13"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!-- 模拟器控制面板 --> |
| | | <Border Grid.Row="0" Background="#FFF3E0" CornerRadius="4" Padding="12,8" Margin="4"> |
| | | <Grid> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="Auto"/> |
| | | <ColumnDefinition Width="*"/> |
| | | </Grid.ColumnDefinitions> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <TextBlock Grid.Row="0" Grid.Column="0" Text="总线状态:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,4"/> |
| | | <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center"> |
| | | <Ellipse x:Name="canBusIndicator" Width="14" Height="14" Fill="#9E9E9E" Margin="0,0,8,0"/> |
| | | <TextBlock x:Name="canBusStatusText" Text="未启动" FontSize="13"/> |
| | | </StackPanel> |
| | | |
| | | <TextBlock Grid.Row="1" Grid.Column="0" Text="总线名称:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,4"/> |
| | | <TextBox Grid.Row="1" Grid.Column="1" x:Name="canBusNameInput" Text="CAN_BUS_1" |
| | | VerticalContentAlignment="Center" Padding="6,4" Margin="0,0,80,4" Width="200" HorizontalAlignment="Left"/> |
| | | |
| | | <TextBlock Grid.Row="2" Grid.Column="0" Text="控制:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,8,0"/> |
| | | <StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal"> |
| | | <Button x:Name="canStartBtn" Content="启动总线" Width="100" Height="30" Margin="0,0,8,0" |
| | | Click="CanStartBtn_Click" Background="#4CAF50" Foreground="White" |
| | | BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/> |
| | | <Button x:Name="canStopBtn" Content="停止总线" Width="100" Height="30" Margin="0,0,8,0" |
| | | Click="CanStopBtn_Click" Background="#F44336" Foreground="White" |
| | | BorderThickness="0" Cursor="Hand" FontWeight="SemiBold" IsEnabled="False"/> |
| | | <Button x:Name="canResetBtn" Content="重置所有" Width="100" Height="30" |
| | | Click="CanResetBtn_Click" Background="#9E9E9E" Foreground="White" |
| | | BorderThickness="0" Cursor="Hand" FontWeight="SemiBold"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- CAN 调试面板 --> |
| | | <Border x:Name="canSimulatorHost" Grid.Row="1" Background="White" Margin="4"/> |
| | | </Grid> |
| | | </TabItem> |
| | | </TabControl> |
| | | |
| | | <!-- 底部结果栏 --> |
| | | <Border Grid.Row="2" Padding="12,6" Background="#FAFAFA" |
| | | BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"> |
| | | <DockPanel> |
| | | <TextBlock x:Name="txtSummary" FontSize="13" FontWeight="SemiBold" |
| | | VerticalAlignment="Center" Foreground="#333" Text="就绪 - 请切换到对应 Tab 进行手动测试"/> |
| | | <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right"> |
| | | <TextBlock x:Name="txtPassed" FontSize="13" Foreground="#4CAF50" |
| | | Margin="0,0,16,0" VerticalAlignment="Center"/> |
| | | <TextBlock x:Name="txtFailed" FontSize="13" Foreground="#F44336" |
| | | VerticalAlignment="Center"/> |
| | | </StackPanel> |
| | | </DockPanel> |
| | | </Border> |
| | | </Grid> |
| | | </Window> |
| New file |
| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.IO; |
| | | using System.Linq; |
| | | using System.Text; |
| | | using System.Threading; |
| | | using System.Threading.Tasks; |
| | | using System.Windows; |
| | | using System.Windows.Media; |
| | | using System.Windows.Threading; |
| | | using Aging.Infrastructure.Drivers; |
| | | using CanSimulator; |
| | | using log4net; |
| | | using Plugin.DeviceConfig.Entity; |
| | | using Plugin.DeviceConfig.Model; |
| | | using Plugin.DeviceDebug.Entity; |
| | | using Plugin.DeviceDebug.Model; |
| | | using Plugin.DeviceDebug.Panels; |
| | | |
| | | namespace Plugin.IntegrationTest |
| | | { |
| | | public partial class MainWindow : Window |
| | | { |
| | | private int _passed; |
| | | private int _failed; |
| | | private int _totalSteps; |
| | | private int _currentStep; |
| | | private string _testDir = string.Empty; |
| | | private string _logFilePath = string.Empty; |
| | | private readonly StringBuilder _logBuffer = new(); |
| | | |
| | | // 嵌入的插件实例 |
| | | private Plugin.DeviceConfig.DeviceConfig? _deviceConfigPlugin; |
| | | private Plugin.DeviceDebug.DeviceDebug? _deviceDebugPlugin; |
| | | private static readonly ILog _testLog = LogManager.GetLogger("IntegrationTest"); |
| | | |
| | | // CAN 模拟器 |
| | | private CanBusSimulator? _canBus; |
| | | private CanDebugPanel? _canDebugPanel; |
| | | |
| | | public MainWindow() |
| | | { |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | private void Window_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | InitializePlugins(); |
| | | InitializeCanSimulator(); |
| | | } |
| | | |
| | | private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) |
| | | { |
| | | // 清理 CAN 模拟器 |
| | | _canBus?.Dispose(); |
| | | CanDriverAdapter.ResetAllBuses(); |
| | | |
| | | // 不删除测试目录,保留数据以便重启后恢复 |
| | | // (如需手动重置,使用工具栏中的 "重置数据" 按钮) |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化两个插件并嵌入到对应 Tab 中 |
| | | /// </summary> |
| | | private void InitializePlugins() |
| | | { |
| | | // 使用固定目录保存数据(重启后不丢失) |
| | | string appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); |
| | | _testDir = Path.Combine(appData, "PluginIntegrationTest"); |
| | | Directory.CreateDirectory(_testDir); |
| | | _logFilePath = Path.Combine(_testDir, "test_result.log"); |
| | | |
| | | txtStatus.Text = $"数据目录: {_testDir}"; |
| | | |
| | | try |
| | | { |
| | | // 初始化仪器仪表注册插件 |
| | | _deviceConfigPlugin = new Plugin.DeviceConfig.DeviceConfig(); |
| | | _deviceConfigPlugin.SetBaseFolder(_testDir); |
| | | _deviceConfigPlugin.SetLogger(_testLog); |
| | | _deviceConfigPlugin.InitFinish(); |
| | | deviceConfigHost.Child = _deviceConfigPlugin; |
| | | |
| | | // 初始化硬件调试插件 |
| | | _deviceDebugPlugin = new Plugin.DeviceDebug.DeviceDebug(); |
| | | _deviceDebugPlugin.SetBaseFolder(_testDir); |
| | | _deviceDebugPlugin.SetLogger(_testLog); |
| | | _deviceDebugPlugin.InitFinish(); |
| | | deviceDebugHost.Child = _deviceDebugPlugin; |
| | | |
| | | // 预置 CAN 测试设备(方便直接验证驱动与面板) |
| | | SeedPresetCanDevices(); |
| | | |
| | | // 刷新插件内部列表(需手动调用插件方法) |
| | | _deviceConfigPlugin.ForceRefreshForTest(); |
| | | _deviceDebugPlugin.ForceRefreshForTest(); |
| | | |
| | | txtStatus.Text = "插件初始化完成 - 可切换 Tab 手动测试"; |
| | | Log("[INFO] 插件初始化成功"); |
| | | Log($"[INFO] 测试目录: {_testDir}"); |
| | | Log("[INFO] DeviceConfig 已嵌入到 '仪器仪表注册' Tab"); |
| | | Log("[INFO] DeviceDebug 已嵌入到 '硬件调试' Tab"); |
| | | Log("[INFO] 预置测试设备: CAN-001, CAN-002 (Transport=Can)"); |
| | | Log(""); |
| | | Log("[TIP] 使用流程:"); |
| | | Log(" 1. 先切换到 '仪器仪表注册' Tab,添加设备、通道、Slot映射等"); |
| | | Log(" 2. 已预置 CAN-001 (CAN_BUS_1:0) 和 CAN-002 (CAN_BUS_1:1) 供测试"); |
| | | Log(" 3. 切换到 '硬件调试' Tab,选择 CAN-001 会自动打开 CAN 面板并连接总线"); |
| | | Log(" 4. 切换到 'CAN 模拟器' Tab 可手动控制总线或另一个节点发送数据"); |
| | | Log(" 5. 可随时点击 '运行自动化测试' 验证核心逻辑"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | txtStatus.Text = $"插件初始化失败: {ex.Message}"; |
| | | Log($"[ERROR] 插件初始化失败: {ex.Message}"); |
| | | Log(ex.StackTrace ?? ""); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 预置 CAN 测试设备(仅当不存在时插入,避免覆盖用户修改) |
| | | /// </summary> |
| | | private void SeedPresetCanDevices() |
| | | { |
| | | try |
| | | { |
| | | var model = new DeviceConfigModel(); |
| | | string dbPath = Path.Combine(_testDir, "data.db"); |
| | | model.InitDatabase(dbPath); |
| | | |
| | | int inserted = 0; |
| | | |
| | | // CAN-001 |
| | | if (model.GetDeviceById("CAN-001") == null) |
| | | { |
| | | model.SaveDevice(new DeviceDefinition |
| | | { |
| | | DeviceId = "CAN-001", |
| | | Kind = DeviceKind.CanDevice, |
| | | DriverType = "CAN", |
| | | Transport = TransportType.Can, |
| | | Endpoint = "CAN_BUS_1:0", |
| | | EffectiveVersion = "V1.0" |
| | | }); |
| | | inserted++; |
| | | } |
| | | |
| | | // CAN-002 |
| | | if (model.GetDeviceById("CAN-002") == null) |
| | | { |
| | | model.SaveDevice(new DeviceDefinition |
| | | { |
| | | DeviceId = "CAN-002", |
| | | Kind = DeviceKind.CanDevice, |
| | | DriverType = "CAN", |
| | | Transport = TransportType.Can, |
| | | Endpoint = "CAN_BUS_1:1", |
| | | EffectiveVersion = "V1.0" |
| | | }); |
| | | inserted++; |
| | | } |
| | | |
| | | Log(inserted > 0 |
| | | ? $"[INFO] 预置 CAN 设备: 新增 {inserted} 个" |
| | | : "[INFO] 预置 CAN 设备已存在,未覆盖"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log($"[WARN] 预置 CAN 设备失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 重置测试数据(删除本地目录中的 db 文件) |
| | | /// </summary> |
| | | private void ResetTestData() |
| | | { |
| | | try |
| | | { |
| | | if (MessageBox.Show("确定要删除本地保存的设备数据吗?\n此操作不可恢复!", |
| | | "确认重置", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) |
| | | { |
| | | return; |
| | | } |
| | | |
| | | // 释放插件后删除数据 |
| | | _deviceConfigPlugin = null; |
| | | _deviceDebugPlugin = null; |
| | | |
| | | foreach (var file in Directory.GetFiles(_testDir, "*.db", SearchOption.AllDirectories)) |
| | | { |
| | | try { File.Delete(file); } catch { } |
| | | } |
| | | |
| | | GC.Collect(); |
| | | GC.WaitForPendingFinalizers(); |
| | | |
| | | MessageBox.Show("数据已重置,请重新启动程序。", "提示", MessageBoxButton.OK, MessageBoxImage.Information); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | MessageBox.Show($"重置失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | private void BtnClear_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | txtLog.Clear(); |
| | | txtSummary.Text = "就绪 - 请切换到对应 Tab 进行手动测试"; |
| | | txtPassed.Text = ""; |
| | | txtFailed.Text = ""; |
| | | progressBar.Value = 0; |
| | | _passed = 0; |
| | | _failed = 0; |
| | | _currentStep = 0; |
| | | btnRun.IsEnabled = true; |
| | | } |
| | | |
| | | private void BtnReset_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | ResetTestData(); |
| | | } |
| | | |
| | | private async void BtnRun_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | btnRun.IsEnabled = false; |
| | | txtLog.Clear(); |
| | | _passed = 0; |
| | | _failed = 0; |
| | | _currentStep = 0; |
| | | |
| | | _totalSteps = CountTestSteps(); |
| | | UpdateProgress(); |
| | | |
| | | Log("========================================"); |
| | | Log(" 仪器仪表注册 & 硬件调试 集成测试"); |
| | | Log($" 时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); |
| | | Log("========================================"); |
| | | Log(""); |
| | | |
| | | // 为自动化测试创建独立的数据库目录 |
| | | string autoTestDir = Path.Combine(_testDir, "auto_test_" + Guid.NewGuid().ToString("N").Substring(0, 8)); |
| | | Directory.CreateDirectory(autoTestDir); |
| | | |
| | | await Task.Run(() => RunAllTests(autoTestDir)); |
| | | |
| | | Log(""); |
| | | Log("========================================"); |
| | | string result = _failed == 0 ? "全部通过 ✓" : $"有 {_failed} 项失败 ✗"; |
| | | Log($" 测试结果: {result}"); |
| | | Log($" 通过: {_passed} 失败: {_failed} 总计: {_passed + _failed}"); |
| | | Log("========================================"); |
| | | |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | txtSummary.Text = _failed == 0 ? "✓ 自动化测试全部通过" : $"✗ 有 {_failed} 项测试失败"; |
| | | txtSummary.Foreground = _failed == 0 |
| | | ? System.Windows.Media.Brushes.Green |
| | | : System.Windows.Media.Brushes.Red; |
| | | txtPassed.Text = $"通过: {_passed}"; |
| | | txtFailed.Text = $"失败: {_failed}"; |
| | | btnRun.IsEnabled = true; |
| | | |
| | | // 复制日志到输出目录 |
| | | try |
| | | { |
| | | string appDir = AppDomain.CurrentDomain.BaseDirectory; |
| | | string copyPath = Path.Combine(appDir, "test_result.log"); |
| | | File.Copy(_logFilePath, copyPath, overwrite: true); |
| | | } |
| | | catch { } |
| | | }); |
| | | } |
| | | |
| | | private int CountTestSteps() => 22; |
| | | |
| | | private void RunAllTests(string testDir) |
| | | { |
| | | string configDbPath = Path.Combine(testDir, "device_config.db"); |
| | | string debugDbPath = Path.Combine(testDir, "device_debug.db"); |
| | | |
| | | // ===== 第一部分:仪器仪表注册 ===== |
| | | RunSection("第一部分:仪器仪表注册 (DeviceConfig)"); |
| | | |
| | | var configModel = TestInit("初始化设备配置数据库", () => |
| | | { |
| | | var model = new DeviceConfigModel(); |
| | | model.InitDatabase(configDbPath); |
| | | return model; |
| | | }); |
| | | |
| | | if (configModel == null) |
| | | { |
| | | SkipRemainingSection(9, "DeviceConfig 数据库初始化失败"); |
| | | } |
| | | else |
| | | { |
| | | var device = TestInit("创建设备定义(电源设备)", () => |
| | | { |
| | | var dev = new DeviceDefinition |
| | | { |
| | | DeviceId = "PS-001", |
| | | Kind = DeviceKind.PowerSupply, |
| | | DriverType = "SCPI", |
| | | Transport = TransportType.Tcp, |
| | | Endpoint = "192.168.1.100:5025", |
| | | EffectiveVersion = "V1.0", |
| | | CreatedAt = DateTime.Now, |
| | | UpdatedAt = DateTime.Now |
| | | }; |
| | | configModel.SaveDevice(dev); |
| | | return dev; |
| | | }); |
| | | |
| | | TestStep("读取设备定义并验证", () => |
| | | { |
| | | var read = configModel.GetDeviceById("PS-001"); |
| | | Assert(read != null, "设备应存在"); |
| | | Assert(read!.Kind == DeviceKind.PowerSupply, "种类应为PowerSupply"); |
| | | Assert(read.Transport == TransportType.Tcp, "传输方式应为Tcp"); |
| | | Assert(read.Endpoint == "192.168.1.100:5025", "端点地址应匹配"); |
| | | }); |
| | | |
| | | TestStep("更新设备定义", () => |
| | | { |
| | | Assert(device != null, "设备对象不应为空"); |
| | | device!.Endpoint = "192.168.1.200:5025"; |
| | | device.EffectiveVersion = "V2.0"; |
| | | configModel.SaveDevice(device); |
| | | var updated = configModel.GetDeviceById("PS-001"); |
| | | Assert(updated!.Endpoint == "192.168.1.200:5025", "端点应已更新"); |
| | | Assert(updated.EffectiveVersion == "V2.0", "版本应已更新"); |
| | | }); |
| | | |
| | | TestStep("添加设备通道", () => |
| | | { |
| | | for (int i = 1; i <= 4; i++) |
| | | { |
| | | var ch = new DeviceChannel |
| | | { |
| | | DeviceId = "PS-001", |
| | | ChannelId = i, |
| | | Capability = i <= 2 ? "Voltage" : "Current", |
| | | MaxVoltage = 30.0, |
| | | MaxCurrent = 5.0, |
| | | Limits = $"{{\"min_v\":0,\"max_v\":30,\"min_i\":0,\"max_i\":5}}", |
| | | SessionKey = "" |
| | | }; |
| | | configModel.SaveChannel(ch); |
| | | } |
| | | var channels = configModel.GetChannelsByDevice("PS-001"); |
| | | Assert(channels.Count == 4, $"应有4个通道,实际{channels.Count}个"); |
| | | }); |
| | | |
| | | TestStep("添加Slot映射", () => |
| | | { |
| | | var mapping = new SlotDeviceMapping |
| | | { |
| | | SlotId = "SLOT-01", |
| | | Capability = "Power", |
| | | Version = "V2.0", |
| | | DeviceId = "PS-001", |
| | | ChannelId = 1, |
| | | MappingVersion = "MV-001" |
| | | }; |
| | | configModel.SaveSlotMapping(mapping); |
| | | var mappings = configModel.GetSlotMappings(); |
| | | Assert(mappings.Count == 1, "应有1条Slot映射"); |
| | | Assert(mappings[0].DeviceId == "PS-001", "映射设备ID应匹配"); |
| | | }); |
| | | |
| | | TestStep("创建配置版本", () => |
| | | { |
| | | var ver = new DeviceConfigVersion |
| | | { |
| | | VersionId = "VER-001", |
| | | Status = VersionStatus.Draft, |
| | | Hash = configModel.ComputeConfigHash(), |
| | | Author = "TestUser", |
| | | Timestamp = DateTime.Now, |
| | | DiffDescription = "初始配置" |
| | | }; |
| | | configModel.SaveConfigVersion(ver); |
| | | var versions = configModel.GetConfigVersions(); |
| | | Assert(versions.Count == 1, "应有1个版本"); |
| | | Assert(versions[0].Status == VersionStatus.Draft, "版本状态应为Draft"); |
| | | Assert(!string.IsNullOrEmpty(versions[0].Hash), "哈希不应为空"); |
| | | }); |
| | | |
| | | TestStep("冲突检测(应无冲突)", () => |
| | | { |
| | | var conflicts = configModel.DetectConflicts(); |
| | | Assert(conflicts.Count == 0, $"不应有冲突,实际: {string.Join("; ", conflicts)}"); |
| | | }); |
| | | |
| | | TestStep("添加第二个设备并检测IP冲突", () => |
| | | { |
| | | var dev2 = new DeviceDefinition |
| | | { |
| | | DeviceId = "PS-002", |
| | | Kind = DeviceKind.PowerSupply, |
| | | DriverType = "SCPI", |
| | | Transport = TransportType.Tcp, |
| | | Endpoint = "192.168.1.200:5025", |
| | | EffectiveVersion = "V1.0", |
| | | CreatedAt = DateTime.Now, |
| | | UpdatedAt = DateTime.Now |
| | | }; |
| | | configModel.SaveDevice(dev2); |
| | | |
| | | var dev3 = new DeviceDefinition |
| | | { |
| | | DeviceId = "PS-003", |
| | | Kind = DeviceKind.PowerSupply, |
| | | DriverType = "ModbusTCP", |
| | | Transport = TransportType.Tcp, |
| | | Endpoint = "192.168.1.200:5025", |
| | | EffectiveVersion = "V1.0", |
| | | CreatedAt = DateTime.Now, |
| | | UpdatedAt = DateTime.Now |
| | | }; |
| | | configModel.SaveDevice(dev3); |
| | | |
| | | var conflicts = configModel.DetectConflicts(); |
| | | Assert(conflicts.Count > 0, "应检测到IP冲突"); |
| | | Log($" [冲突] {conflicts[0]}"); |
| | | }); |
| | | |
| | | TestStep("删除设备和清理", () => |
| | | { |
| | | configModel.DeleteDevice("PS-003"); |
| | | configModel.DeleteChannel("PS-001", 4); |
| | | var allDevices = configModel.GetAllDevices(); |
| | | Assert(allDevices.Count == 2, $"删除后应有2个设备,实际{allDevices.Count}"); |
| | | var channels = configModel.GetChannelsByDevice("PS-001"); |
| | | Assert(channels.Count == 3, $"删除通道后应有3个,实际{channels.Count}"); |
| | | }); |
| | | } |
| | | |
| | | Log(""); |
| | | |
| | | // ===== 第二部分:硬件调试 ===== |
| | | RunSection("第二部分:硬件调试 (DeviceDebug)"); |
| | | |
| | | var debugModel = TestInit("初始化调试数据库", () => |
| | | { |
| | | var model = new DeviceDebugModel(); |
| | | model.InitDatabase(debugDbPath); |
| | | return model; |
| | | }); |
| | | |
| | | DebugSessionManager? sessionMgr = null; |
| | | if (debugModel == null) |
| | | { |
| | | SkipRemainingSection(8, "DeviceDebug 数据库初始化失败"); |
| | | } |
| | | else |
| | | { |
| | | sessionMgr = TestInit("创建会话管理器", () => |
| | | { |
| | | return new DebugSessionManager(debugModel, resourceManager: null); |
| | | }); |
| | | |
| | | if (sessionMgr == null) |
| | | { |
| | | SkipRemainingSection(7, "会话管理器创建失败"); |
| | | } |
| | | else |
| | | { |
| | | string? sessionId = null; |
| | | TestStep("打开调试会话(ReadOnly)", () => |
| | | { |
| | | var session = sessionMgr.OpenSession("PS-001", "OP001", "张工", "V2.0"); |
| | | sessionId = session.SessionId; |
| | | Assert(!string.IsNullOrEmpty(sessionId), "会话ID不应为空"); |
| | | Assert(session.State == DebugSessionState.ReadOnly, "初始状态应为ReadOnly"); |
| | | Assert(session.DeviceId == "PS-001", "设备ID应匹配"); |
| | | Log($" 会话ID: {sessionId}"); |
| | | }); |
| | | |
| | | if (sessionId == null) |
| | | { |
| | | SkipRemainingSection(6, "打开会话失败"); |
| | | } |
| | | else |
| | | { |
| | | TestStep("请求维护模式(ReadOnly → AcquiringMaintenance)", () => |
| | | { |
| | | bool result = sessionMgr.RequestMaintenance(sessionId!); |
| | | Assert(result, "请求维护应成功"); |
| | | var state = sessionMgr.GetSessionState(sessionId!); |
| | | Assert(state == DebugSessionState.AcquiringMaintenance, $"状态应为AcquiringMaintenance,实际{state}"); |
| | | }); |
| | | |
| | | TestStep("激活维护模式(AcquiringMaintenance → MaintenanceActive)", () => |
| | | { |
| | | sessionMgr.ActivateMaintenance(sessionId!); |
| | | var state = sessionMgr.GetSessionState(sessionId!); |
| | | Assert(state == DebugSessionState.MaintenanceActive, $"状态应为MaintenanceActive,实际{state}"); |
| | | }); |
| | | |
| | | TestStep("验证命令执行权限", () => |
| | | { |
| | | var cmd = new PowerSetCommand { Voltage = 12.5, CurrentLimit = 3.0, Output = true }; |
| | | bool allowed = sessionMgr.ValidateCommand(sessionId!, cmd); |
| | | Assert(allowed, "MaintenanceActive状态下应允许命令执行"); |
| | | |
| | | var readOnlySession = sessionMgr.OpenSession("PS-002", "OP002", "李工", "V1.0"); |
| | | bool readOnlyAllowed = sessionMgr.ValidateCommand(readOnlySession.SessionId, cmd); |
| | | Assert(!readOnlyAllowed, "ReadOnly状态下应拒绝命令执行"); |
| | | }); |
| | | |
| | | TestStep("记录调试操作审计", () => |
| | | { |
| | | var op = new DebugOperation |
| | | { |
| | | SessionId = sessionId!, |
| | | CommandType = "PowerSet", |
| | | Parameters = "{\"voltage\":12.5,\"current\":3.0,\"output\":true}", |
| | | Outcome = "Success", |
| | | DeviceReadback = "V=12.48,I=2.99", |
| | | ErrorCode = "", |
| | | ErrorMessage = "" |
| | | }; |
| | | debugModel.RecordOperation(op); |
| | | |
| | | var ops = debugModel.GetOperationsBySession(sessionId!); |
| | | Assert(ops.Count == 1, $"应有1条操作记录,实际{ops.Count}"); |
| | | Assert(ops[0].CommandType == "PowerSet", "命令类型应匹配"); |
| | | Assert(ops[0].Outcome == "Success", "结果应为Success"); |
| | | }); |
| | | |
| | | TestStep("关闭会话(MaintenanceActive → Closing → Closed)", () => |
| | | { |
| | | sessionMgr.CloseSession(sessionId!); |
| | | var state = sessionMgr.GetSessionState(sessionId!); |
| | | Assert(state == DebugSessionState.Closed, $"状态应为Closed,实际{state}"); |
| | | |
| | | var session = debugModel.GetSession(sessionId!); |
| | | Assert(session != null, "会话记录应仍存在"); |
| | | Assert(session!.ClosedAt.HasValue, "关闭时间应已设置"); |
| | | }); |
| | | |
| | | TestStep("故障和恢复测试", () => |
| | | { |
| | | var faultSession = sessionMgr.OpenSession("PS-001", "OP001", "张工", "V2.0"); |
| | | string faultId = faultSession.SessionId; |
| | | sessionMgr.RequestMaintenance(faultId); |
| | | sessionMgr.ActivateMaintenance(faultId); |
| | | |
| | | sessionMgr.FaultSession(faultId, "通信超时"); |
| | | var state = sessionMgr.GetSessionState(faultId); |
| | | Assert(state == DebugSessionState.Faulted, $"状态应为Faulted,实际{state}"); |
| | | |
| | | sessionMgr.MarkRecoveryRequired(faultId); |
| | | state = sessionMgr.GetSessionState(faultId); |
| | | Assert(state == DebugSessionState.RecoveryRequired, $"状态应为RecoveryRequired,实际{state}"); |
| | | }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | Log(""); |
| | | |
| | | // ===== 第三部分:跨模块联动验证 ===== |
| | | RunSection("第三部分:跨模块联动验证"); |
| | | |
| | | if (configModel == null || debugModel == null) |
| | | { |
| | | SkipRemainingSection(4, "前置模块初始化失败,跳过联动测试"); |
| | | } |
| | | else |
| | | { |
| | | TestStep("设备配置 → 调试会话数据一致性", () => |
| | | { |
| | | var configDevice = configModel.GetDeviceById("PS-001"); |
| | | Assert(configDevice != null, "配置中应存在PS-001"); |
| | | |
| | | var activeSessions = debugModel.GetActiveSessionsByDevice("PS-001"); |
| | | Assert(activeSessions.Count > 0, "PS-001应有活跃会话记录"); |
| | | Assert(activeSessions.All(s => s.DeviceId == "PS-001"), "所有会话应引用PS-001"); |
| | | }); |
| | | |
| | | TestStep("配置版本哈希一致性", () => |
| | | { |
| | | var currentHash = configModel.ComputeConfigHash(); |
| | | var versions = configModel.GetConfigVersions(); |
| | | Assert(versions.Count > 0, "应有版本记录"); |
| | | |
| | | string originalHash = versions[0].Hash; |
| | | Log($" 原始哈希: {originalHash.Substring(0, 16)}..."); |
| | | Log($" 当前哈希: {currentHash.Substring(0, 16)}..."); |
| | | Log($" 哈希变化: {(originalHash != currentHash ? "是(配置已更新)" : "否")}"); |
| | | Assert(true, "哈希计算流程正常"); |
| | | }); |
| | | |
| | | TestStep("操作审计完整性验证", () => |
| | | { |
| | | var allSessions = debugModel.GetAllSessions(); |
| | | int totalOps = 0; |
| | | foreach (var s in allSessions) |
| | | { |
| | | var ops = debugModel.GetOperationsBySession(s.SessionId); |
| | | totalOps += ops.Count; |
| | | } |
| | | Assert(totalOps > 0, $"应有操作审计记录,实际{totalOps}条"); |
| | | Log($" 总会话数: {allSessions.Count}, 总操作记录: {totalOps}"); |
| | | }); |
| | | |
| | | TestStep("设备组模板功能验证", () => |
| | | { |
| | | var template = new DeviceGroupTemplate |
| | | { |
| | | TemplateId = "TPL-001", |
| | | DeviceType = "PowerSupply", |
| | | DriverType = "SCPI", |
| | | Transport = "Tcp", |
| | | DeviceCount = 4, |
| | | ChannelsPerDevice = 4, |
| | | AddressRule = "192.168.1.{0}", |
| | | SlotStart = 1 |
| | | }; |
| | | configModel.SaveGroupTemplate(template); |
| | | var templates = configModel.GetGroupTemplates(); |
| | | Assert(templates.Count == 1, "应有1个模板"); |
| | | Assert(templates[0].DeviceCount == 4, "设备数量应为4"); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | #region CAN 模拟器 |
| | | |
| | | /// <summary> |
| | | /// 初始化 CAN 模拟器 |
| | | /// </summary> |
| | | private void InitializeCanSimulator() |
| | | { |
| | | try |
| | | { |
| | | // 创建 CAN 调试面板并嵌入到 Tab 中 |
| | | _canDebugPanel = new CanDebugPanel(); |
| | | canSimulatorHost.Child = _canDebugPanel; |
| | | |
| | | Log("[INFO] CAN 模拟器已嵌入到 'CAN 模拟器' Tab"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log($"[ERROR] CAN 模拟器初始化失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 启动总线按钮点击 |
| | | /// </summary> |
| | | private void CanStartBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | string busName = canBusNameInput.Text.Trim(); |
| | | if (string.IsNullOrEmpty(busName)) |
| | | { |
| | | MessageBox.Show("请输入总线名称", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); |
| | | return; |
| | | } |
| | | |
| | | // 通过 DriverFactory 创建 CAN 驱动来启动总线 |
| | | var driver = DriverFactory.CreateDriver("CAN_SIM_0", "CAN", $"{busName}:0"); |
| | | driver.ConnectAsync(CancellationToken.None).GetAwaiter().GetResult(); |
| | | |
| | | // 获取总线实例 |
| | | var buses = CanDriverAdapter.GetAllBuses(); |
| | | if (buses.TryGetValue(busName, out var bus)) |
| | | { |
| | | _canBus = bus; |
| | | bus.Start(); |
| | | |
| | | // 更新 UI |
| | | canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50")); |
| | | canBusStatusText.Text = $"运行中 ({busName})"; |
| | | canStartBtn.IsEnabled = false; |
| | | canStopBtn.IsEnabled = true; |
| | | |
| | | Log($"[CAN] 总线 {busName} 已启动"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log($"[CAN] 启动总线失败: {ex.Message}"); |
| | | MessageBox.Show($"启动失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 停止总线按钮点击 |
| | | /// </summary> |
| | | private void CanStopBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | _canBus?.Stop(); |
| | | |
| | | // 更新 UI |
| | | canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | canBusStatusText.Text = "已停止"; |
| | | canStartBtn.IsEnabled = true; |
| | | canStopBtn.IsEnabled = false; |
| | | |
| | | Log("[CAN] 总线已停止"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log($"[CAN] 停止总线失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 重置所有按钮点击 |
| | | /// </summary> |
| | | private void CanResetBtn_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | CanDriverAdapter.ResetAllBuses(); |
| | | _canBus = null; |
| | | |
| | | // 更新 UI |
| | | canBusIndicator.Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9E9E9E")); |
| | | canBusStatusText.Text = "未启动"; |
| | | canStartBtn.IsEnabled = true; |
| | | canStopBtn.IsEnabled = false; |
| | | |
| | | Log("[CAN] 所有总线已重置"); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Log($"[CAN] 重置失败: {ex.Message}"); |
| | | } |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region 测试辅助方法 |
| | | |
| | | private T? TestInit<T>(string name, Func<T> action) where T : class |
| | | { |
| | | _currentStep++; |
| | | UpdateProgress(); |
| | | try |
| | | { |
| | | var result = action(); |
| | | _passed++; |
| | | LogStep("PASS", name); |
| | | return result; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _failed++; |
| | | LogStep("FAIL", name, ex.Message); |
| | | return default; |
| | | } |
| | | } |
| | | |
| | | private void TestStep(string name, Action action) |
| | | { |
| | | _currentStep++; |
| | | UpdateProgress(); |
| | | try |
| | | { |
| | | action(); |
| | | _passed++; |
| | | LogStep("PASS", name); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | _failed++; |
| | | LogStep("FAIL", name, ex.Message); |
| | | } |
| | | } |
| | | |
| | | private void Assert(bool condition, string message) |
| | | { |
| | | if (!condition) |
| | | throw new Exception($"断言失败: {message}"); |
| | | } |
| | | |
| | | private void RunSection(string title) |
| | | { |
| | | Log($"--- {title} ---"); |
| | | Log(""); |
| | | } |
| | | |
| | | private void SkipRemainingSection(int count, string reason) |
| | | { |
| | | for (int i = 0; i < count; i++) |
| | | { |
| | | _currentStep++; |
| | | _failed++; |
| | | UpdateProgress(); |
| | | LogStep("FAIL", $"[SKIP] 依赖失败: {reason}"); |
| | | } |
| | | } |
| | | |
| | | private void LogStep(string status, string name, string? error = null) |
| | | { |
| | | string line = status == "PASS" |
| | | ? $" [PASS] {name}" |
| | | : $" [FAIL] {name} → {error}"; |
| | | Log(line); |
| | | } |
| | | |
| | | private void Log(string message) |
| | | { |
| | | _logBuffer.AppendLine(message); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | txtLog.AppendText(message + Environment.NewLine); |
| | | txtLog.ScrollToEnd(); |
| | | }); |
| | | if (!string.IsNullOrEmpty(_logFilePath)) |
| | | { |
| | | try { File.AppendAllText(_logFilePath, message + Environment.NewLine); } |
| | | catch { } |
| | | } |
| | | } |
| | | |
| | | private void UpdateProgress() |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | if (_totalSteps > 0) |
| | | { |
| | | double pct = (_currentStep * 100.0) / _totalSteps; |
| | | progressBar.Value = Math.Min(pct, 100); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | } |
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>WinExe</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | <AutoGenerateBindingRedirects>True</AutoGenerateBindingRedirects> |
| | | <PlatformTarget>x64</PlatformTarget> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\Plugin.DeviceConfig\Plugin.DeviceConfig.csproj" /> |
| | | <ProjectReference Include="..\Plugin.DeviceDebug\Plugin.DeviceDebug.csproj" /> |
| | | <ProjectReference Include="..\CanSimulator\CanSimulator.csproj" /> |
| | | <ProjectReference Include="..\Aging.Infrastructure\Aging.Infrastructure.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <Content Include="..\Libs\SQLite.Interop.dll" Link="SQLite.Interop.dll"> |
| | | <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |
| | | </Content> |
| | | <Content Include="..\Libs\SQLite.Interop.dll" Link="x64\SQLite.Interop.dll"> |
| | | <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> |
| | | </Content> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| New file |
| | |
| | | using HMI.Core.Annotation; |
| | | using System.Windows; |
| | | |
| | | [assembly:ThemeInfo( |
| | | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located |
| | | //(used if a resource is not found in the page, |
| | | // or application resource dictionaries) |
| | | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located |
| | | //(used if a resource is not found in the page, |
| | | // app, or any theme specific resource dictionaries) |
| | | )] |
| | | [assembly: PluginClassName("Plugin.StandardTest.StandardTestUI")] |
| New file |
| | |
| | | namespace Plugin.StandardTest.Entity |
| | | { |
| | | public class TestStep |
| | | { |
| | | public string Name { get; set; } |
| | | public string Step { get; set; } |
| | | public string Value { get; set; } |
| | | public string LimitUp { get; set; } |
| | | public string LimitDown { get; set; } |
| | | public string Unit { get; set; } |
| | | public string ConsumeTime { get; set; } |
| | | public string Result { get; set; } |
| | | } |
| | | } |
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>Library</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | <Platforms>AnyCPU;x64</Platforms> |
| | | <AssemblyVersion>1.0.0.0</AssemblyVersion> |
| | | <FileVersion>1.0.0.0</FileVersion> |
| | | <IncludeSymbols>False</IncludeSymbols> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> |
| | | <PackageReference Include="LiveChartsCore" Version="2.0.0-rc5.4" /> |
| | | <PackageReference Include="LiveChartsCore.SkiaSharpView" Version="2.0.0-rc5.4" /> |
| | | <PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc5.4" /> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" /> |
| | | <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> |
| | | <PackageReference Include="Panuon.WPF.UI" Version="1.3.0.2" /> |
| | | <PackageReference Include="SuperSocket" Version="1.6.6.1" /> |
| | | <PackageReference Include="SuperSocket.Engine" Version="1.6.6.1" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" /> |
| | | <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" /> |
| | | <ProjectReference Include="..\HMI.StdControl\HMI.StdControl.csproj" /> |
| | | <ProjectReference Include="..\PLCHelper\PLCHelper.csproj" /> |
| | | <ProjectReference Include="..\Plugin.TsSupport\Plugin.TsSupport.csproj" /> |
| | | <ProjectReference Include="..\SimpleUtil\HMI.LogUtil\HMI.LogUtil.csproj" /> |
| | | <ProjectReference Include="..\Util.ManualDebug\Util.ManualDebug.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <Reference Include="HslCommunication"> |
| | | <HintPath>..\Libs\HslCommunication.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="NationalInstruments.TestStand.Interop.API"> |
| | | <HintPath>..\Libs\NationalInstruments.TestStand.Interop.API.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="NationalInstruments.TestStand.Interop.UI.AxControls"> |
| | | <HintPath>..\Libs\NationalInstruments.TestStand.Interop.UI.AxControls.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="NPOI"> |
| | | <HintPath>..\HMI.ParameterManage\bin\x64\Debug\NPOI.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="System.Net.Http" /> |
| | | <Reference Include="System.Windows.Forms" /> |
| | | <Reference Include="WindowsFormsIntegration" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <Compile Update="StandardTestUI.xaml.cs"> |
| | | <SubType>Code</SubType> |
| | | </Compile> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| New file |
| | |
| | | <pu:WindowX x:Class="Plugin.StandardTest.Scanner" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | WindowStartupLocation="CenterScreen" |
| | | Closing="Window_Closing" |
| | | FontSize="20" |
| | | mc:Ignorable="d" |
| | | Style="{StaticResource ModalWindow}" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:cc="clr-namespace:HMI.Core;assembly=HMI.Core" |
| | | InteropOwnersMask="False" |
| | | SizeToContent="Height" |
| | | pu:WindowXModalDialog.Buttons="{x:Null}" |
| | | Title="{Binding title}" Width="600"> |
| | | <Window.Resources> |
| | | <Style TargetType="Label"> |
| | | <Setter Property="HorizontalAlignment" Value="Right"/> |
| | | <Setter Property="FontSize" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=Window}, |
| | | Converter={StaticResource fc}, ConverterParameter='80,1920'}" /> |
| | | </Style> |
| | | <Style TargetType="TextBox"> |
| | | <Setter Property="FontSize" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=Window}, |
| | | Converter={StaticResource fc}, ConverterParameter='100,1920'}" /> |
| | | </Style> |
| | | <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}"> |
| | | <Setter Property="FontSize" Value="{Binding ActualWidth,RelativeSource={RelativeSource AncestorType=Window}, |
| | | Converter={StaticResource fc}, ConverterParameter='20,1920'}" /> |
| | | </Style> |
| | | </Window.Resources> |
| | | |
| | | <Grid Margin="10"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition/> |
| | | <ColumnDefinition/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!--<Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Stretch" Padding="0" |
| | | Content="{DynamicResource Tr.PleaseScanSnr}" |
| | | Margin="0,0,0,20" |
| | | />--> |
| | | <GroupBox Grid.Row="1" Header="{DynamicResource Tr.PleaseScanSnr}" Padding="0,0,0,10" Grid.ColumnSpan="2" > |
| | | <StackPanel> |
| | | <pu:FormGroup |
| | | Header="{DynamicResource Tr.PositionSnr}" |
| | | dp:FormGroupHelper.IsRequired="True" |
| | | Style="{StaticResource InputFormGroupStyle}"> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="positionText" TextWrapping="NoWrap"/> |
| | | </pu:FormGroup> |
| | | |
| | | <pu:FormGroup |
| | | Header="{DynamicResource Tr.ProductSnr}" |
| | | dp:FormGroupHelper.IsRequired="True" Style="{StaticResource InputFormGroupStyle}"> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" x:Name="snrText" TextWrapping="NoWrap"/> |
| | | </pu:FormGroup> |
| | | </StackPanel> |
| | | |
| | | </GroupBox> |
| | | |
| | | <Button Grid.Row="2" Click="Con_Click" Style="{StaticResource PrimaryButton}" |
| | | Content="{DynamicResource Tr.ContinueScan}"/> |
| | | <Button Grid.Row="2" Grid.Column="1" Click="Ok_Click" Style="{StaticResource SuccessButton}" |
| | | Content="{DynamicResource Tr.FinishScan}"/> |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using HMI.Core.Util; |
| | | using HMI.UI.Entity; |
| | | using HMI.UI.Util; |
| | | using HMI.Util; |
| | | using Panuon.WPF.UI; |
| | | using System; |
| | | using System.ComponentModel; |
| | | using System.IO.Ports; |
| | | using System.Windows; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | public class BarcodeEventArg |
| | | { |
| | | public string name { get; private set; } |
| | | public string positionCode { get; private set; } |
| | | public string snrCode { get; private set; } |
| | | |
| | | public BarcodeEventArg(string name, string positionCode, string snrCode) |
| | | { |
| | | this.name = name; |
| | | this.positionCode = positionCode; |
| | | this.snrCode = snrCode; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// Scanner.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class Scanner : WindowX |
| | | { |
| | | private string _title; |
| | | |
| | | public string title |
| | | { |
| | | get { return _title; } |
| | | set => Set(ref _title, value); |
| | | } |
| | | |
| | | public event EventHandler<BarcodeEventArg> FinishBarcode; |
| | | SerialPort serialPort; |
| | | public Scanner(string title) |
| | | { |
| | | this.title = title; |
| | | this.DataContext = this; |
| | | InitializeComponent(); |
| | | Loaded += Scanner_Loaded; |
| | | } |
| | | |
| | | private void Scanner_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | InitScanner(); |
| | | Loaded -= Scanner_Loaded; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设置扫码枪 |
| | | /// </summary> |
| | | private void InitScanner() |
| | | { |
| | | string comStr = ""; |
| | | if (serialPort != null) return; |
| | | |
| | | string section = this.Tag.ToString(); |
| | | ScannerConfig common = ScannerConfig.ReadFromIni("", "Config"); |
| | | |
| | | string portName = IniFileHelper.ReadIniData(section, "PortName", "",""); |
| | | if (string.IsNullOrEmpty(portName)) |
| | | { |
| | | if (section.StartsWith("A")) |
| | | { |
| | | portName = "COM1"; |
| | | } |
| | | else |
| | | { |
| | | portName = "COM2"; |
| | | } |
| | | } |
| | | common.PortName = portName; |
| | | |
| | | |
| | | // 配置串口 |
| | | serialPort = new SerialPort |
| | | { |
| | | PortName = common.PortName, |
| | | BaudRate = common.BaudRate, |
| | | Parity = Parity.None, |
| | | DataBits = common.DataBits, |
| | | StopBits = common.StopBits, |
| | | Handshake = Handshake.None, |
| | | DtrEnable = common.DtrEnable, |
| | | RtsEnable = common.RtsEnable |
| | | //ReadTimeout = 500, |
| | | //WriteTimeout = 500, |
| | | //ReciveBufferSize=4096 |
| | | }; |
| | | |
| | | try |
| | | { |
| | | // 打开串口 |
| | | serialPort.Open(); |
| | | // 注册接收数据事件 |
| | | serialPort.DataReceived += SerialPort_DataReceived; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | MsgBoxHelper.Error(this, $"{this.lang("Tr.InitGunFailed")}: {ex.StackTrace}"); |
| | | } |
| | | } |
| | | |
| | | private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | SerialPort sp = (SerialPort)sender; |
| | | string barcode = ReadDataFrom(sp); |
| | | |
| | | //string barcode = |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | if (snrText.IsFocused) |
| | | { |
| | | snrText.Text = barcode; |
| | | Con_Click(null, null); |
| | | } |
| | | else |
| | | { |
| | | positionText.Text = barcode; |
| | | //positionText.Focus(); |
| | | //snrText.Text = ""; |
| | | snrText.Focus(); |
| | | } |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | MessageBox.Show($"{this.lang("Tr.GetGunData")}: {ex.StackTrace}"); |
| | | } |
| | | } |
| | | |
| | | private void Con_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | string position = positionText.Text, snr = snrText.Text; |
| | | if (string.IsNullOrEmpty(position) || string.IsNullOrEmpty(snr)) |
| | | { |
| | | ToastHelper.Warning(this, this.lang("Tr.PleaseInputSnrs")); |
| | | return; |
| | | } |
| | | try |
| | | { |
| | | FinishBarcode.Invoke(this, new BarcodeEventArg(Tag.ToString(), position, snr)); |
| | | //positionText.Text = ""; |
| | | //snrText.Text = ""; |
| | | positionText.Focus(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Warning(this, ex.Message); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void Ok_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | string position = positionText.Text, snr = snrText.Text; |
| | | if (string.IsNullOrEmpty(position) || string.IsNullOrEmpty(snr)) |
| | | { |
| | | ToastHelper.Warning(this, this.lang("Tr.PleaseInputSnrs")); |
| | | return; |
| | | } |
| | | try |
| | | { |
| | | FinishBarcode.Invoke(this, new BarcodeEventArg(Tag.ToString(), position, snr)); |
| | | this.Close(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ToastHelper.Warning(this, ex.Message); |
| | | } |
| | | } |
| | | |
| | | private string ReadDataFrom(SerialPort sp) |
| | | { |
| | | string barcode = ""; |
| | | while (sp.BytesToRead > 0) |
| | | { |
| | | byte[] buffer = new byte[sp.BytesToRead]; |
| | | sp.Read(buffer, 0, buffer.Length); |
| | | barcode += System.Text.Encoding.UTF8.GetString(buffer); |
| | | System.Threading.Thread.Sleep(50); |
| | | } |
| | | return barcode; |
| | | } |
| | | |
| | | private void Window_Closing(object sender, CancelEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | serialPort.Close(); |
| | | serialPort = null; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using HMI.UI.Entity; |
| | | using System.IO.Ports; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | public class ScannerManager |
| | | { |
| | | public static Dictionary<string, SerialPort> map = new Dictionary<string, SerialPort>(); |
| | | public static Dictionary<SerialPort, Queue<TextBox>> controlMap = new Dictionary<SerialPort, Queue<TextBox>>(); |
| | | |
| | | public static Dictionary<SerialPort, TextBox> focusMap = new Dictionary<SerialPort, TextBox>(); |
| | | public static Dictionary<TextBox, SerialPort> reverseMap = new Dictionary<TextBox, SerialPort>(); |
| | | |
| | | |
| | | public static void RegisterCom(ScannerConfig setting, TextBox tb) |
| | | { |
| | | if (!map.ContainsKey(setting.PortName)) |
| | | { |
| | | // 配置串口 |
| | | var serialPort = new SerialPort |
| | | { |
| | | PortName = setting.PortName, |
| | | BaudRate = setting.BaudRate, |
| | | Parity = Parity.None, |
| | | DataBits = setting.DataBits, |
| | | StopBits = setting.StopBits, |
| | | Handshake = Handshake.None, |
| | | DtrEnable = setting.DtrEnable, |
| | | RtsEnable = setting.RtsEnable |
| | | //ReadTimeout = 500, |
| | | //WriteTimeout = 500, |
| | | //ReciveBufferSize=4096 |
| | | }; |
| | | |
| | | try |
| | | { |
| | | // 打开串口 |
| | | serialPort.Open(); |
| | | map[setting.PortName] = serialPort; |
| | | controlMap[serialPort] = new Queue<TextBox>(); |
| | | reverseMap[tb] = serialPort; |
| | | tb.GotFocus += Tb_GotFocus; |
| | | controlMap[serialPort].Enqueue(tb); |
| | | |
| | | // 注册接收数据事件 |
| | | serialPort.DataReceived += SerialPort_DataReceived; ; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | } |
| | | } |
| | | else { |
| | | |
| | | } |
| | | } |
| | | |
| | | private static string ReadDataFrom(SerialPort sp) |
| | | { |
| | | string barcode = ""; |
| | | while (sp.BytesToRead > 0) |
| | | { |
| | | byte[] buffer = new byte[sp.BytesToRead]; |
| | | sp.Read(buffer, 0, buffer.Length); |
| | | barcode += System.Text.Encoding.UTF8.GetString(buffer); |
| | | System.Threading.Thread.Sleep(50); |
| | | } |
| | | return barcode; |
| | | } |
| | | |
| | | private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) |
| | | { |
| | | var sp = sender as SerialPort; |
| | | string barcode = ReadDataFrom(sp); |
| | | TextBox tb; |
| | | if (focusMap.ContainsKey(sp)) |
| | | { |
| | | tb = focusMap[sp]; |
| | | } |
| | | else |
| | | { |
| | | tb = controlMap[sp].Dequeue(); |
| | | } |
| | | //string barcode = |
| | | tb.Dispatcher.Invoke(() => |
| | | { |
| | | tb.Text = barcode; |
| | | }); |
| | | } |
| | | |
| | | private static void Tb_GotFocus(object sender, System.Windows.RoutedEventArgs e) |
| | | { |
| | | var tb = sender as TextBox; |
| | | SerialPort sp = reverseMap[tb]; |
| | | focusMap[sp] = tb; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <hb:NotifyControl x:Class="Plugin.StandardTest.ScreenGrid" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | mc:Ignorable="d" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | Background="LightGray" Padding="0" Margin="0" |
| | | d:DesignHeight="450" d:DesignWidth="800"> |
| | | <Grid x:Name="grid" dp:GridHelper.Space="2" Margin="0"> |
| | | |
| | | </Grid> |
| | | </hb:NotifyControl> |
| New file |
| | |
| | | using HMI.Base; |
| | | using HMI.UI.DpUtil; |
| | | using System.Collections.ObjectModel; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | public class ScreenGridConfig |
| | | { |
| | | public int rows { get; set; } |
| | | public int columns { get; set; } |
| | | public ObservableCollection<bool> values { get; set; } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// ScreenTouch.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class ScreenGrid : NotifyControl |
| | | { |
| | | public ScreenGrid(int rows, int columns) |
| | | { |
| | | InitializeComponent(); |
| | | |
| | | for (int i = 0; i < rows; i++) |
| | | { |
| | | grid.RowDefinitions.Add(new RowDefinition()); |
| | | } |
| | | |
| | | for (int i = 0; i < columns; i++) |
| | | { |
| | | grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto}); |
| | | } |
| | | } |
| | | |
| | | public void AddScreen(ScreenTouch screen, int row, int col) |
| | | { |
| | | grid.Children.Add(screen); |
| | | Grid.SetRow(screen, row); |
| | | Grid.SetColumn(screen, col); |
| | | |
| | | GridHelper.RefreshGridSpace(grid); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <hb:NotifyControl x:Class="Plugin.StandardTest.ScreenTouch" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | mc:Ignorable="d" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | d:DesignHeight="450" d:DesignWidth="800"> |
| | | <UniformGrid x:Name="uniform" dp:UniformGridHelper.InnerBorder="0.5" |
| | | dp:UniformGridHelper.InnerBorderBrush="#37375f"> |
| | | </UniformGrid> |
| | | </hb:NotifyControl> |
| New file |
| | |
| | | using HMI.Base; |
| | | using HMI.UI.DpUtil; |
| | | using System; |
| | | using System.Collections.ObjectModel; |
| | | using System.Globalization; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Data; |
| | | using System.Windows.Media; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | public class ColorConverter : IMultiValueConverter |
| | | { |
| | | public static ColorConverter Instance = new ColorConverter(); |
| | | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) |
| | | { |
| | | if (values[0] is ObservableCollection<string> list && values[1] is string tag) |
| | | { |
| | | if (list?.Contains(tag) == true) return Brushes.Green; |
| | | return Brushes.White; |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) |
| | | { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// ScreenTouch.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class ScreenTouch : NotifyControl |
| | | { |
| | | public static readonly DependencyProperty ValuesProperty = DependencyProperty.Register( |
| | | nameof(Values), |
| | | typeof(ObservableCollection<bool>), |
| | | typeof(ScreenTouch), |
| | | new PropertyMetadata(null, OnValuesChanged)); |
| | | |
| | | private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) |
| | | { |
| | | if (d is ScreenTouch control) |
| | | { |
| | | // 如果有旧的集合,取消订阅 |
| | | if (e.OldValue is ObservableCollection<bool> oldCollection) |
| | | { |
| | | oldCollection.CollectionChanged -= control.Value_CollectionChanged; |
| | | } |
| | | |
| | | // 如果有新的集合,订阅变更事件 |
| | | if (e.NewValue is ObservableCollection<bool> newCollection) |
| | | { |
| | | newCollection.CollectionChanged += control.Value_CollectionChanged; |
| | | } |
| | | |
| | | // 刷新所有单元格 |
| | | control.PaintPoints(); |
| | | } |
| | | } |
| | | |
| | | |
| | | public ScreenTouch(int rows, int columns) |
| | | { |
| | | this.rows = rows; |
| | | this.columns = columns; |
| | | InitializeComponent(); |
| | | Refresh(); |
| | | Values = new ObservableCollection<bool>(new bool[rows * columns]); |
| | | } |
| | | |
| | | public ObservableCollection<bool> Values |
| | | { |
| | | get => (ObservableCollection<bool>)GetValue(ValuesProperty); |
| | | set |
| | | { |
| | | SetValue(ValuesProperty, value); |
| | | } |
| | | } |
| | | |
| | | private void Value_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) |
| | | { |
| | | PaintPoints(); |
| | | } |
| | | |
| | | public int rows { get; private set; } |
| | | public int columns { get; private set; } |
| | | |
| | | private Border[] children; |
| | | |
| | | public void Refresh() |
| | | { |
| | | uniform.Children.Clear(); |
| | | uniform.Rows = rows; |
| | | uniform.Columns = columns; |
| | | |
| | | children = new Border[rows * columns]; |
| | | |
| | | |
| | | for (int i = 0; i < children.Length; i++) |
| | | { |
| | | var border = new Border |
| | | { |
| | | BorderThickness = new Thickness(0), |
| | | Background = Brushes.White |
| | | }; |
| | | children[i] = border; |
| | | uniform.Children.Add(border); |
| | | } |
| | | UniformGridHelper.RefreshGridInnerBorder(uniform); |
| | | } |
| | | |
| | | public void Refresh(int rows, int columns) |
| | | { |
| | | this.rows = rows; |
| | | this.columns = columns; |
| | | Refresh(); |
| | | } |
| | | |
| | | public void Clear() |
| | | { |
| | | Values = new ObservableCollection<bool>(new bool[rows * columns]); |
| | | } |
| | | |
| | | public void DrawPoint(int x, int y) |
| | | { |
| | | Values[y * columns + x] = true; |
| | | } |
| | | |
| | | public void DrawPoints(int[][] points) |
| | | { |
| | | var temp = new ObservableCollection<bool>(new bool[rows * columns]); |
| | | for (int i = 0; i < points.Length; i++) |
| | | { |
| | | int[] point = points[i]; |
| | | int index = point[1] * columns + point[0]; |
| | | //children[point[0]][point[1]].Background = Brushes.Green; |
| | | temp[index] = true; |
| | | } |
| | | Values = temp; |
| | | } |
| | | |
| | | public void DrawNewPoints(int[][] points) |
| | | { |
| | | var temp = Values == null ? |
| | | new ObservableCollection<bool>(new bool[rows * columns]) : |
| | | new ObservableCollection<bool>(Values); |
| | | for (int i = 0; i < points.Length; i++) |
| | | { |
| | | int[] point = points[i]; |
| | | if (point == null) |
| | | { |
| | | continue; |
| | | } |
| | | int index = point[1] * columns + point[0]; |
| | | //children[point[0]][point[1]].Background = Brushes.Green; |
| | | temp[index] = true; |
| | | } |
| | | Values = temp; |
| | | } |
| | | |
| | | private void PaintPoints() |
| | | { |
| | | for (int i = 0; i < Values.Count; i++) |
| | | { |
| | | if (Values[i]) |
| | | { |
| | | children[i].Background = Brushes.Green; |
| | | } |
| | | else |
| | | { |
| | | children[i].Background = Brushes.White; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <pu:WindowX x:Class="Plugin.StandardTest.ScreenTouchWin" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | mc:Ignorable="d" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | SizeChanged="Window_SizeChanged" |
| | | Style="{StaticResource ModalWindow}" |
| | | pu:WindowXModalDialog.Buttons="{x:Null}" |
| | | WindowStartupLocation="CenterScreen" |
| | | WindowState="Maximized" |
| | | Background="LightGray" |
| | | Title="Screens" Height="450" Width="800"> |
| | | <Grid x:Name="grid" HorizontalAlignment="Center"> |
| | | |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using Panuon.WPF.UI; |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using System.Collections.ObjectModel; |
| | | using System.Linq; |
| | | using System.Text; |
| | | using System.Threading.Tasks; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Data; |
| | | using System.Windows.Documents; |
| | | using System.Windows.Input; |
| | | using System.Windows.Media; |
| | | using System.Windows.Media.Imaging; |
| | | using System.Windows.Shapes; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | /// <summary> |
| | | /// ScreenTouchWin.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class ScreenTouchWin : WindowX |
| | | { |
| | | private ScreenGrid screenGrid; |
| | | private ScreenTouch[] screens; |
| | | |
| | | public ScreenTouchWin(ScreenTouch[] configs) |
| | | { |
| | | screenGrid = new ScreenGrid(1, configs.Length); |
| | | screens = new ScreenTouch[configs.Length]; |
| | | InitializeComponent(); |
| | | |
| | | int space = 2 * (screens.Length + 1); |
| | | for (int i = 0; i < screens.Length; i++) |
| | | { |
| | | var screen = new ScreenTouch(configs[i].rows, configs[i].columns) |
| | | { |
| | | Width = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length), 0), |
| | | Height = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9), 0) |
| | | }; |
| | | |
| | | screen.SetBinding(ScreenTouch.ValuesProperty, new Binding(nameof(ScreenTouch.Values)) { Source = configs[i], Mode = BindingMode.OneWay }); |
| | | |
| | | screens[i] = screen; |
| | | screenGrid.AddScreen(screen, 0, i); |
| | | } |
| | | |
| | | grid.Children.Add(screenGrid); |
| | | } |
| | | |
| | | private void Window_SizeChanged(object sender, SizeChangedEventArgs e) |
| | | { |
| | | if (screenGrid == null) |
| | | { |
| | | return; |
| | | } |
| | | int space = 2 * (screens.Length + 1); |
| | | |
| | | for (int i = 0; i < screens.Length; i++) |
| | | { |
| | | var screen = screens[i]; |
| | | screen.Width = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length), 0); |
| | | screen.Height = Math.Max(Math.Floor((this.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9), 0); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using SuperSocket.SocketBase; |
| | | using SuperSocket.SocketBase.Protocol; |
| | | using System.Text; |
| | | |
| | | namespace Plugin.StandardTest.Setting |
| | | { |
| | | public class DTReceiveFilter : ReceiveFilterBase<StringRequestInfo> |
| | | { |
| | | public override StringRequestInfo Filter(byte[] readBuffer, int offset, int length, bool toBeCopied, out int rest) |
| | | { |
| | | rest = 0; |
| | | int i = offset; |
| | | List<byte> bytes = new List<byte>(); |
| | | while (i < readBuffer.Length) |
| | | { |
| | | byte b = readBuffer[i++]; |
| | | if (b == '\n') |
| | | { |
| | | break; |
| | | } |
| | | bytes.Add(b); |
| | | } |
| | | byte[] arr = bytes.ToArray(); |
| | | return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), Encoding.UTF8.GetString(arr), null);//sb.ToString() |
| | | } |
| | | } |
| | | |
| | | public class StringParser : IRequestInfoParser<StringRequestInfo> |
| | | { |
| | | |
| | | public StringRequestInfo ParseRequestInfo(string source) |
| | | { |
| | | return new StringRequestInfo(DateTime.Now.ToString("yyyyMMddHHmmssfff"), source, null); |
| | | } |
| | | } |
| | | |
| | | public class TcpServer : AppServer<TcpSession> |
| | | { |
| | | public TcpServer() : base(new TerminatorReceiveFilterFactory("\n", Encoding.UTF8, new StringParser())) |
| | | { |
| | | |
| | | } |
| | | } |
| | | |
| | | public class TcpSession : AppSession<TcpSession, StringRequestInfo> |
| | | { |
| | | |
| | | } |
| | | } |
| New file |
| | |
| | | namespace Plugin.StandardTest.Setting |
| | | { |
| | | public class TsSetting |
| | | { |
| | | } |
| | | } |
| New file |
| | |
| | | <UserControl x:Class="Plugin.StandardTest.SingleControl" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | xmlns:cdp="clr-namespace:HMI.Core.DpUtil;assembly=HMI.Core" |
| | | mc:Ignorable="d" |
| | | xmlns:sys="clr-namespace:System;assembly=mscorlib" |
| | | Loaded="UserControl_Loaded" |
| | | Background="#ccc6c7" |
| | | PreviewMouseRightButtonDown="control_PreviewMouseRightButtonDown" |
| | | FontSize="{Binding ActualHeight,RelativeSource={RelativeSource AncestorType=UserControl}, |
| | | Converter={StaticResource hfc}, ConverterParameter='12,816'}" |
| | | x:Name="control" |
| | | SizeChanged="control_SizeChanged" |
| | | d:DesignHeight="450" d:DesignWidth="800"> |
| | | <UserControl.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonListView.xaml"/> |
| | | <ResourceDictionary Source="/HMI.UI;component/Style/PanuonDataGrid.xaml"/> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | <sys:Double x:Key="nanD">NaN</sys:Double> |
| | | |
| | | <Style TargetType="Label" x:Key="baseLabel"> |
| | | <Setter Property="HorizontalContentAlignment" Value="Left"/> |
| | | <Setter Property="VerticalContentAlignment" Value="Center"/> |
| | | <Setter Property="Padding" Value="5"/> |
| | | <Setter Property="FontSize" Value="{Binding ActualWidth,ElementName=control, |
| | | Converter={StaticResource fc}, ConverterParameter='25,1093'}"/> |
| | | </Style> |
| | | <Style TargetType="TextBlock" x:Key="baseText"> |
| | | <Setter Property="HorizontalAlignment" Value="Left"/> |
| | | <Setter Property="VerticalAlignment" Value="Center"/> |
| | | <Setter Property="Padding" Value="0,0,2,0"/> |
| | | <Setter Property="FontSize" Value="{Binding ActualWidth,ElementName=control, |
| | | Converter={StaticResource fc}, ConverterParameter='25,1093'}"/> |
| | | </Style> |
| | | <Brush x:Key="passedColor">#beed00</Brush> |
| | | </ResourceDictionary> |
| | | </UserControl.Resources> |
| | | |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <Grid dp:GridHelper.InnerBorder="1" dp:GridHelper.Space="2" x:Name="tableHeader" |
| | | dp:GridHelper.InnerBorderBrush="{DynamicResource CommonBoarder}"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="Auto"/> |
| | | <ColumnDefinition /> |
| | | <ColumnDefinition Width="Auto"/> |
| | | </Grid.ColumnDefinitions> |
| | | <StackPanel Grid.RowSpan="3"> |
| | | <StackPanel.Style> |
| | | <Style TargetType="StackPanel"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Status}" Value="Passed"> |
| | | <Setter Property="Background" Value="{StaticResource passedColor}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Testing"> |
| | | <Setter Property="Background" Value="Gold"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Failed"> |
| | | <Setter Property="Background" Value="{DynamicResource DangerColor}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Failed"> |
| | | <Setter Property="Background" Value="{DynamicResource DangerColor}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Ready"> |
| | | <Setter Property="Background" Value="DeepSkyBlue"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </StackPanel.Style> |
| | | |
| | | <Label> |
| | | <Label.Style> |
| | | <Style TargetType="Label"> |
| | | <Setter Property="VerticalAlignment" Value="Center"/> |
| | | <Setter Property="Padding" Value="10,10,10,0"/> |
| | | <Setter Property="Margin" Value="0"/> |
| | | <Setter Property="FontFamily" Value="{StaticResource iconfont}"/> |
| | | <Setter Property="Foreground" Value="White"/> |
| | | <Setter Property="HorizontalContentAlignment" Value="Center"/> |
| | | <Setter Property="VerticalContentAlignment" Value="Center"/> |
| | | <Setter Property="FontSize" Value="{Binding ActualWidth,ElementName=control, |
| | | Converter={StaticResource fc}, ConverterParameter='50,816'}"/> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Status}" Value="Passed"> |
| | | <Setter Property="Content" Value=""/> |
| | | <Setter Property="Background" Value="{StaticResource passedColor}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Testing"> |
| | | <Setter Property="Content" Value=""/> |
| | | <Setter Property="Background" Value="Gold"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Failed"> |
| | | <Setter Property="Content" Value=""/> |
| | | <Setter Property="Background" Value="{DynamicResource DangerColor}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Failed"> |
| | | <Setter Property="Content" Value=""/> |
| | | <Setter Property="Background" Value="{DynamicResource DangerColor}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Ready"> |
| | | <Setter Property="Content" Value=""/> |
| | | <Setter Property="Background" Value="DeepSkyBlue"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Label.Style> |
| | | </Label> |
| | | |
| | | <Label Content="{Binding Status}" VerticalAlignment="Center" VerticalContentAlignment="Center" |
| | | Padding="0" Margin="0" |
| | | HorizontalContentAlignment="Center" |
| | | Foreground="#FFF" |
| | | FontWeight="Bold" |
| | | FontSize="{Binding ActualWidth,ElementName=control, |
| | | Converter={StaticResource fc}, ConverterParameter='15,816'}"/> |
| | | </StackPanel> |
| | | |
| | | <!--<Label HorizontalContentAlignment="Left" Grid.Column="1" Content="{Binding Status}"/>--> |
| | | |
| | | <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="0" Grid.Column="1"> |
| | | <StackPanel Orientation="Horizontal"> |
| | | <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Total}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding TotalNum}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Pass}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding PassNum}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.Fail}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding FailNum}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.PassRate}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding PassRate,StringFormat={}{0:P2}}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Text="{DynamicResource Ts.ProbeCount}"/> |
| | | <TextBlock Style="{StaticResource baseText}" Margin="2,0,18,0" Text="{Binding ProbeCount}"/> |
| | | </StackPanel> |
| | | </Label> |
| | | |
| | | <Label Style="{StaticResource baseLabel}" Background="#FFF" Grid.Row="1" Grid.Column="1"> |
| | | <StackPanel Orientation="Horizontal"> |
| | | <Label Style="{StaticResource baseLabel}" Content="{DynamicResource Ts.CurrentSnr}"/> |
| | | <Label Style="{StaticResource baseLabel}" Margin="2,0,18,0" Content="{Binding Barcode}"/> |
| | | <Label Style="{StaticResource baseLabel}" Content="{DynamicResource Ts.TimeSpan}"/> |
| | | <Label Style="{StaticResource baseLabel}" Margin="2,0,18,0" Content="{Binding ElapseTime}"/> |
| | | </StackPanel> |
| | | </Label> |
| | | <Border x:Name="screenContainer" BorderThickness="0" Grid.Column="2" Grid.RowSpan="2" MouseLeftButtonDown="screenContainer_MouseLeftButtonDown"> |
| | | |
| | | </Border> |
| | | </Grid> |
| | | |
| | | <Grid Grid.Row="2" x:Name="simplePanel" dp:GridHelper.Space="2" dp:GridHelper.SpaceIgnore="TOP"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Label VerticalContentAlignment="Center" |
| | | HorizontalContentAlignment="Center" |
| | | FontSize="{Binding ActualWidth,ElementName=control, |
| | | Converter={StaticResource fc}, ConverterParameter='50,816'}"> |
| | | <Label.Style> |
| | | <Style TargetType="Label"> |
| | | <Setter Property="Foreground" Value="#848286"/> |
| | | <Setter Property="Content" Value="{Binding CurrentStep}"/> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Status}" Value="Passed"> |
| | | <Setter Property="Background" Value="{StaticResource passedColor}"/> |
| | | <Setter Property="Content" Value="--PASS--"/> |
| | | <Setter Property="Foreground" Value="#000"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Status}" Value="Failed"> |
| | | <Setter Property="Background" Value="{DynamicResource DangerColor}"/> |
| | | <Setter Property="Content" Value="--FAIL--"/> |
| | | <Setter Property="Foreground" Value="#000"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Label.Style> |
| | | </Label> |
| | | <ProgressBar Grid.Row="1" MinHeight="20" Value="{Binding ProcessValue}" |
| | | Height="{Binding ActualHeight,ElementName=control, |
| | | Converter={StaticResource hfc}, ConverterParameter='30,816'}"/> |
| | | <Label Grid.Row="1" |
| | | VerticalContentAlignment="Center" |
| | | HorizontalContentAlignment="Center" |
| | | Content="{Binding ProcessValueLabel}" Background="Transparent" Foreground="#000"/> |
| | | </Grid> |
| | | |
| | | <Grid Grid.Row="2" x:Name="detailPanel"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | |
| | | |
| | | <Canvas Grid.Row="0" x:Name="canvas" Background="Transparent"> |
| | | <DataGrid Canvas.Left="0" Canvas.Top="0" Height="{Binding ActualHeight,ElementName=canvas}" |
| | | Width="{Binding ActualWidth,ElementName=canvas}" |
| | | Style="{StaticResource DefaultDataGrid}" Margin="2,0,2,2" |
| | | FontSize="{DynamicResource tableFontSize}" |
| | | dp:DataGridHelper.ShowRowNumber="True" |
| | | VirtualizingPanel.IsContainerVirtualizable="True" |
| | | VirtualizingPanel.IsVirtualizing="True" |
| | | VirtualizingPanel.VirtualizationMode="Recycling" |
| | | RowHeight="{StaticResource nanD}" |
| | | ColumnHeaderHeight="{Binding ActualWidth,ElementName=control, |
| | | Converter={StaticResource fc}, ConverterParameter='32,1093,30,45'}" |
| | | HorizontalScrollBarVisibility="Disabled" |
| | | ItemsSource="{Binding Datasource}" x:Name="table"> |
| | | |
| | | <DataGrid.Columns> |
| | | <DataGridTextColumn Header="#" |
| | | ElementStyle="{StaticResource TableTextBlock}" |
| | | Binding="{Binding Header, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}, Mode=FindAncestor}}" |
| | | Width="0.4*"/> |
| | | <DataGridTextColumn Width="2.5*" Binding="{Binding Name}" |
| | | ElementStyle="{StaticResource TableTextBlock}"> |
| | | <DataGridTextColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.Name}"/> |
| | | </DataGridTextColumn.Header> |
| | | </DataGridTextColumn> |
| | | <DataGridTextColumn Width="0.8*" Binding="{Binding Unit}" |
| | | ElementStyle="{StaticResource TableTextBlock}"> |
| | | <DataGridTextColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.Unit}"/> |
| | | </DataGridTextColumn.Header> |
| | | </DataGridTextColumn> |
| | | <DataGridTextColumn Width="2*" Binding="{Binding Value}" |
| | | ElementStyle="{StaticResource TableTextBlock}"> |
| | | <DataGridTextColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.Value}"/> |
| | | </DataGridTextColumn.Header> |
| | | </DataGridTextColumn> |
| | | <DataGridTextColumn Width="1.5*" Binding="{Binding LimitDown}" |
| | | ElementStyle="{StaticResource TableTextBlock}"> |
| | | <DataGridTextColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.LimitDown}"/> |
| | | </DataGridTextColumn.Header> |
| | | </DataGridTextColumn> |
| | | <DataGridTextColumn Width="1.5*" Binding="{Binding LimitUp}" |
| | | ElementStyle="{StaticResource TableTextBlock}"> |
| | | <DataGridTextColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.LimitUp}"/> |
| | | </DataGridTextColumn.Header> |
| | | </DataGridTextColumn> |
| | | <DataGridTextColumn Width="0.6*" Binding="{Binding ConsumeTime}" |
| | | ElementStyle="{StaticResource TableTextBlock}"> |
| | | <DataGridTextColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.ConsumeTime}"/> |
| | | </DataGridTextColumn.Header> |
| | | </DataGridTextColumn> |
| | | <DataGridTemplateColumn Width="1*"> |
| | | <DataGridTemplateColumn.Header> |
| | | <TextBlock Text="{DynamicResource Test.Result}"/> |
| | | </DataGridTemplateColumn.Header> |
| | | <DataGridTemplateColumn.CellTemplate> |
| | | <DataTemplate> |
| | | <Grid x:Name="border" Margin="0"> |
| | | <TextBlock x:Name="text" Margin="-1" Padding="0" VerticalAlignment="Center" Text="{Binding Result}"/> |
| | | </Grid> |
| | | <DataTemplate.Triggers> |
| | | <DataTrigger Binding="{Binding Result}" Value="Passed"> |
| | | <Setter TargetName="border" Property="Background" Value="{StaticResource passedColor}"/> |
| | | <Setter TargetName="text" Property="Foreground" Value="#000"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding Result}" Value="Failed"> |
| | | <Setter TargetName="border" Property="Background" Value="{DynamicResource DangerColor}"/> |
| | | <Setter TargetName="text" Property="Foreground" Value="#000"/> |
| | | </DataTrigger> |
| | | </DataTemplate.Triggers> |
| | | </DataTemplate> |
| | | </DataGridTemplateColumn.CellTemplate> |
| | | </DataGridTemplateColumn> |
| | | </DataGrid.Columns> |
| | | </DataGrid> |
| | | </Canvas> |
| | | |
| | | <Grid Grid.Row="1" Height="20" Margin="0"> |
| | | <ProgressBar Grid.Row="3" x:Name="testProcess" Value="{Binding ProcessValue}"/> |
| | | <Label Width="45" DockPanel.Dock="Right" x:Name="testProcessLabel" Padding="0" |
| | | Content="{Binding ProcessValueLabel}" |
| | | VerticalContentAlignment="Center"/> |
| | | </Grid> |
| | | |
| | | <Grid Background="#f1eded" Grid.Row="2" Height="25"> |
| | | <DockPanel> |
| | | <StackPanel Margin="10,0,0,0" DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="8"> |
| | | <StackPanel Orientation="Horizontal"> |
| | | <Ellipse Width="10" Height="10"> |
| | | <Ellipse.Style> |
| | | <Style TargetType="Ellipse"> |
| | | <Setter Property="Fill" Value="LightGray"/> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding TcpServerStatus,ElementName=control}" Value="ERROR"> |
| | | <Setter Property="Fill" Value="Red"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding TcpServerStatus,ElementName=control}" Value="RUNNING"> |
| | | <Setter Property="Fill" Value="Green"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Ellipse.Style> |
| | | </Ellipse> |
| | | <TextBlock Text="{Binding TcpServerInfo}"/> |
| | | <Button Padding="0" Margin="0" BorderThickness="0" Click="Button_Click">🔄</Button> |
| | | </StackPanel> |
| | | </StackPanel> |
| | | <Label Margin="10,0,0,0" Padding="0" FontSize="16" VerticalContentAlignment="Center" Content="{Binding SocketName}"/> |
| | | <Label Margin="20,0,0,0" Padding="0" FontSize="14" VerticalContentAlignment="Center" Content="{Binding CurrentStep}"/> |
| | | |
| | | <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="0,0,5,0"> |
| | | <!--<Button FontSize="12" x:Name="scannerSetting" |
| | | Margin="0,0,10,0" Click="Button_Click" Content="{DynamicResource App.ScannerSetting}"> |
| | | <Button.Style> |
| | | <Style TargetType="Button" BasedOn="{StaticResource LinkButton}"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding IsAutoMode}" Value="True"> |
| | | <Setter Property="Visibility" Value="Collapsed"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Button.Style> |
| | | </Button>--> |
| | | |
| | | <Label Padding="0" FontSize="12" |
| | | VerticalAlignment="Center" |
| | | VerticalContentAlignment="Center" Content="{DynamicResource Ts.ProbeCount}"/> |
| | | <Label Padding="0" FontSize="12" Margin="0,0,15,0" |
| | | VerticalAlignment="Center" |
| | | VerticalContentAlignment="Center"> |
| | | <TextBlock> |
| | | <Run Text="{Binding ProbeCount}"/> |
| | | <Run Text="/"/> |
| | | <Run Text="{Binding ProbeWarn,Mode=OneWay}"/> |
| | | </TextBlock> |
| | | </Label> |
| | | |
| | | <StackPanel x:Name="probeWarnPanel" Orientation="Horizontal" Visibility="Collapsed"> |
| | | <Label x:Name="alarmLabel" Padding="0" FontSize="12" |
| | | VerticalAlignment="Center" |
| | | VerticalContentAlignment="Center" Content="{DynamicResource Ts.ProbeAlarmCount}"/> |
| | | <Label x:Name="alarmValue" Padding="0" FontSize="12" |
| | | VerticalAlignment="Center" |
| | | VerticalContentAlignment="Center" Content="{Binding ProbeWarn,Mode=OneWay}"/> |
| | | </StackPanel> |
| | | |
| | | <Button FontSize="12" Style="{StaticResource LinkButton}" |
| | | cdp:PermissionHelper.RequiredPermission="Permission:ClearProbeCount" |
| | | x:Name="clear" Margin="10,0,0,0" Click="Button_Click" |
| | | Content="{DynamicResource App.Clear}" Cursor=""/> |
| | | </StackPanel> |
| | | |
| | | <Label x:Name="probeCountAlarm" Visibility="Collapsed" DockPanel.Dock="Left" Padding="30,0" FontSize="12" Foreground="Red" |
| | | VerticalAlignment="Center" |
| | | VerticalContentAlignment="Center" Content="{DynamicResource Ts.ProbeAlarm}"/> |
| | | </DockPanel> |
| | | </Grid> |
| | | </Grid> |
| | | |
| | | <Canvas Grid.Row="0" Grid.RowSpan="5" Visibility="Collapsed" |
| | | VirtualizingPanel.IsVirtualizing="False" |
| | | VirtualizingPanel.VirtualizationMode="Standard" |
| | | Background="#AAFFFFFF" x:Name="pausePanel" SizeChanged="pausePanel_Loaded"> |
| | | <Border BorderBrush="#000" Background="#FFF" BorderThickness="0.5" |
| | | CornerRadius="5" |
| | | x:Name="visualModal"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="Auto"/> |
| | | <ColumnDefinition /> |
| | | </Grid.ColumnDefinitions> |
| | | <Border Padding="10,5,0,5" Grid.ColumnSpan="2" Background="{DynamicResource WarningColor}" |
| | | CornerRadius="5,5,0,0"> |
| | | <Label Padding="0" Margin="0" Foreground="#FFF" Content="{DynamicResource App.Warn}"/> |
| | | </Border> |
| | | |
| | | <TextBlock Margin="20,20,20,0" Grid.Row="1" |
| | | FontSize="40" |
| | | FontFamily="/Panuon.WPF.UI;component/Resources/Fonts/#PanuonIcon" |
| | | Foreground="#F9D01A" Text=""/> |
| | | |
| | | <Label Margin="0,20,20,0" Grid.Row="1" Grid.Column="1" Content="{DynamicResource Ts.PauseMsg}"/> |
| | | <StackPanel Margin="10" Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" HorizontalAlignment="Right"> |
| | | <Button Style="{StaticResource ModalButton}" Tag="Cancel" |
| | | x:Name="cancelPauseBtn" |
| | | Click="Button_Click" Content="{DynamicResource App.Cancel}"/> |
| | | <Button Style="{StaticResource ModalButton}" |
| | | x:Name="continuePauseBtn" |
| | | Click="Button_Click" |
| | | Background="{DynamicResource WarningColor}" |
| | | Tag="OK" Content="{DynamicResource App.Continue}"/> |
| | | </StackPanel> |
| | | </Grid> |
| | | </Border> |
| | | </Canvas> |
| | | </Grid> |
| | | </UserControl> |
| New file |
| | |
| | | using HMI.Core.Entity; |
| | | using HMI.Core.Manager; |
| | | using HMI.Core.Setting; |
| | | using HMI.Core.Util; |
| | | using HMI.UI.Util; |
| | | using HMI.Util; |
| | | using NationalInstruments.TestStand.Interop.API; |
| | | using Plugin.StandardTest.Entity; |
| | | using Plugin.StandardTest.Setting; |
| | | using SuperSocket.SocketBase.Config; |
| | | using SuperSocket.SocketBase.Protocol; |
| | | using System.Collections; |
| | | using System.Collections.ObjectModel; |
| | | using System.ComponentModel; |
| | | using System.Configuration; |
| | | using System.IO; |
| | | using System.Runtime.CompilerServices; |
| | | using System.Timers; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using Util.ManualDebug; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | /// <summary> |
| | | /// SingleControl.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class SingleControl : UserControl, INotifyPropertyChanged |
| | | { |
| | | #region ... |
| | | public event PropertyChangedEventHandler PropertyChanged; |
| | | |
| | | protected void Set([CallerMemberName] string name = null) |
| | | { |
| | | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); |
| | | } |
| | | |
| | | protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null) |
| | | { |
| | | old = @new; |
| | | if (propertyName != null) |
| | | { |
| | | Set(propertyName); |
| | | } |
| | | } |
| | | #endregion |
| | | private int index; |
| | | private string CountIniPath; |
| | | |
| | | private string status = "Ready"; |
| | | |
| | | private ScreenTouch[] screens; |
| | | private ScreenGrid screenGrid; |
| | | |
| | | public string Status |
| | | { |
| | | get { return status; } |
| | | set { Set(ref status, value); } |
| | | } |
| | | |
| | | private double totalNum; |
| | | public double TotalNum |
| | | { |
| | | get => totalNum; |
| | | private set |
| | | { |
| | | Set(ref totalNum, value); |
| | | } |
| | | } |
| | | |
| | | public StandardTestUI parent { get; set; } |
| | | |
| | | private int passNum; |
| | | public int PassNum |
| | | { |
| | | get => passNum; |
| | | private set { Set(ref passNum, value); } |
| | | } |
| | | |
| | | private int failNum; |
| | | public int FailNum |
| | | { |
| | | get => failNum; |
| | | private set { Set(ref failNum, value); } |
| | | } |
| | | |
| | | private double passRate; |
| | | public double PassRate |
| | | { |
| | | get => passRate; |
| | | private set |
| | | { |
| | | Set(ref passRate, value); |
| | | } |
| | | } |
| | | |
| | | public string NumInfo |
| | | { |
| | | get => this.lang("Ts.NumInfo", TotalNum, PassNum, FailNum, string.Format("{0:0.00}", PassRate)); |
| | | } |
| | | |
| | | public string TestInfo |
| | | { |
| | | get => this.lang("Ts.TestInfo", Barcode, ElapseTime); |
| | | } |
| | | |
| | | private string barcode = string.Empty; |
| | | public string Barcode |
| | | { |
| | | get { return barcode; } |
| | | set { Set(ref barcode, value); } |
| | | } |
| | | |
| | | private string model = string.Empty; |
| | | public string Model |
| | | { |
| | | get { return model; } |
| | | set { Set(ref model, value); } |
| | | } |
| | | |
| | | private string elapseTime = "00:00:00"; |
| | | public string ElapseTime |
| | | { |
| | | get { return elapseTime; } |
| | | set |
| | | { |
| | | if (value == "Start Timing") |
| | | { |
| | | this.refreshTimer.Enabled = true; |
| | | TestStartTime = DateTime.Now; |
| | | StepStartTime = DateTime.Now; |
| | | } |
| | | else |
| | | { |
| | | this.refreshTimer.Enabled = false; |
| | | if (value != "Stop Timing") |
| | | { |
| | | Set(ref elapseTime, value + "s"); |
| | | Set("TestInfo"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private string socketName = string.Empty; |
| | | public string SocketName |
| | | { |
| | | get { return socketName; } |
| | | set |
| | | { |
| | | Set(ref socketName, value); |
| | | } |
| | | } |
| | | |
| | | private ObservableCollection<TestStep> _datasource = new ObservableCollection<TestStep>(); |
| | | public ObservableCollection<TestStep> Datasource |
| | | { |
| | | get => _datasource; |
| | | set |
| | | { |
| | | Set(ref _datasource, value); |
| | | } |
| | | } |
| | | |
| | | private DateTime TestStartTime = new DateTime(); |
| | | private DateTime StepStartTime = new DateTime(); |
| | | private TimeSpan TimeCount = new TimeSpan(); |
| | | |
| | | private System.Timers.Timer refreshTimer; |
| | | |
| | | private string currentStep = ""; |
| | | public string CurrentStep |
| | | { |
| | | get => currentStep; |
| | | set |
| | | { |
| | | Set(ref currentStep, value); |
| | | } |
| | | } |
| | | |
| | | private int _probeCount; |
| | | public int ProbeCount |
| | | { |
| | | get => _probeCount; |
| | | set |
| | | { |
| | | Set(ref _probeCount, value); |
| | | try |
| | | { |
| | | if (ProbeWarn <= 0) probeCountAlarm.Visibility = Visibility.Collapsed; |
| | | else if (value >= ProbeWarn) probeCountAlarm.Visibility = Visibility.Visible; |
| | | else probeCountAlarm.Visibility = Visibility.Collapsed; |
| | | } |
| | | catch |
| | | { |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 进度值 |
| | | /// </summary> |
| | | private int _processValue; |
| | | public int ProcessValue |
| | | { |
| | | get => _processValue; |
| | | set |
| | | { |
| | | Set(ref _processValue, value); |
| | | } |
| | | } |
| | | |
| | | public string ProcessValueLabel |
| | | { |
| | | get |
| | | { |
| | | if (ProcessValue > 0) return $"{ProcessValue}%"; |
| | | return ""; |
| | | } |
| | | set |
| | | { |
| | | } |
| | | } |
| | | |
| | | public void SetSimpleUI(bool flag) |
| | | { |
| | | if (flag) |
| | | { |
| | | simplePanel.Visibility = Visibility.Visible; |
| | | detailPanel.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | simplePanel.Visibility = Visibility.Collapsed; |
| | | detailPanel.Visibility = Visibility.Visible; |
| | | } |
| | | } |
| | | |
| | | private int _probeWarn; |
| | | public int ProbeWarn |
| | | { |
| | | get => _probeWarn; |
| | | private set |
| | | { |
| | | Set(ref _probeWarn, value); |
| | | } |
| | | } |
| | | |
| | | private bool isAutoMode; |
| | | public bool IsAutoMode |
| | | { |
| | | get => isAutoMode; |
| | | set |
| | | { |
| | | Set(ref isAutoMode, value); |
| | | } |
| | | } |
| | | |
| | | public SingleControl(int Index, string CountIniPath, int probeWarn) |
| | | { |
| | | DataContext = this; |
| | | this.CountIniPath = CountIniPath; |
| | | this.index = Index; |
| | | ProbeWarn = probeWarn; |
| | | |
| | | refreshTimer = new System.Timers.Timer() |
| | | { |
| | | Interval = 100 |
| | | }; |
| | | refreshTimer.Elapsed += RefreshTimer_Elapsed; |
| | | ReadCounter(); |
| | | InitializeComponent(); |
| | | RefreshProbeCount(null); |
| | | try |
| | | { |
| | | var value = ParameterManager.GetCommonParameterByName("AutoTestMode"); |
| | | IsAutoMode = bool.Parse(value.Value); |
| | | } |
| | | catch |
| | | { |
| | | IsAutoMode = true; |
| | | } |
| | | } |
| | | |
| | | private void RefreshTimer_Elapsed(object sender, ElapsedEventArgs e) |
| | | { |
| | | TimeCount = DateTime.Now - TestStartTime; |
| | | elapseTime = string.Format("{0:00}:{1:00}:{2:00}", TimeCount.Hours, TimeCount.Minutes, TimeCount.Seconds); |
| | | Set("ElapseTime"); |
| | | Set("TestInfo"); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取TS的每步测试的数据添加到表格 |
| | | /// 项目名称,测试参数,测试值,上限,下限,单位,结果 |
| | | /// </summary> |
| | | /// <param name="TS_Data"></param> |
| | | public void AddTestData(string name, string step_name, string value, string HightLimit, string LowLimit, string unit, string Status) |
| | | { |
| | | try |
| | | { |
| | | var data = new TestStep |
| | | { |
| | | Name = name, |
| | | Step = step_name, |
| | | Value = value, |
| | | LimitUp = HightLimit, |
| | | LimitDown = LowLimit, |
| | | Unit = unit, |
| | | Result = Status |
| | | }; |
| | | table.Dispatcher.Invoke(new Action(() => |
| | | { |
| | | var now = DateTime.Now; |
| | | var span = now - StepStartTime; |
| | | StepStartTime = now; |
| | | data.ConsumeTime = $"{(span.TotalMilliseconds / 1000d).ToString("F2")}s"; |
| | | Datasource.Add(data); |
| | | table.ScrollIntoView(data); |
| | | })); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | //屏蔽错误 |
| | | } |
| | | } |
| | | |
| | | public void UpdateStatus(string status) |
| | | { |
| | | if (status == "Passed") |
| | | { |
| | | UpdateTestCounter(true); |
| | | } |
| | | else if (status == "Error" || status == "Failed") |
| | | { |
| | | UpdateTestCounter(false); |
| | | } |
| | | else if (Status != "Testing" && status == "Testing") |
| | | { |
| | | UpdateProbeCount(ProbeCount + 1); |
| | | } |
| | | Status = status; |
| | | } |
| | | |
| | | public void IniTestData() |
| | | { |
| | | Datasource.Clear(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 设置测试进度条 |
| | | /// </summary> |
| | | /// <param name="ProcessVal"></param> |
| | | public void SetTestProcess(int ProcessVal) |
| | | { |
| | | ProcessValue = ProcessVal; |
| | | Set("ProcessValueLabel"); |
| | | } |
| | | |
| | | private void UserControl_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | StartTcpServer(); |
| | | this.UpdateTestCounter(null); |
| | | Loaded -= UserControl_Loaded; |
| | | } |
| | | |
| | | private void UpdateTestCounter(bool? result) |
| | | { |
| | | if (result == true) |
| | | { |
| | | PassNum += 1; |
| | | //Interlocked.Increment(ref passNum); |
| | | //Set("PassNum"); |
| | | } |
| | | else if (result == false) |
| | | { |
| | | FailNum += 1; |
| | | //Interlocked.Increment(ref failNum); |
| | | //Set("FailNum"); |
| | | } |
| | | |
| | | TotalNum = PassNum + FailNum; |
| | | if (TotalNum != 0) |
| | | { |
| | | PassRate = ((double)PassNum / TotalNum); |
| | | } |
| | | else |
| | | { |
| | | PassRate = 0.0; |
| | | } |
| | | Set("NumInfo"); |
| | | this.WriteCounter(); |
| | | } |
| | | |
| | | private void WriteCounter() |
| | | { |
| | | IniFileHelper.WriteIniData("Socket" + this.index.ToString(), "Pass", |
| | | this.passNum.ToString(), this.CountIniPath); |
| | | IniFileHelper.WriteIniData("Socket" + this.index.ToString(), "Fail", this.failNum.ToString(), |
| | | this.CountIniPath); |
| | | } |
| | | |
| | | private void ReadCounter() |
| | | { |
| | | try |
| | | { |
| | | this.PassNum = Convert.ToInt32(IniFileHelper.ReadIniData("Socket" + this.index.ToString(), "Pass", "", CountIniPath)); |
| | | this.FailNum = Convert.ToInt32(IniFileHelper.ReadIniData("Socket" + this.index.ToString(), "Fail", "", CountIniPath)); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | //do nothing |
| | | } |
| | | } |
| | | |
| | | public void ResetCounter() |
| | | { |
| | | this.PassNum = 0; |
| | | this.FailNum = 0; |
| | | this.UpdateTestCounter(null); |
| | | } |
| | | |
| | | public void WriteToExcel(string path) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | try |
| | | { |
| | | string suffix = Path.GetExtension(path); |
| | | if (!string.Equals(".xlsx", suffix, StringComparison.OrdinalIgnoreCase) && |
| | | !string.Equals(".xls", suffix, StringComparison.OrdinalIgnoreCase)) |
| | | { |
| | | path = path.Replace(suffix, ".xls"); |
| | | } |
| | | |
| | | FileInfo fi = new FileInfo(path); |
| | | if (!fi.Directory.Exists) |
| | | { |
| | | fi.Directory.Create(); //文件不存在,创建文件 |
| | | } |
| | | List<string[]> rows = new List<string[]>(); |
| | | rows.Add(new string[] { "测试时间", "测试人", "测试设备", "条形码", "测试结果" }); |
| | | rows.Add(new string[] { DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), UserInfo.User.Name, ParamSetting.StationName, Barcode, Status }); |
| | | rows.Add(new string[] { "项目名称", "测试参数", "测试值", "上限", "下限", "单位", "结果" }); |
| | | if (Datasource != null && Datasource.Count > 0) |
| | | { |
| | | foreach (var data in Datasource) |
| | | { |
| | | rows.Add(new string[] { data.Name, data.Step, data.Value, data.LimitUp, data.LimitDown, data.Unit, data.Result }); |
| | | } |
| | | } |
| | | ExcelHelper.ExportExcel(path, rows); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | StandardTestUI.logger.Error("Write CSV File Failed.", ex); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | |
| | | public void WriteToCSV(string path) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | try |
| | | { |
| | | |
| | | FileInfo fi = new FileInfo(path); |
| | | if (!fi.Directory.Exists) |
| | | { |
| | | fi.Directory.Create(); //文件不存在,创建文件 |
| | | } |
| | | |
| | | using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write)) |
| | | using (StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.UTF8)) |
| | | { |
| | | string title = "项目名称,测试参数,测试值,上限,下限,单位,结果"; |
| | | sw.WriteLine(title); |
| | | |
| | | if (Datasource != null && Datasource.Count > 0) |
| | | { |
| | | foreach (var data in Datasource) |
| | | { |
| | | sw.WriteLine($"{data.Name},{data.Step},{data.Value},{data.LimitUp},{data.LimitDown},{data.Unit},{data.Result}"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | StandardTestUI.logger.Error("Write CSV File Failed.", ex); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | #region TCP相关 |
| | | |
| | | private string tcpServerStatus; |
| | | public string TcpServerStatus |
| | | { |
| | | get => tcpServerStatus; |
| | | set |
| | | { |
| | | Set(ref tcpServerStatus, value); |
| | | } |
| | | } |
| | | |
| | | private string tcpServerInfo; |
| | | public string TcpServerInfo |
| | | { |
| | | get => tcpServerInfo; |
| | | set |
| | | { |
| | | Set(ref tcpServerInfo, value); |
| | | } |
| | | } |
| | | |
| | | private TcpServer tcpServer; |
| | | |
| | | private int GetConfigPort() |
| | | { |
| | | try |
| | | { |
| | | var customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig"); |
| | | if (customConfig.ContainsKey("PortStart")) |
| | | { |
| | | int port = int.Parse(customConfig["PortStart"].ToString()); |
| | | return port; |
| | | } |
| | | else |
| | | { |
| | | return 6660; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | return 6660; |
| | | } |
| | | } |
| | | |
| | | private void StartTcpServer() |
| | | { |
| | | int port = GetConfigPort(); |
| | | var serverConfig = new ServerConfig() |
| | | { |
| | | MaxRequestLength = 1024 * 1024, |
| | | Port = port + index |
| | | }; |
| | | try |
| | | { |
| | | if (tcpServer == null) |
| | | { |
| | | tcpServer = new TcpServer(); |
| | | tcpServer.NewRequestReceived += TcpServer_NewRequestReceived; |
| | | if (!tcpServer.Setup(serverConfig)) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed")); |
| | | return; |
| | | } |
| | | if (!tcpServer.Start()) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed")); |
| | | return; |
| | | } |
| | | ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running")); |
| | | } |
| | | else if (tcpServer.State != SuperSocket.SocketBase.ServerState.Running) |
| | | { |
| | | if (!tcpServer.Setup(serverConfig)) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed")); |
| | | return; |
| | | } |
| | | if (!tcpServer.Start()) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed")); |
| | | return; |
| | | } |
| | | ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running")); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", $"{ex.Message}"); |
| | | tcpServer?.Stop(); |
| | | tcpServer = null; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新Tcp服务器状态 |
| | | /// </summary> |
| | | /// <param name="status"></param> |
| | | /// <param name="info"></param> |
| | | private void ChangeTcpServerStatus(string status, string info) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | TcpServerStatus = status; |
| | | TcpServerInfo = info; |
| | | }); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 停止Tcp服务器 |
| | | /// </summary> |
| | | private void StopTcpServer() |
| | | { |
| | | try |
| | | { |
| | | tcpServer?.Stop(); |
| | | tcpServer = null; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | } |
| | | } |
| | | |
| | | private void TcpServer_NewRequestReceived(TcpSession session, StringRequestInfo requestInfo) |
| | | { |
| | | string msg = requestInfo.Body; |
| | | StandardTestUI.logger.Debug($"TCP Data From {session.RemoteEndPoint}: {msg}."); |
| | | string[] datas_10015 = msg.Split(';'); |
| | | int cellIndex = int.Parse(datas_10015[1]); |
| | | |
| | | string strs = datas_10015[2]; |
| | | string[] values = strs.Split(','); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | AddTestData("", values[0], values[2], values[4], values[5], values[3], values[6]); |
| | | //new Info |
| | | //{ |
| | | // step = values[0], |
| | | // value = values[2], |
| | | // unit = values[3], |
| | | // limitUp = values[4], |
| | | // limitDown = values[5], |
| | | // result = values[6], |
| | | // updateTime = values[7] |
| | | //} |
| | | }); |
| | | //session.Send("1"); |
| | | } |
| | | #endregion |
| | | |
| | | public void ShowPausePanel() |
| | | { |
| | | pausePanel.Visibility = Visibility.Visible; |
| | | } |
| | | |
| | | private void Button_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | Button btn = sender as Button; |
| | | switch (btn.Name) |
| | | { |
| | | case "clear": |
| | | UpdateProbeCount(0); |
| | | break; |
| | | case "cancelPauseBtn": |
| | | parent.testExecutionWrapper.SetStationGlobalValue($"StationState.IsContinue_{index}", 1, false); |
| | | pausePanel.Visibility = Visibility.Collapsed; |
| | | break; |
| | | case "continuePauseBtn": |
| | | parent.testExecutionWrapper.SetStationGlobalValue($"StationState.IsContinue_{index}", 1, true); |
| | | pausePanel.Visibility = Visibility.Collapsed; |
| | | break; |
| | | case "scannerSetting": |
| | | //ScannerSetting setting = new ScannerSetting() |
| | | //{ |
| | | // Owner = Window.GetWindow(this) |
| | | //}; |
| | | //setting.Open(true, $"Socket{index}", CountIniPath); |
| | | ShowScannerPanel(); |
| | | break; |
| | | case "scannerOkBtn": |
| | | SendSnr2Ts(); |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | public void ShowScannerPanel() |
| | | { |
| | | //scannerPanel.Visibility = Visibility.Visible; |
| | | } |
| | | |
| | | private void SendSnr2Ts() |
| | | { |
| | | |
| | | } |
| | | |
| | | private void pausePanel_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | if (sender == pausePanel) |
| | | { |
| | | var can = pausePanel; |
| | | var modal = visualModal; |
| | | Canvas.SetLeft(modal, (pausePanel.ActualWidth - visualModal.ActualWidth) / 2); |
| | | Canvas.SetTop(modal, (pausePanel.ActualHeight - visualModal.ActualHeight) / 2); |
| | | } |
| | | else |
| | | { |
| | | //var can = scannerPanel; |
| | | //var modal = scannerVisualModal; |
| | | //Canvas.SetLeft(modal, (scannerPanel.ActualWidth - scannerVisualModal.ActualWidth) / 2); |
| | | //Canvas.SetTop(modal, (scannerPanel.ActualHeight - scannerVisualModal.ActualHeight) / 2); |
| | | } |
| | | } |
| | | |
| | | public void RefreshScreen(int[][] configs) |
| | | { |
| | | //foreach (UIElement item in canvas.Children) |
| | | //{ |
| | | // if (item is ScreenTouch) |
| | | // { |
| | | // canvas.Children.Remove(item); |
| | | // } |
| | | //} |
| | | screenContainer.Child = null; |
| | | |
| | | |
| | | screenGrid = new ScreenGrid(1, configs.Length); |
| | | |
| | | screens = new ScreenTouch[configs.Length]; |
| | | |
| | | int space = 2 * (screens.Length + 1); |
| | | for (int i = 0; i < configs.Length; i++) |
| | | { |
| | | var screen = new ScreenTouch(configs[i][0], configs[i][1]) |
| | | { |
| | | Width = Math.Max((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length, 0), |
| | | Height = Math.Max((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length / 16 * 9, 0) |
| | | }; |
| | | screens[i] = screen; |
| | | screenGrid.AddScreen(screen, 0, i); |
| | | } |
| | | screenContainer.Child = screenGrid; |
| | | |
| | | //canvas.Children.Add(screens[0]); |
| | | //Canvas.SetBottom(screens[0], 0); |
| | | //Canvas.SetRight(screens[0], 0); |
| | | //Panel.SetZIndex(screens[0], 999); |
| | | } |
| | | |
| | | public void DrawScreenPoint(int index, int x, int y) |
| | | { |
| | | screens[index].DrawPoint(x, y); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 绘图之前清空已有的 |
| | | /// </summary> |
| | | /// <param name="index"></param> |
| | | /// <param name="points"></param> |
| | | public void DrawScreenPoints(int index, int[][] points) |
| | | { |
| | | screens[index].DrawPoints(points); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 绘图之前不清空已有的 |
| | | /// </summary> |
| | | /// <param name="index"></param> |
| | | /// <param name="points"></param> |
| | | public void DrawNewScreenPoints(int index, int[][] points) |
| | | { |
| | | screens[index].DrawNewPoints(points); |
| | | } |
| | | |
| | | public void ClearScreenPoint(int index) |
| | | { |
| | | screens[index].Clear(); |
| | | } |
| | | |
| | | public void RefreshProbeCount(ProductData product) |
| | | { |
| | | try |
| | | { |
| | | var num = IniFileHelper.ReadIniData($"Socket{this.index.ToString()}", "ProbeCount", "0", this.CountIniPath); |
| | | this.ProbeCount = int.Parse(num); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | this.ProbeCount = 0; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新探针使用次数 |
| | | /// </summary> |
| | | /// <param name="count"></param> |
| | | private void UpdateProbeCount(int count) |
| | | { |
| | | /// 写到配置文件 |
| | | IniFileHelper.WriteIniData("Socket" + this.index.ToString(), "ProbeCount", count.ToString(), this.CountIniPath); |
| | | ProbeCount = count; |
| | | } |
| | | |
| | | public void RefreshProduct(ProductData product) |
| | | { |
| | | if (product == null) |
| | | { |
| | | screenContainer.Child = null; |
| | | return; |
| | | } |
| | | try |
| | | { |
| | | string screenSize = ProductManager.GetParameterByProductAndName(product.Name, "ScreenTouch")?.Value; |
| | | if (string.IsNullOrEmpty(screenSize)) |
| | | { |
| | | screenContainer.Child = null; |
| | | return; |
| | | } |
| | | string[] arr = screenSize.Split(';'); |
| | | int[][] configs = new int[arr.Length][]; |
| | | |
| | | for (int i = 0; i < arr.Length; i++) |
| | | { |
| | | string[] size = arr[i].Split(','); |
| | | configs[i] = new int[2] { int.Parse(size[0]), int.Parse(size[1]) }; |
| | | } |
| | | RefreshScreen(configs); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | StandardTestUI.logger.Error("RefreshProduct Failed.", ex); |
| | | screenContainer.Child = null; |
| | | } |
| | | } |
| | | |
| | | private void control_SizeChanged(object sender, SizeChangedEventArgs e) |
| | | { |
| | | if (screenGrid == null) |
| | | { |
| | | return; |
| | | } |
| | | //screenGrid.Height = tableHeader.ActualHeight - 2; |
| | | //screenGrid.Width = tableHeader.ActualHeight / 9 * 16 - 2; |
| | | |
| | | int space = 2 * (screens.Length + 1); |
| | | |
| | | for (int i = 0; i < screens.Length; i++) |
| | | { |
| | | var screen = screens[i]; |
| | | screen.Width = Math.Max(Math.Floor((tableHeader.ActualHeight / 9 * 16 - space) / screens.Length), 0); |
| | | screen.Height = Math.Max(Math.Floor(screen.Width / 16 * 9), 0); |
| | | } |
| | | } |
| | | |
| | | private void screenContainer_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) |
| | | { |
| | | if (e.ClickCount == 2) |
| | | { |
| | | new ScreenTouchWin(screens) |
| | | { |
| | | Owner = Window.GetWindow(this) |
| | | }.ShowDialog(); |
| | | } |
| | | } |
| | | |
| | | private void control_PreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) |
| | | { |
| | | if (parent.EnableManualDebug) |
| | | { |
| | | ContextMenu contextMenu = new ContextMenu(); |
| | | contextMenu.Closed += ContextMenu_Closed; |
| | | var item = new MenuItem |
| | | { |
| | | IsCheckable = false, |
| | | Name = "manualDebugItem", |
| | | Header = this.lang("手动调试"), |
| | | }; |
| | | item.Click += Item_Click; ; |
| | | contextMenu.Items.Add(item); |
| | | this.ContextMenu = contextMenu; |
| | | } |
| | | else |
| | | { |
| | | |
| | | this.ContextMenu = null; |
| | | } |
| | | } |
| | | |
| | | private void Item_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | string manualDebugSeq = ParameterManager.GetCommonParameterByName("ManualDebugSeqPath")?.Value; |
| | | if (string.IsNullOrEmpty(manualDebugSeq)) |
| | | { |
| | | MsgBoxHelper2.Error("请先设置手动调试序列路径[ManualDebugSeqPath]!"); |
| | | return; |
| | | } |
| | | string modelPath = parent.tsModelPath; |
| | | var win = new ManualDebugWindow(manualDebugSeq, modelPath, false) |
| | | { |
| | | Title = $"工位{this.index}手动调试", |
| | | |
| | | Owner = Window.GetWindow(this), |
| | | OvenIndex = index, |
| | | OvenName = "", |
| | | CellCount = parent.tsSockets.Count, |
| | | |
| | | CellIndex = 0, |
| | | CellName = "", |
| | | }; |
| | | win.ShowDialog(); |
| | | } |
| | | |
| | | private void ContextMenu_Closed(object sender, RoutedEventArgs e) |
| | | { |
| | | this.ContextMenu = null; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <hb:PluginMaskControl x:Class="Plugin.StandardTest.StandardTestUI" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | mc:Ignorable="d" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | xmlns:uic="clr-namespace:HMI.UI.Converter;assembly=HMI.UI" |
| | | xmlns:pw="https://opensource.panuon.com/wpf" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF" |
| | | xmlns:sys="clr-namespace:System;assembly=mscorlib" |
| | | xmlns:ui="clr-namespace:HMI.UI;assembly=HMI.UI" |
| | | Padding="2,5,2,5" |
| | | Loaded="PluginControl_Loaded" |
| | | SizeChanged="PluginControl_SizeChanged" |
| | | Background="{DynamicResource WorkspaceBackground}" |
| | | d:DesignHeight="450" d:DesignWidth="800"> |
| | | <UserControl.Resources> |
| | | <ResourceDictionary> |
| | | <sys:Double x:Key="tableFontSize">20</sys:Double> |
| | | <Style x:Key="FormComboxStyle" TargetType="ComboBox" BasedOn="{StaticResource FormComboxStyle}"> |
| | | <Setter Property="Height" Value="28"/> |
| | | </Style> |
| | | </ResourceDictionary> |
| | | </UserControl.Resources> |
| | | |
| | | <Grid> |
| | | <Grid> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition /> |
| | | <ColumnDefinition Width="Auto"/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition/> |
| | | </Grid.RowDefinitions> |
| | | <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost> |
| | | |
| | | <DockPanel Grid.Row="0" Grid.ColumnSpan="2" Margin="5,0,5,0" LastChildFill="False"> |
| | | <DockPanel.Resources> |
| | | <Style TargetType="CheckBox"> |
| | | <Setter Property="FontSize" Value="20"/> |
| | | <Setter Property="VerticalContentAlignment" Value="Center"/> |
| | | </Style> |
| | | </DockPanel.Resources> |
| | | |
| | | <StackPanel Orientation="Horizontal" VerticalAlignment="Center" dp:StackPanelHelper.ChildSpace="10"> |
| | | <pu:FormGroup HeaderHeight="Auto" HeaderPadding="0" x:Name="selectModelPanel" Visibility="Collapsed"> |
| | | <pu:FormGroup.Header> |
| | | <TextBlock FontSize="20" Foreground="#000" Text="{DynamicResource Ts.SelectProductModel}"/> |
| | | </pu:FormGroup.Header> |
| | | <ComboBox Style="{StaticResource FormComboxStyle}" MinWidth="180" ItemsSource="{Binding Products}" |
| | | DisplayMemberPath="Name" SelectedValuePath="Name" |
| | | SelectedItem="{Binding SelectedProduct}" x:Name="CbProduct"/> |
| | | </pu:FormGroup> |
| | | |
| | | <pu:FormGroup HeaderHeight="Auto" HeaderPadding="0" x:Name="autoModelPanel" Visibility="Collapsed"> |
| | | <pu:FormGroup.Header> |
| | | <TextBlock FontSize="20" Foreground="#000" Text="当前产品"/> |
| | | </pu:FormGroup.Header> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" MinWidth="180" Text="{Binding CurrentProductModel}"/> |
| | | </pu:FormGroup> |
| | | |
| | | <CheckBox x:Name="CbEnableMes" |
| | | IsChecked="{Binding EnableMes,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.EnableMes}" |
| | | /> |
| | | <CheckBox x:Name="CbEnableNGPause" |
| | | IsChecked="{Binding EnableNGPause,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.EnableNGPause}" |
| | | /> |
| | | <CheckBox x:Name="CbEnableDebugMode" |
| | | IsChecked="{Binding EnableDebugMode,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.DebugMode}"/> |
| | | |
| | | <CheckBox x:Name="CbDisableScanner" |
| | | IsChecked="{Binding DisableScanner,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.BlockScanner}"/> |
| | | |
| | | <CheckBox x:Name="CbEnableSimpleMode" |
| | | IsChecked="{Binding EnableSimpleMode,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.EnableSimpleMode}"/> |
| | | |
| | | <CheckBox x:Name="CbUI_ShowInfoDock" |
| | | IsChecked="{Binding UI_ShowInfoDock,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.UI_ShowInfoDock}"/> |
| | | |
| | | <CheckBox FontSize="20" x:Name="Cb_ManualDebug" Visibility="Collapsed" |
| | | IsChecked="{Binding EnableManualDebug,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | VerticalContentAlignment="Center" Content="手动调试" /> |
| | | |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="5"> |
| | | <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0" |
| | | Content="{DynamicResource HMI.User.Code}" /> |
| | | <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0" |
| | | Content="{Binding CurrentUserCode}" /> |
| | | </StackPanel> |
| | | |
| | | |
| | | <Label x:Name="workorderPanel" VerticalContentAlignment="Center" Visibility="Collapsed" Padding="0" Margin="0" MouseDoubleClick="Label_MouseDoubleClick"> |
| | | <StackPanel Orientation="Horizontal" |
| | | dp:StackPanelHelper.ChildSpace="5"> |
| | | <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0" |
| | | Content="{DynamicResource Ts.WorkOrder}"/> |
| | | <TextBlock x:Name="TbWorkOrder" FontSize="20" MinWidth="50" Text="{Binding CurrentWorkorder}"/> |
| | | <!--<ui:EditBox FontSize="20" x:Name="workOrderEditor" MinWidth="50" |
| | | CanEdit="True" |
| | | ShowEmpty="True" |
| | | PreEditing="RequirePassword" |
| | | Value="{Binding CurrentWorkorder,Mode=TwoWay}"/>--> |
| | | </StackPanel> |
| | | </Label> |
| | | |
| | | <!--<StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="5"> |
| | | <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0" Content="字体大小"/> |
| | | <ComboBox Name="CbFontSize" Width="60" ItemsSource="{Binding FontSizeSource}" |
| | | SelectionChanged="CbFontSize_SelectionChanged"/> |
| | | </StackPanel>--> |
| | | </StackPanel> |
| | | |
| | | |
| | | |
| | | <StackPanel DockPanel.Dock="Right" Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="10"> |
| | | <Button Style="{StaticResource SuccessButton}" |
| | | x:Name="BtnStartSeq" Click="Button_Click" |
| | | Content="{DynamicResource Ts.BtnStartSeq}"/> |
| | | <Button Grid.Row="1" Style="{StaticResource DangerButton}" IsEnabled="False" |
| | | x:Name="BtnStopSeq" Click="Button_Click" |
| | | Content="{DynamicResource Ts.BtnStopSeq}"/> |
| | | <Button Grid.Row="2" Style="{StaticResource PrimaryButton}" |
| | | x:Name="BtnClearNum" Click="Button_Click" |
| | | Content="{DynamicResource Ts.BtnClearNum}"/> |
| | | <Button Grid.Row="3" |
| | | x:Name="BtnSample" Click="Button_Click"> |
| | | <Button.Style> |
| | | <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding SampleStatus}" Value="True"> |
| | | <Setter Property="Content" Value="{DynamicResource Ts.BtnIsSample}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding SampleStatus}" Value="False"> |
| | | <Setter Property="Content" Value="{DynamicResource Ts.BtnNotSample}"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Button.Style> |
| | | </Button> |
| | | </StackPanel> |
| | | </DockPanel> |
| | | |
| | | <Grid dp:GridHelper.Space="5" Grid.Row="1"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | |
| | | <UniformGrid x:Name="panel" Grid.Row="1" Columns="2" dp:UniformGridHelper.InnerBorder="1" |
| | | dp:UniformGridHelper.InnerBorderBrush="#3a3a3a"> |
| | | |
| | | |
| | | </UniformGrid> |
| | | <!--<Border x:Name="alarmPanel" Height="80" VerticalAlignment="Bottom" Visibility="Visible" Background="Red"/> |
| | | <Border Grid.Row="1" x:Name="alarmPanel" Visibility="{Binding AlarmVisiable,Converter={StaticResource vbConverter}}" |
| | | Height="50" Background="Red"/>--> |
| | | </Grid> |
| | | |
| | | |
| | | |
| | | <Grid Grid.Row="1" Grid.Column="1" x:Name="detailPanel"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition /> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <!--<GroupBox> |
| | | <pu:FormGroup Orientation="Vertical" |
| | | HeaderHeight="Auto" |
| | | HeaderPadding="0" Header="{DynamicResource Ts.SelectProductModel}"> |
| | | <ComboBox Style="{StaticResource FormComboxStyle}" |
| | | ItemsSource="{Binding Products}" |
| | | DisplayMemberPath="Name" SelectedValuePath="Name" |
| | | SelectedItem="{Binding SelectedProduct}" |
| | | x:Name="CbProduct"/> |
| | | </pu:FormGroup> |
| | | </GroupBox>--> |
| | | |
| | | <GroupBox Grid.Row="2"> |
| | | <StackPanel> |
| | | <pu:FormGroup Header="{DynamicResource Ts.MesStatus}" GroupName="Number"> |
| | | <Label Grid.Row="1"> |
| | | <Label.Style> |
| | | <Style TargetType="Label"> |
| | | <Setter Property="Foreground" Value="#FFF"/> |
| | | <Setter Property="HorizontalContentAlignment" Value="Center"/> |
| | | <Setter Property="FontSize" Value="16"/> |
| | | <Setter Property="FontWeight" Value="Bold"/> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding EnableMes}" Value="True"> |
| | | <Setter Property="Background" Value="Green"/> |
| | | <Setter Property="Content" Value="{DynamicResource Ts.MesStatus.Enable}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding EnableMes}" Value="False"> |
| | | <Setter Property="Background" Value="Red"/> |
| | | <Setter Property="Content" Value="{DynamicResource Ts.MesStatus.Disable}"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Label.Style> |
| | | </Label> |
| | | </pu:FormGroup> |
| | | <pu:FormGroup Header="{DynamicResource Ts.Total}" GroupName="Number"> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" |
| | | x:Name="TbTotal"/> |
| | | </pu:FormGroup> |
| | | <pu:FormGroup Header="{DynamicResource Ts.Pass}" GroupName="Number"> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" |
| | | x:Name="TbPass"/> |
| | | </pu:FormGroup> |
| | | <pu:FormGroup Header="{DynamicResource Ts.Fail}" GroupName="Number"> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" |
| | | x:Name="TbFail"/> |
| | | </pu:FormGroup> |
| | | <pu:FormGroup Header="{DynamicResource Ts.PassRate}" GroupName="Number"> |
| | | <TextBox Style="{StaticResource EditorTextBoxStyle}" IsReadOnly="True" |
| | | x:Name="TbPassRate"/> |
| | | </pu:FormGroup> |
| | | </StackPanel> |
| | | </GroupBox> |
| | | |
| | | <GroupBox Grid.Row="3"> |
| | | <lvc:PieChart Grid.Row="3" x:Name="pie" Grid.Column="1" Background="#FFF" BorderThickness="1" BorderBrush="{DynamicResource CommonBoarder}" |
| | | Title="{Binding Chart1Title}" |
| | | TooltipTextPaint="{Binding TooltipTextPaint}" |
| | | Padding="0" |
| | | Margin="2" |
| | | Series="{Binding Chart1_series}" |
| | | LegendPosition="Top"> |
| | | </lvc:PieChart> |
| | | </GroupBox> |
| | | |
| | | <!-- <GroupBox Grid.Row="4"> |
| | | <Grid dp:GridHelper.Space="20"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | </Grid.RowDefinitions> |
| | | <Button Style="{StaticResource SuccessButton}" |
| | | x:Name="BtnStartSeq" Click="Button_Click" |
| | | Content="{DynamicResource Ts.BtnStartSeq}"/> |
| | | <Button Grid.Row="1" Style="{StaticResource DangerButton}" IsEnabled="False" |
| | | x:Name="BtnStopSeq" Click="Button_Click" |
| | | Content="{DynamicResource Ts.BtnStopSeq}"/> |
| | | <Button Grid.Row="2" Style="{StaticResource PrimaryButton}" |
| | | x:Name="BtnClearNum" Click="Button_Click" |
| | | Content="{DynamicResource Ts.BtnClearNum}"/> |
| | | <Button Grid.Row="3" |
| | | x:Name="BtnSample" Click="Button_Click"> |
| | | <Button.Style> |
| | | <Style TargetType="Button" BasedOn="{StaticResource PrimaryButton}"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding SampleStatus}" Value="True"> |
| | | <Setter Property="Content" Value="{DynamicResource Ts.BtnIsSample}"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding SampleStatus}" Value="False"> |
| | | <Setter Property="Content" Value="{DynamicResource Ts.BtnNotSample}"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Button.Style> |
| | | </Button> |
| | | |
| | | <pu:Switch Grid.Row="3" BoxWidth="120" |
| | | CheckedBackground="{DynamicResource PrimaryColor}" |
| | | CheckedBorderBrush="#36abff" |
| | | ToggleBrush="#030303" |
| | | CheckedForeground="White" |
| | | CheckedToggleBrush="#FFFFFF" |
| | | IsChecked="{Binding SampleStatus,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | ToggleSize="25" Cursor="Hand" > |
| | | <pu:Switch.BoxTemplate> |
| | | <DataTemplate> |
| | | <TextBlock x:Name="text" |
| | | Text="{Binding Converter={StaticResource lc},ConverterParameter='Ts.BtnNotSample'}" |
| | | Padding="30,0" |
| | | VerticalAlignment="Center" HorizontalAlignment="Center"/> |
| | | <DataTemplate.Triggers> |
| | | <DataTrigger Binding="{Binding IsChecked, |
| | | RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=pu:Switch,AncestorLevel=1}}" |
| | | Value="True"> |
| | | <Setter TargetName="text" Property="Text" |
| | | Value="{Binding Converter={StaticResource lc},ConverterParameter='Ts.BtnIsSample'}"/> |
| | | <Setter TargetName="text" Property="Foreground" Value="{DynamicResource PrimaryColorContract}"/> |
| | | </DataTrigger> |
| | | </DataTemplate.Triggers> |
| | | </DataTemplate> |
| | | </pu:Switch.BoxTemplate> |
| | | </pu:Switch> |
| | | </Grid> |
| | | </GroupBox> --> |
| | | </Grid> |
| | | </Grid> |
| | | </Grid> |
| | | </hb:PluginMaskControl> |
| New file |
| | |
| | | using HMI.Base; |
| | | using HMI.Core.Api; |
| | | using HMI.Core.Entity; |
| | | using HMI.Core.Manager; |
| | | using HMI.Core.Setting; |
| | | using HMI.Core.Util; |
| | | using HMI.Simple.Util; |
| | | using HMI.UI.Util; |
| | | using HMI.Util; |
| | | using LiveChartsCore; |
| | | using LiveChartsCore.SkiaSharpView; |
| | | using LiveChartsCore.SkiaSharpView.Painting; |
| | | using log4net; |
| | | using NationalInstruments.TestStand.Interop.API; |
| | | using NPOI.HSSF.UserModel; |
| | | using NPOI.SS.UserModel; |
| | | using Plugin.TsSupport; |
| | | using SkiaSharp; |
| | | using System.Collections.ObjectModel; |
| | | using System.ComponentModel; |
| | | using System.IO; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | /// <summary> |
| | | /// UserControl1.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class StandardTestUI : PluginMaskControl |
| | | { |
| | | #region TestStand事件 |
| | | /// <summary> |
| | | /// TS 引擎 |
| | | /// </summary> |
| | | //private Engine engine = null; |
| | | |
| | | ///// <summary> |
| | | ///// 测试序列 |
| | | ///// </summary> |
| | | //private SequenceFile testSeq = null; |
| | | |
| | | ///// <summary> |
| | | ///// 测试模型序列 |
| | | ///// </summary> |
| | | //private SequenceFile modeSeq = null; |
| | | |
| | | //private Execution execution = null; |
| | | |
| | | private string testSeqFile = string.Empty; |
| | | |
| | | public ITestExecutionWrapper testExecutionWrapper { get; private set; } |
| | | #endregion |
| | | |
| | | public static ILog logger { get; private set; } |
| | | public List<SingleControl> tsSockets { get; private set; } = new List<SingleControl>(); |
| | | /// <summary> |
| | | /// 样件模式状态 |
| | | /// </summary> |
| | | private bool sampleStatus; |
| | | public bool SampleStatus |
| | | { |
| | | get { return sampleStatus; } |
| | | set |
| | | { |
| | | Set(ref sampleStatus, value); |
| | | SampleTest(value); |
| | | } |
| | | } |
| | | |
| | | private string _currentUserCode; |
| | | |
| | | public string CurrentUserCode |
| | | { |
| | | get { return _currentUserCode; } |
| | | set { Set(ref _currentUserCode, value); } |
| | | } |
| | | |
| | | |
| | | private bool isLoadOK = false; |
| | | |
| | | private int PassAmount = 0; |
| | | private int FailAmount = 0; |
| | | |
| | | /// <summary> |
| | | /// Socket数量 |
| | | /// </summary> |
| | | private int socketNum = 1; |
| | | |
| | | private ProductData selectedProduct; |
| | | |
| | | private MSMQHelper mSMQHelper = new MSMQHelper("HMI_Alarm"); |
| | | |
| | | public ProductData SelectedProduct |
| | | { |
| | | get { return selectedProduct; } |
| | | set |
| | | { |
| | | Set(ref selectedProduct, value); |
| | | CurrentProductModel = value?.Name; |
| | | if (tsSockets != null) |
| | | { |
| | | foreach (SingleControl socket in tsSockets) |
| | | { |
| | | socket.RefreshProduct(value); |
| | | // socket.RefreshProbeCount(value); |
| | | } |
| | | } |
| | | SetTestSeqFile(value); |
| | | } |
| | | } |
| | | |
| | | private string currentProductModel; |
| | | public string CurrentProductModel |
| | | { |
| | | get => currentProductModel; |
| | | set |
| | | { |
| | | Set(ref currentProductModel, value); |
| | | } |
| | | } |
| | | |
| | | public override void SetLogger(ILog logger) |
| | | { |
| | | base.SetLogger(logger); |
| | | StandardTestUI.logger = logger; |
| | | } |
| | | |
| | | private ObservableCollection<ProductData> products; |
| | | |
| | | public ObservableCollection<ProductData> Products |
| | | { |
| | | get { return products; } |
| | | set |
| | | { |
| | | Set(ref products, value); |
| | | } |
| | | } |
| | | |
| | | public List<double> FontSizeSource { get; set; } = new List<double> { |
| | | 10,11,12,13,14,15,16,17,18,19,20 |
| | | }; |
| | | |
| | | private string CountIniPath; |
| | | private bool IsOptRequirePassword; |
| | | |
| | | private bool alarmVisiable; |
| | | public bool AlarmVisiable |
| | | { |
| | | get => alarmVisiable; |
| | | set |
| | | { |
| | | Set(ref alarmVisiable, value); |
| | | } |
| | | } |
| | | |
| | | private bool _enableMes; |
| | | public bool EnableMes |
| | | { |
| | | get => _enableMes; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("EnableMes", value)) |
| | | { |
| | | Set(ref _enableMes, value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool _enableCanLog; |
| | | public bool EnableCanLog |
| | | { |
| | | get => _enableCanLog; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("EnableCanLog", value)) |
| | | { |
| | | Set(ref _enableCanLog, value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool _enableDebugMode; |
| | | public bool EnableDebugMode |
| | | { |
| | | get => _enableDebugMode; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("EnableDebugMode", value)) |
| | | { |
| | | Set(ref _enableDebugMode, value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool _enableNGPause; |
| | | public bool EnableNGPause |
| | | { |
| | | get => _enableNGPause; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("EnableNGPause", value)) |
| | | { |
| | | Set(ref _enableNGPause, value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool _enableSimpleMode; |
| | | public bool EnableSimpleMode |
| | | { |
| | | get => _enableSimpleMode; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("EnableSimpleMode", value)) |
| | | { |
| | | Set(ref _enableSimpleMode, value); |
| | | foreach (var cell in tsSockets) cell.SetSimpleUI(value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool _ui_showInfoDock; |
| | | public bool UI_ShowInfoDock |
| | | { |
| | | get => _ui_showInfoDock; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("UI_ShowInfoDock", value)) |
| | | { |
| | | Set(ref _ui_showInfoDock, value); |
| | | detailPanel.Visibility = value ? Visibility.Visible : Visibility.Collapsed; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool autoExecuteSeq; |
| | | public bool AutoExecuteSeq |
| | | { |
| | | get => autoExecuteSeq; |
| | | set |
| | | { |
| | | Set(ref autoExecuteSeq, value); |
| | | } |
| | | } |
| | | |
| | | private string currentWorkorder; |
| | | public string CurrentWorkorder |
| | | { |
| | | get => currentWorkorder; |
| | | set |
| | | { |
| | | if (WriteIni("UI_CurrentWorkorder", value)) |
| | | { |
| | | Set(ref currentWorkorder, value); |
| | | testExecutionWrapper?.SetStationGlobalValue("StationState.WorkOrder", 1, value); |
| | | testExecutionWrapper?.CommitToDisk(false); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool _DisableScanner; |
| | | public bool DisableScanner |
| | | { |
| | | get => _DisableScanner; |
| | | set |
| | | { |
| | | if (RequirePassword() == true) |
| | | { |
| | | if (WriteConfig("DisableScanner", value)) |
| | | { |
| | | Set(ref _DisableScanner, value); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private bool enableManualDebug; |
| | | public bool EnableManualDebug |
| | | { |
| | | get => enableManualDebug; |
| | | set |
| | | { |
| | | if (IsTesting) |
| | | { |
| | | MsgBoxHelper2.Warn("请先结束自动测试"); |
| | | return; |
| | | } |
| | | Set(ref enableManualDebug, value); |
| | | } |
| | | } |
| | | |
| | | public string tsModelPath { get; private set; } |
| | | private string dataLogPath; |
| | | private SqliteHelper sqliteHelper; |
| | | |
| | | private int probeWarn; |
| | | |
| | | #region 图表 |
| | | private SolidColorPaint tooltipTextPaint = new SolidColorPaint(SKColors.Black) |
| | | { |
| | | SKTypeface = SKFontManager.Default.MatchCharacter('汉') |
| | | }; |
| | | |
| | | public SolidColorPaint TooltipTextPaint |
| | | { |
| | | get => tooltipTextPaint; |
| | | set |
| | | { |
| | | Set(ref tooltipTextPaint, value); |
| | | } |
| | | } |
| | | |
| | | public ISeries[] Chart1_series { get; set; } = new PieSeries<double>[] |
| | | { |
| | | new PieSeries<double> { |
| | | Name="Passed", |
| | | Values = new double[]{ 0}, |
| | | DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer, |
| | | DataLabelsSize = 15, |
| | | DataLabelsPaint = new SolidColorPaint(new SKColor(30, 30, 30)){ |
| | | SKTypeface = SKFontManager.Default.MatchCharacter('汉') |
| | | }, |
| | | Fill = new SolidColorPaint(new SKColor(0, 128, 0)), |
| | | DataLabelsFormatter = |
| | | point =>point.StackedValue.Total>0? |
| | | $"Passed\r\n计数:{point.Coordinate.PrimaryValue}\r\n占比:{point.StackedValue.Share:P2}":"", |
| | | ToolTipLabelFormatter = point => $"{point.StackedValue.Share:P2}" |
| | | }, |
| | | new PieSeries<double> { Values = new double[]{ 0} , |
| | | Name="Failed", |
| | | DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer, |
| | | DataLabelsSize = 15, |
| | | DataLabelsPaint = new SolidColorPaint(new SKColor(30, 30, 30)){ |
| | | SKTypeface = SKFontManager.Default.MatchCharacter('汉') |
| | | }, |
| | | Fill = new SolidColorPaint(new SKColor(255, 0, 0)), |
| | | DataLabelsFormatter = |
| | | point =>point.StackedValue.Total>0? |
| | | $"Failed\r\n计数:{point.Coordinate.PrimaryValue}\r\n占比:{point.StackedValue.Share:P2}":"", |
| | | ToolTipLabelFormatter = point => $"{point.StackedValue.Share:P2}"}, |
| | | }; |
| | | #endregion |
| | | |
| | | public StandardTestUI() |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | |
| | | InitChart(); |
| | | InitTsEngine(); |
| | | } |
| | | |
| | | public override void SetBaseFolder(string folder) |
| | | { |
| | | CountIniPath = Path.Combine(folder, "Count.ini"); |
| | | base.SetBaseFolder(folder); |
| | | } |
| | | |
| | | private void CheckSqlHelper() |
| | | { |
| | | string today = DateTime.Now.ToString("yyyyMMdd"); |
| | | string dbPath = Path.Combine(dataLogPath, $"Data_{today}.db"); |
| | | if (!File.Exists(dbPath)) |
| | | { |
| | | SqliteHelper.CreateDatabase(dbPath); |
| | | } |
| | | if (sqliteHelper == null || sqliteHelper.DataBaseFile != dbPath) |
| | | { |
| | | sqliteHelper?.Dispose(); |
| | | sqliteHelper = new SqliteHelper(dbPath); |
| | | sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS serialData ( |
| | | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| | | sn TEXT NOT NULL, |
| | | model TEXT, |
| | | station TEXT NOT NULL, |
| | | step TEXT NOT NULL, |
| | | position TEXT, |
| | | limitDown TEXT, |
| | | limitUp TEXT, |
| | | unit TEXT, |
| | | offset TEXT, |
| | | value TEXT, |
| | | status TEXT, |
| | | finalStatus TEXT, |
| | | consumeTime TEXT, |
| | | totalConsumeTime TEXT, |
| | | time DATETIME NOT NULL);"); |
| | | sqliteHelper.ExecuteSql(@"CREATE TABLE IF NOT EXISTS testResult ( |
| | | id INTEGER PRIMARY KEY AUTOINCREMENT, |
| | | sn TEXT NOT NULL, |
| | | model TEXT, |
| | | station TEXT NOT NULL, |
| | | position TEXT, |
| | | status TEXT, |
| | | result TEXT NOT NULL, |
| | | time DATETIME NOT NULL);"); |
| | | } |
| | | } |
| | | |
| | | public override void InitFinish() |
| | | { |
| | | this.ReadCount(out int pass, out int fail); |
| | | this.DisplayCount(pass, fail); |
| | | ReadConfig(); |
| | | InitProducts(); |
| | | CheckSqlHelper(); |
| | | base.InitFinish(); |
| | | |
| | | if (ParamSetting.EnableAutoModel) |
| | | { |
| | | selectModelPanel.Visibility = Visibility.Collapsed; |
| | | autoModelPanel.Visibility = Visibility.Visible; |
| | | CurrentProductModel = string.Empty; |
| | | } |
| | | |
| | | try |
| | | { |
| | | UserInfo user = UserInfo.User; |
| | | CurrentUserCode = user?.Code; |
| | | |
| | | testExecutionWrapper?.SetStationGlobalValue("StationState.OPID", 1, CurrentUserCode); |
| | | testExecutionWrapper?.CommitToDisk(false); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn("Get Login User Failed.", ex); |
| | | } |
| | | RefreshUIConfig(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 刷新UI配置 |
| | | /// </summary> |
| | | private void RefreshUIConfig() |
| | | { |
| | | try |
| | | { |
| | | bool enableManualDebug = ParameterManager.GetCommonParameterByName("EnableManualDebug", false); |
| | | if (enableManualDebug) |
| | | { |
| | | Cb_ManualDebug.Visibility = Visibility.Visible; |
| | | EnableManualDebug = false; |
| | | } |
| | | else |
| | | { |
| | | Cb_ManualDebug.Visibility = Visibility.Collapsed; |
| | | EnableManualDebug = false; |
| | | } |
| | | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("RefreshUIConfig Error.", ex); |
| | | } |
| | | } |
| | | |
| | | //private MQConnector mqConnector; |
| | | //private void InitMQ() |
| | | //{ |
| | | // mqConnector = new MQConnector("UI"); |
| | | // mqConnector.BeginReceive(HandleMQUI); |
| | | //} |
| | | //void HandleMQUI(string msg) |
| | | //{ |
| | | // string[] arr = msg.Split('|'); |
| | | // int eventCode = int.Parse(arr[0]); |
| | | // int index = int.Parse(arr[1]); |
| | | // string strData = arr[2]; |
| | | // Dispatcher.BeginInvoke(new Action(() => |
| | | // { |
| | | // HandleUIMessage(eventCode, index, strData); |
| | | // })); |
| | | //} |
| | | |
| | | /// <summary> |
| | | /// 初始化Winform图表 |
| | | /// </summary> |
| | | private void InitChart() |
| | | { |
| | | // pieChart.Plot.Axes.Frameless(); |
| | | // pieChart.Plot.HideGrid(); |
| | | // pieChart.Plot.DataBackground.Color = ScottPlot.Colors.Transparent; |
| | | |
| | | // var legend = pieChart.Plot.ShowLegend(Alignment.UpperRight); |
| | | // legend.Alignment = Alignment.UpperRight; |
| | | // legend.Padding = new PixelPadding(0); |
| | | // legend.FontSize = (float)WidthSizeConverter.CountFontSize(20, 300, 300); |
| | | // pieChart.UserInputProcessor.Disable(); |
| | | // pieChart.Plot.Font.Automatic(); |
| | | |
| | | // List<PieSlice> slices = new List<PieSlice> { |
| | | //new PieSlice() { Value = 5, FillColor = ScottPlot.Colors.Green, |
| | | // LabelFontSize = (float)WidthSizeConverter.CountFontSize(20,300,300), |
| | | // Label = "Pass",LegendText="Pass" }, |
| | | //new PieSlice() { Value = 2, FillColor = ScottPlot.Colors.Red, |
| | | // LabelFontSize = (float)WidthSizeConverter.CountFontSize(20,300,300), |
| | | // Label = "Fail",LegendText="Fail" } }; |
| | | |
| | | |
| | | // pie = pieChart.Plot.Add.Pie(slices); |
| | | } |
| | | |
| | | private bool? RequirePassword() |
| | | { |
| | | if (!IsOptRequirePassword) return true; |
| | | var win = new RequirePasswordWindow |
| | | { |
| | | Owner = Window.GetWindow(this) |
| | | }; |
| | | return win.ShowDialog(); |
| | | } |
| | | |
| | | private void InitProducts() |
| | | { |
| | | RefershUI(); |
| | | //Products = ProductManager.GetProducts<ObservableCollection<ProductData>>(); |
| | | //if (Products != null && Products.Count > 0) |
| | | //{ |
| | | // string lastModel = testExecutionWrapper?.GetStationGlobalValue<string>("StationState.ProductModel", 1); |
| | | // var tempProduct = Products[0]; |
| | | // if (!string.IsNullOrEmpty(lastModel)) |
| | | // { |
| | | // var product = Products.FirstOrDefault(p => p.Name == lastModel); |
| | | // if (product != null) |
| | | // { |
| | | // tempProduct = product; |
| | | // } |
| | | // } |
| | | // SelectedProduct = tempProduct; |
| | | //} |
| | | //RefershUI(); |
| | | //if (tsSockets != null) |
| | | //{ |
| | | // foreach (SingleControl socket in tsSockets) |
| | | // { |
| | | // socket.RefreshProduct(SelectedProduct); |
| | | // } |
| | | //} |
| | | //SampleStatus = false; |
| | | } |
| | | |
| | | private bool _isTesting = false; |
| | | public bool IsTesting |
| | | { |
| | | get { return _isTesting; } |
| | | set |
| | | { |
| | | Set(ref _isTesting, value); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | BtnStartSeq.IsEnabled = !value; |
| | | BtnStopSeq.IsEnabled = value; |
| | | CbProduct.IsEnabled = !value; |
| | | }); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 初始化TestStand引擎 |
| | | /// </summary> |
| | | private void InitTsEngine() |
| | | { |
| | | if (ParamSetting.EngineType == "Tap") |
| | | { |
| | | testExecutionWrapper = new TapWrapper(); |
| | | } |
| | | else |
| | | { |
| | | testExecutionWrapper = new TeststandWrapper(); |
| | | } |
| | | |
| | | testExecutionWrapper.EndExecutionEvent += TestExecutionWrapper_EndExecutionEvent; |
| | | testExecutionWrapper.UIMessageEvent += TestExecutionWrapper_UIMessageEvent; |
| | | if (testExecutionWrapper is TeststandWrapper tsw) |
| | | { |
| | | var AxApplicationMgr = tsw.AxApplicationMgr; |
| | | ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).BeginInit(); |
| | | host.Child = AxApplicationMgr; |
| | | ((System.ComponentModel.ISupportInitialize)(AxApplicationMgr)).EndInit(); |
| | | } |
| | | try |
| | | { |
| | | |
| | | testSeqFile = ParameterManager.GetCommonParameterByName<string>("BurnInMainSeq", string.Empty); |
| | | } |
| | | catch |
| | | { |
| | | |
| | | } |
| | | testExecutionWrapper.Init(); |
| | | } |
| | | |
| | | private void TestExecutionWrapper_UIMessageEvent(object sender, UIMessageEvent e) |
| | | { |
| | | try |
| | | { |
| | | int eventCode = Convert.ToInt32(e.EventCode); |
| | | int MethodCode = Convert.ToInt16(e.NumberData);//SocketIndex |
| | | var data = e.Data; |
| | | string strData = e.StrData; |
| | | |
| | | try |
| | | { |
| | | log.Debug($"Event: {eventCode}, MethodCode: {MethodCode}, StringData: {strData}"); |
| | | } |
| | | catch |
| | | { |
| | | |
| | | } |
| | | HandleUIMessage(eventCode, MethodCode, strData, data); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn("Ts Event Error.", ex); |
| | | } |
| | | } |
| | | |
| | | private void TestExecutionWrapper_EndExecutionEvent(object sender, ExecutionEndEvent e) |
| | | { |
| | | log.Info("AxApplicationMgr_EndExecution"); |
| | | if (ReleaseSequenceFile()) |
| | | { |
| | | IsTesting = false; |
| | | log.Info("Release SeqFile Success!"); |
| | | if (canSwitchModel) switchAction?.Invoke(); |
| | | } |
| | | else |
| | | { |
| | | log.Warn("Release SeqFile Fail!"); |
| | | } |
| | | } |
| | | |
| | | private volatile bool canSwitchModel = false; |
| | | private volatile Action switchAction = null; |
| | | |
| | | private void PluginControl_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | ShowMask(() => |
| | | { |
| | | try |
| | | { |
| | | //Dispatcher.Invoke(() => |
| | | //{ |
| | | |
| | | //}); |
| | | isLoadOK = true; |
| | | if (AutoExecuteSeq) StartSequence(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("TsExecution Loaded Failed.", ex); |
| | | MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.UserControlLoadFailed")}: {ex.Message}"); |
| | | } |
| | | |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | if (isLoadOK) Loaded -= PluginControl_Loaded; |
| | | }); |
| | | }, (ex) => |
| | | { |
| | | ToastHelper.Error("Loaded Failed."); |
| | | log.Error("Loaded Error.", ex); |
| | | }, null, LangUtil.Get("App.UsercontrolIsLoading")); |
| | | } |
| | | |
| | | private void RefershUI() |
| | | { |
| | | panel.Children.Clear(); |
| | | string[] names = ParamSetting.SocketNames.Split(','); |
| | | |
| | | //重新生成子面板 |
| | | for (int i = 0; i < this.socketNum; i++) |
| | | { |
| | | string name = string.Empty; |
| | | if (i < names.Length) |
| | | { |
| | | name = names[i]; |
| | | } |
| | | |
| | | SingleControl control = new SingleControl(i, CountIniPath, this.probeWarn) |
| | | { |
| | | SocketName = name |
| | | }; |
| | | control.parent = this; |
| | | panel.Children.Add(control); |
| | | tsSockets.Add(control); |
| | | |
| | | //createChildInNewThread(i, CountIniPath, ProbeWarn); |
| | | } |
| | | if (tsSockets.Count == 1) |
| | | { |
| | | panel.Columns = 1; |
| | | } |
| | | else if (tsSockets.Count > 0 && (tsSockets.Count % 3) == 0) |
| | | { |
| | | panel.Columns = 3; |
| | | } |
| | | else |
| | | { |
| | | panel.Columns = 2; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 读取Count配置文件的pass和fail数 |
| | | /// </summary> |
| | | /// <param name="pass">合格数</param> |
| | | /// <param name="fail">不合格数</param> |
| | | private void ReadCount(out int pass, out int fail) |
| | | { |
| | | pass = Convert.ToInt32(IniFileHelper.ReadIniData("Count", "Pass", "0", CountIniPath)); |
| | | fail = Convert.ToInt32(IniFileHelper.ReadIniData("Count", "Fail", "0", CountIniPath)); |
| | | var fontSize = Convert.ToDouble(IniFileHelper.ReadIniData("Config", "TableFontSize", "12", CountIniPath)); |
| | | this.Resources["tableFontSize"] = 20d; |
| | | //CbFontSize.SelectedItem = fontSize; |
| | | } |
| | | |
| | | private string csvDir; |
| | | |
| | | /// <summary> |
| | | /// 读取配置文件里的几个属性 |
| | | /// </summary> |
| | | /// <param name="pass"></param> |
| | | /// <param name="fail"></param> |
| | | private void ReadConfig() |
| | | { |
| | | HMI.Core.Entity.ParameterData value; |
| | | |
| | | |
| | | bool flag = false; |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("DefaultEnableMes"); |
| | | if (value != null) |
| | | { |
| | | flag = bool.Parse(value.Value); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | flag = false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("SocketNum"); |
| | | if (value != null) |
| | | { |
| | | this.socketNum = int.Parse(value.Value); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | this.socketNum = 2; |
| | | } |
| | | |
| | | if (flag) |
| | | { |
| | | EnableMes = true; |
| | | } |
| | | else |
| | | { |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("EnableMes"); |
| | | if (value == null) |
| | | { |
| | | CbEnableMes.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | _enableMes = bool.Parse(value.Value); |
| | | Set("EnableMes"); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | _enableMes = false; |
| | | Set("EnableMes"); |
| | | } |
| | | } |
| | | |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("EnableNGPause"); |
| | | if (value == null) |
| | | { |
| | | CbEnableNGPause.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | _enableNGPause = bool.Parse(value.Value); |
| | | Set("EnableNGPause"); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | _enableNGPause = false; |
| | | Set("EnableNGPause"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("EnableDebugMode"); |
| | | if (value == null) |
| | | { |
| | | CbEnableDebugMode.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | _enableDebugMode = bool.Parse(value.Value); |
| | | Set("EnableDebugMode"); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | _enableDebugMode = false; |
| | | Set("EnableDebugMode"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("DisableScanner"); |
| | | if (value == null) |
| | | { |
| | | CbDisableScanner.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | _DisableScanner = bool.Parse(value.Value); |
| | | Set("DisableScanner"); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | _DisableScanner = false; |
| | | Set("DisableScanner"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("EnableCanLog"); |
| | | _enableCanLog = bool.Parse(value.Value); |
| | | Set("EnableCanLog"); |
| | | } |
| | | catch |
| | | { |
| | | _enableCanLog = false; |
| | | Set("EnableCanLog"); |
| | | } |
| | | |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("EnableSimpleMode"); |
| | | if (value == null) |
| | | { |
| | | CbEnableSimpleMode.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | EnableSimpleMode = bool.Parse(value.Value); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | EnableSimpleMode = false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("UI_ShowInfoDock"); |
| | | if (value == null) |
| | | { |
| | | CbUI_ShowInfoDock.Visibility = Visibility.Collapsed; |
| | | } |
| | | else |
| | | { |
| | | UI_ShowInfoDock = bool.Parse(value.Value); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | UI_ShowInfoDock = false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | value = ParameterManager.GetCommonParameterByName("AutoExecuteSeq"); |
| | | AutoExecuteSeq = bool.Parse(value.Value); |
| | | } |
| | | catch |
| | | { |
| | | AutoExecuteSeq = true; |
| | | } |
| | | |
| | | try |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("TestReportFolder"); |
| | | csvDir = value.Value; |
| | | if (!Directory.Exists(csvDir)) |
| | | { |
| | | Directory.CreateDirectory(csvDir); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | csvDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestReport"); |
| | | } |
| | | |
| | | try |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("IsOptRequirePassword"); |
| | | IsOptRequirePassword = bool.Parse(value.Value); |
| | | } |
| | | catch |
| | | { |
| | | IsOptRequirePassword = false; |
| | | } |
| | | |
| | | try |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("UI_CurrentWorkorder"); |
| | | CurrentWorkorder = value.Value; |
| | | } |
| | | catch |
| | | { |
| | | CurrentWorkorder = string.Empty; |
| | | } |
| | | |
| | | try |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("TS_ShowTitleWorkorder"); |
| | | var boo = bool.Parse(value.Value); |
| | | if (boo) |
| | | { |
| | | workorderPanel.Visibility = Visibility.Visible; |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | } |
| | | |
| | | dataLogPath = ParamSetting.DataLogFolder; |
| | | |
| | | try |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("TsModelPath"); |
| | | tsModelPath = value?.Value ?? @"C:\Users\Public\Documents\National Instruments\TestStand 2020 (64-bit)\Components\Models\ParallelModel.seq"; |
| | | } |
| | | catch |
| | | { |
| | | //@"C:\Program Files\National Instruments\TestStand 2020\Components\Models\TestStandModels\ParallelModel.seq" |
| | | tsModelPath = @"C:\Users\Public\Documents\National Instruments\TestStand 2020 (64-bit)\Components\Models\ParallelModel.seq"; |
| | | } |
| | | |
| | | try |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("ProbeWarn"); |
| | | probeWarn = int.Parse(value?.Value); |
| | | } |
| | | catch |
| | | { |
| | | probeWarn = 0; |
| | | } |
| | | //Convert.ToBoolean(IniFileHelper.ReadIniData("Config", "EnableMes", "true", CountIniPath)); |
| | | //EnableCanLog = Convert.ToBoolean(IniFileHelper.ReadIniData("Config", "EnableCanLog", "true", CountIniPath)); |
| | | //EnableDebugMode = Convert.ToBoolean(IniFileHelper.ReadIniData("Config", "EnableDebugMode", "true", CountIniPath)); |
| | | } |
| | | |
| | | private bool WriteConfig(string name, bool value) |
| | | { |
| | | try |
| | | { |
| | | if (ParameterManager.UpdateCommonParameter(name, value, out string errMsg)) |
| | | { |
| | | foreach (var ts in tsSockets) |
| | | { |
| | | testExecutionWrapper.SetStationGlobalValue($"SystemSetting.{name}", 1, value); |
| | | if (testExecutionWrapper is TeststandWrapper tsw) |
| | | { |
| | | if (name == "EnableDebugMode" && tsw.context?.Sequence != null) |
| | | { |
| | | tsw.context.Sequence.FailureAction = |
| | | value ? FailureActions.FailureAction_None : FailureActions.FailureAction_GotoCleanup; |
| | | } |
| | | } |
| | | |
| | | } |
| | | return true; |
| | | } |
| | | else |
| | | { |
| | | throw new Exception(errMsg); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | MsgBoxHelper2.Error(ex.Message); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private bool WriteIni(string name, object value) |
| | | { |
| | | //IniFileHelper.WriteIniData("Config", name, value.ToString(), CountIniPath); |
| | | try |
| | | { |
| | | if (ParameterManager.UpdateCommonParameter(name, value, out string errMsg)) |
| | | { |
| | | return true; |
| | | } |
| | | else |
| | | { |
| | | throw new Exception(errMsg); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | MsgBoxHelper2.Error(ex.Message); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void WriteCount(double pass, double fail) |
| | | { |
| | | IniFileHelper.WriteIniData("Count", "Pass", pass.ToString(), CountIniPath); |
| | | IniFileHelper.WriteIniData("Count", "Fail", fail.ToString(), CountIniPath); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 刷新计数 |
| | | /// </summary> |
| | | /// <param name="pass">合格数</param> |
| | | /// <param name="fail">不合格数</param> |
| | | private void DisplayCount(double pass, double fail) |
| | | { |
| | | double total = pass + fail; |
| | | double passRate; |
| | | if (total == 0) |
| | | { |
| | | passRate = 0.00; |
| | | } |
| | | else |
| | | { |
| | | passRate = (double)pass / (double)total * 100.00; |
| | | } |
| | | this.TbPass.Text = pass.ToString(); |
| | | this.TbFail.Text = fail.ToString(); |
| | | this.TbTotal.Text = total.ToString(); |
| | | this.TbPassRate.Text = string.Format("{0:0.00}", passRate) + "%"; |
| | | |
| | | Dispatcher.BeginInvoke(new Action(() => |
| | | { |
| | | Chart1_series[0].Values = new double[] { pass }; |
| | | Chart1_series[1].Values = new double[] { fail }; |
| | | // webview2.CoreWebView2?.ExecuteScriptAsync($"drawChart1({res})"); |
| | | })); |
| | | |
| | | WriteCount(pass, fail); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 启动样件测试 |
| | | /// </summary> |
| | | /// <param name="flag"></param> |
| | | /// <exception cref="Exception"></exception> |
| | | private void SampleTest(bool flag) |
| | | { |
| | | try |
| | | { |
| | | testExecutionWrapper.SetStationGlobalValue("StationState.Sample", 1, flag); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | throw new Exception(LangUtil.Get("Ts.SampleTestFail"), ex); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 启动测试序列 |
| | | /// </summary> |
| | | private void StartSequence() |
| | | { |
| | | log.Info($"Start ATE Seq isLoadOK:{isLoadOK}"); |
| | | if (!isLoadOK) return; |
| | | if (ParamSetting.EnableAutoModel) |
| | | { |
| | | try |
| | | { |
| | | var path = ParameterManager.GetCommonParameterByName<string>("CommonMainSeq", ""); |
| | | if (string.IsNullOrEmpty(path)) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | MsgBoxHelper.Error(this, $"请先设置CommonMainSeq参数"); |
| | | }); |
| | | return; |
| | | } |
| | | this.testSeqFile = path; |
| | | testExecutionWrapper?.StartSequence(this.testSeqFile, this.tsModelPath); |
| | | //this.modeSeq = this.engine.GetSequenceFileEx(tsModelPath); |
| | | //this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType); |
| | | //this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null); |
| | | IsTesting = true; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | log.Error("Start Sequence Failed.", ex); |
| | | MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.StartSequenceFail")}: {ex.Message}"); |
| | | }); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | //MaskWindow mask = new MaskWindow(); |
| | | try |
| | | { |
| | | //for (int i = 0; i < tsSockets.Count; i++) |
| | | //{ |
| | | // testExecutionWrapper?.SetStationGlobalValue($"StationHMI.UUTHMI[{i}].Model", 1, CurrentProductModel); |
| | | //} |
| | | //testExecutionWrapper?.CommitToDisk(false); |
| | | testExecutionWrapper?.StartSequence(this.testSeqFile, this.tsModelPath); |
| | | IsTesting = true; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | log.Error("Start Sequence Failed.", ex); |
| | | MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.StartSequenceFail")}: {ex.Message}"); |
| | | }); |
| | | } |
| | | finally |
| | | { |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | public void StopSequenceClick() |
| | | { |
| | | canSwitchModel = false; |
| | | switchAction = null; |
| | | ShowMask(() => |
| | | { |
| | | bool stoped = StopSequence(); |
| | | }, null, null, LangUtil.Get("App.SequenceIsStopping")); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 停止测试序列 |
| | | /// </summary> |
| | | private bool StopSequence() |
| | | { |
| | | try |
| | | { |
| | | testExecutionWrapper.SetStationGlobalValue("StationHMI.AllStop", 1, true); |
| | | |
| | | |
| | | //bool res = this.execution.WaitForEndEx(-1, true, null, null); |
| | | bool res = this.testExecutionWrapper.WaitForExit(); |
| | | //if (this.execution != null) |
| | | // Marshal.ReleaseComObject(this.execution); |
| | | //this.execution = null; |
| | | return res; |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("Stop Sequence Failed.", ex); |
| | | MsgBoxHelper.Error(this, $"{LangUtil.Get("Ts.StopSequenceFail")}: {ex.Message}"); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void Button_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | Button btn = sender as Button; |
| | | switch (btn.Name) |
| | | { |
| | | case "BtnStartSeq": |
| | | StartClick(); |
| | | break; |
| | | case "BtnStopSeq": |
| | | StopSequenceClick(); |
| | | break; |
| | | case "BtnClearNum": |
| | | ClearNum(); |
| | | break; |
| | | case "BtnSample": |
| | | SampleStatus = !SampleStatus; |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | } |
| | | |
| | | private void StartClick() |
| | | { |
| | | canSwitchModel = true; |
| | | ShowMask(() => |
| | | { |
| | | StartSequence(); |
| | | }, null, null, LangUtil.Get("Ts.SequenceIsStarting")); |
| | | } |
| | | |
| | | public void UpdatePositionStatus(int index, string status) |
| | | { |
| | | this.tsSockets[index].UpdateStatus(status); |
| | | OnStatusPublished(index, status); |
| | | this.UpdateTestCounter(); |
| | | } |
| | | |
| | | private void OnStatusPublished(int index, string status) |
| | | { |
| | | if (ParamSetting.CustomerCode != "SOUND") |
| | | { |
| | | return; |
| | | } |
| | | if (status == "Passed" || status == "Failed") |
| | | { |
| | | SaveSOUNDExcel(index, status); |
| | | } |
| | | } |
| | | |
| | | private string ConvertStatus(string status) |
| | | { |
| | | if (string.Equals(status, "passed", StringComparison.OrdinalIgnoreCase)) |
| | | { |
| | | return "Pass"; |
| | | } |
| | | else |
| | | { |
| | | return "Fail"; |
| | | } |
| | | } |
| | | |
| | | #region 桑德定制 |
| | | private ICell CreateCell(ISheet sheet, int r, int c, ICellStyle style) |
| | | { |
| | | var row = sheet.GetRow(r); |
| | | if (row == null) |
| | | { |
| | | row = sheet.CreateRow(r); |
| | | } |
| | | var cell = row.GetCell(c); |
| | | if (cell == null) |
| | | { |
| | | cell = row.CreateCell(c); |
| | | } |
| | | if (style != null) |
| | | { |
| | | cell.CellStyle = style; |
| | | } |
| | | return cell; |
| | | } |
| | | |
| | | private string GetOutputFolder() |
| | | { |
| | | var value = ParameterManager.GetCommonParameterByName("SOUND_Output"); |
| | | if (value != null) |
| | | { |
| | | return value.Value; |
| | | } |
| | | return ParamSetting.DataLogFolder; |
| | | } |
| | | |
| | | private void SaveSOUNDExcel(int index, string status) |
| | | { |
| | | lock (this) |
| | | { |
| | | try |
| | | { |
| | | string outputFolder = GetOutputFolder(); |
| | | |
| | | var socket = this.tsSockets[index]; |
| | | DateTime testTime = DateTime.Now; |
| | | string filePath = Path.Combine(outputFolder, $"{testTime.ToString("yy-MM-dd")}_{CurrentProductModel}.xls"); |
| | | if (!File.Exists(filePath)) |
| | | { |
| | | Dictionary<string, int> columnMap = new Dictionary<string, int>(); |
| | | HSSFWorkbook workbook = new HSSFWorkbook(); |
| | | ISheet sheet = workbook.CreateSheet("Sheet1"); |
| | | |
| | | ICellStyle border = workbook.CreateCellStyle(); |
| | | border.BorderBottom = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BorderTop = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BorderLeft = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BorderRight = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BottomBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | border.TopBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | border.LeftBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | border.RightBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | |
| | | |
| | | ICellStyle red = workbook.CreateCellStyle(); |
| | | red.CloneStyleFrom(border); |
| | | red.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Red.Index; |
| | | red.FillPattern = FillPattern.SolidForeground; |
| | | |
| | | ICellStyle green = workbook.CreateCellStyle(); |
| | | green.CloneStyleFrom(border); |
| | | green.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.BrightGreen.Index; |
| | | green.FillPattern = FillPattern.SolidForeground; |
| | | |
| | | CreateCell(sheet, 1, 0, border); |
| | | CreateCell(sheet, 1, 1, border); |
| | | CreateCell(sheet, 1, 2, border); |
| | | CreateCell(sheet, 1, 3, border); |
| | | CreateCell(sheet, 1, 4, border); |
| | | |
| | | CreateCell(sheet, 2, 0, border); |
| | | CreateCell(sheet, 2, 1, border); |
| | | CreateCell(sheet, 2, 2, border); |
| | | CreateCell(sheet, 2, 3, border); |
| | | CreateCell(sheet, 2, 4, border).SetCellValue("上限"); |
| | | |
| | | CreateCell(sheet, 3, 0, border).SetCellValue("测试时间"); |
| | | CreateCell(sheet, 3, 1, border).SetCellValue("测试人"); |
| | | CreateCell(sheet, 3, 2, border).SetCellValue("测试设备"); |
| | | CreateCell(sheet, 3, 3, border).SetCellValue("条形码"); |
| | | CreateCell(sheet, 3, 4, border).SetCellValue("下限"); |
| | | |
| | | int column = 5; |
| | | int row = 4; |
| | | |
| | | CreateCell(sheet, row, 0, border).SetCellValue(testTime.ToString("*HH:mm:ss*")); |
| | | CreateCell(sheet, row, 1, border).SetCellValue(UserInfo.User.Name); |
| | | CreateCell(sheet, row, 2, border).SetCellValue(ParamSetting.StationName); |
| | | CreateCell(sheet, row, 3, border).SetCellValue(socket.Barcode); |
| | | CreateCell(sheet, row, 4, status == "Passed" ? green : red).SetCellValue(ConvertStatus(status)); |
| | | |
| | | foreach (var data in socket.Datasource) |
| | | { |
| | | int col = column; |
| | | if (columnMap.ContainsKey(data.Name)) |
| | | { |
| | | col = columnMap[data.Name]; |
| | | } |
| | | else |
| | | { |
| | | columnMap[data.Name] = column++; |
| | | } |
| | | |
| | | CreateCell(sheet, 1, col, border).SetCellValue(data.Name); |
| | | CreateCell(sheet, 2, col, border).SetCellValue(data.LimitUp); |
| | | CreateCell(sheet, 3, col, border).SetCellValue(data.LimitDown); |
| | | CreateCell(sheet, row, col, data.Result == "Failed" ? red : border).SetCellValue(data.Value); |
| | | } |
| | | using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate)) |
| | | { |
| | | workbook.Write(fs); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | using (FileStream fs = File.OpenRead(filePath)) |
| | | { |
| | | IWorkbook workbook = new HSSFWorkbook(fs); |
| | | ICellStyle border = workbook.CreateCellStyle(); |
| | | border.BorderBottom = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BorderTop = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BorderLeft = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BorderRight = NPOI.SS.UserModel.BorderStyle.Thin; |
| | | border.BottomBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | border.TopBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | border.LeftBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | border.RightBorderColor = NPOI.HSSF.Util.HSSFColor.Black.Index; |
| | | |
| | | |
| | | ICellStyle red = workbook.CreateCellStyle(); |
| | | red.CloneStyleFrom(border); |
| | | red.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Red.Index; |
| | | red.FillPattern = FillPattern.SolidForeground; |
| | | |
| | | ICellStyle green = workbook.CreateCellStyle(); |
| | | green.CloneStyleFrom(border); |
| | | green.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.BrightGreen.Index; |
| | | green.FillPattern = FillPattern.SolidForeground; |
| | | |
| | | ISheet sheet = workbook.GetSheet("Sheet1"); |
| | | Dictionary<string, int> columnMap = new Dictionary<string, int>(); |
| | | var titleRow = sheet.GetRow(1); |
| | | for (int i = 0; i < titleRow.LastCellNum; i++) |
| | | { |
| | | if (!string.IsNullOrEmpty(titleRow.GetCell(i).StringCellValue)) |
| | | { |
| | | columnMap[titleRow.GetCell(i).StringCellValue] = i; |
| | | } |
| | | } |
| | | int row = sheet.LastRowNum + 1; |
| | | CreateCell(sheet, row, 0, border).SetCellValue(testTime.ToString("*HH:mm:ss*")); |
| | | CreateCell(sheet, row, 1, border).SetCellValue(UserInfo.User.Name); |
| | | CreateCell(sheet, row, 2, border).SetCellValue(ParamSetting.StationName); |
| | | CreateCell(sheet, row, 3, border).SetCellValue(socket.Barcode); |
| | | CreateCell(sheet, row, 4, status == "Passed" ? green : red).SetCellValue(ConvertStatus(status)); |
| | | |
| | | foreach (var data in socket.Datasource) |
| | | { |
| | | int col = titleRow.LastCellNum; |
| | | if (columnMap.ContainsKey(data.Name)) |
| | | { |
| | | col = columnMap[data.Name]; |
| | | } |
| | | else |
| | | { |
| | | columnMap[data.Name] = col; |
| | | } |
| | | |
| | | CreateCell(sheet, 1, col, border).SetCellValue(data.Name); |
| | | CreateCell(sheet, 2, col, border).SetCellValue(data.LimitUp); |
| | | CreateCell(sheet, 3, col, border).SetCellValue(data.LimitDown); |
| | | CreateCell(sheet, row, col, data.Result == "Failed" ? red : border).SetCellValue(data.Value); |
| | | } |
| | | using (FileStream nfs = new FileStream(filePath, FileMode.OpenOrCreate)) |
| | | { |
| | | workbook.Write(nfs); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("SOUND Export Excel Data Error.", ex); |
| | | } |
| | | } |
| | | } |
| | | #endregion |
| | | |
| | | public void UpdatePositionProcessValue(int index, int value) |
| | | { |
| | | this.tsSockets[index].SetTestProcess(value); |
| | | } |
| | | |
| | | public void PausePosition(int index) |
| | | { |
| | | this.tsSockets[index].ShowPausePanel(); |
| | | } |
| | | |
| | | public void AddTestData(int index, string data) |
| | | { |
| | | string[] TestItems = data.Split(','); |
| | | tsSockets[index].AddTestData(TestItems[0], TestItems[1], TestItems[2], TestItems[3], |
| | | TestItems[4], TestItems[5], TestItems[6]); |
| | | } |
| | | |
| | | public void SetPositionBarcode(int index, string strData) |
| | | { |
| | | if (strData.Contains(";")) |
| | | { |
| | | var strs = strData.Split(';'); |
| | | this.tsSockets[index].Barcode = strs[0]; |
| | | this.tsSockets[index].Model = strs[1]; |
| | | } |
| | | else |
| | | { |
| | | this.tsSockets[index].Barcode = strData; |
| | | } |
| | | } |
| | | |
| | | public void SavePositionCSV(int index, string csvPath) |
| | | { |
| | | //var time = DateTime.Now; |
| | | //string status = this.tsSockets[index].Status == "Passed" ? "PASS" : "FAIL"; |
| | | //string csvPath = Path.Combine(csvDir, SelectedProduct?.Name ?? "未知产品", time.ToString("yyyyMMdd"), status, |
| | | // $"{this.tsSockets[index].Barcode}-{time.ToString("HHmmss")}.csv"); |
| | | this.tsSockets[index].WriteToCSV(csvPath); |
| | | } |
| | | |
| | | public void SaveTemplateCSV(int index) |
| | | { |
| | | var time = DateTime.Now; |
| | | string status = this.tsSockets[index].Status == "Passed" ? "PASS" : "FAIL"; |
| | | string csvPath = Path.Combine(csvDir, CurrentProductModel ?? "未知产品", time.ToString("yyyyMMdd"), status, $"{this.tsSockets[index].Barcode}-{time.ToString("HHmmss")}.csv"); |
| | | this.tsSockets[index].WriteToCSV(csvPath); |
| | | } |
| | | |
| | | public void SavePositionDB(int index, string status) |
| | | { |
| | | CheckSqlHelper(); |
| | | var socket = this.tsSockets[index]; |
| | | var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); |
| | | string snr = socket.Barcode; |
| | | string model = CurrentProductModel; |
| | | |
| | | List<string> sqls = new List<string>(); |
| | | foreach (var data in socket.Datasource) |
| | | { |
| | | sqls.Add($@"INSERT INTO serialData (sn,model,station,step,position,limitDown,limitUp,unit,value,status,finalStatus,consumeTime,totalConsumeTime,time) VALUES |
| | | ('{snr}','{model}','','{data.Name}','{index}','{data.LimitDown}','{data.LimitUp}','{data.Unit}','{data.Value}','{data.Result}','{socket.Status}','{data.ConsumeTime}','{socket.ElapseTime}','{time}')"); |
| | | } |
| | | sqliteHelper.ExecuteBatchSql(sqls); |
| | | |
| | | var ssql = $@"INSERT INTO testResult (sn,model,station,result,time,position,status) VALUES |
| | | ('{snr}','{model}','','{socket.Status}','{time}','{index}','{status}')"; |
| | | sqliteHelper.ExecuteSql(ssql); |
| | | } |
| | | |
| | | private void HandleUIMessage(int eventCode, int MethodCode, string strData, object xData) |
| | | { |
| | | if (eventCode == 10035) |
| | | { |
| | | string[] arr_10035 = strData.Split(';'); |
| | | |
| | | int[][] points = new int[arr_10035.Length - 1][]; |
| | | |
| | | for (int i = 1; i < arr_10035.Length; i++) |
| | | { |
| | | string[] strs = arr_10035[i].Split(','); |
| | | points[i - 1] = new int[2] { int.Parse(strs[0]), int.Parse(strs[1]) }; |
| | | } |
| | | this.tsSockets[MethodCode].DrawScreenPoints(int.Parse(arr_10035[0]), points); |
| | | return; |
| | | |
| | | } |
| | | else if (eventCode == 100350) |
| | | { |
| | | string[] arr_100350 = strData.Split(';'); |
| | | |
| | | int[][] points_0 = new int[arr_100350.Length - 1][]; |
| | | |
| | | for (int i = 1; i < arr_100350.Length; i++) |
| | | { |
| | | if (string.IsNullOrEmpty(arr_100350[i])) |
| | | { |
| | | continue; |
| | | } |
| | | string[] strs = arr_100350[i].Split(','); |
| | | points_0[i - 1] = new int[2] { int.Parse(strs[0]), int.Parse(strs[1]) }; |
| | | } |
| | | this.tsSockets[MethodCode].DrawNewScreenPoints(int.Parse(arr_100350[0]), points_0); |
| | | return; |
| | | } |
| | | else if (eventCode == 10036) |
| | | { |
| | | this.tsSockets[MethodCode].ClearScreenPoint(int.Parse(strData)); |
| | | return; |
| | | } |
| | | else if (eventCode == 10036) |
| | | { |
| | | this.tsSockets[MethodCode].ClearScreenPoint(int.Parse(strData)); |
| | | return; |
| | | } |
| | | if (MethodCode == 0) |
| | | { |
| | | /// 设置句柄 |
| | | testExecutionWrapper?.SetContext((SequenceContext)xData); |
| | | return; |
| | | } |
| | | if (MethodCode == 3) |
| | | { |
| | | /// 更新炉子时间信息 |
| | | string[] datas_10014 = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas_10014[0]); |
| | | string startTime = datas_10014[1]; |
| | | string consumeTime = datas_10014[2]; |
| | | string planEndTime = datas_10014[3]; |
| | | tsSockets[ovenIndex].ElapseTime = consumeTime; |
| | | return; |
| | | } |
| | | else if (MethodCode == 4) |
| | | { |
| | | /// 修改工位底部信息栏 |
| | | string[] datas_10014 = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas_10014[0]); |
| | | string statusBar = datas_10014[1]; |
| | | this.tsSockets[ovenIndex].CurrentStep = statusBar; |
| | | return; |
| | | } |
| | | else if (MethodCode == 6) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | {/// 更新穴位状态 |
| | | string[] datas_10015 = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas_10015[0]); |
| | | int cellIndex = int.Parse(datas_10015[1]); |
| | | string status_10015 = datas_10015[2]; |
| | | UpdatePositionStatus(ovenIndex, status_10015); |
| | | }); |
| | | return; |
| | | } |
| | | else if (MethodCode == 9) |
| | | { |
| | | if (string.Equals(strData, "stop", StringComparison.OrdinalIgnoreCase)) |
| | | { |
| | | IsTesting = false; |
| | | } |
| | | return; |
| | | } |
| | | else if (MethodCode == 11) |
| | | { |
| | | // 清空测试数据 |
| | | string[] datas_10015 = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas_10015[0]); |
| | | this.tsSockets[ovenIndex].IniTestData(); |
| | | return; |
| | | } |
| | | else if (MethodCode == 18) |
| | | { |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | string filePath = datas[2]; |
| | | |
| | | if (ParamSetting.CustomerCode == "SOUND") |
| | | { |
| | | this.tsSockets[ovenIndex].WriteToExcel(filePath); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | SavePositionDB(ovenIndex, tsSockets[ovenIndex].Status); |
| | | }); |
| | | return; |
| | | } |
| | | try |
| | | { |
| | | SavePositionCSV(ovenIndex, filePath); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | var sss = this.tsSockets[ovenIndex].Status; |
| | | SavePositionDB(ovenIndex, sss); |
| | | }); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn("写CSV/DB数据失败.", ex); |
| | | SaveTemplateCSV(ovenIndex); |
| | | } |
| | | return; |
| | | } |
| | | else if (MethodCode == 9000) |
| | | { |
| | | //更新型号 |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | CurrentProductModel = datas[2]; |
| | | return; |
| | | } |
| | | else if (MethodCode == 9001) |
| | | { |
| | | //更新条码 |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | SetPositionBarcode(ovenIndex, datas[2]); |
| | | return; |
| | | } |
| | | else if (MethodCode == 9002) |
| | | { |
| | | //显示测试进度 |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | int ProcessVal = Convert.ToInt32(datas[2]); |
| | | this.tsSockets[ovenIndex].SetTestProcess(ProcessVal); |
| | | return; |
| | | } |
| | | else if (MethodCode == 9003) |
| | | { |
| | | //开始计时 |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | tsSockets[ovenIndex].ElapseTime = "Start Timing"; |
| | | return; |
| | | } |
| | | else if (MethodCode == 9004) |
| | | { |
| | | //结束计时 |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | tsSockets[ovenIndex].ElapseTime = "Stop Timing"; |
| | | return; |
| | | } |
| | | else if (MethodCode == 9005) |
| | | { |
| | | //手动设置耗时 |
| | | string[] datas = strData.Split(';'); |
| | | int ovenIndex = int.Parse(datas[0]); |
| | | int cellIndex = int.Parse(datas[1]); |
| | | tsSockets[ovenIndex].ElapseTime = datas[2]; |
| | | return; |
| | | } |
| | | } |
| | | |
| | | private void SwitchSequenceByModel(int index, string model, string snr) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | var target = Products.FirstOrDefault(e => e.Name == model); |
| | | |
| | | ShowMask(() => |
| | | { |
| | | bool stoped = StopSequence(); |
| | | //if (stoped) |
| | | //{ |
| | | // IsTesting = false; |
| | | //} |
| | | }, null, null, "换型中,请稍后..."); |
| | | |
| | | if (target == null) |
| | | { |
| | | MsgBoxHelper2.Warn($"请先在产品管理页面配置型号 [{model}] 的产品信息!"); |
| | | return; |
| | | } |
| | | |
| | | switchAction = () => |
| | | { |
| | | switchAction = null; |
| | | log?.Info($"尝试自动换型: {switchAction}"); |
| | | if (canSwitchModel) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | try |
| | | { |
| | | log?.Info("开始自动换型"); |
| | | SelectedProduct = target; |
| | | testExecutionWrapper?.SetStationGlobalValue($"CacheSNR[{index}]", 1, snr); |
| | | testExecutionWrapper?.CommitToDisk(false); |
| | | StartClick(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log?.Error("自动换型失败", ex); |
| | | } |
| | | }); |
| | | } |
| | | }; |
| | | }); |
| | | } |
| | | |
| | | private void ShowAlarm(AlarmText alarm) |
| | | { |
| | | AlarmShowHelper.ShowAlarm(alarm); |
| | | mSMQHelper.Send("Alarm", "PC", $"ATE测试 {alarm.AlarmInfo}", 2); |
| | | } |
| | | |
| | | private void UpdateTestCounter() |
| | | { |
| | | double PassNum = 0; |
| | | double FailNum = 0; |
| | | foreach (SingleControl socket in this.tsSockets) |
| | | { |
| | | PassNum += socket.PassNum; |
| | | FailNum += socket.FailNum; |
| | | } |
| | | this.DisplayCount(PassNum, FailNum); |
| | | } |
| | | |
| | | private void SetTestSeqFile(ProductData product) |
| | | { |
| | | if (product == null) |
| | | { |
| | | testSeqFile = null; |
| | | return; |
| | | } |
| | | string name = product.Name; |
| | | List<HMI.Core.Entity.ParameterData> parameters = ProductManager.GetParametersByProductName(name); |
| | | testSeqFile = parameters.FirstOrDefault(p => p.Name == "TestSequence")?.Value?.ToString(); |
| | | if (AutoExecuteSeq) |
| | | { |
| | | StartSequence(); |
| | | } |
| | | } |
| | | |
| | | private void ClearNum() |
| | | { |
| | | if (MsgBoxHelper.WarnMessage(Window.GetWindow(this), LangUtil.Get("Ts.MakeSureClearNum")) == MessageBoxResult.OK) |
| | | { |
| | | PassAmount = 0; |
| | | FailAmount = 0; |
| | | //this.OpcHandle.DoFctWriteSingle("Count_Clear", "1", out string ErrMsg); |
| | | foreach (SingleControl socket in tsSockets) |
| | | { |
| | | socket.ResetCounter(); |
| | | } |
| | | DisplayCount(PassAmount, FailAmount); |
| | | } |
| | | } |
| | | |
| | | public override void OnClose(object sender, CancelEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | StopSequence(); |
| | | base.OnClose(sender, e); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("OnClose Error.", ex); |
| | | } |
| | | } |
| | | |
| | | public override void DoClose(object sender, EventArgs e) |
| | | { |
| | | testExecutionWrapper.Dispose(); |
| | | base.DoClose(sender, e); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 释放所有的序列文件 |
| | | /// </summary> |
| | | private bool ReleaseSequenceFile() |
| | | { |
| | | try |
| | | { |
| | | lock (this) |
| | | { |
| | | //execution.Abort(); |
| | | int count = 10; |
| | | while (count > 0) |
| | | { |
| | | try |
| | | { |
| | | return testExecutionWrapper.ReleaseSeqFile(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn($"ReleaseSequenceFile In {count} Times Fail.", ex); |
| | | count--; |
| | | System.Threading.Thread.Sleep(1000); |
| | | } |
| | | } |
| | | log.Warn($"ReleaseSequenceFile Failed."); |
| | | return false; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn("ReleaseSequenceFile Error.", ex); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private void PluginControl_SizeChanged(object sender, SizeChangedEventArgs e) |
| | | { |
| | | |
| | | } |
| | | |
| | | private void CbFontSize_SelectionChanged(object sender, SelectionChangedEventArgs e) |
| | | { |
| | | try |
| | | { |
| | | if (e.AddedItems != null && e.AddedItems.Count > 0) |
| | | { |
| | | var fontSize = (double)e.AddedItems[0]; |
| | | this.Resources["tableFontSize"] = fontSize; |
| | | IniFileHelper.WriteIniData("Config", "TableFontSize", fontSize.ToString(), CountIniPath); |
| | | } |
| | | } |
| | | catch |
| | | { |
| | | } |
| | | } |
| | | |
| | | private void Label_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) |
| | | { |
| | | //workOrderEditor.Focus(); |
| | | //workOrderEditor.IsEditing = true; |
| | | if (RequirePassword() == true) |
| | | { |
| | | var win = new WorkOrderWin |
| | | { |
| | | WorkOrder = CurrentWorkorder, |
| | | Owner = Window.GetWindow(this) |
| | | }; |
| | | if (win.ShowDialog() == true) |
| | | { |
| | | CurrentWorkorder = win.WorkOrder; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <pu:WindowX x:Class="Plugin.StandardTest.WorkOrderWin" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:local="clr-namespace:Plugin.StandardTest" |
| | | mc:Ignorable="d" |
| | | Style="{StaticResource ModalWindow}" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | SizeToContent="Height" |
| | | FontSize="14" |
| | | WindowStartupLocation="CenterOwner" |
| | | pu:WindowXModalDialog.OKButtonContent="确定" |
| | | Title="请输入工单号" Width="500"> |
| | | <Grid dp:GridHelper.Space="10"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <pu:FormGroup Header="工单号" Orientation="Horizontal" Style="{StaticResource RequiredInputFormGroupStyle}"> |
| | | <TextBox Text="{Binding WorkOrder}" Style="{StaticResource EditorTextBoxStyle}"/> |
| | | </pu:FormGroup> |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using Panuon.WPF.UI; |
| | | using System.ComponentModel; |
| | | |
| | | namespace Plugin.StandardTest |
| | | { |
| | | /// <summary> |
| | | /// RequirePasswordWindow.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class WorkOrderWin : WindowX |
| | | { |
| | | public WorkOrderWin() |
| | | { |
| | | this.DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | private string workOrder; |
| | | public string WorkOrder |
| | | { |
| | | get => workOrder; |
| | | set |
| | | { |
| | | Set(ref workOrder, value); |
| | | } |
| | | } |
| | | |
| | | protected override void OnClosing(CancelEventArgs e) |
| | | { |
| | | //Press Continue Button |
| | | if (DialogResult == true) |
| | | { |
| | | } |
| | | base.OnClosing(e); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | using HMI.Core.Annotation; |
| | | using System.Windows; |
| | | |
| | | [assembly:ThemeInfo( |
| | | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located |
| | | //(used if a resource is not found in the page, |
| | | // or application resource dictionaries) |
| | | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located |
| | | //(used if a resource is not found in the page, |
| | | // app, or any theme specific resource dictionaries) |
| | | )] |
| | | [assembly:PluginClassName("Plugin.TestPlanEditor.EditorUI")] |
| New file |
| | |
| | | <hb:SimpleMaskControl x:Class="Plugin.TestPlanEditor.EditorUI" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:local="clr-namespace:Plugin.TestPlanEditor" |
| | | mc:Ignorable="d" |
| | | xmlns:hb="clr-namespace:HMI.Base;assembly=HMI.Base" |
| | | xmlns:ote="clr-namespace:OpenTapEditor;assembly=SeqEditor" |
| | | xmlns:ui="clr-namespace:UILib;assembly=UILib" |
| | | xmlns:op="clr-namespace:OpenTap;assembly=OpenTap" |
| | | xmlns:sys="clr-namespace:System;assembly=mscorlib" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | d:DesignHeight="450" d:DesignWidth="800"> |
| | | <UserControl.Resources> |
| | | <ResourceDictionary> |
| | | <ResourceDictionary.MergedDictionaries> |
| | | <ResourceDictionary Source="/UILib;component/Styles.xaml"/> |
| | | <ResourceDictionary Source="/SeqEditor;component/UIStyle.xaml"/> |
| | | <pu:StyleDictionary Excludes="Menu,ListView,ToggleButton,ContextMenu,TabControl,CheckBox,TextBox,Button" /> |
| | | </ResourceDictionary.MergedDictionaries> |
| | | |
| | | <sys:Double x:Key="GlobalFontSize">14</sys:Double> |
| | | <ui:ReverseBoolConverter x:Key="ReverseBool"/> |
| | | <ObjectDataProvider x:Key="BreakConditionValues" |
| | | MethodName="GetValues" |
| | | ObjectType="{x:Type sys:Enum}"> |
| | | <ObjectDataProvider.MethodParameters> |
| | | <x:Type TypeName="op:BreakCondition"/> |
| | | </ObjectDataProvider.MethodParameters> |
| | | </ObjectDataProvider> |
| | | |
| | | <sys:Double x:Key="{x:Static SystemParameters.VerticalScrollBarWidthKey}">3</sys:Double> |
| | | <sys:Double x:Key="{x:Static SystemParameters.HorizontalScrollBarHeightKey}">3</sys:Double> |
| | | |
| | | <Style TargetType="ScrollViewer"> |
| | | <Style.Resources> |
| | | <Style TargetType="ScrollBar" BasedOn="{StaticResource ScrollStyle_Mobile}"/> |
| | | </Style.Resources> |
| | | </Style> |
| | | |
| | | <Style TargetType="TabItem"> |
| | | <Setter Property="Padding" Value="5"/> |
| | | <Setter Property="Background" Value="#f2f3f5"/> |
| | | <Setter Property="BorderThickness" Value="0"/> |
| | | <Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/> |
| | | <Setter Property="FontSize" Value="10"/> |
| | | <Setter Property="Cursor" Value="Hand"/> |
| | | <Setter Property="Template"> |
| | | <Setter.Value> |
| | | <ControlTemplate TargetType="TabItem"> |
| | | <Border x:Name="border" Background="{TemplateBinding Background}" |
| | | BorderBrush="{TemplateBinding BorderBrush}" |
| | | BorderThickness="{TemplateBinding BorderThickness}" |
| | | Padding="{TemplateBinding Padding}"> |
| | | <ContentPresenter x:Name="contentPresenter" |
| | | ContentSource="Header" |
| | | HorizontalAlignment="Center" |
| | | VerticalAlignment="Center"/> |
| | | </Border> |
| | | <ControlTemplate.Triggers> |
| | | <Trigger Property="IsMouseOver" Value="True"> |
| | | <Setter TargetName="border" Property="Background" Value="#e0e9f2"/> |
| | | </Trigger> |
| | | <Trigger Property="IsSelected" Value="True"> |
| | | <Setter TargetName="border" Property="Background" Value="#bcddee"/> |
| | | <Setter TargetName="border" Property="BorderThickness" Value="0,0,0,2"/> |
| | | <Setter TargetName="border" Property="BorderBrush" Value="#3498DB"/> |
| | | </Trigger> |
| | | </ControlTemplate.Triggers> |
| | | </ControlTemplate> |
| | | </Setter.Value> |
| | | </Setter> |
| | | </Style> |
| | | </ResourceDictionary> |
| | | </UserControl.Resources> |
| | | |
| | | <Grid> |
| | | <ote:SeqEditorUI x:Name="editor"/> |
| | | </Grid> |
| | | </hb:SimpleMaskControl> |
| New file |
| | |
| | | using AddInPlugin; |
| | | using HMI.Base; |
| | | |
| | | namespace Plugin.TestPlanEditor |
| | | { |
| | | /// <summary> |
| | | /// EditorUI.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class EditorUI : SimpleMaskControl |
| | | { |
| | | public EditorUI() |
| | | { |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | public override void InitFinish() |
| | | { |
| | | try |
| | | { |
| | | LvlibpLoader.PreLoad(); |
| | | log.Debug("PreLoad completed."); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("Labview PreLoad error.", ex); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | <Project Sdk="Microsoft.NET.Sdk"> |
| | | |
| | | <PropertyGroup> |
| | | <OutputType>Library</OutputType> |
| | | <TargetFramework>net472</TargetFramework> |
| | | <Nullable>enable</Nullable> |
| | | <LangVersion>10.0</LangVersion> |
| | | <ImplicitUsings>enable</ImplicitUsings> |
| | | <UseWPF>true</UseWPF> |
| | | </PropertyGroup> |
| | | |
| | | <ItemGroup> |
| | | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" /> |
| | | <PackageReference Include="log4net" Version="3.0.4" /> |
| | | <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" /> |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\..\..\VisualStudio\UILib\UILib.csproj" /> |
| | | <ProjectReference Include="..\..\..\VisualStudio\UtilLib\UtilLib.csproj" /> |
| | | <ProjectReference Include="..\..\OpenTapEditor\Editor\SeqEditor.csproj" /> |
| | | <ProjectReference Include="..\..\OpenTapEditor\OpenTap\BasicSteps\Tap.Plugins.BasicSteps.csproj" /> |
| | | <ProjectReference Include="..\..\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj" /> |
| | | <ProjectReference Include="..\..\OpenTapEditor\Plugin\AddInPlugin\AddInPlugin.csproj" /> |
| | | <ProjectReference Include="..\HMI.Base\HMI.Base.csproj" /> |
| | | <ProjectReference Include="..\HMI.Core\HMI.Core.csproj" /> |
| | | <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" /> |
| | | <ProjectReference Include="..\HMI.Util\HMI.Util.csproj" /> |
| | | <ProjectReference Include="..\SimpleUtil\HMI.LogUtil\HMI.LogUtil.csproj" /> |
| | | <ProjectReference Include="..\SimpleUtil\HMI.Util.Base\HMI.Util.Base.csproj" /> |
| | | </ItemGroup> |
| | | |
| | | </Project> |
| | |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | mc:Ignorable="d" |
| | | xmlns:sb="clr-namespace:Util.SB;assembly=Util.ManualDebug" |
| | | xmlns:sb="clr-namespace:Util.ManualDebug;assembly=Util.ManualDebug" |
| | | Title="ManualDebugWindow" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | Style="{StaticResource ModalWindow}" |
| | |
| | | pu:WindowXModalDialog.Buttons="{x:Null}" |
| | | Height="1000" Width="900"> |
| | | <Grid x:Name="sb"> |
| | | <sb:ManualDebug OvenName="{Binding OvenName,Mode=OneWay}" |
| | | <sb:OldManualDebug OvenName="{Binding OvenName,Mode=OneWay}" |
| | | CellName="{Binding CellName,Mode=OneWay}" |
| | | OvenIndex="{Binding OvenIndex,Mode=OneWay}" |
| | | CellIndex="{Binding CellIndex,Mode=OneWay}"/> |
| | |
| | | using System.ComponentModel; |
| | | using System.IO; |
| | | using System.Windows; |
| | | using Util.SB; |
| | | using Util.ManualDebug; |
| | | |
| | | namespace Plugin.TestResult |
| | | { |
| | |
| | | public partial class ManualDebugWindow : WindowX |
| | | { |
| | | |
| | | private ManualDebug debug; |
| | | private OldManualDebug debug; |
| | | |
| | | public ManualDebugWindow(string seqPath, string modelPath) |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | debug = (ManualDebug)sb.Children[0]; |
| | | debug = (OldManualDebug)sb.Children[0]; |
| | | ReadBtns(); |
| | | //List<BtnConfig> settings = new List<BtnConfig> { new BtnConfig { Title = "上电激活", Name = "Active" } }; |
| | | debug.seqPath = seqPath; |
| | |
| | | <Resource Include="Resource\barcode.png" /> |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\..\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj"> |
| | | <Project>{efbd516d-3dc9-9d75-f082-48fbc76715f7}</Project> |
| | | <Name>Tap.Engine</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\HMI.Base\HMI.Base.csproj"> |
| | | <Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project> |
| | | <Name>HMI.Base</Name> |
| | |
| | | } |
| | | else |
| | | { |
| | | executionWrapper = new TsExecutionWrapper(); |
| | | executionWrapper = new TeststandWrapper(); |
| | | } |
| | | executionWrapper.UIMessageEvent += ExecutionWrapper_UIMessageEvent; |
| | | |
| | | if (executionWrapper is TsExecutionWrapper tsWrapper) |
| | | if (executionWrapper is TeststandWrapper tsWrapper) |
| | | { |
| | | ((System.ComponentModel.ISupportInitialize)(tsWrapper.AxApplicationMgr)).BeginInit(); |
| | | host.Child = tsWrapper.AxApplicationMgr; |
| | |
| | | public void ReleaseResource() |
| | | { |
| | | executionWrapper.UIMessageEvent -= ExecutionWrapper_UIMessageEvent; |
| | | |
| | | executionWrapper.Dispose(); |
| | | foreach (var table in tables) |
| | | { |
| | | table.RelaseResource(); |
| | |
| | | </None> |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\..\..\Project\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj"> |
| | | <Project>{efbd516d-3dc9-9d75-f082-48fbc76715f7}</Project> |
| | | <Name>Tap.Engine</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\HMI.Base\HMI.Base.csproj"> |
| | | <Project>{2a6f9720-a890-4036-9d73-41a0dae28341}</Project> |
| | | <Name>HMI.Base</Name> |
| | |
| | | <Project>{CF5BEBE9-2803-49C3-B773-73D082E0984A}</Project> |
| | | <Name>HMI.Simple.Util</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\Util.ManualDebug\Util.ManualDebug.csproj"> |
| | | <Project>{d0944e62-ee52-4abe-832a-a09d98814db1}</Project> |
| | | <Name>Util.ManualDebug</Name> |
| | | </ProjectReference> |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <WCFMetadata Include="Connected Services\" /> |
| | |
| | | // 生成号 |
| | | // 修订号 |
| | | // |
| | | [assembly: AssemblyVersion("1.0.0.7")] |
| | | [assembly: AssemblyFileVersion("1.0.0.7")] |
| | | [assembly: AssemblyVersion("1.0.0.8")] |
| | | [assembly: AssemblyFileVersion("1.0.0.8")] |
| | | [assembly: PluginClassName("Plugin.TsExecution.TsExecution")] |
| | |
| | | xmlns:sys="clr-namespace:System;assembly=mscorlib" |
| | | Loaded="UserControl_Loaded" |
| | | Background="#ccc6c7" |
| | | PreviewMouseRightButtonDown="control_PreviewMouseRightButtonDown" |
| | | FontSize="{Binding ActualHeight,RelativeSource={RelativeSource AncestorType=UserControl}, |
| | | Converter={StaticResource hfc}, ConverterParameter='12,816'}" |
| | | x:Name="control" |
| | |
| | | using HMI.Core.Manager; |
| | | using HMI.Core.Setting; |
| | | using HMI.Core.Util; |
| | | using HMI.UI.Util; |
| | | using HMI.Util; |
| | | using NationalInstruments.TestStand.Interop.API; |
| | | using NPOI.HSSF.UserModel; |
| | | using NPOI.SS.Formula.Functions; |
| | | using NPOI.SS.UserModel; |
| | | using Plugin.TsExecution.Entity; |
| | | using System; |
| | | using System.Collections.Generic; |
| | |
| | | using System.Timers; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using Util.ManualDebug; |
| | | |
| | | namespace Plugin.TsExecution |
| | | { |
| | |
| | | Set(ref totalNum, value); |
| | | } |
| | | } |
| | | |
| | | public TsExecution parent { get; set; } |
| | | |
| | | private int passNum; |
| | | public int PassNum |
| | |
| | | try |
| | | { |
| | | var value = ParameterManager.GetCommonParameterByName("AutoTestMode"); |
| | | IsAutoMode = bool.Parse(value.Value); |
| | | IsAutoMode = value == null ? true : bool.Parse(value.Value); |
| | | } |
| | | catch |
| | | { |
| | |
| | | |
| | | public void IniTestData() |
| | | { |
| | | Datasource.Clear(); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | Datasource.Clear(); |
| | | }); |
| | | } |
| | | |
| | | /// <summary> |
| | |
| | | |
| | | private void SendSnr2Ts() |
| | | { |
| | | |
| | | |
| | | } |
| | | |
| | | private void pausePanel_Loaded(object sender, RoutedEventArgs e) |
| | |
| | | }.ShowDialog(); |
| | | } |
| | | } |
| | | |
| | | private void control_PreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) |
| | | { |
| | | if (parent.EnableManualDebug) |
| | | { |
| | | ContextMenu contextMenu = new ContextMenu(); |
| | | contextMenu.Closed += ContextMenu_Closed; |
| | | var item = new MenuItem |
| | | { |
| | | IsCheckable = false, |
| | | Name = "manualDebugItem", |
| | | Header = this.lang("手动调试"), |
| | | }; |
| | | item.Click += Item_Click; ; |
| | | contextMenu.Items.Add(item); |
| | | this.ContextMenu = contextMenu; |
| | | } |
| | | else |
| | | { |
| | | |
| | | this.ContextMenu = null; |
| | | } |
| | | } |
| | | |
| | | private void Item_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | string manualDebugSeq = ParameterManager.GetCommonParameterByName("ManualDebugSeqPath")?.Value; |
| | | if (string.IsNullOrEmpty(manualDebugSeq)) |
| | | { |
| | | MsgBoxHelper2.Error("请先设置手动调试序列路径[ManualDebugSeqPath]!"); |
| | | return; |
| | | } |
| | | string modelPath = parent.tsModelPath; |
| | | var win = new ManualDebugWindow(manualDebugSeq, modelPath, false) |
| | | { |
| | | Title = $"工位{this.index}手动调试", |
| | | |
| | | Owner = Window.GetWindow(this), |
| | | OvenIndex = 0, |
| | | OvenName = "", |
| | | CellCount = parent.tsSockets.Count, |
| | | |
| | | CellIndex = this.index, |
| | | CellName = "", |
| | | }; |
| | | win.ShowDialog(); |
| | | } |
| | | |
| | | private void ContextMenu_Closed(object sender, RoutedEventArgs e) |
| | | { |
| | | this.ContextMenu = null; |
| | | } |
| | | } |
| | | } |
| | |
| | | IsChecked="{Binding UI_ShowInfoDock,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | Content="{DynamicResource Ts.UI_ShowInfoDock}"/> |
| | | |
| | | <CheckBox FontSize="20" x:Name="Cb_ManualDebug" Visibility="Collapsed" |
| | | IsChecked="{Binding EnableManualDebug,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" |
| | | VerticalContentAlignment="Center" Content="手动调试" /> |
| | | |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="5"> |
| | | <Label FontSize="20" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Padding="0" |
| | | Content="{DynamicResource HMI.User.Code}" /> |
| | |
| | | using NationalInstruments.TestStand.Interop.API; |
| | | using NPOI.HSSF.UserModel; |
| | | using NPOI.SS.UserModel; |
| | | using OpenTap; |
| | | using Plugin.TsSupport; |
| | | using ScottPlot; |
| | | using ScottPlot.Plottables; |
| | |
| | | using System.Linq; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Forms.DataVisualization.Charting; |
| | | |
| | | namespace Plugin.TsExecution |
| | | { |
| | |
| | | #endregion |
| | | |
| | | public static ILog logger { get; private set; } |
| | | private List<SingleControl> tsSockets = new List<SingleControl>(); |
| | | public List<SingleControl> tsSockets { get; private set; } = new List<SingleControl>(); |
| | | /// <summary> |
| | | /// 样件模式状态 |
| | | /// </summary> |
| | |
| | | } |
| | | } |
| | | |
| | | private string tsModelPath; |
| | | private bool enableManualDebug; |
| | | public bool EnableManualDebug |
| | | { |
| | | get => enableManualDebug; |
| | | set |
| | | { |
| | | if (IsTesting) |
| | | { |
| | | MsgBoxHelper2.Warn("请先结束自动测试"); |
| | | return; |
| | | } |
| | | Set(ref enableManualDebug, value); |
| | | } |
| | | } |
| | | |
| | | public string tsModelPath { get; private set; } |
| | | private string dataLogPath; |
| | | private SqliteHelper sqliteHelper; |
| | | |
| | |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | |
| | | InitChart(); |
| | | InitTsEngine(); |
| | | } |
| | | |
| | | public override void SetBaseFolder(string folder) |
| | |
| | | |
| | | public override void InitFinish() |
| | | { |
| | | InitTsEngine(); |
| | | this.ReadCount(out int pass, out int fail); |
| | | this.DisplayCount(pass, fail); |
| | | ReadConfig(); |
| | |
| | | catch (Exception ex) |
| | | { |
| | | log.Warn("Get Login User Failed.", ex); |
| | | } |
| | | RefreshUIConfig(); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 刷新UI配置 |
| | | /// </summary> |
| | | private void RefreshUIConfig() |
| | | { |
| | | try |
| | | { |
| | | bool enableManualDebug = ParameterManager.GetCommonParameterByName("EnableManualDebug", false); |
| | | if (enableManualDebug) |
| | | { |
| | | Cb_ManualDebug.Visibility = Visibility.Visible; |
| | | EnableManualDebug = false; |
| | | } |
| | | else |
| | | { |
| | | Cb_ManualDebug.Visibility = Visibility.Collapsed; |
| | | EnableManualDebug = false; |
| | | } |
| | | |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | log.Error("RefreshUIConfig Error.", ex); |
| | | } |
| | | } |
| | | |
| | |
| | | { |
| | | if (ParamSetting.EngineType == "Tap") |
| | | { |
| | | testExecutionWrapper = new TapWrapper(); |
| | | var wrapper = new TapWrapper(); |
| | | wrapper.RegisteUserInput(this); |
| | | log.Debug($"Wrapper UI is {wrapper.ui}"); |
| | | testExecutionWrapper = wrapper; |
| | | } |
| | | else |
| | | { |
| | |
| | | { |
| | | SocketName = name |
| | | }; |
| | | control.parent = this; |
| | | panel.Children.Add(control); |
| | | tsSockets.Add(control); |
| | | |
| | |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("TestReportFolder"); |
| | | csvDir = value.Value; |
| | | csvDir = value?.Value ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "TestReport"); |
| | | if (!Directory.Exists(csvDir)) |
| | | { |
| | | Directory.CreateDirectory(csvDir); |
| | |
| | | { |
| | | |
| | | value = ParameterManager.GetCommonParameterByName("IsOptRequirePassword"); |
| | | IsOptRequirePassword = bool.Parse(value.Value); |
| | | IsOptRequirePassword = value == null ? false : bool.Parse(value.Value); |
| | | } |
| | | catch |
| | | { |
| | |
| | | |
| | | public void UpdatePositionStatus(int index, string status) |
| | | { |
| | | this.tsSockets[index].UpdateStatus(status); |
| | | OnStatusPublished(index, status); |
| | | this.UpdateTestCounter(); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | this.tsSockets[index].UpdateStatus(status); |
| | | OnStatusPublished(index, status); |
| | | this.UpdateTestCounter(); |
| | | }); |
| | | } |
| | | |
| | | private void OnStatusPublished(int index, string status) |
| | |
| | | break; |
| | | |
| | | case 10006://发送产品条码 |
| | | SetPositionBarcode(index, strData); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | SetPositionBarcode(index, strData); |
| | | }); |
| | | break; |
| | | |
| | | case 10007://开始测试计时 |
| | | tsSockets[index].ElapseTime = "Start Timing"; |
| | | BtnStopSeq.IsEnabled = false; |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | tsSockets[index].ElapseTime = "Start Timing"; |
| | | BtnStopSeq.IsEnabled = false; |
| | | }); |
| | | break; |
| | | |
| | | case 10008://停止测试计时 |
| | | tsSockets[index].ElapseTime = "Stop Timing"; |
| | | BtnStopSeq.IsEnabled = true; |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | tsSockets[index].ElapseTime = "Stop Timing"; |
| | | BtnStopSeq.IsEnabled = true; |
| | | }); |
| | | break; |
| | | |
| | | case 10009: |
| | |
| | | |
| | | break; |
| | | case 10017://更新测试计时,针对部分计时有PLC完成的项目 |
| | | this.tsSockets[index].ElapseTime = strData; |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | this.tsSockets[index].ElapseTime = strData; |
| | | }); |
| | | break; |
| | | case (int)UIMessageCodes.UIMsg_EndExecution://执行结束 |
| | | //IsTesting = false; |
| | |
| | | break; |
| | | case 10030://显示测试进度 |
| | | int ProcessVal = Convert.ToInt32(strData); |
| | | this.tsSockets[index].SetTestProcess(ProcessVal); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | this.tsSockets[index].SetTestProcess(ProcessVal); |
| | | }); |
| | | break; |
| | | case 10031://导出CSV |
| | | |
| | |
| | | |
| | | public override void DoClose(object sender, EventArgs e) |
| | | { |
| | | testExecutionWrapper?.Dispose(); |
| | | base.DoClose(sender, e); |
| | | } |
| | | |
| | |
| | | |
| | | bool ReleaseSeqFile(); |
| | | void SetContext(object context); |
| | | |
| | | void Dispose(); |
| | | } |
| | | } |
| | |
| | | <SpecificVersion>False</SpecificVersion> |
| | | <HintPath>..\Libs\NationalInstruments.TestStand.Interop.UI.AxControls.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="PresentationFramework" /> |
| | | <Reference Include="System" /> |
| | | <Reference Include="System.Core" /> |
| | | <Reference Include="System.Windows.Forms" /> |
| | |
| | | <Reference Include="System.Data" /> |
| | | <Reference Include="System.Net.Http" /> |
| | | <Reference Include="System.Xml" /> |
| | | <Reference Include="WindowsBase" /> |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <Compile Include="ITestExecutionWrapper.cs" /> |
| | |
| | | <Compile Include="TestAppManager.cs" /> |
| | | <Compile Include="Properties\AssemblyInfo.cs" /> |
| | | <Compile Include="TeststandWrapper.cs" /> |
| | | <Compile Include="TsExecutionWrapper.cs" /> |
| | | </ItemGroup> |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\..\..\Project\OpenTapEditor\OpenTap\BasicSteps\Tap.Plugins.BasicSteps.csproj"> |
| | | <Project>{4BCFADC0-54F9-7032-B918-E4DD407CAA76}</Project> |
| | | <Name>Tap.Plugins.BasicSteps</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\..\..\Project\OpenTapEditor\OpenTap\Engine\Tap.Engine.csproj"> |
| | | <Project>{EFBD516D-3DC9-9D75-F082-48FBC76715F7}</Project> |
| | | <Name>Tap.Engine</Name> |
| | | </ProjectReference> |
| | | <ProjectReference Include="..\..\..\Project\OpenTapEditor\Plugin\AddInPlugin\AddInPlugin.csproj"> |
| | | <Project>{0d150ca1-d6ea-3362-7330-028939e66e1c}</Project> |
| | | <Name>AddInPlugin</Name> |
| | | </ProjectReference> |
| | | </ItemGroup> |
| | | <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> |
| | | </Project> |
| | |
| | | using System; |
| | | //using AddInPlugin; |
| | | //using AddInPlugin.Util; |
| | | //using Microsoft.Win32; |
| | | //using OpenTap; |
| | | //using OpenTap.Addin; |
| | | //using OpenTap.Addin.Util; |
| | | //using System.ComponentModel; |
| | | //using System.IO; |
| | | using AddInPlugin; |
| | | using AddInPlugin.Util; |
| | | using Microsoft.Win32; |
| | | using OpenTap; |
| | | using OpenTap.Addin; |
| | | using OpenTap.Addin.Listener; |
| | | using OpenTap.Addin.Reader; |
| | | using OpenTap.Addin.Util; |
| | | using OpenTap.Plugins.BasicSteps; |
| | | using System; |
| | | using System.ComponentModel; |
| | | using System.IO; |
| | | using System.Windows; |
| | | using System.Windows.Threading; |
| | | |
| | | namespace Plugin.TsSupport |
| | | { |
| | | |
| | | public class TapWrapper : ITestExecutionWrapper |
| | | public class Net47Getter : IRegGetter |
| | | { |
| | | public string GetAppPath(string keyPath) |
| | | { |
| | | using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath)) |
| | | { |
| | | if (key == null) |
| | | { |
| | | throw new Exception("注册表不存在,请先运行一次编辑器"); |
| | | } |
| | | |
| | | string value = key.GetValue("Location")?.ToString(); |
| | | if (string.IsNullOrEmpty(value)) |
| | | { |
| | | throw new Exception("注册表不存在,请先运行一次编辑器"); |
| | | } |
| | | return value; |
| | | } |
| | | } |
| | | } |
| | | |
| | | public class TapWrapper : ITestExecutionWrapper, IResultListener, IUserInputInterface |
| | | { |
| | | TapThread testPlanThread; |
| | | TestPlan Plan; |
| | | TestPlanRun PlanRun; |
| | | |
| | | public event PropertyChangedEventHandler PropertyChanged; |
| | | public event EventHandler<UIMessageEvent> UIMessageEvent; |
| | | public event EventHandler<ExecutionEndEvent> EndExecutionEvent; |
| | | |
| | | public void CommitToDisk(bool option) |
| | | { |
| | | private PostUIMessageHandler uIMessageHandler; |
| | | |
| | | } |
| | | public string Name { get; set; } |
| | | |
| | | public T GetFileGlobalValue<T>(string key, int option) |
| | | { |
| | | return default(T); |
| | | } |
| | | public bool IsConnected => true; |
| | | |
| | | public T GetStationGlobalValue<T>(string key, int option) |
| | | static TapWrapper() |
| | | { |
| | | return default(T); |
| | | RegUtil.Register(new Net47Getter()); |
| | | } |
| | | |
| | | public object Init() |
| | | { |
| | | StationGlobalsManager.Register(new DefaultStationGlobalsManager()); |
| | | VariableResolverFactory.Register(new DefaultVariableResolverFactory()); |
| | | ResultSettings.Current.Add(this); |
| | | UserInput.SetInterface(this); |
| | | LvlibpLoader.PreLoad(); |
| | | PropertyInfoCache.Refresh(typeof(MethodVariable)); |
| | | uIMessageHandler = PostUIMessageHandler.Get(); |
| | | uIMessageHandler.OnPostUIMsg += PostUIMsgHandler_OnPostUIMsg; |
| | | return null; |
| | | } |
| | | |
| | | private void PostUIMsgHandler_OnPostUIMsg(object sender, PostUIMsg e) |
| | | { |
| | | UIMessageEvent?.Invoke(this, new UIMessageEvent() |
| | | { |
| | | Origin = e, |
| | | EventCode = e.EventCode, |
| | | BoolData = e.BoolData, |
| | | NumberData = e.NumberData, |
| | | StrData = e.StringData |
| | | }); |
| | | } |
| | | |
| | | public object StartSequence(string filePath, string modelPath) |
| | | { |
| | | Plan = TestPlanReader.ReadTestPlan(filePath); |
| | | //Plan = TestPlan.Load(filePath); |
| | | testPlanThread = TapThread.Start(() => |
| | | { |
| | | var res = ResultSettings.Current; |
| | | res.Add(uIMessageHandler); |
| | | Plan.Execute(ResultSettings.Current); |
| | | }); |
| | | return Plan; |
| | | } |
| | | |
| | | |
| | | public T GetFileGlobalValue<T>(string key, int option) |
| | | { |
| | | return PlanRun.Resolve<T>(null, $"FileGlobals.{key}"); |
| | | } |
| | | |
| | | public T GetStationGlobalValue<T>(string key, int option) |
| | | { |
| | | if (PlanRun == null) |
| | | { |
| | | return StationGlobalsManager.GetValue<T>(key); |
| | | } |
| | | return PlanRun.Resolve<T>(null, $"StationGlobals.{key}"); |
| | | } |
| | | |
| | | public T GetValue<T>(string key, int option) |
| | | { |
| | | return default(T); |
| | | } |
| | | |
| | | public void SetValue<T>(string key, T value, int option) |
| | | { |
| | | |
| | | } |
| | | |
| | | public void SetFileGlobalValue<T>(string key, int option, T value) |
| | | { |
| | | PlanRun.Update(null, $"FileGlobals.{key}", value); |
| | | } |
| | | |
| | | public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false) |
| | | { |
| | | if (PlanRun == null) |
| | | { |
| | | StationGlobalsManager.SetValue(key, value); |
| | | } |
| | | else |
| | | { |
| | | PlanRun.Update(null, $"StationGlobals.{key}", value); |
| | | } |
| | | } |
| | | |
| | | public void CommitToDisk(bool option) |
| | | { |
| | | StationGlobalsManager.WriteToLocal(null); |
| | | } |
| | | |
| | | public void OnTestPlanRunStart(TestPlanRun planRun) |
| | | { |
| | | PlanRun = planRun; |
| | | } |
| | | |
| | | public void OnTestPlanRunCompleted(TestPlanRun planRun, Stream logStream) |
| | | { |
| | | PlanRun = null; |
| | | } |
| | | |
| | | public void OnTestStepRunStart(TestStepRun stepRun) |
| | | { |
| | | |
| | | } |
| | | |
| | | public void OnTestStepRunCompleted(TestStepRun stepRun) |
| | | { |
| | | |
| | | } |
| | | |
| | | public void OnResultPublished(Guid stepRunID, ResultTable result) |
| | | { |
| | | |
| | | } |
| | | |
| | | public void Open() |
| | | { |
| | | |
| | | } |
| | | |
| | | public void Close() |
| | | { |
| | | EndExecutionEvent?.Invoke(this, new ExecutionEndEvent()); |
| | | Plan = null; |
| | | PlanRun = null; |
| | | testPlanThread = null; |
| | | } |
| | | |
| | | public bool WaitForExit() |
| | | { |
| | | return true; |
| | | } |
| | | |
| | | public bool ReleaseSeqFile() |
| | |
| | | return true; |
| | | } |
| | | |
| | | public void Dispose() |
| | | { |
| | | } |
| | | |
| | | public void SetContext(object context) |
| | | { |
| | | |
| | | } |
| | | |
| | | public void SetFileGlobalValue<T>(string key, int option, T value) |
| | | { |
| | | public DispatcherObject ui { get; private set; } |
| | | |
| | | public void RegisteUserInput(DispatcherObject ui) |
| | | { |
| | | this.ui = ui; |
| | | } |
| | | |
| | | public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false) |
| | | public void RequestUserInput(object dataObject, TimeSpan Timeout, bool modal) |
| | | { |
| | | |
| | | } |
| | | |
| | | public object StartSequence(string filePath, string modelPath) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | public bool WaitForExit() |
| | | { |
| | | return true; |
| | | if (dataObject is DialogRequest req) |
| | | { |
| | | this.ui?.Dispatcher.BeginInvoke(new Action(() => |
| | | { |
| | | if (req.Buttons == InputButtons.OkCancel) |
| | | { |
| | | var res = MessageBox.Show(req.Message, req.Name, MessageBoxButton.OKCancel); |
| | | req.Input2 = res == MessageBoxResult.OK ? WaitForInputResult2.Ok : WaitForInputResult2.Cancel; |
| | | } |
| | | else |
| | | { |
| | | var res = MessageBox.Show(req.Message, req.Name, MessageBoxButton.YesNo); |
| | | req.Input1 = res == MessageBoxResult.Yes ? WaitForInputResult1.Yes : WaitForInputResult1.No; |
| | | } |
| | | })).Wait(); |
| | | } |
| | | else if (dataObject is OpenTap.TestPlan.PlanLoadError err) |
| | | { |
| | | this.ui?.Dispatcher.BeginInvoke(new Action(() => |
| | | { |
| | | var res = MessageBox.Show(err.Message, err.Name, MessageBoxButton.OKCancel); |
| | | err.Response = res == MessageBoxResult.OK ? TestPlan.PlanLoadErrorResponse.Ignore : TestPlan.PlanLoadErrorResponse.Abort; |
| | | })).Wait(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | //public class Net47Getter : IRegGetter |
| | | //{ |
| | | // public string GetAppPath(string keyPath) |
| | | // { |
| | | // using (RegistryKey key = Registry.CurrentUser.OpenSubKey(keyPath)) |
| | | // { |
| | | // if (key == null) |
| | | // { |
| | | // throw new Exception("注册表不存在,请先运行一次编辑器"); |
| | | // } |
| | | |
| | | // string value = key.GetValue("Location")?.ToString(); |
| | | // if (string.IsNullOrEmpty(value)) |
| | | // { |
| | | // throw new Exception("注册表不存在,请先运行一次编辑器"); |
| | | // } |
| | | // return value; |
| | | // } |
| | | // } |
| | | //} |
| | | |
| | | //public class TapWrapper : ITestExecutionWrapper, IResultListener |
| | | //{ |
| | | // TapThread testPlanThread; |
| | | // TestPlan Plan; |
| | | // TestPlanRun PlanRun; |
| | | |
| | | // public event PropertyChangedEventHandler PropertyChanged; |
| | | |
| | | // public event EventHandler<UIMessageEvent> UIMessageEvent; |
| | | // public event EventHandler<ExecutionEndEvent> EndExecutionEvent; |
| | | |
| | | // public string Name { get; set; } |
| | | |
| | | // public bool IsConnected => true; |
| | | |
| | | // public object Init() |
| | | // { |
| | | // RegUtil.Register(new Net47Getter()); |
| | | // StationGlobalsManager.Register(new DefaultStationGlobalsManager()); |
| | | // VariableResolverFactory.Register(new DefaultVariableResolverFactory()); |
| | | // ResultSettings.Current.Add(this); |
| | | // return null; |
| | | // } |
| | | |
| | | // private void PostUIMsgHandler_OnPostUIMsg(object sender, PostUIMsg e) |
| | | // { |
| | | // UIMessageEvent?.Invoke(this, new UIMessageEvent() |
| | | // { |
| | | // Origin = e, |
| | | // EventCode = e.EventCode, |
| | | // BoolData = e.BoolData, |
| | | // NumberData = e.NumberData, |
| | | // StrData = e.StringData |
| | | // }); |
| | | // } |
| | | |
| | | // public object StartSequence(string filePath, string modelPath) |
| | | // { |
| | | // Plan = TestPlan.Load(filePath); |
| | | // testPlanThread = TapThread.Start(() => |
| | | // { |
| | | // var res = ResultSettings.Current; |
| | | // Plan.Execute(ResultSettings.Current, null); |
| | | // }); |
| | | // return Plan; |
| | | // } |
| | | |
| | | |
| | | // public T GetFileGlobalValue<T>(string key, int option) |
| | | // { |
| | | // return PlanRun.Resolve<T>(null, $"FileGlobals.{key}"); |
| | | // } |
| | | |
| | | // public T GetStationGlobalValue<T>(string key, int option) |
| | | // { |
| | | // if (PlanRun == null) |
| | | // { |
| | | // return StationGlobalsManager.GetValue<T>(key); |
| | | // } |
| | | // return PlanRun.Resolve<T>(null, $"StationGlobals.{key}"); |
| | | // } |
| | | |
| | | // public T GetValue<T>(string key, int option) |
| | | // { |
| | | // return default(T); |
| | | // } |
| | | |
| | | // public void SetValue<T>(string key, T value, int option) |
| | | // { |
| | | |
| | | // } |
| | | |
| | | // public void SetFileGlobalValue<T>(string key, int option, T value) |
| | | // { |
| | | // PlanRun.Update(null, $"FileGlobals.{key}", value); |
| | | // } |
| | | |
| | | // public void SetStationGlobalValue<T>(string key, int option, T value, bool inContext = false) |
| | | // { |
| | | // if (PlanRun == null) |
| | | // { |
| | | // StationGlobalsManager.SetValue(key, value); |
| | | // } |
| | | // else |
| | | // { |
| | | // PlanRun.Update(null, $"StationGlobals.{key}", value); |
| | | // } |
| | | // } |
| | | |
| | | // public void CommitToDisk(bool option) |
| | | // { |
| | | // StationGlobalsManager.WriteToLocal(null); |
| | | // } |
| | | |
| | | // public void OnTestPlanRunStart(TestPlanRun planRun) |
| | | // { |
| | | // PlanRun = planRun; |
| | | // } |
| | | |
| | | // public void OnTestPlanRunCompleted(TestPlanRun planRun, Stream logStream) |
| | | // { |
| | | |
| | | |
| | | // } |
| | | |
| | | // public void OnTestStepRunStart(TestStepRun stepRun) |
| | | // { |
| | | |
| | | // } |
| | | |
| | | // public void OnTestStepRunCompleted(TestStepRun stepRun) |
| | | // { |
| | | |
| | | // } |
| | | |
| | | // public void OnResultPublished(Guid stepRunID, ResultTable result) |
| | | // { |
| | | |
| | | // } |
| | | |
| | | // public void Open() |
| | | // { |
| | | // PostUIMsgHandler.OnPostUIMsg += PostUIMsgHandler_OnPostUIMsg; |
| | | // } |
| | | |
| | | // public void Close() |
| | | // { |
| | | // PostUIMsgHandler.OnPostUIMsg -= PostUIMsgHandler_OnPostUIMsg; |
| | | // EndExecutionEvent?.Invoke(this, new ExecutionEndEvent()); |
| | | // PlanRun = null; |
| | | // } |
| | | |
| | | // public bool WaitForExit() |
| | | // { |
| | | // return true; |
| | | // } |
| | | |
| | | // public bool ReleaseSeqFile() |
| | | // { |
| | | // return true; |
| | | // } |
| | | |
| | | // public void SetContext(object context) |
| | | // { |
| | | |
| | | // } |
| | | //} |
| | | } |
| | |
| | | return instance; |
| | | } |
| | | } |
| | | |
| | | public static void Dispose() |
| | | { |
| | | lock (lockObj) |
| | | { |
| | | if (instance != null) |
| | | { |
| | | instance.Dispose(); |
| | | instance = null; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | return this.AxApplicationMgr; |
| | | } |
| | | |
| | | private SequenceFile GetFile(string path) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | public object StartSequence(string filePath, string modelPath) |
| | | { |
| | | this.testSeqFile = filePath; |
| | | |
| | | this.modeSeq = this.engine.GetSequenceFileEx(modelPath); |
| | | this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 2, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType); |
| | | this.testSeq = this.engine.GetSequenceFileEx(this.testSeqFile, 107, TypeConflictHandlerTypes.ConflictHandler_UseGlobalType); |
| | | this.execution = this.engine.NewExecution(this.testSeq, "Test UUTs", this.modeSeq, false, 0, null, null, null); |
| | | return execution; |
| | | } |
| | |
| | | { |
| | | this.context = (SequenceContext)context; |
| | | } |
| | | |
| | | public void Dispose() |
| | | { |
| | | AxApplicationMgr = null; |
| | | engine = null; |
| | | TestAppManager.Dispose(); |
| | | } |
| | | } |
| | | } |
| | |
| | | </ItemGroup> |
| | | |
| | | <ItemGroup> |
| | | <ProjectReference Include="..\..\UILib\UILib.csproj" /> |
| | | <ProjectReference Include="..\..\UtilLib\UtilLib.csproj" /> |
| | | <ProjectReference Include="..\..\..\VisualStudio\UILib\UILib.csproj" /> |
| | | <ProjectReference Include="..\..\..\VisualStudio\UtilLib\UtilLib.csproj" /> |
| | | <ProjectReference Include="..\HMI.UI\HMI.UI.csproj" /> |
| | | </ItemGroup> |
| | | |
| New file |
| | |
| | | ## TestPlatfoming |
| | | |
| | | 平台测试程序 |
| | | |
| | |
| | | <Reference Include="System" /> |
| | | <Reference Include="System.Core" /> |
| | | <Reference Include="System.Data.SQLite"> |
| | | <HintPath>..\..\..\HMI.Old\Plugin_OPCServer\Libs\System.Data.SQLite.dll</HintPath> |
| | | <HintPath>..\..\Libs\System.Data.SQLite.dll</HintPath> |
| | | </Reference> |
| | | <Reference Include="System.Xml.Linq" /> |
| | | <Reference Include="System.Data.DataSetExtensions" /> |
| New file |
| | |
| | | <UserControl x:Class="Util.ManualDebug.ManualCell" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:local="clr-namespace:Util.ManualDebug" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | mc:Ignorable="d" Padding="5" MaxHeight="80" |
| | | d:DesignHeight="450" d:DesignWidth="800"> |
| | | <UserControl.Style> |
| | | <Style TargetType="UserControl"> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding Selected}" Value="True"> |
| | | <Setter Property="Background" Value="{DynamicResource SuccessColor}"/> |
| | | <Setter Property="Foreground" Value="#FFF"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </UserControl.Style> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | </Grid.RowDefinitions> |
| | | <TextBlock FontSize="20" FontWeight="Bold" Text="{Binding Title}"/> |
| | | <pu:Switch Grid.Row="1" FontSize="18" BoxHeight="20" Cursor="Hand" |
| | | Content="关" |
| | | CheckedBackground="Green" |
| | | Background="Red" |
| | | CheckedContent="开" |
| | | IsChecked="{Binding Selected,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/> |
| | | </Grid> |
| | | </UserControl> |
| New file |
| | |
| | | using System.ComponentModel; |
| | | using System.Runtime.CompilerServices; |
| | | using System.Windows.Controls; |
| | | |
| | | namespace Util.ManualDebug |
| | | { |
| | | /// <summary> |
| | | /// ManualCell.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class ManualCell : UserControl, INotifyPropertyChanged |
| | | { |
| | | #region ... |
| | | public event PropertyChangedEventHandler PropertyChanged; |
| | | |
| | | protected void Set([CallerMemberName] string name = null) |
| | | { |
| | | |
| | | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); |
| | | } |
| | | |
| | | protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null) |
| | | { |
| | | old = @new; |
| | | if (propertyName != null) |
| | | { |
| | | Set(propertyName); |
| | | } |
| | | } |
| | | #endregion |
| | | private string title; |
| | | public string Title |
| | | { |
| | | get => title; |
| | | set => Set(ref title, value); |
| | | } |
| | | |
| | | private bool selected; |
| | | public bool Selected |
| | | { |
| | | get => selected; |
| | | set |
| | | { |
| | | if (selected != value) |
| | | { |
| | | Set(ref selected, value); |
| | | SelectedChanged?.Invoke(this, new EventArgs()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private int index; |
| | | public int Index |
| | | { |
| | | get => index; set => Set(ref index, value); |
| | | } |
| | | |
| | | public event EventHandler SelectedChanged; |
| | | |
| | | public ManualCell() |
| | | { |
| | | this.DataContext = this; |
| | | InitializeComponent(); |
| | | } |
| | | |
| | | } |
| | | } |
| | |
| | | <UserControl x:Class="Util.SB.ManualDebug" |
| | | <UserControl x:Class="Util.ManualDebug.ManualDebug" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | xmlns:local="clr-namespace:Util.SB" |
| | | xmlns:local="clr-namespace:Util.ManualDebug" |
| | | xmlns:dp="clr-namespace:HMI.UI.DpUtil;assembly=HMI.UI" |
| | | mc:Ignorable="d" |
| | | x:Name="self" |
| | | MinHeight="400" MinWidth="500"> |
| | |
| | | </UserControl.Resources> |
| | | |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition Height="Auto"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid Margin="10"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="5"/> |
| | | <RowDefinition Height="*"/> |
| | | <RowDefinition Height="5"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost> |
| | | <Grid.ColumnDefinitions> |
| | | <ColumnDefinition Width="Auto"/> |
| | | <ColumnDefinition/> |
| | | </Grid.ColumnDefinitions> |
| | | |
| | | <!-- 按钮和状态区域 --> |
| | | <Border Grid.Row="0" Background="{StaticResource TsPanelBrush}" |
| | | <Grid x:Name="CellSelectBorder"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition/> |
| | | <RowDefinition/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <Border Background="{StaticResource TsPanelBrush}" Margin="0,0,10,5" |
| | | BorderBrush="{StaticResource BorderBrush}" |
| | | BorderThickness="1" CornerRadius="4" Padding="15"> |
| | | <StackPanel> |
| | | <TextBlock Text="穴位选择" FontSize="14" FontWeight="SemiBold" |
| | | Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/> |
| | | |
| | | <UniformGrid IsEnabled="False" x:Name="CellGrid" dp:UniformGridHelper.InnerBorder="1" Columns="1" |
| | | dp:UniformGridHelper.InnerBorderBrush="{StaticResource BorderBrush}"> |
| | | |
| | | </UniformGrid> |
| | | </StackPanel> |
| | | </Border> |
| | | |
| | | <Border Grid.Row="1" Background="{StaticResource TsPanelBrush}" Margin="0,5,10,0" |
| | | BorderBrush="{StaticResource BorderBrush}" |
| | | BorderThickness="1" CornerRadius="4" Padding="15"> |
| | | <StackPanel> |
| | | <TextBlock Text="穴位测试结果" FontSize="14" FontWeight="SemiBold" |
| | | Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/> |
| | | |
| | | <UniformGrid IsEnabled="False" x:Name="CellResultGrid" Columns="1" dp:UniformGridHelper.InnerBorder="1" |
| | | dp:UniformGridHelper.InnerBorderBrush="{StaticResource BorderBrush}"> |
| | | |
| | | </UniformGrid> |
| | | </StackPanel> |
| | | </Border> |
| | | </Grid> |
| | | |
| | | <Grid Grid.Column="1"> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="5"/> |
| | | <RowDefinition Height="*"/> |
| | | <RowDefinition Height="5"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <WindowsFormsHost x:Name="host" Visibility="Collapsed"></WindowsFormsHost> |
| | | |
| | | <!-- 按钮和状态区域 --> |
| | | <Border Grid.Row="0" Background="{StaticResource TsPanelBrush}" |
| | | BorderBrush="{StaticResource BorderBrush}" |
| | | BorderThickness="1" CornerRadius="4" Padding="15"> |
| | | <StackPanel> |
| | | <TextBlock Text="手动调试控制" FontSize="14" FontWeight="SemiBold" |
| | | <StackPanel> |
| | | <TextBlock Text="手动调试控制" FontSize="14" FontWeight="SemiBold" |
| | | Foreground="{StaticResource PrimaryBrush}" Margin="0,0,0,15"/> |
| | | |
| | | <WrapPanel x:Name="btnPanel"> |
| | | <WrapPanel x:Name="btnPanel"> |
| | | |
| | | </WrapPanel> |
| | | </StackPanel> |
| | | </Border> |
| | | </WrapPanel> |
| | | </StackPanel> |
| | | </Border> |
| | | |
| | | <!-- 分隔条1 --> |
| | | <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" |
| | | <!-- 分隔条1 --> |
| | | <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" |
| | | Background="Transparent" Cursor="SizeNS"/> |
| | | |
| | | <!-- TX/RX 数据区域 --> |
| | | <Border Grid.Row="2" Background="{StaticResource TsPanelBrush}" |
| | | <!-- TX/RX 数据区域 --> |
| | | <Border Grid.Row="2" Background="{StaticResource TsPanelBrush}" |
| | | BorderBrush="{StaticResource BorderBrush}" |
| | | BorderThickness="1" CornerRadius="4" Padding="10"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10"> |
| | | <TextBlock Text="测试数据" FontSize="14" FontWeight="SemiBold" |
| | | <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10"> |
| | | <TextBlock Text="测试数据" FontSize="14" FontWeight="SemiBold" |
| | | Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/> |
| | | <Button Content="清空" Style="{StaticResource ActionButtonStyle}" |
| | | <Button Content="清空" Style="{StaticResource ActionButtonStyle}" |
| | | Margin="15,0,0,0" Click="OnClearClick"/> |
| | | </StackPanel> |
| | | </StackPanel> |
| | | |
| | | <DataGrid x:Name="DataGridComm" Grid.Row="1" |
| | | <DataGrid x:Name="DataGridComm" Grid.Row="1" |
| | | ItemsSource="{Binding TestData,ElementName=self}"> |
| | | <DataGrid.Columns> |
| | | <DataGridTextColumn Header="测试项" Binding="{Binding Name}" Width="120" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="收" Binding="{Binding Receive}" Width="*" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="发" Binding="{Binding Send}" Width="*" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="单位" Binding="{Binding Unit}" Width="60" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="上限" Binding="{Binding UpperLimit}" Width="80" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="下限" Binding="{Binding LowerLimit}" Width="80" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="结果" Binding="{Binding Result}" Width="70" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="时间" Binding="{Binding Time}" Width="90" IsReadOnly="True"/> |
| | | </DataGrid.Columns> |
| | | <DataGrid.RowStyle> |
| | | <Style TargetType="DataGridRow"> |
| | | <Style.Triggers> |
| | | <DataTrigger Value="True"> |
| | | <DataTrigger.Binding> |
| | | <MultiBinding Converter="{x:Static local:ResultFailedConverter.Instance}"> |
| | | <Binding Path="Result"/> |
| | | </MultiBinding> |
| | | </DataTrigger.Binding> |
| | | <Setter Property="Background" Value="#FFE0E0"/> |
| | | <Setter Property="Foreground" Value="#C53030"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </DataGrid.RowStyle> |
| | | </DataGrid> |
| | | </Grid> |
| | | </Border> |
| | | <DataGrid.Columns> |
| | | <DataGridTextColumn Header="测试项" Binding="{Binding Name}" Width="120" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="收" Binding="{Binding Receive}" Width="*" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="发" Binding="{Binding Send}" Width="*" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="单位" Binding="{Binding Unit}" Width="60" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="上限" Binding="{Binding UpperLimit}" Width="80" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="下限" Binding="{Binding LowerLimit}" Width="80" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="结果" Binding="{Binding Result}" Width="70" IsReadOnly="True"/> |
| | | <DataGridTextColumn Header="时间" Binding="{Binding Time}" Width="90" IsReadOnly="True"/> |
| | | </DataGrid.Columns> |
| | | <DataGrid.RowStyle> |
| | | <Style TargetType="DataGridRow"> |
| | | <Style.Triggers> |
| | | <DataTrigger Value="True"> |
| | | <DataTrigger.Binding> |
| | | <MultiBinding Converter="{x:Static local:ResultFailedConverter.Instance}"> |
| | | <Binding Path="Result"/> |
| | | </MultiBinding> |
| | | </DataTrigger.Binding> |
| | | <Setter Property="Background" Value="#FFE0E0"/> |
| | | <Setter Property="Foreground" Value="#C53030"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </DataGrid.RowStyle> |
| | | </DataGrid> |
| | | </Grid> |
| | | </Border> |
| | | |
| | | <!-- 分隔条2 --> |
| | | <GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch" |
| | | <!-- 分隔条2 --> |
| | | <GridSplitter Grid.Row="3" Height="5" HorizontalAlignment="Stretch" |
| | | Background="Transparent" Cursor="SizeNS"/> |
| | | |
| | | <!-- 日志区域 --> |
| | | <Border Grid.Row="4" Background="{StaticResource TsPanelBrush}" |
| | | <!-- 日志区域 --> |
| | | <Border Grid.Row="4" Background="{StaticResource TsPanelBrush}" |
| | | BorderBrush="{StaticResource BorderBrush}" |
| | | BorderThickness="1" CornerRadius="4" Padding="10"> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | <Grid> |
| | | <Grid.RowDefinitions> |
| | | <RowDefinition Height="Auto"/> |
| | | <RowDefinition Height="*"/> |
| | | </Grid.RowDefinitions> |
| | | |
| | | <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8"> |
| | | <TextBlock Text="日志" FontSize="14" FontWeight="SemiBold" |
| | | <StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,8"> |
| | | <TextBlock Text="日志" FontSize="14" FontWeight="SemiBold" |
| | | Foreground="{StaticResource PrimaryBrush}" VerticalAlignment="Center"/> |
| | | <Button Content="清空日志" Style="{StaticResource ActionButtonStyle}" |
| | | <Button Content="清空日志" Style="{StaticResource ActionButtonStyle}" |
| | | Margin="15,0,0,0" Click="OnClearLogClick"/> |
| | | </StackPanel> |
| | | </StackPanel> |
| | | |
| | | <RichTextBox x:Name="LogRichTextBox" Grid.Row="1" |
| | | <RichTextBox x:Name="LogRichTextBox" Grid.Row="1" |
| | | IsReadOnly="True" |
| | | VerticalScrollBarVisibility="Auto" |
| | | FontFamily="Consolas" |
| | |
| | | BorderThickness="1" |
| | | BorderBrush="Black" |
| | | Padding="4"/> |
| | | </Grid> |
| | | </Grid> |
| | | </Border> |
| | | </Grid> |
| | | <Border Background="#AAFFFFFF" Grid.ColumnSpan="2" x:Name="mask"> |
| | | <TextBlock Text="初始化中..." |
| | | FontSize="16" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/> |
| | | </Border> |
| | | </Grid> |
| | | <Border Background="#AAFFFFFF" x:Name="mask"> |
| | | <TextBlock Text="初始化中..." |
| | | FontSize="16" FontWeight="Bold" Foreground="{StaticResource PrimaryBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/> |
| | | </Border> |
| | | |
| | | <StatusBar FontSize="14" Grid.Row="1"> |
| | | <StatusBar.ItemsPanel> |
| | | <ItemsPanelTemplate> |
| | | <DockPanel LastChildFill="False"/> |
| | | </ItemsPanelTemplate> |
| | | </StatusBar.ItemsPanel> |
| | | |
| | | <StatusBarItem DockPanel.Dock="Left"> |
| | | <StackPanel Orientation="Horizontal" dp:StackPanelHelper.ChildSpace="8"> |
| | | <StackPanel Orientation="Horizontal"> |
| | | <Ellipse Width="10" Height="10"> |
| | | <Ellipse.Style> |
| | | <Style TargetType="Ellipse"> |
| | | <Setter Property="Fill" Value="LightGray"/> |
| | | <Style.Triggers> |
| | | <DataTrigger Binding="{Binding TcpServerStatus,ElementName=self}" Value="ERROR"> |
| | | <Setter Property="Fill" Value="Red"/> |
| | | </DataTrigger> |
| | | <DataTrigger Binding="{Binding TcpServerStatus,ElementName=self}" Value="RUNNING"> |
| | | <Setter Property="Fill" Value="Green"/> |
| | | </DataTrigger> |
| | | </Style.Triggers> |
| | | </Style> |
| | | </Ellipse.Style> |
| | | </Ellipse> |
| | | <TextBlock Text="{Binding TcpServerInfo,ElementName=self}"/> |
| | | <Button Padding="0" Margin="0" BorderThickness="0" Click="Button_Click">🔄</Button> |
| | | </StackPanel> |
| | | </StackPanel> |
| | | </StatusBarItem> |
| | | </StatusBar> |
| | | </Grid> |
| | | </UserControl> |
| | |
| | | using HMI.Core.Util; |
| | | using HMI.UI.DpUtil; |
| | | using NationalInstruments.TestStand.Interop.API; |
| | | using Plugin.TsSupport; |
| | | using SuperSocket.SocketBase.Config; |
| | | using System.Collections; |
| | | using System.Collections.ObjectModel; |
| | | using System.ComponentModel; |
| | | using System.Configuration; |
| | | using System.Globalization; |
| | | using System.Runtime.CompilerServices; |
| | | using System.Windows; |
| | | using System.Windows.Controls; |
| | | using System.Windows.Data; |
| | | using System.Windows.Documents; |
| | | using System.Windows.Media; |
| | | using Util.ManualDebug.Model; |
| | | |
| | | namespace Util.SB |
| | | namespace Util.ManualDebug |
| | | { |
| | | public class BtnConfig |
| | | { |
| | |
| | | Warn = 2, |
| | | Error = 3 |
| | | } |
| | | |
| | | public partial class ManualDebug : UserControl |
| | | |
| | | public partial class ManualDebug : UserControl, INotifyPropertyChanged |
| | | { |
| | | #region ... |
| | | public event PropertyChangedEventHandler PropertyChanged; |
| | | |
| | | protected void Set([CallerMemberName] string name = null) |
| | | { |
| | | |
| | | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); |
| | | } |
| | | |
| | | protected void Set<T>(ref T old, T @new, [CallerMemberName] string propertyName = null) |
| | | { |
| | | old = @new; |
| | | if (propertyName != null) |
| | | { |
| | | Set(propertyName); |
| | | } |
| | | } |
| | | #endregion |
| | | public static readonly DependencyProperty OvenNameProperty = DependencyProperty.Register( |
| | | "OvenName", |
| | | typeof(string), |
| | |
| | | |
| | | public static readonly DependencyProperty CellIndexProperty = DependencyProperty.Register( |
| | | "CellIndex", |
| | | typeof(int), |
| | | typeof(ManualDebug), |
| | | new PropertyMetadata(-1)); |
| | | |
| | | public static readonly DependencyProperty CellCountProperty = DependencyProperty.Register( |
| | | "CellCount", |
| | | typeof(int), |
| | | typeof(ManualDebug), |
| | | new PropertyMetadata(0)); |
| | |
| | | set { SetValue(CellIndexProperty, value); } |
| | | } |
| | | |
| | | public int CellCount |
| | | { |
| | | get { return (int)GetValue(CellCountProperty); } |
| | | set { SetValue(CellCountProperty, value); } |
| | | } |
| | | |
| | | public ObservableCollection<TestDataItem> TestData { get; private set; } = new ObservableCollection<TestDataItem>(); |
| | | |
| | | private Dictionary<string, StatusBtn> btnCache = new Dictionary<string, StatusBtn>(); |
| | |
| | | |
| | | private ITestExecutionWrapper executionWrapper; |
| | | |
| | | private TcpServer tcpServer; |
| | | |
| | | public string seqPath { get; set; } |
| | | public string modelPath { get; set; } = "C:\\Program Files\\National Instruments\\TestStand 2020\\Components\\Models\\TestStandModels\\ParallelModel.seq"; |
| | | |
| | | private string tcpServerStatus; |
| | | public string TcpServerStatus |
| | | { |
| | | get => tcpServerStatus; |
| | | set |
| | | { |
| | | Set(ref tcpServerStatus, value); |
| | | } |
| | | } |
| | | |
| | | private string tcpServerInfo; |
| | | public string TcpServerInfo |
| | | { |
| | | get => tcpServerInfo; |
| | | set |
| | | { |
| | | Set(ref tcpServerInfo, value); |
| | | } |
| | | } |
| | | |
| | | public ManualDebug() |
| | | { |
| | |
| | | RefreshButtons(null); |
| | | } |
| | | |
| | | public void HideCells(bool flag) |
| | | { |
| | | CellSelectBorder.Visibility = flag ? Visibility.Collapsed : Visibility.Visible; |
| | | } |
| | | |
| | | private List<ManualCell> Cells = new List<ManualCell>(); |
| | | private List<StatusCell> StatusCells = new List<StatusCell>(); |
| | | |
| | | public void RefreshGrid(CellConfig[] cells) |
| | | { |
| | | if (cells != null) |
| | | { |
| | | int i = 0; |
| | | foreach (var cell in cells) |
| | | { |
| | | var mCell = new ManualCell |
| | | { |
| | | Title = cell.Name, |
| | | Index = cell.Index |
| | | }; |
| | | mCell.SelectedChanged += MCell_SelectedChanged; |
| | | Cells.Add(mCell); |
| | | CellGrid.Children.Add(mCell); |
| | | |
| | | var sCell = new StatusCell |
| | | { |
| | | Title = cell.Name, |
| | | Index = cell.Index |
| | | }; |
| | | StatusCells.Add(sCell); |
| | | CellResultGrid.Children.Add(sCell); |
| | | |
| | | BindingOperations.SetBinding(sCell, StatusCell.StatusProperty, new Binding(nameof(ManualCell.Selected)) |
| | | { |
| | | Source = mCell, |
| | | Mode = BindingMode.OneWay, |
| | | //UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, |
| | | Converter = CellStatusConverter.Instance |
| | | }); |
| | | } |
| | | |
| | | UniformGridHelper.RefreshGridInnerBorder(CellGrid); |
| | | UniformGridHelper.RefreshGridInnerBorder(CellResultGrid); |
| | | } |
| | | } |
| | | |
| | | private void MCell_SelectedChanged(object sender, EventArgs e) |
| | | { |
| | | object[] arr = Cells.Select(c => (object)c.Selected).ToArray(); |
| | | SetArray($"Running.CellIndex", PropertyValueTypes.PropValType_Boolean, arr); |
| | | } |
| | | |
| | | private void ManualDebug_Loaded(object sender, RoutedEventArgs e) |
| | | { |
| | | StartTcpServer(); |
| | | Task.Run(() => |
| | | { |
| | | System.Threading.Thread.Sleep(500); |
| | |
| | | }); |
| | | } |
| | | |
| | | private int GetConfigPort() |
| | | { |
| | | try |
| | | { |
| | | var customConfig = (Hashtable)ConfigurationManager.GetSection("TestResultConfig"); |
| | | if (customConfig.ContainsKey("PortStart")) |
| | | { |
| | | int port = int.Parse(customConfig["PortStart"].ToString()) - 1; |
| | | return port; |
| | | } |
| | | else |
| | | { |
| | | return 6659; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | AppendLog(LogLevel.Error, $"读取起始端口错误: {ex.Message} {ex.StackTrace} 使用默认端口6659"); |
| | | return 6659; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 启动Tcp服务器 |
| | | /// </summary> |
| | | private void StartTcpServer() |
| | | { |
| | | int port = GetConfigPort(); |
| | | var serverConfig = new ServerConfig() |
| | | { |
| | | MaxRequestLength = 1024 * 1024, |
| | | Port = port |
| | | }; |
| | | try |
| | | { |
| | | if (tcpServer == null) |
| | | { |
| | | tcpServer = new TcpServer(); |
| | | tcpServer.NewRequestReceived += TcpServer_NewRequestReceived; |
| | | if (!tcpServer.Setup(serverConfig)) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed")); |
| | | return; |
| | | } |
| | | if (!tcpServer.Start()) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed")); |
| | | return; |
| | | } |
| | | ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running")); |
| | | AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}"); |
| | | } |
| | | else if (tcpServer.State != SuperSocket.SocketBase.ServerState.Running) |
| | | { |
| | | if (!tcpServer.Setup(serverConfig)) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.SettingFailed")); |
| | | return; |
| | | } |
| | | if (!tcpServer.Start()) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", this.lang("Tr.TcpServer.StartFailed")); |
| | | return; |
| | | } |
| | | ChangeTcpServerStatus("RUNNING", this.lang("Tr.TcpServer.Running")); |
| | | AppendLog(LogLevel.Debug, $"数据接收服务器启动,端口:{port}"); |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | ChangeTcpServerStatus("ERROR", $"{ex.Message}"); |
| | | AppendLog(LogLevel.Error, $"数据接收服务器启动错误: {ex.Message} {ex.StackTrace}"); |
| | | tcpServer?.Stop(); |
| | | tcpServer = null; |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 更新Tcp服务器状态 |
| | | /// </summary> |
| | | /// <param name="status"></param> |
| | | /// <param name="info"></param> |
| | | private void ChangeTcpServerStatus(string status, string info) |
| | | { |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | TcpServerStatus = status; |
| | | TcpServerInfo = info; |
| | | }); |
| | | } |
| | | |
| | | private void TcpServer_NewRequestReceived(TcpSession session, SuperSocket.SocketBase.Protocol.StringRequestInfo requestInfo) |
| | | { |
| | | string msg = requestInfo.Body; |
| | | string[] datas_10015 = msg.Split(';'); |
| | | //int cellIndex = int.Parse(datas_10015[1]); |
| | | |
| | | string strs = datas_10015[2]; |
| | | string[] arr = strs.Split(','); |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | AddTestData(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7]); |
| | | }); |
| | | } |
| | | |
| | | public void RefreshButtons(List<BtnConfig> settings) |
| | | { |
| | | if (settings == null) return; |
| | | |
| | | for (int i = 0; i < settings.Count; i++) { |
| | | for (int i = 0; i < settings.Count; i++) |
| | | { |
| | | var setting = settings[i]; |
| | | |
| | | var margin = new Thickness(0, 0, 20, 0); |
| | |
| | | } |
| | | } |
| | | |
| | | private void SetArray(string name, PropertyValueTypes type, object[] arr) |
| | | { |
| | | try |
| | | { |
| | | var context = ((TeststandWrapper)executionWrapper).context; |
| | | context.FileGlobals.DeleteSubProperty(name, 2); |
| | | |
| | | context.FileGlobals.NewSubProperty(name, type, true, null, 1); |
| | | context.FileGlobals.GetPropertyObject(name, 0).SetNumElements(arr.Length); |
| | | |
| | | for (int i = 0; i < arr.Length; i++) |
| | | { |
| | | if (type == PropertyValueTypes.PropValType_Boolean) |
| | | { |
| | | context.FileGlobals.SetValBoolean($"{name}[{i}]", 0, arr[i] == null ? false : (bool)arr[i]); |
| | | } |
| | | else |
| | | { |
| | | context.FileGlobals.SetValString($"{name}[{i}]", 0, arr[i]?.ToString()); |
| | | } |
| | | // (type == PropertyValueTypes.PropValType_String) |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | AppendLog(LogLevel.Error, $"写数组 {name} 错误: {ex.Message} {ex.StackTrace}"); |
| | | } |
| | | } |
| | | |
| | | private void Wrapper_UIMessageEvent(object sender, UIMessageEvent e) |
| | | { |
| | | try |
| | |
| | | string key = $"Running.OvenName"; |
| | | executionWrapper?.SetFileGlobalValue(key, 1, OvenName); |
| | | |
| | | key = $"Running.CellName"; |
| | | executionWrapper?.SetFileGlobalValue(key, 1, CellName); |
| | | |
| | | key = $"Running.OvenIndex"; |
| | | executionWrapper?.SetFileGlobalValue(key, 1, OvenIndex); |
| | | |
| | | key = $"Running.CellIndex"; |
| | | executionWrapper?.SetFileGlobalValue(key, 1, CellIndex); |
| | | //key = $"Running.CellIndex"; |
| | | //executionWrapper?.SetFileGlobalValue(key, 1, CellIndex); |
| | | if (CellIndex >= 0) |
| | | { |
| | | key = $"Running.CellIndex"; |
| | | object[] arr = new object[CellCount]; |
| | | arr[CellIndex] = true; |
| | | SetArray(key, PropertyValueTypes.PropValType_Boolean, arr); |
| | | |
| | | key = $"Running.CellName"; |
| | | string[] names = new string[CellCount]; |
| | | names[CellIndex] = CellName; |
| | | SetArray(key, PropertyValueTypes.PropValType_String, names); |
| | | } |
| | | else |
| | | { |
| | | string[] names = Cells.Select(c => c.Title).ToArray(); |
| | | SetArray($"Running.CellName", PropertyValueTypes.PropValType_String, names); |
| | | } |
| | | |
| | | |
| | | Dispatcher.Invoke(() => |
| | | { |
| | | CellGrid.IsEnabled = true; |
| | | AppendLog(LogLevel.Debug, "Context初始化完成"); |
| | | }); |
| | | return; |
| | | } |
| | | if (e.EventCode == 10001) |
| | |
| | | }); |
| | | return; |
| | | } |
| | | if (e.EventCode == 10004) |
| | | { |
| | | int index = Convert.ToInt32(e.NumberData); |
| | | //CellStatus status = (CellStatus)Enum.Parse(typeof(CellStatus), e.StrData); |
| | | CellStatus status = (CellStatus)int.Parse(e.StrData); |
| | | Dispatcher.BeginInvoke(() => |
| | | { |
| | | if (index < StatusCells.Count) |
| | | { |
| | | StatusCells[index].SetCurrentValue(StatusCell.StatusProperty, status); |
| | | } |
| | | }); |
| | | return; |
| | | } |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | AppendLog(LogLevel.Error, $"处理UI信息失败 {ex.Message} {ex.StackTrace}"); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | private void Btn_BtnClick(object sender, RoutedEventArgs e) |
| | | { |
| | |
| | | { |
| | | executionWrapper?.SetFileGlobalValue("Running.StopTest", 1, true); |
| | | executionWrapper?.WaitForExit(); |
| | | executionWrapper?.Dispose(); |
| | | tcpServer?.Stop(); |
| | | } |
| | | catch (Exception ex) |
| | | { |
| | | |
| | | } |
| | | } |
| | | |
| | | private void Button_Click(object sender, RoutedEventArgs e) |
| | | { |
| | | StartTcpServer(); |
| | | } |
| | | } |
| | | |
| | | public class TestDataItem : INotifyPropertyChanged |
| New file |
| | |
| | | <pu:WindowX x:Class="Util.ManualDebug.ManualDebugWindow" |
| | | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
| | | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
| | | xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
| | | xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
| | | mc:Ignorable="d" |
| | | Title="ManualDebugWindow" |
| | | xmlns:pu="https://opensource.panuon.com/wpf-ui" |
| | | xmlns:local="clr-namespace:Util.ManualDebug" |
| | | Style="{StaticResource ModalWindow}" |
| | | Closing="Window_Closing" |
| | | WindowState="Maximized" |
| | | WindowStartupLocation="CenterScreen" |
| | | pu:WindowXModalDialog.Buttons="{x:Null}" |
| | | Height="1000" Width="900"> |
| | | <Grid x:Name="sb"> |
| | | <local:ManualDebug OvenName="{Binding OvenName,Mode=OneWay}" |
| | | CellCount="{Binding CellCount,Mode=OneWay}" |
| | | CellName="{Binding CellName,Mode=OneWay}" |
| | | OvenIndex="{Binding OvenIndex,Mode=OneWay}" |
| | | CellIndex="{Binding CellIndex,Mode=OneWay}"/> |
| | | </Grid> |
| | | </pu:WindowX> |
| New file |
| | |
| | | using HMI.Util; |
| | | using Panuon.WPF.UI; |
| | | using System.ComponentModel; |
| | | using System.IO; |
| | | using System.Windows; |
| | | using Util.ManualDebug.Model; |
| | | |
| | | namespace Util.ManualDebug |
| | | { |
| | | /// <summary> |
| | | /// ManualDebugWindow.xaml 的交互逻辑 |
| | | /// </summary> |
| | | public partial class ManualDebugWindow : WindowX |
| | | { |
| | | |
| | | private ManualDebug debug; |
| | | |
| | | public ManualDebugWindow(string seqPath, string modelPath, bool isMuilt) : this(seqPath, modelPath, isMuilt, null, 1) |
| | | { |
| | | } |
| | | |
| | | public ManualDebugWindow(string seqPath, string modelPath, bool isMuilt, CellConfig[] cells, int col) |
| | | { |
| | | DataContext = this; |
| | | InitializeComponent(); |
| | | debug = (ManualDebug)sb.Children[0]; |
| | | ReadBtns(); |
| | | if (!isMuilt) |
| | | { |
| | | debug.HideCells(true); |
| | | } |
| | | //List<BtnConfig> settings = new List<BtnConfig> { new BtnConfig { Title = "上电激活", Name = "Active" } }; |
| | | debug.seqPath = seqPath; |
| | | debug.modelPath = modelPath; |
| | | debug.CellGrid.Columns = col; |
| | | debug.CellResultGrid.Columns = col; |
| | | debug.RefreshGrid(cells); |
| | | } |
| | | |
| | | private string ovenName; |
| | | public string OvenName |
| | | { |
| | | get => ovenName; |
| | | set |
| | | { |
| | | Set(ref ovenName, value); |
| | | } |
| | | } |
| | | |
| | | private string cellName; |
| | | public string CellName |
| | | { |
| | | get => cellName; |
| | | set |
| | | { |
| | | Set(ref cellName, value); |
| | | } |
| | | } |
| | | |
| | | private int ovenIndex; |
| | | public int OvenIndex |
| | | { |
| | | get => ovenIndex; |
| | | set |
| | | { |
| | | Set(ref ovenIndex, value); |
| | | } |
| | | } |
| | | |
| | | private int cellIndex = -1; |
| | | public int CellIndex |
| | | { |
| | | get => cellIndex; |
| | | set |
| | | { |
| | | Set(ref cellIndex, value); |
| | | } |
| | | } |
| | | |
| | | private int cellCount = 0; |
| | | public int CellCount |
| | | { |
| | | get => cellCount; |
| | | set |
| | | { |
| | | Set(ref cellCount, value); |
| | | } |
| | | } |
| | | |
| | | private void ReadBtns() |
| | | { |
| | | string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Settings.ini"); |
| | | var map = IniFileHelper.GetKeyValuesBySection(path, "Btns"); |
| | | List<BtnConfig> settings = new List<BtnConfig>(); |
| | | foreach (var kv in map) |
| | | { |
| | | settings.Add(new BtnConfig { Title = kv.Key, Name = kv.Value }); |
| | | } |
| | | debug.RefreshButtons(settings); |
| | | } |
| | | |
| | | private void Window_Closing(object sender, CancelEventArgs e) |
| | | { |
| | | if (MessageBox.Show("确定要关闭吗?", "提示", MessageBoxButton.OKCancel) == MessageBoxResult.OK) |
| | | { |
| | | debug.Stop(); |
| | | } |
| | | else |
| | | { |
| | | e.Cancel = true; |
| | | } |
| | | } |
| | | } |
| | | } |
| Util.ManualDebug/Model/CellConfig.cs
Util.ManualDebug/Model/TcpServer.cs
Util.ManualDebug/OldManualDebug.xaml
Util.ManualDebug/OldManualDebug.xaml.cs
Util.ManualDebug/StatusBtn.xaml
Util.ManualDebug/StatusBtn.xaml.cs
Util.ManualDebug/StatusCell.xaml
Util.ManualDebug/StatusCell.xaml.cs
Util.ManualDebug/Util.ManualDebug.csproj
WPFProject/App.config
WPFProject/App.xaml
WPFProject/App.xaml.cs
WPFProject/BuildCmd.bat
WPFProject/Config/Database/database.db
WPFProject/PanuonWindow.xaml
WPFProject/PanuonWindow.xaml.cs
WPFProject/TapProject.csproj
WPFProject/TapProject.sln
WPFProject/packages.config
check_parent.py
debug_check.py
docs/TapProject_代码结构.drawio
docs/TapProject_代码结构_01.drawio.png
docs/TapProject_代码结构_01_预览.png
docs/TapProject_代码结构_02.drawio.png
docs/TapProject_代码结构_02_预览.png
docs/TapProject_代码结构分析.md
fix_menu.py |