将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。

This commit is contained in:
QI Mingxuan
2026-04-16 17:31:13 +08:00
parent 6ec4c3ddaa
commit 2bd6e566c3
581 changed files with 74600 additions and 222 deletions
@@ -0,0 +1,23 @@
namespace XP.Hardware.RaySource.Abstractions.Enums
{
/// <summary>
/// 射线源状态枚举 | X-Ray Source Status Enum
/// </summary>
public enum RaySourceStatus
{
/// <summary>
/// 射线源不可用(未初始化)| Unavailable (Not Initialized)
/// </summary>
Unavailable = -1,
/// <summary>
/// 射线源已关闭 | Closed
/// </summary>
Closed = 0,
/// <summary>
/// 射线源已开启 | Opened
/// </summary>
Opened = 1
}
}
@@ -0,0 +1,12 @@
using Prism.Events;
namespace XP.Hardware.RaySource.Abstractions.Events
{
/// <summary>
/// 错误触发事件 | Error Occurred Event
/// 携带错误信息字符串 | Carries error message string
/// </summary>
public class ErrorOccurredEvent : PubSubEvent<string>
{
}
}
@@ -0,0 +1,41 @@
using Prism.Events;
namespace XP.Hardware.RaySource.Abstractions.Events
{
/// <summary>
/// 操作结果数据模型 | Operation Result Data Model
/// 携带操作名称和成功状态 | Carries operation name and success status
/// </summary>
public class OperationResultData
{
/// <summary>
/// 操作名称 | Operation name
/// </summary>
public string OperationName { get; set; }
/// <summary>
/// 操作是否成功 | Operation success status
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 附加消息 | Additional message
/// </summary>
public string Message { get; set; }
public OperationResultData(string operationName, bool isSuccess, string message = null)
{
OperationName = operationName;
IsSuccess = isSuccess;
Message = message;
}
}
/// <summary>
/// 操作结果事件 | Operation Result Event
/// 用于通知操作执行结果 | Used to notify operation execution result
/// </summary>
public class OperationResultEvent : PubSubEvent<OperationResultData>
{
}
}
@@ -0,0 +1,13 @@
using Prism.Events;
using XP.Hardware.RaySource.Abstractions.Enums;
namespace XP.Hardware.RaySource.Abstractions.Events
{
/// <summary>
/// 射线源状态变更事件 | X-Ray Source Status Changed Event
/// 携带 RaySourceStatus 枚举值,表示三态状态变更 | Carries RaySourceStatus enum for tri-state status changes
/// </summary>
public class RaySourceStatusChangedEvent : PubSubEvent<RaySourceStatus>
{
}
}
@@ -0,0 +1,95 @@
using Prism.Events;
using System;
namespace XP.Hardware.RaySource.Abstractions.Events
{
/// <summary>
/// 系统状态数据模型 | System Status Data Model
/// 携带全量系统状态信息 | Carries complete system status information
/// </summary>
public class SystemStatusData
{
/// <summary>
/// 设定电压值(kV| Set voltage value (kV)
/// </summary>
public float SetVoltage { get; set; }
/// <summary>
/// 实际电压值(kV| Actual voltage value (kV)
/// </summary>
public float ActualVoltage { get; set; }
/// <summary>
/// 设定电流值(μA| Set current value (μA)
/// </summary>
public float SetCurrent { get; set; }
/// <summary>
/// 实际电流值(μA| Actual current value (μA)
/// </summary>
public float ActualCurrent { get; set; }
/// <summary>
/// 射线开启状态 | X-ray on status
/// </summary>
public bool IsXRayOn { get; set; }
/// <summary>
/// 暖机状态描述 | Warm-up status description
/// </summary>
public string WarmUpStatus { get; set; }
/// <summary>
/// 真空状态描述 | Vacuum status description
/// </summary>
public string VacuumStatus { get; set; }
/// <summary>
/// 启动状态描述 | Startup status description
/// </summary>
public string StartUpStatus { get; set; }
/// <summary>
/// 自动定心状态描述 | Auto-center status description
/// </summary>
public string AutoCenterStatus { get; set; }
/// <summary>
/// 灯丝调整状态描述 | Filament adjust status description
/// </summary>
public string FilamentAdjustStatus { get; set; }
/// <summary>
/// 连锁状态 | Interlock status
/// </summary>
public bool IsInterlockActive { get; set; }
/// <summary>
/// 看门狗状态 | Watchdog status
/// </summary>
public string WatchdogStatus { get; set; }
/// <summary>
/// 功率模式 | Power mode
/// </summary>
public string PowerMode { get; set; }
/// <summary>
/// TXI状态 | TXI status
/// </summary>
public string TxiStatus { get; set; }
/// <summary>
/// 时间戳 | Timestamp
/// </summary>
public DateTime Timestamp { get; set; } = DateTime.Now;
}
/// <summary>
/// 状态更新事件 | Status Updated Event
/// 用于通知UI层系统状态变化 | Used to notify UI layer of system status changes
/// </summary>
public class StatusUpdatedEvent : PubSubEvent<SystemStatusData>
{
}
}
@@ -0,0 +1,12 @@
using Prism.Events;
namespace XP.Hardware.RaySource.Abstractions.Events
{
/// <summary>
/// PVI 变量连接状态变更事件 | PVI variables connection state changed event
/// Payload: true = 已连接, false = 已断开 | true = connected, false = disconnected
/// </summary>
public class VariablesConnectedEvent : PubSubEvent<bool>
{
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace XP.Hardware.RaySource.Abstractions
{
/// <summary>
/// 射线源工厂接口 | X-Ray Source Factory Interface
/// 定义创建射线源实例的工厂方法 | Defines factory methods for creating X-ray source instances
/// </summary>
public interface IRaySourceFactory
{
/// <summary>
/// 创建指定类型的射线源实例 | Create X-ray source instance of specified type
/// </summary>
/// <param name="sourceType">射线源类型名称 | X-ray source type name</param>
/// <returns>射线源实例 | X-ray source instance</returns>
IXRaySource CreateRaySource(string sourceType);
/// <summary>
/// 获取支持的射线源类型列表 | Get list of supported X-ray source types
/// </summary>
/// <returns>类型名称列表 | List of type names</returns>
IEnumerable<string> GetSupportedSourceTypes();
}
}
@@ -0,0 +1,116 @@
using System;
namespace XP.Hardware.RaySource.Abstractions
{
/// <summary>
/// 射线源接口 | X-Ray Source Strategy
/// 定义所有射线源必须实现的核心操作 | Defines core operations required for all X-ray sources
/// </summary>
public interface IXRaySource : IDisposable
{
/// <summary>
/// 是否已连接 | Is connected
/// </summary>
bool IsConnected { get; }
/// <summary>
/// 初始化射线源 | Initialize X-ray source
/// </summary>
XRayResult Initialize();
/// <summary>
/// 连接 PVI 变量并启动实时状态通讯 | Connect PVI variables and start real-time status communication
/// 对应硬件操作流程中的"开启实时状态通讯"步骤 | Corresponds to "start real-time status communication" step in hardware operation flow
/// </summary>
XRayResult ConnectVariables();
/// <summary>
/// 开启射线 | Turn on X-ray
/// </summary>
XRayResult TurnOn();
/// <summary>
/// 关闭射线 | Turn off X-ray
/// </summary>
XRayResult TurnOff();
/// <summary>
/// 完全关闭设备(释放资源)| Fully shut down device (release resources)
/// </summary>
XRayResult CloseOff();
/// <summary>
/// 设置电压(单位:kV| Set voltage (unit: kV)
/// </summary>
/// <param name="voltage">目标电压值 | Target voltage value</param>
XRayResult SetVoltage(float voltage);
/// <summary>
/// 设置电流(单位:μA| Set current (unit: μA)
/// </summary>
/// <param name="current">目标电流值 | Target current value</param>
XRayResult SetCurrent(float current);
/// <summary>
/// 设置焦点(部分设备支持)| Set focus (supported by some devices)
/// </summary>
/// <param name="focus">焦点参数 | Focus parameter</param>
XRayResult SetFocus(float focus);
/// <summary>
/// 读取实际电压值 | Read actual voltage value
/// </summary>
XRayResult ReadVoltage();
/// <summary>
/// 读取实际电流值 | Read actual current value
/// </summary>
XRayResult ReadCurrent();
/// <summary>
/// 读取系统状态 | Read system status
/// </summary>
XRayResult ReadSystemStatus();
/// <summary>
/// 检查错误状态 | Check error status
/// </summary>
XRayResult CheckErrors();
/// <summary>
/// TXI 开启 | TXI On
/// </summary>
XRayResult TxiOn();
/// <summary>
/// TXI 关闭 | TXI Off
/// </summary>
XRayResult TxiOff();
/// <summary>
/// 暖机设置 | Warm-up setting
/// </summary>
XRayResult WarmUp();
/// <summary>
/// 训机设置 | Training setting
/// </summary>
XRayResult Training();
/// <summary>
/// 灯丝校准 | Filament calibration
/// </summary>
XRayResult FilamentCalibration();
/// <summary>
/// 全部电压自动定心 | Auto-center all voltages
/// </summary>
XRayResult AutoCenter();
/// <summary>
/// 设置功率模式 | Set power mode
/// </summary>
/// <param name="mode">功率模式值:1=Micro Focus2=High Power</param>
XRayResult SetPowerMode(int mode);
}
}
@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace XP.Hardware.RaySource.Abstractions
{
/// <summary>
/// 射线源操作结果封装类 | X-Ray Source Operation Result Wrapper
/// 用于统一返回操作状态、错误信息和数据 | Unified return of operation status, error messages, and data
/// </summary>
public class XRayResult
{
/// <summary>
/// 操作是否成功 | Operation success status
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 错误消息(成功时为null| Error message (null when successful)
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// 操作返回的数据 | Operation return data
/// </summary>
public object Data { get; set; }
/// <summary>
/// 创建成功结果 | Create success result
/// </summary>
/// <param name="data">返回数据 | Return data</param>
/// <returns>成功结果对象 | Success result object</returns>
public static XRayResult Ok(object data = null) =>
new XRayResult { Success = true, Data = data };
/// <summary>
/// 创建错误结果 | Create error result
/// </summary>
/// <param name="message">错误消息 | Error message</param>
/// <returns>错误结果对象 | Error result object</returns>
public static XRayResult Error(string message) =>
new XRayResult { Success = false, ErrorMessage = message };
/// <summary>
/// 获取浮点数据(带类型安全)| Get float data (type-safe)
/// </summary>
public float GetFloat() => Data != null ? Convert.ToSingle(Data) : 0f;
/// <summary>
/// 获取整数数据(带类型安全)| Get integer data (type-safe)
/// </summary>
public int GetInt() => Data != null ? Convert.ToInt32(Data) : 0;
}
}
@@ -0,0 +1,95 @@
using System;
namespace XP.Hardware.RaySource.Abstractions
{
/// <summary>
/// 射线源抽象基类 | Abstract Base Class for X-Ray Sources
/// 提供策略接口的基础实现框架 | Provides foundational implementation framework for strategy interface
/// </summary>
public abstract class XRaySourceBase : IXRaySource
{
protected bool _isDisposed = false;
protected bool _isInitialized = false;
protected bool _isConnected = false;
/// <summary>
/// 射线源名称(用于日志)| X-ray source name (for logging)
/// </summary>
public virtual string SourceName => "Generic X-Ray Source";
/// <summary>
/// 是否已连接 | Is connected
/// </summary>
public virtual bool IsConnected => _isConnected && _isInitialized;
#region IXRaySource Implementation
public abstract XRayResult Initialize();
public abstract XRayResult ConnectVariables();
public abstract XRayResult TurnOn();
public abstract XRayResult TurnOff();
public abstract XRayResult CloseOff();
public abstract XRayResult SetVoltage(float voltage);
public abstract XRayResult SetCurrent(float current);
public abstract XRayResult SetFocus(float focus);
public abstract XRayResult ReadVoltage();
public abstract XRayResult ReadCurrent();
public abstract XRayResult ReadSystemStatus();
public abstract XRayResult CheckErrors();
public abstract XRayResult TxiOn();
public abstract XRayResult TxiOff();
public abstract XRayResult WarmUp();
public abstract XRayResult Training();
public abstract XRayResult FilamentCalibration();
public abstract XRayResult AutoCenter();
public abstract XRayResult SetPowerMode(int mode);
#endregion
#region IDisposable Implementation
/// <summary>
/// 释放资源 | Release resources
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 受保护的释放方法 | Protected dispose method
/// </summary>
/// <param name="disposing">是否由用户代码调用 | Whether called from user code</param>
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// 释放托管资源 | Release managed resources
try
{
// 尝试安全关闭 | Attempt safe shutdown
if (_isInitialized)
{
CloseOff();
}
}
catch (Exception ex)
{
Console.WriteLine(
$"[射线源关闭异常 | X-Ray Source Close Exception] {SourceName}: {ex.Message}");
}
}
_isDisposed = true;
}
}
/// <summary>
/// 析构函数(作为安全网)| Finalizer (as safety net)
/// </summary>
~XRaySourceBase()
{
Dispose(false);
}
#endregion
}
}
@@ -0,0 +1,237 @@
using System;
using System.Configuration;
using System.IO;
using System.Reflection;
using XP.Common.Logging.Interfaces;
namespace XP.Hardware.RaySource.Config
{
/// <summary>
/// 配置加载器 | Configuration Loader
/// 从App.config读取配置,适配.NET 8 ConfigurationManager | Load configuration from App.config, adapted for .NET 8 ConfigurationManager
/// </summary>
public class ConfigLoader
{
private const string CONFIG_SECTION = "RaySource";
/// <summary>
/// 加载射线源配置 | Load X-ray source configuration
/// </summary>
/// <param name="configFilePath">配置文件路径(可选,默认使用App.config| Configuration file path (optional, defaults to App.config)</param>
/// <param name="logger">日志服务(可选)| Logger service (optional)</param>
/// <returns>配置对象 | Configuration object</returns>
public static RaySourceConfig LoadConfig(string configFilePath = null, ILoggerService logger = null)
{
var config = new RaySourceConfig();
try
{
// 如果指定了配置文件路径,使用自定义配置文件
// If a configuration file path is specified, use custom configuration file
if (!string.IsNullOrEmpty(configFilePath) && File.Exists(configFilePath))
{
var fileMap = new ExeConfigurationFileMap { ExeConfigFilename = configFilePath };
var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
LoadFromConfiguration(config, configuration.AppSettings.Settings, logger);
}
else
{
// 使用默认App.config | Use default App.config
LoadFromAppSettings(config, logger);
}
}
catch (Exception ex)
{
Console.WriteLine($"[配置加载警告 | Config Load Warning] 使用默认配置: {ex.Message}");
// 加载失败时返回默认配置 | Return default configuration on load failure
}
return config;
}
/// <summary>
/// 从AppSettings加载配置 | Load configuration from AppSettings
/// </summary>
private static void LoadFromAppSettings(RaySourceConfig config, ILoggerService logger = null)
{
// PVI通讯参数 | PVI Communication Parameters
config.SourceType = GetAppSetting(CONFIG_SECTION + ":" + "SourceType", config.SourceType);
config.PlcIpAddress = GetAppSetting(CONFIG_SECTION + ":" + "PlcIpAddress", config.PlcIpAddress);
config.PlcPort = GetAppSetting(CONFIG_SECTION + ":" + "PlcPort", config.PlcPort);
config.StationNumber = GetAppSetting(CONFIG_SECTION + ":" + "StationNumber", config.StationNumber);
config.PortNumber = GetAppSetting(CONFIG_SECTION + ":" + "PortNumber", config.PortNumber);
config.CpuName = GetAppSetting(CONFIG_SECTION + ":" + "CpuName", config.CpuName);
config.ConnectionTimeout = GetAppSetting(CONFIG_SECTION + ":" + "ConnectionTimeout", config.ConnectionTimeout);
// 硬件参数范围 | Hardware Parameter Ranges
config.MinVoltage = GetAppSetting(CONFIG_SECTION + ":" + "MinVoltage", config.MinVoltage);
config.MaxVoltage = GetAppSetting(CONFIG_SECTION + ":" + "MaxVoltage", config.MaxVoltage);
config.MinCurrent = GetAppSetting(CONFIG_SECTION + ":" + "MinCurrent", config.MinCurrent);
config.MaxCurrent = GetAppSetting(CONFIG_SECTION + ":" + "MaxCurrent", config.MaxCurrent);
// 外部程序配置 | External Program Configuration
config.AdvanceExePath = GetAppSetting(CONFIG_SECTION + ":" + "AdvanceExePath", config.AdvanceExePath);
// 操作超时配置 | Operation Timeout Configuration
config.InitializationTimeout = GetAppSetting(CONFIG_SECTION + ":" + "InitializationTimeout", config.InitializationTimeout);
config.WarmUpTimeout = GetAppSetting(CONFIG_SECTION + ":" + "WarmUpTimeout", config.WarmUpTimeout);
config.StartUpTimeout = GetAppSetting(CONFIG_SECTION + ":" + "StartUpTimeout", config.StartUpTimeout);
config.AutoCenterTimeout = GetAppSetting(CONFIG_SECTION + ":" + "AutoCenterTimeout", config.AutoCenterTimeout);
config.FilamentAdjustTimeout = GetAppSetting(CONFIG_SECTION + ":" + "FilamentAdjustTimeout", config.FilamentAdjustTimeout);
config.GeneralOperationTimeout = GetAppSetting(CONFIG_SECTION + ":" + "GeneralOperationTimeout", config.GeneralOperationTimeout);
// 灯丝寿命配置 | Filament Lifetime Configuration
config.SerialNumber = GetAppSetting(CONFIG_SECTION + ":" + "SerialNumber", config.SerialNumber);
config.TotalLifeThreshold = GetAppSetting(CONFIG_SECTION + ":" + "TotalLifeThreshold", config.TotalLifeThreshold);
}
/// <summary>
/// 从Configuration对象加载配置 | Load configuration from Configuration object
/// </summary>
private static void LoadFromConfiguration(RaySourceConfig config, KeyValueConfigurationCollection settings, ILoggerService logger = null)
{
config.SourceType = GetSetting(settings, CONFIG_SECTION + ":" + "SourceType", config.SourceType);
config.PlcIpAddress = GetSetting(settings, CONFIG_SECTION + ":" + "PlcIpAddress", config.PlcIpAddress);
config.PlcPort = GetSetting(settings, CONFIG_SECTION + ":" + "PlcPort", config.PlcPort);
config.StationNumber = GetSetting(settings, CONFIG_SECTION + ":" + "StationNumber", config.StationNumber);
config.PortNumber = GetSetting(settings, CONFIG_SECTION + ":" + "PortNumber", config.PortNumber);
config.CpuName = GetSetting(settings, CONFIG_SECTION + ":" + "CpuName", config.CpuName);
config.ConnectionTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "ConnectionTimeout", config.ConnectionTimeout);
config.MinVoltage = GetSetting(settings, CONFIG_SECTION + ":" + "MinVoltage", config.MinVoltage);
config.MaxVoltage = GetSetting(settings, CONFIG_SECTION + ":" + "MaxVoltage", config.MaxVoltage);
config.MinCurrent = GetSetting(settings, CONFIG_SECTION + ":" + "MinCurrent", config.MinCurrent);
config.MaxCurrent = GetSetting(settings, CONFIG_SECTION + ":" + "MaxCurrent", config.MaxCurrent);
// 外部程序配置 | External Program Configuration
config.AdvanceExePath = GetSetting(settings, CONFIG_SECTION + ":" + "AdvanceExePath", config.AdvanceExePath);
config.InitializationTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "InitializationTimeout", config.InitializationTimeout);
config.WarmUpTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "WarmUpTimeout", config.WarmUpTimeout);
config.StartUpTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "StartUpTimeout", config.StartUpTimeout);
config.AutoCenterTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "AutoCenterTimeout", config.AutoCenterTimeout);
config.FilamentAdjustTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "FilamentAdjustTimeout", config.FilamentAdjustTimeout);
config.GeneralOperationTimeout = GetSetting(settings, CONFIG_SECTION + ":" + "GeneralOperationTimeout", config.GeneralOperationTimeout);
// 灯丝寿命配置 | Filament Lifetime Configuration
config.SerialNumber = GetSetting(settings, CONFIG_SECTION + ":" + "SerialNumber", config.SerialNumber);
config.TotalLifeThreshold = GetSetting(settings, CONFIG_SECTION + ":" + "TotalLifeThreshold", config.TotalLifeThreshold);
}
/// <summary>
/// 保存配置到AppSettings | Save configuration to AppSettings
/// </summary>
public static void SaveConfigToAppSettings(RaySourceConfig config)
{
// 获取程序默认配置文件路径(对应 ConfigurationManager.AppSettings 的文件)
Assembly entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
string assemblyPath = entryAssembly.Location;
string defaultConfigPath = Path.ChangeExtension(assemblyPath, ".config");
// 复用原有保存逻辑
SaveConfig(config, defaultConfigPath);
// 刷新 ConfigurationManager,让 AppSettings 立即读取最新值
ConfigurationManager.RefreshSection("appSettings");
}
/// <summary>
/// 保存配置到文件 | Save configuration to file
/// </summary>
public static void SaveConfig(RaySourceConfig config, string configFilePath)
{
try
{
var fileMap = new ExeConfigurationFileMap { ExeConfigFilename = configFilePath };
var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "SourceType", config.SourceType);
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "PlcIpAddress", config.PlcIpAddress);
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "PlcPort", config.PlcPort.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "StationNumber", config.StationNumber.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "PortNumber", config.PortNumber.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "CpuName", config.CpuName);
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "ConnectionTimeout", config.ConnectionTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "MinVoltage", config.MinVoltage.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "MaxVoltage", config.MaxVoltage.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "MinCurrent", config.MinCurrent.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "MaxCurrent", config.MaxCurrent.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "AdvanceExePath", config.AdvanceExePath);
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "InitializationTimeout", config.InitializationTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "WarmUpTimeout", config.WarmUpTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "StartUpTimeout", config.StartUpTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "AutoCenterTimeout", config.AutoCenterTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "FilamentAdjustTimeout", config.FilamentAdjustTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "GeneralOperationTimeout", config.GeneralOperationTimeout.ToString());
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "SerialNumber", config.SerialNumber);
SetSetting(configuration.AppSettings.Settings, CONFIG_SECTION + ":" + "TotalLifeThreshold", config.TotalLifeThreshold.ToString());
configuration.Save(ConfigurationSaveMode.Modified);
}
catch (Exception ex)
{
throw new InvalidOperationException($"保存配置失败: {ex.Message}", ex);
}
}
#region | Helper Methods
private static string GetAppSetting(string key, string defaultValue)
{
return ConfigurationManager.AppSettings[key] ?? defaultValue;
}
private static int GetAppSetting(string key, int defaultValue)
{
var value = ConfigurationManager.AppSettings[key];
return int.TryParse(value, out var result) ? result : defaultValue;
}
private static float GetAppSetting(string key, float defaultValue)
{
var value = ConfigurationManager.AppSettings[key];
return float.TryParse(value, out var result) ? result : defaultValue;
}
private static bool GetAppSetting(string key, bool defaultValue)
{
var value = ConfigurationManager.AppSettings[key];
return bool.TryParse(value, out var result) ? result : defaultValue;
}
private static string GetSetting(KeyValueConfigurationCollection settings, string key, string defaultValue)
{
return settings[key]?.Value ?? defaultValue;
}
private static int GetSetting(KeyValueConfigurationCollection settings, string key, int defaultValue)
{
var value = settings[key]?.Value;
return int.TryParse(value, out var result) ? result : defaultValue;
}
private static float GetSetting(KeyValueConfigurationCollection settings, string key, float defaultValue)
{
var value = settings[key]?.Value;
return float.TryParse(value, out var result) ? result : defaultValue;
}
private static bool GetSetting(KeyValueConfigurationCollection settings, string key, bool defaultValue)
{
var value = settings[key]?.Value;
return bool.TryParse(value, out var result) ? result : defaultValue;
}
private static void SetSetting(KeyValueConfigurationCollection settings, string key, string value)
{
if (settings[key] == null)
settings.Add(key, value);
else
settings[key].Value = value;
}
#endregion
}
}
@@ -0,0 +1,166 @@
using System;
namespace XP.Hardware.RaySource.Config
{
/// <summary>
/// 射线源配置实体类 | X-Ray Source Configuration Entity
/// 包含PVI通讯参数、硬件参数和操作超时配置 | Contains PVI communication parameters, hardware parameters, and operation timeout configuration
/// </summary>
public class RaySourceConfig
{
#region PVI通讯参数 | PVI Communication Parameters
/// <summary>
/// 射线源类型 | Ray source type
/// 可选值: Comet225 | Available values: Comet225
/// </summary>
public string SourceType { get; set; } = "Comet225";
/// <summary>
/// PLC IP地址 | PLC IP Address
/// </summary>
public string PlcIpAddress { get; set; } = "192.168.12.10";
/// <summary>
/// PLC端口号 | PLC Port Number
/// </summary>
public int PlcPort { get; set; } = 11159;
/// <summary>
/// 源站号 | Source Station Number
/// </summary>
public int StationNumber { get; set; } = 1;
/// <summary>
/// 源端口号 | Source Port Number
/// </summary>
public int PortNumber { get; set; } = 11;
/// <summary>
/// CPU名称 | CPU Name
/// </summary>
public string CpuName { get; set; } = "cpu";
/// <summary>
/// 连接超时时间(毫秒)| Connection timeout (milliseconds)
/// </summary>
public int ConnectionTimeout { get; set; } = 5000;
#endregion
#region | Hardware Parameter Ranges
/// <summary>
/// 最小电压(kV| Minimum voltage (kV)
/// </summary>
public float MinVoltage { get; set; } = 20f;
/// <summary>
/// 最大电压(kV| Maximum voltage (kV)
/// </summary>
public float MaxVoltage { get; set; } = 225f;
/// <summary>
/// 最小电流(μA| Minimum current (μA)
/// </summary>
public float MinCurrent { get; set; } = 10f;
/// <summary>
/// 最大电流(μA| Maximum current (μA)
/// </summary>
public float MaxCurrent { get; set; } = 1000f;
#endregion
#region | External Program Configuration
/// <summary>
/// 高级设置外部程序路径 | Advance settings external program path
/// </summary>
public string AdvanceExePath { get; set; } = @"C:\Program Files (x86)\Feinfocus\FXEControl_3.1.1.65\FXEControl.exe";
/// <summary>
/// Host 进程可执行文件路径(可选)
/// 为空时默认在主程序输出目录下查找 XP.Hardware.RaySource.Comet.Host.exe
/// </summary>
public string HostExePath { get; set; }
#endregion
#region | Operation Timeout Configuration
/// <summary>
/// 初始化超时时间(毫秒)| Initialization timeout (milliseconds)
/// </summary>
public int InitializationTimeout { get; set; } = 30000;
/// <summary>
/// 暖机超时时间(毫秒)| Warm-up timeout (milliseconds)
/// </summary>
public int WarmUpTimeout { get; set; } = 300000; // 5分钟
/// <summary>
/// 启动超时时间(毫秒)| Startup timeout (milliseconds)
/// </summary>
public int StartUpTimeout { get; set; } = 180000; // 3分钟
/// <summary>
/// 自动定心超时时间(毫秒)| Auto-center timeout (milliseconds)
/// </summary>
public int AutoCenterTimeout { get; set; } = 120000; // 2分钟
/// <summary>
/// 灯丝调整超时时间(毫秒)| Filament adjust timeout (milliseconds)
/// </summary>
public int FilamentAdjustTimeout { get; set; } = 120000; // 2分钟
/// <summary>
/// 一般操作超时时间(毫秒)| General operation timeout (milliseconds)
/// </summary>
public int GeneralOperationTimeout { get; set; } = 10000;
#endregion
#region 寿 | Filament Lifetime Configuration
/// <summary>
/// 射线源序列号 | Ray source serial number
/// 用于唯一标识设备,灯丝寿命管理依赖此字段匹配设备记录
/// Used to uniquely identify the device, filament lifetime management relies on this field to match device records
/// </summary>
public string SerialNumber { get; set; } = "";
/// <summary>
/// 灯丝总寿命阈值(小时)| Filament total lifetime threshold (hours)
/// 默认值为 1000 小时,超过阈值时系统将发出预警
/// Default value is 1000 hours, system will issue a warning when threshold is exceeded
/// </summary>
public int TotalLifeThreshold { get; set; } = 1000;
#endregion
/// <summary>
/// 验证配置参数有效性 | Validate configuration parameters
/// </summary>
/// <returns>验证结果 | Validation result</returns>
public (bool IsValid, string ErrorMessage) Validate()
{
if (string.IsNullOrWhiteSpace(PlcIpAddress))
return (false, "PLC IP地址不能为空");
if (PlcPort <= 0 || PlcPort > 65535)
return (false, "PLC端口号必须在1-65535之间");
if (MinVoltage >= MaxVoltage)
return (false, "最小电压必须小于最大电压");
if (MinCurrent >= MaxCurrent)
return (false, "最小电流必须小于最大电流");
if (ConnectionTimeout <= 0)
return (false, "连接超时时间必须大于0");
return (true, null);
}
}
}
@@ -0,0 +1,66 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace XP.Hardware.RaySource.Converters
{
/// <summary>
/// 灯丝寿命百分比转颜色转换器 | Filament lifetime percentage to color converter
/// 根据寿命使用百分比返回对应的颜色画刷:
/// 低于 80% → 绿色 (#FF4CAF50)
/// 80%-89% → 黄色 (#FFFFC107)
/// 90% 及以上 → 红色 (#FFE53935)
/// </summary>
public class FilamentLifetimeColorConverter : IValueConverter
{
// 预定义颜色画刷(避免重复创建)| Pre-defined color brushes (avoid repeated creation)
private static readonly SolidColorBrush GreenBrush = new(Color.FromArgb(0xFF, 0x4C, 0xAF, 0x50));
private static readonly SolidColorBrush YellowBrush = new(Color.FromArgb(0xFF, 0xFF, 0xC1, 0x07));
private static readonly SolidColorBrush RedBrush = new(Color.FromArgb(0xFF, 0xE5, 0x39, 0x35));
static FilamentLifetimeColorConverter()
{
// 冻结画刷以提升性能 | Freeze brushes for better performance
GreenBrush.Freeze();
YellowBrush.Freeze();
RedBrush.Freeze();
}
/// <summary>
/// 将寿命百分比(0-100)转换为对应颜色画刷 | Convert lifetime percentage (0-100) to corresponding color brush
/// </summary>
/// <param name="value">寿命百分比值(double0-100| Lifetime percentage value (double, 0-100)</param>
/// <param name="targetType">目标类型 | Target type</param>
/// <param name="parameter">转换参数(未使用)| Converter parameter (unused)</param>
/// <param name="culture">区域信息 | Culture info</param>
/// <returns>对应颜色的 SolidColorBrush | SolidColorBrush of corresponding color</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not double percentage)
{
return GreenBrush;
}
if (percentage >= 90)
{
return RedBrush;
}
if (percentage >= 80)
{
return YellowBrush;
}
return GreenBrush;
}
/// <summary>
/// 反向转换(不支持,单向绑定)| Reverse conversion (not supported, one-way binding)
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Media;
using XP.Hardware.RaySource.Abstractions.Enums;
namespace XP.Hardware.RaySource.Converters
{
/// <summary>
/// 射线源状态转背景色转换器(径向渐变)
/// </summary>
public class RaySourceStatusToColorConverter : IValueConverter
{
/// <summary>
/// 状态转颜色(不可用=灰色,关闭=绿色,开启=红色)- 中心渐变效果
/// </summary>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is RaySourceStatus status)
{
return status switch
{
RaySourceStatus.Unavailable => CreateRadialGradient(
Color.FromRgb(224, 224, 224), // 中心浅灰
Color.FromRgb(189, 189, 189)), // 边缘深灰
RaySourceStatus.Closed => CreateRadialGradient(
Color.FromRgb(139, 195, 74), // 中心浅绿 #8BC34A
Color.FromRgb(76, 175, 80)), // 边缘深绿 #4CAF50
RaySourceStatus.Opened => CreateRadialGradient(
Color.FromRgb(255, 138, 128), // 中心浅红 #FF8A80
Color.FromRgb(244, 67, 54)), // 边缘深红 #F44336
_ => CreateRadialGradient(
Color.FromRgb(224, 224, 224), // 中心浅灰
Color.FromRgb(189, 189, 189)) // 边缘深灰
};
}
return CreateRadialGradient(
Color.FromRgb(224, 224, 224),
Color.FromRgb(189, 189, 189));
}
/// <summary>
/// 创建径向渐变画刷
/// </summary>
private RadialGradientBrush CreateRadialGradient(Color centerColor, Color edgeColor)
{
var brush = new RadialGradientBrush();
brush.GradientOrigin = new System.Windows.Point(0.5, 0.5);
brush.Center = new System.Windows.Point(0.5, 0.5);
brush.RadiusX = 0.8;
brush.RadiusY = 0.8;
brush.GradientStops.Add(new GradientStop(centerColor, 0.0));
brush.GradientStops.Add(new GradientStop(edgeColor, 1.0));
return brush;
}
/// <summary>
/// 反向转换(无需实现)
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 状态转边框色转换器
/// </summary>
public class RaySourceStatusToBorderColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is RaySourceStatus status)
{
return status switch
{
RaySourceStatus.Unavailable => new SolidColorBrush(Color.FromRgb(158, 158, 158)), // 深灰 #9E9E9E
RaySourceStatus.Closed => new SolidColorBrush(Color.FromRgb(46, 125, 50)), // 深绿 #2E7D32
RaySourceStatus.Opened => new SolidColorBrush(Color.FromRgb(198, 40, 40)), // 深红 #C62828
_ => new SolidColorBrush(Color.FromRgb(153, 153, 153))
};
}
return new SolidColorBrush(Color.FromRgb(153, 153, 153));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 状态转滑块启用状态转换器
/// </summary>
public class RaySourceStatusToSliderEnabledConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is RaySourceStatus status)
{
return status == RaySourceStatus.Opened; // 开启时滑块可用,关闭时禁用
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- 射线源类型 | Ray Source Type -->
<add key="RaySource:SourceType" value="Comet225" />
<!-- PVI 通讯参数 | PVI Communication Parameters -->
<add key="RaySource:PlcIpAddress" value="192.168.12.10" />
<add key="RaySource:PlcPort" value="11159" />
<add key="RaySource:StationNumber" value="1" />
<add key="RaySource:PortNumber" value="11" />
<add key="RaySource:CpuName" value="cpu" />
<add key="RaySource:ConnectionTimeout" value="5000" />
<!-- 硬件参数范围 | Hardware Parameter Ranges -->
<add key="RaySource:MinVoltage" value="20" />
<add key="RaySource:MaxVoltage" value="225" />
<add key="RaySource:MinCurrent" value="10" />
<add key="RaySource:MaxCurrent" value="1000" />
<!-- 外部程序配置 | External Program Configuration -->
<add key="RaySource:AdvanceExePath" value="C:\Program Files (x86)\Feinfocus\FXEControl_3.1.1.65\FXEControl.exe" />
<add key="RaySource:HostExePath" value="" />
<!-- 操作超时配置 | Operation Timeout Configuration -->
<add key="RaySource:InitializationTimeout" value="30000" />
<add key="RaySource:WarmUpTimeout" value="300000" />
<add key="RaySource:StartUpTimeout" value="180000" />
<add key="RaySource:AutoCenterTimeout" value="120000" />
<add key="RaySource:FilamentAdjustTimeout" value="120000" />
<add key="RaySource:GeneralOperationTimeout" value="10000" />
<!-- 灯丝寿命管理配置 | Filament Lifetime Management Configuration -->
<add key="RaySource:SerialNumber" value="SN08602861" />
<add key="RaySource:TotalLifeThreshold" value="1000" />
</appSettings>
</configuration>
+680
View File
@@ -0,0 +1,680 @@
# X 射线源模块使用指南 | X-Ray Source Module Usage Guide
## 1. 模块概述 | Module Overview
XP.Hardware.RaySource 是 XplorePlane X 射线检测系统的核心硬件控制模块,负责与工业 X 射线源设备进行通讯和控制。该模块采用策略模式设计,通过 Named Pipe IPC 进程隔离架构与 .NET Framework 4.8 Host 子进程通信,支持多种 X 射线源型号的统一管理。
### 核心特性 | Key Features
- 支持多种 X 射线源型号(当前支持 Comet 225kV
- 基于 Named Pipe IPC 的进程隔离架构(.NET 8 ↔ .NET Framework 4.8
- 完整的设备生命周期管理(初始化、连接变量、暖机、训机、灯丝校准、自动定心、开关射线)
- 电压电流精确控制(20-225kV / 10-1000μA
- TXI 开关控制、功率模式切换(Micro Focus / High Power
- 实时状态监控和错误检测
- 灯丝寿命管理(使用时长记录、累计统计、预警提醒)
- 基于 Prism 事件聚合器的松耦合通讯
- 安全机制:紧急关闭优先级、参数范围验证、双重检查锁定
---
## 2. 模块注册 | Module Registration
X 射线源模块已通过 Prism 的模块化系统注册,在应用启动时自动加载。
### 在 App.xaml.cs 中注册
```csharp
using Prism.Modularity;
using XP.Hardware.RaySource.Module;
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
// 注册 X 射线源模块 | Register X-Ray Source module
moduleCatalog.AddModule<RaySourceModule>();
base.ConfigureModuleCatalog(moduleCatalog);
}
```
### 模块自动注册的服务
| 服务 | 类型 | 生命周期 | 说明 |
|------|------|----------|------|
| `RaySourceConfig` | 实例 | 单例 | 配置对象(从 App.config 加载) |
| `IRaySourceFactory``RaySourceFactory` | 接口→实现 | 单例 | 射线源工厂 |
| `IRaySourceService``RaySourceService` | 接口→实现 | 单例 | 射线源业务服务 |
| `IFilamentLifetimeService``FilamentLifetimeService` | 接口→实现 | 单例 | 灯丝寿命管理服务 |
### 模块初始化行为
模块 `OnInitialized` 时会自动执行:
1. 初始化灯丝寿命服务(建表、异常恢复、重算累计寿命)
2. 检查灯丝寿命预警(≥90% 弹出模态对话框)
---
## 3. 配置文件设置 | Configuration File Setup
`App.config` 中添加 X 射线源配置:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!-- 射线源类型 | Ray Source Type -->
<add key="RaySource:SourceType" value="Comet225" />
<!-- PVI 通讯参数 | PVI Communication Parameters -->
<add key="RaySource:PlcIpAddress" value="192.168.12.10" />
<add key="RaySource:PlcPort" value="11159" />
<add key="RaySource:StationNumber" value="1" />
<add key="RaySource:PortNumber" value="11" />
<add key="RaySource:CpuName" value="cpu" />
<add key="RaySource:ConnectionTimeout" value="5000" />
<!-- 硬件参数范围 | Hardware Parameter Ranges -->
<add key="RaySource:MinVoltage" value="20" />
<add key="RaySource:MaxVoltage" value="225" />
<add key="RaySource:MinCurrent" value="10" />
<add key="RaySource:MaxCurrent" value="1000" />
<!-- 操作超时配置 | Operation Timeout Configuration -->
<add key="RaySource:InitializationTimeout" value="30000" />
<add key="RaySource:WarmUpTimeout" value="300000" />
<add key="RaySource:StartUpTimeout" value="180000" />
<add key="RaySource:AutoCenterTimeout" value="120000" />
<add key="RaySource:FilamentAdjustTimeout" value="120000" />
<add key="RaySource:GeneralOperationTimeout" value="10000" />
<!-- 灯丝寿命管理配置 | Filament Lifetime Management Configuration -->
<add key="RaySource:SerialNumber" value="SN08602861" />
<add key="RaySource:TotalLifeThreshold" value="1000" />
</appSettings>
</configuration>
```
---
## 4. 在 ViewModel 中使用 | Usage in ViewModel
### 4.1 注入服务
```csharp
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Abstractions.Enums;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Config;
using XP.Hardware.RaySource.Services;
namespace YourNamespace.ViewModels
{
public class YourViewModel : BindableBase
{
private readonly IRaySourceService _raySourceService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
public YourViewModel(
IRaySourceService raySourceService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_raySourceService = raySourceService;
_eventAggregator = eventAggregator;
_logger = logger.ForModule("YourViewModel");
// 订阅事件 | Subscribe to events
SubscribeEvents();
}
private void SubscribeEvents()
{
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>()
.Subscribe(OnRaySourceStatusChanged, ThreadOption.UIThread);
_eventAggregator.GetEvent<StatusUpdatedEvent>()
.Subscribe(OnStatusUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<ErrorOccurredEvent>()
.Subscribe(OnErrorOccurred, ThreadOption.UIThread);
_eventAggregator.GetEvent<OperationResultEvent>()
.Subscribe(OnOperationResult, ThreadOption.UIThread);
_eventAggregator.GetEvent<VariablesConnectedEvent>()
.Subscribe(OnVariablesConnected, ThreadOption.UIThread);
}
private void OnRaySourceStatusChanged(RaySourceStatus newStatus)
{
_logger.Info("射线源状态变化: {Status}", newStatus);
}
private void OnStatusUpdated(SystemStatusData data)
{
_logger.Debug("状态更新: 电压={Voltage}kV, 电流={Current}μA",
data.ActualVoltage, data.ActualCurrent);
}
private void OnErrorOccurred(string errorMessage)
{
_logger.Error(null, "错误发生: {Error}", errorMessage);
}
private void OnOperationResult(OperationResultData data)
{
_logger.Info("操作 {Op} {Result}: {Msg}",
data.OperationName, data.IsSuccess ? "成功" : "失败", data.Message);
}
private void OnVariablesConnected(bool isConnected)
{
_logger.Info("变量连接状态: {Connected}", isConnected);
}
}
}
```
### 4.2 初始化射线源
```csharp
// 方式一:分步初始化 | Step-by-step initialization
public void InitializeRaySource()
{
// 步骤1:初始化(启动 Host 进程,建立 IPC 连接)
XRayResult result = _raySourceService.Initialize();
if (!result.Success)
{
_logger.Error(null, "初始化失败: {Error}", result.ErrorMessage);
return;
}
// 步骤2:连接变量(创建和激活 PVI 变量)
XRayResult connectResult = _raySourceService.ConnectVariables();
if (!connectResult.Success)
{
_logger.Error(null, "连接变量失败: {Error}", connectResult.ErrorMessage);
}
}
// 方式二:异步一键初始化 | Async one-step initialization
public async Task AutoInitializeAsync()
{
XRayResult result = await _raySourceService.InitializeAndConnectAsync();
if (result.Success)
{
_logger.Info("自动初始化完成");
}
}
```
### 4.3 开启/关闭射线
```csharp
// 开启射线 | Turn on X-ray
public void TurnOnXRay()
{
if (!_raySourceService.IsInitialized || !_raySourceService.IsConnected)
{
_logger.Warn("射线源未就绪");
return;
}
XRayResult result = _raySourceService.TurnOn();
if (result.Success)
{
_logger.Info("射线已开启");
}
}
// 关闭射线 | Turn off X-ray
public void TurnOffXRay()
{
XRayResult result = _raySourceService.TurnOff();
if (result.Success)
{
_logger.Info("射线已关闭");
}
}
// 紧急关闭(最高优先级,任何状态下可执行)| Emergency shutdown
public void EmergencyShutdown()
{
_raySourceService.EmergencyShutdown();
}
// 断开连接(保留实例以便重连)| Disconnect
public void Disconnect()
{
_raySourceService.Disconnect();
}
```
---
## 5. 电压电流控制 | Voltage and Current Control
```csharp
// 设置电压(范围由配置文件定义,默认 20-225kV)
XRayResult result = _raySourceService.SetVoltage(100f);
// 设置电流(范围由配置文件定义,默认 10-1000μA)
XRayResult result = _raySourceService.SetCurrent(500f);
// 读取实际电压
XRayResult voltageResult = _raySourceService.ReadVoltage();
if (voltageResult.Success)
{
float voltage = voltageResult.GetFloat();
}
// 读取实际电流
XRayResult currentResult = _raySourceService.ReadCurrent();
if (currentResult.Success)
{
float current = currentResult.GetFloat();
}
```
---
## 6. 设备操作 | Device Operations
```csharp
// TXI 开启/关闭
_raySourceService.TxiOn();
_raySourceService.TxiOff();
// 暖机
_raySourceService.WarmUp();
// 训机
_raySourceService.Training();
// 灯丝校准
_raySourceService.FilamentCalibration();
// 全部电压自动定心
_raySourceService.AutoCenter();
// 设置功率模式(1=Micro Focus2=High Power
_raySourceService.SetPowerMode(1); // 微焦点模式
_raySourceService.SetPowerMode(2); // 高功率模式
```
注意:TXI、暖机、训机、灯丝校准、自动定心、功率模式切换等操作需要射线源已连接变量(`IsConnected == true`)。
---
## 7. 状态监控和错误处理 | Status Monitoring and Error Handling
### 7.1 读取系统状态
```csharp
XRayResult result = _raySourceService.ReadSystemStatus();
if (result.Success && result.Data is SystemStatusData statusData)
{
// statusData 包含全量状态信息:
// - SetVoltage / ActualVoltage(设定/实际电压)
// - SetCurrent / ActualCurrent(设定/实际电流)
// - IsXRayOn(射线开启状态)
// - WarmUpStatus / VacuumStatus / StartUpStatus(暖机/真空/启动状态)
// - AutoCenterStatus / FilamentAdjustStatus(自动定心/灯丝调整状态)
// - IsInterlockActive(连锁状态)
// - WatchdogStatus / PowerMode / TxiStatus(看门狗/功率模式/TXI状态)
}
```
### 7.2 检查和清除错误
```csharp
// 检查错误
XRayResult errorResult = _raySourceService.CheckErrors();
// 清除错误
XRayResult clearResult = _raySourceService.ClearErrors();
```
### 7.3 状态属性
```csharp
bool isInitialized = _raySourceService.IsInitialized; // 是否已初始化
bool isConnected = _raySourceService.IsConnected; // PVI 变量是否已连接
bool isXRayOn = _raySourceService.IsXRayOn; // 射线是否开启
RaySourceStatus status = _raySourceService.CurrentStatus; // 当前状态(三态)
```
---
## 8. Prism 事件通讯 | Prism Event Communication
模块使用 Prism 事件聚合器实现跨模块通讯,支持以下事件:
### 8.1 射线源状态变化事件
```csharp
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>()
.Subscribe(OnRaySourceStatusChanged, ThreadOption.UIThread);
private void OnRaySourceStatusChanged(RaySourceStatus newStatus)
{
// newStatus: Unavailable(-1) / Closed(0) / Opened(1)
switch (newStatus)
{
case RaySourceStatus.Unavailable:
// 射线源不可用(未初始化或连接丢失)
break;
case RaySourceStatus.Closed:
// 射线源已连接,射线关闭
break;
case RaySourceStatus.Opened:
// 射线源已连接,射线开启
break;
}
}
```
### 8.2 系统状态更新事件
```csharp
_eventAggregator.GetEvent<StatusUpdatedEvent>()
.Subscribe(OnStatusUpdated, ThreadOption.UIThread);
private void OnStatusUpdated(SystemStatusData data)
{
// 全量状态数据,由 Host 进程定期推送
float actualVoltage = data.ActualVoltage;
float actualCurrent = data.ActualCurrent;
bool isXRayOn = data.IsXRayOn;
string warmUpStatus = data.WarmUpStatus;
// ... 更多字段见 SystemStatusData 定义
}
```
### 8.3 错误事件
```csharp
_eventAggregator.GetEvent<ErrorOccurredEvent>()
.Subscribe(OnErrorOccurred, ThreadOption.UIThread);
private void OnErrorOccurred(string errorMessage)
{
_logger.Error(null, "射线源错误: {Error}", errorMessage);
}
```
### 8.4 操作结果事件
```csharp
_eventAggregator.GetEvent<OperationResultEvent>()
.Subscribe(OnOperationResult, ThreadOption.UIThread);
private void OnOperationResult(OperationResultData data)
{
// data.OperationName - 操作名称
// data.IsSuccess - 是否成功
// data.Message - 结果消息
}
```
### 8.5 PVI 变量连接状态事件
```csharp
_eventAggregator.GetEvent<VariablesConnectedEvent>()
.Subscribe(OnVariablesConnected, ThreadOption.UIThread);
private void OnVariablesConnected(bool isConnected)
{
// true = PVI 变量已连接,射线源准备就绪
// false = PVI 变量已断开
}
```
---
## 9. 灯丝寿命管理 | Filament Lifetime Management
### 9.1 自动联动
灯丝寿命管理与射线源开关自动联动:
- 射线源开启(TurnOn)→ 自动开始记录灯丝使用时长
- 射线源关闭(TurnOff / Disconnect / EmergencyShutdown)→ 自动停止记录并累加统计
- 异常断联 → 下次初始化时自动恢复未关闭的记录
### 9.2 手动查询
```csharp
// 获取实时累计使用秒数(含当前运行时长)
double totalSeconds = _filamentLifetimeService.GetCurrentTotalLifeSeconds();
// 获取寿命百分比(0-100
double percentage = _filamentLifetimeService.GetLifetimePercentage();
// 获取阈值秒数
double thresholdSeconds = _filamentLifetimeService.GetThresholdSeconds();
// 检查是否需要预警(≥90%
bool shouldWarn = _filamentLifetimeService.ShouldShowLifetimeWarning();
```
### 9.3 数据持久化
灯丝寿命数据通过 SQLite 持久化,包含两张表:
- `RaySourceFilamentLifetimeStatistics`:累计统计表(按序列号唯一)
- `RaySourceFilamentUsageLogs`:使用流水表(每次开关记录一条)
---
## 10. 在应用退出时释放资源 | Release Resources on Application Exit
```csharp
protected override void OnExit(ExitEventArgs e)
{
Log.Information("XplorePlane 主应用退出");
// 释放射线源资源 | Release X-ray source resources
try
{
var raySourceService = Container.Resolve<IRaySourceService>();
raySourceService?.Dispose();
Log.Information("射线源资源已成功释放");
}
catch (Exception ex)
{
Log.Error(ex, "射线源资源释放失败,忽略该错误继续退出");
}
Log.CloseAndFlush();
base.OnExit(e);
}
```
---
## 11. 安全机制 | Safety Mechanisms
### 11.1 参数范围验证
```csharp
// 服务层自动校验参数范围,超出范围返回错误结果
XRayResult result = _raySourceService.SetVoltage(300f); // 超出 225kV 上限
// result.Success == false
// result.ErrorMessage == "电压参数超出范围:20-225kV"
```
### 11.2 紧急关闭优先级
紧急关闭具有最高优先级,可以在任何状态下执行:
```csharp
// 紧急关闭:依次关闭射线 → 完全关闭设备 → 状态重置为 Unavailable
_raySourceService.EmergencyShutdown();
```
### 11.3 业务规则校验
- 未初始化禁止操作(所有操作前检查 `IsInitialized`
- 防重复开启/关闭(双重检查锁定模式)
- 异常断联自动检测(状态变为 Unavailable 时重置标志位)
### 11.4 实时调节支持
连接射线源后,无论射线是否开启,都可以实时调节电压和电流。
---
## 12. 异常处理 | Exception Handling
### 12.1 XRayResult 结果处理
所有操作返回 `XRayResult` 对象:
```csharp
XRayResult result = _raySourceService.TurnOn();
if (result.Success)
{
var data = result.Data; // 获取返回数据
}
else
{
string error = result.ErrorMessage; // 获取错误消息
_logger.Error(null, "操作失败: {Error}", error);
}
```
### 12.2 IPC 通信异常
当 Host 进程崩溃或管道断开时:
- `CometIpcClient` 设置 `_isPipeConnected = false`,阻止后续命令发送
- 返回 `null` 响应,服务层转换为错误结果
- 推送 `Disconnected` 状态,触发 UI 更新
---
## 13. 日志记录 | Logging
模块使用 `ILoggerService` 进行结构化日志记录:
```csharp
// 各层日志模块名称
_logger = logger.ForModule("RaySource.Service"); // 服务层
_logger = logger.ForModule("RaySource.Factory"); // 工厂层
_logger = logger.ForModule<Comet225RaySource>(); // 适配器层
_logger = logger.ForModule<CometIpcClient>(); // IPC 客户端
_logger = logger.ForModule<CometHostManager>(); // Host 管理器
_logger = logger.ForModule("RaySource.ViewModel"); // 操作视图模型
_logger = logger.ForModule("RaySource.ConfigVM"); // 配置视图模型
_logger = logger.ForModule<FilamentLifetimeService>(); // 灯丝寿命服务
```
### 日志级别
- **Debug**: 详细调试信息(IPC 消息收发、电压电流读取等)
- **Info**: 一般信息(初始化成功、射线开启、状态变更等)
- **Warn**: 警告信息(参数超出范围、重复操作、异常恢复等)
- **Error**: 错误信息(操作失败、通讯异常、IPC 超时等)
- **Fatal**: 致命错误(系统崩溃等)
---
## 14. 故障排查 | Troubleshooting
### 14.1 初始化失败
**可能原因**:
- Host 可执行文件未找到(检查 `{主程序目录}/Host/XP.Hardware.RaySource.Comet.Host.exe`
- PLC IP 地址或端口配置错误
- PLC 未启动或网络不通
- Named Pipe 连接超时
- 残留的 Host 进程未清理
**解决方案**:
1. 确认 Host 可执行文件和 BR.AN.PviServices.dll 已正确部署
2. 检查 `App.config` 中的 PLC 连接参数
3. 使用 `ping` 命令测试 PLC 网络连接
4. 查看日志中 Host 进程 PID 和管道连接状态
5. 手动终止残留的 `XP.Hardware.RaySource.Comet.Host.exe` 进程
### 14.2 射线无法开启
**可能原因**:
- 射线源未初始化或变量未连接
- 设备处于错误状态
- 互锁信号未满足
**解决方案**:
1. 确认 `IsInitialized == true``IsConnected == true`
2. 调用 `CheckErrors()` 检查错误状态
3. 检查设备互锁信号
### 14.3 电压电流设置失败
**可能原因**:
- 参数超出配置范围
- 射线源未初始化
- IPC 管道已断开
**解决方案**:
1. 验证参数在有效范围内
2. 检查 `IsInitialized` 状态
3. 查看日志中 IPC 通信状态
### 14.4 Host 进程异常退出
**可能原因**:
- BR.AN.PviServices.dll 缺失或版本不匹配
- PVI Service 未正确安装
- Host 进程内存异常
**解决方案**:
1. 检查 Host 目录下 DLL 文件完整性
2. 确认 B&R Automation Studio 和 PVI Services 已安装
3. 查看 Host 进程 stderr 日志输出
### 14.5 灯丝寿命数据异常
**可能原因**:
- SerialNumber 配置为空
- SQLite 数据库文件损坏
- 异常断电导致未关闭记录
**解决方案**:
1. 确认 `App.config``RaySource:SerialNumber` 已配置
2. 检查数据库文件完整性
3. 服务初始化时会自动恢复未关闭记录(标记为异常中断,DurationSeconds=0
---
## 15. 注意事项 | Notes
1. **线程安全**: `IRaySourceService` 是单例,内部使用 `lock` 保护关键操作
2. **同步 API**: 所有 `IXRaySource``IRaySourceService` 方法为同步方法,ViewModel 中通过 `Task.Run` 包装避免阻塞 UI
3. **InitializeAndConnectAsync**: 唯一的异步便捷方法,内部通过 `Task.Run` 包装同步调用
4. **实时调节**: 连接后可随时调节电压电流,无论射线是否开启
5. **资源释放**: 应用退出时务必调用 `Dispose()` 释放资源(会触发紧急关闭和 Host 进程终止)
6. **事件订阅**: 记得在 ViewModel 销毁时取消事件订阅
7. **配置验证**: 启动前自动验证配置参数的有效性
8. **日志记录**: 所有关键操作都有详细的结构化日志记录
9. **错误处理**: 始终检查 `XRayResult.Success` 并处理错误
10. **Host 进程**: CometHostManager 会自动清理残留进程并管理 Host 生命周期
---
## 16. 文档索引 | Documentation Index
- **[GUIDENCE.md](./GUIDENCE.md)** - 本文档,详细使用指南
- **[README.md](./README.md)** - 项目概述和快速参考
- **[ProjectStructure.md](./ProjectStructure.md)** - 项目结构详解和调用链路
- **[README_RaySourceOperateView.md](./README_RaySourceOperateView.md)** - 操作视图使用说明
- **[App.config.example](./App.config.example)** - 配置文件示例
---
**最后更新 | Last Updated**: 2026-03-26
@@ -0,0 +1,310 @@
# XP.Hardware.RaySource 项目结构文档
## 1. 项目概览
| 项目 | 框架 | 类型 | 命名空间 | 程序集名称 |
|------|------|------|----------|-----------|
| XP.Hardware.RaySource | .NET 8.0 (net8.0-windows7.0) | WPF 类库 | `XP.Hardware.RaySource.*` | XP.Hardware.RaySource |
| XP.Hardware.RaySource.Comet.Host | .NET Framework 4.8 | WinForms 控制台 | `XP.Hardware.RaySource.Comet.Host` | XP.Hardware.RaySource.Comet.Host |
| XP.Hardware.RaySource.Comet.Messages | netstandard2.0 | 类库 | `XP.Hardware.RaySource.Comet.Messages` | XP.Hardware.RaySource.Comet.Messages |
## 2. 版本信息
### XP.Hardware.RaySource
- 目标框架:`net8.0-windows7.0`
- SDK 风格项目
- NuGet 依赖:
- `Prism.Wpf` 9.0.537
- `System.Configuration.ConfigurationManager` 8.0.0
- `Telerik.UI.for.Wpf.NetCore.Xaml` 2024.1.408
- 项目引用:
- `XP.Common`(日志、数据库、多语言基础设施)
- `XP.Hardware.RaySource.Comet.Messages`IPC 消息定义)
### XP.Hardware.RaySource.Comet.Host
- 目标框架:`.NET Framework 4.8`
- WinForms 进程(因依赖 B&R PVI Services COM 组件,无法迁移到 .NET 8)
- 外部依赖:
- `BR.AN.PviServices.dll`Version 1.1.0.0,贝加莱 PVI 通讯库)
- `Newtonsoft.Json`JSON 序列化)
- 项目引用:
- `XP.Hardware.RaySource.Comet.Messages`IPC 消息定义)
### XP.Hardware.RaySource.Comet.Messages
- 目标框架:`netstandard2.0`(跨框架共享)
- 定义 IPC 命令和响应消息类型
- 被 RaySource.NET 8)和 Comet.Host.NET Framework 4.8)共同引用
## 3. 引用关系
```
┌─────────────────────────────────────────────────────────────┐
│ XP.Hardware.RaySource (.NET 8.0 WPF 类库) │
│ │
│ ProjectReference ──► XP.Common │
│ ProjectReference ──► XP.Hardware.RaySource.Comet.Messages │
│ │
│ NuGet: Prism.Wpf, Telerik, ConfigurationManager │
└────────────────────┬────────────────────────────────────────┘
│ Named Pipe IPC(进程间通信)
┌─────────────────────────────────────────────────────────────┐
│ XP.Hardware.RaySource.Comet.Host (.NET Framework 4.8) │
│ │
│ ProjectReference ──► XP.Hardware.RaySource.Comet.Messages │
│ 外部引用 ──► BR.AN.PviServices.dll(贝加莱 PVI 通讯库) │
│ NuGet: Newtonsoft.Json │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ XP.Hardware.RaySource.Comet.Messages (netstandard2.0) │
│ │
│ 共享消息库,定义 IPC 命令和响应类型 │
│ 被 RaySource 和 Comet.Host 共同引用 │
│ NuGet: Newtonsoft.Json │
└─────────────────────────────────────────────────────────────┘
```
### 进程隔离架构说明
RaySource.NET 8)通过 Named Pipe 与 Comet.Host.NET Framework 4.8)进行进程间通信。之所以采用这种架构,是因为 `BR.AN.PviServices.dll`(贝加莱 PVI 通讯库)仅支持 .NET Framework,无法直接在 .NET 8 中使用。因此将 PVI 通讯层隔离到独立的 .NET Framework 4.8 Host 子进程中,通过 Named Pipe 双管道模式实现跨框架通信。
`Comet.Messages` 作为 `netstandard2.0` 共享库,定义了两端通信的命令和响应消息格式,确保序列化/反序列化的一致性。
## 4. XP.Hardware.RaySource 项目结构
```
XP.Hardware.RaySource/
├── Abstractions/ # 抽象层(接口、基类、枚举、事件)
│ ├── Enums/
│ │ └── RaySourceStatus.cs # 射线源状态枚举(Unavailable=-1 / Closed=0 / Opened=1
│ ├── Events/
│ │ ├── ErrorOccurredEvent.cs # 错误触发事件(PubSubEvent<string>
│ │ ├── OperationResultEvent.cs # 操作结果事件(PubSubEvent<OperationResultData>
│ │ ├── RaySourceStatusChangedEvent.cs # 射线源状态变更事件(PubSubEvent<RaySourceStatus>
│ │ ├── StatusUpdatedEvent.cs # 系统状态更新事件(PubSubEvent<SystemStatusData>
│ │ └── VariablesConnectedEvent.cs # PVI 变量连接状态事件(PubSubEvent<bool>
│ ├── IRaySourceFactory.cs # 射线源工厂接口
│ ├── IXRaySource.cs # 射线源策略接口(核心接口,同步方法)
│ ├── XRayResult.cs # 操作结果封装类(Ok/Error 静态工厂方法)
│ └── XRaySourceBase.cs # 射线源抽象基类(含 Dispose 模式)
├── Config/
│ ├── ConfigLoader.cs # 配置加载器(支持加载和保存到 App.config
│ └── RaySourceConfig.cs # 射线源配置实体类(含验证方法)
├── Converters/
│ ├── RaySourceOperateConverter.cs # WPF 值转换器(状态→径向渐变色/边框色/启用状态)
│ └── FilamentLifetimeColorConverter.cs # 灯丝寿命百分比→颜色(绿/黄/红)
├── Factories/
│ └── RaySourceFactory.cs # 射线源工厂实现(创建 CometHostManager + CometIpcClient + Comet225RaySource
├── Implementations/
│ ├── Comet225RaySource.cs # Comet 225kV 射线源适配器(委托 IPC 客户端和 Host 管理器)
│ ├── CometHostManager.cs # Host 进程生命周期管理器(启动/监控/关闭子进程)
│ └── CometIpcClient.cs # Named Pipe IPC 客户端(双管道通信,消息收发)
├── Module/
│ └── RaySourceModule.cs # Prism 模块入口(DI 注册 + 灯丝寿命初始化 + 预警检查)
├── Services/
│ ├── IRaySourceService.cs # 射线源业务服务接口(含 InitializeAndConnectAsync
│ ├── RaySourceService.cs # 射线源业务服务实现(单例,业务规则校验 + 灯丝寿命联动)
│ ├── IFilamentLifetimeService.cs # 灯丝寿命管理服务接口
│ └── FilamentLifetimeService.cs # 灯丝寿命管理服务实现(SQLite 持久化)
├── ViewModels/
│ ├── RaySourceConfigViewModel.cs # 配置视图模型(初始化/连接/断开/设备状态监控/灯丝寿命进度条)
│ └── RaySourceOperateViewModel.cs # 操作视图模型(开关/电压电流调节/自动初始化)
├── Views/
│ ├── RaySourceConfigView.xaml(.cs) # 配置视图(设备状态面板 + 功能按钮 + 灯丝寿命进度条)
│ ├── RaySourceConfigWindow.xaml(.cs) # 配置窗口(包裹 ConfigView 的独立窗口)
│ └── RaySourceOperateView.xaml(.cs) # 操作视图(腰圆状态指示器 + 开关按钮 + 电压电流滑块)
└── Documents/ # 文档目录
```
## 5. 调用链路详解
### 5.1 整体架构(适配器模式 + IPC 进程隔离 + 事件驱动)
```
┌──────────────┐ ┌──────────────────┐ ┌───────────────────┐ ┌────────────────┐ ┌──────────────┐
│ ViewModel │───►│ RaySourceService │───►│ Comet225RaySource │───►│ CometIpcClient │───►│ Host 进程 │
│ (UI 层) │ │ (业务服务层) │ │ (适配器层) │ │ (IPC 通信层) │ │ (PVI 通讯层) │
└──────────────┘ └──────────────────┘ └───────────────────┘ └────────────────┘ └──────────────┘
▲ ▲ │ │
│ │ │ │
└────────── Prism EventAggregator ◄──────────────────────────────────┘◄─── Named Pipe ──────┘
(推送消息路由为 Prism 事件)
```
### 5.2 调用流程
#### 初始化流程
```
1. RaySourceModule.RegisterTypes()
→ 注册 RaySourceConfig(从 App.config 加载)
→ 注册 IRaySourceFactory → RaySourceFactory(单例)
→ 注册 IRaySourceService → RaySourceService(单例)
→ 注册 IFilamentLifetimeService → FilamentLifetimeService(单例)
2. RaySourceModule.OnInitialized()
→ FilamentLifetimeService.Initialize()(建表、异常恢复、重算累计寿命)
→ 检查灯丝寿命预警(≥90% 弹出对话框)
3. ViewModel 调用 IRaySourceService.Initialize()
→ RaySourceService.Initialize()
→ 验证配置参数
→ IRaySourceFactory.CreateRaySource("Comet225")
→ new CometHostManager(logger, config)
→ new CometIpcClient(config, eventAggregator, logger)
→ new Comet225RaySource(hostManager, ipcClient, logger)
→ Comet225RaySource.Initialize()
→ CometHostManager.EnsureRunning() // 启动 Host 子进程
→ 清理残留进程 → 启动新进程 → 等待 Named Pipe 就绪
→ CometIpcClient.Initialize()
→ Connect() // 连接双管道
→ SendCommand(InitializeCommand) // 发送初始化命令
→ Host 端执行 PVI Service/CPU 连接
→ 等待 ServiceConnected 推送确认
```
#### 变量连接流程
```
4. ViewModel 调用 IRaySourceService.ConnectVariables()
→ RaySourceService.ConnectVariables()
→ Comet225RaySource.ConnectVariables()
→ CometIpcClient.ConnectVariables()
→ SendCommand(ConnectVariablesCommand)
→ Host 端执行:CreateVariables → ActivateVariables → BindEventHandlers
→ Host 推送 VariablesConnected → RaySourceConnected
→ CometIpcClient 路由推送为 Prism 事件
→ VariablesConnectedEvent(true)
→ RaySourceStatusChangedEvent(Closed)
```
#### 操作流程(以开启射线为例)
```
5. ViewModel.ExecuteTurnOn()
→ RaySourceService.TurnOn() // 业务规则校验(状态检查、防重复、双重检查锁定)
→ Comet225RaySource.TurnOn() // 适配器层
→ CometIpcClient.TurnOn() // IPC 通信层
→ SendCommand(TurnOnCommand) // 序列化为 JSON,写入命令管道
→ Host 端执行 PVI 变量写入
→ Host 返回响应 → CometIpcClient 接收
→ 更新状态为 Opened
→ 发布 RaySourceStatusChangedEvent
→ FilamentLifetimeService.StartFilamentUsage() // 记录灯丝使用开始
```
#### 状态反馈流程(事件驱动)
```
6. Host 端 PVI 变量值变化
→ Host 构造推送消息(StatusChanged / XRayStateChanged / ErrorOccurred 等)
→ 写入响应管道
→ CometIpcClient.ReceiveLoop() 接收
→ 识别为推送消息(IsPush=true
→ HandlePushMessage() 路由到对应 Prism 事件
→ StatusUpdatedEvent(全量状态数据)
→ RaySourceStatusChangedEvent(三态变更)
→ ErrorOccurredEvent(错误信息)
→ VariablesConnectedEvent(连接状态)
→ ViewModel 通过事件订阅更新 UI
```
### 5.3 IPC 通信机制
#### Named Pipe 双管道模式
| 管道名称 | 方向 | 用途 |
|----------|------|------|
| `XP.Hardware.RaySource.Comet.Cmd` | 客户端 → Host | 命令管道(写入命令 JSON) |
| `XP.Hardware.RaySource.Comet.Rsp` | Host → 客户端 | 响应管道(接收响应和推送) |
#### 消息类型
- **命令消息**:客户端发送,Host 处理后返回响应(请求-响应模式)
- **推送消息**:Host 主动推送状态变化(`IsPush=true`),包括:
- `StatusChanged`:全量状态数据(电压/电流/各子系统状态)
- `XRayStateChanged`:射线开关状态变化
- `ErrorOccurred`:错误信息
- `ConnectionStateChanged`:连接状态变化(ServiceConnected / VariablesConnected / RaySourceConnected / Disconnected
- `Log`:Host 端日志转发(按级别映射到主进程 ILoggerService
#### 超时机制
- 命令超时:35000ms(大于 Host 端 PVI 连接超时 30s
- 管道连接超时:使用 `RaySourceConfig.ConnectionTimeout`(默认 5000ms
- Host 进程关闭超时:5000ms(超时则强制终止)
### 5.4 日志桥接机制
Host 进程(.NET Framework 4.8)通过 Named Pipe 推送日志消息到主进程。`CometIpcClient` 接收 `LogResponse` 推送后,根据 `Level` 字段映射到 `ILoggerService` 对应方法:
```
Host 端日志 → LogResponseJSON)→ Named Pipe → CometIpcClient.HandleLogPush()
→ Level="Debug" → _logger.Debug(message, args)
→ Level="Info" → _logger.Info(message, args)
→ Level="Warn" → _logger.Warn(message, args)
→ Level="Error" → _logger.Error(null, message, args)
→ Level="Fatal" → _logger.Fatal(null, message, args)
```
## 6. 灯丝寿命管理架构
### 6.1 数据库表结构
#### RaySourceFilamentLifetimeStatistics(累计统计表)
| 字段 | 类型 | 说明 |
|------|------|------|
| Id | INTEGER PK | 自增主键 |
| SourceType | TEXT | 射线源类型 |
| SerialNumber | TEXT | 序列号(唯一索引) |
| TotalLifeSeconds | REAL | 累计使用秒数 |
| LastUpdateTime | TEXT | 最后更新时间(ISO 8601 |
#### RaySourceFilamentUsageLogs(使用流水表)
| 字段 | 类型 | 说明 |
|------|------|------|
| Id | INTEGER PK | 自增主键 |
| SourceType | TEXT | 射线源类型 |
| SerialNumber | TEXT | 序列号 |
| StartTime | TEXT | 开始时间(ISO 8601 |
| EndTime | TEXT | 结束时间(可为 NULL |
| DurationSeconds | REAL | 持续秒数 |
| Status | INTEGER | 0=正常,1=异常中断 |
### 6.2 灯丝寿命联动
- 射线源开启(TurnOn)→ `StartFilamentUsage()`:插入流水记录
- 射线源关闭(TurnOff)→ `StopFilamentUsage()`:事务更新流水记录 + 累加统计表
- 断开连接 / 紧急关闭 → `StopFilamentUsage()`:同上
- 异常断联 → 下次初始化时 `RecoverUnclosedRecords()`:将未关闭记录标记为异常中断
### 6.3 预警机制
- 模块初始化时检查灯丝寿命百分比
- 百分比 ≥ 90% 时弹出模态预警对话框
- ConfigView 中显示灯丝寿命进度条(60 秒定时刷新)
- 进度条颜色:< 80% 绿色,80%-89% 黄色,≥ 90% 红色
## 7. 设计模式总结
| 模式 | 应用位置 | 说明 |
|------|---------|------|
| 策略模式 | `IXRaySource``Comet225RaySource` | 支持多种射线源实现的可扩展架构 |
| 工厂模式 | `IRaySourceFactory``RaySourceFactory` | 根据配置类型创建对应射线源实例 |
| 适配器模式 | `Comet225RaySource` | 将 IPC 通信适配为 IXRaySource 接口 |
| 进程隔离 | `CometHostManager` + `CometIpcClient` | 通过 Named Pipe 实现 .NET 8 与 .NET Framework 4.8 跨框架通信 |
| 事件驱动 | Prism EventAggregator + IPC 推送 | 解耦 PVI 通讯层与 UI 层的状态同步 |
| 依赖注入 | Prism DI 容器 | 通过 RaySourceModule 注册所有服务 |
| 单例模式 | RaySourceService、RaySourceFactory、FilamentLifetimeService | 全局唯一的服务和工厂实例 |
| 双重检查锁定 | RaySourceService.TurnOn/TurnOff | 防止并发重复操作 |
---
**最后更新 | Last Updated**: 2026-03-26
+413
View File
@@ -0,0 +1,413 @@
# XP.Hardware.RaySource
工业 X 射线源控制模块 | Industrial X-Ray Source Control Module
---
## 项目概述 | Project Overview
XP.Hardware.RaySource 是 XplorePlane X 射线检测系统的核心硬件控制模块,负责与工业 X 射线源设备进行通讯和控制。该模块采用策略模式和工厂模式设计,支持多种 X 射线源型号的统一管理,提供完整的设备生命周期控制和安全机制。
### 主要特性 | Key Features
- 支持多种 X 射线源型号(当前支持 Comet 225kV
- 基于 Named Pipe IPC 的进程隔离架构(.NET 8 主进程 ↔ .NET Framework 4.8 Host 进程)
- 完整的设备生命周期管理(初始化、连接变量、暖机、训机、灯丝校准、自动定心、开关射线)
- 精确的电压电流控制(20-225kV / 10-1000μA
- 实时状态监控和错误检测
- 基于 Prism 事件聚合器的松耦合跨模块通讯
- 安全机制:紧急关闭优先级、参数范围验证
- 灯丝寿命管理:使用时长记录、累计统计、预警提醒
- TXI 开关控制、功率模式切换(Micro Focus / High Power
- 完整的日志记录和异常处理
---
## 框架架构 | Architecture
```
XP.Hardware.RaySource/
├── Abstractions/ # 抽象层 | Abstraction Layer
│ ├── IXRaySource.cs # 策略接口(同步方法)
│ ├── XRaySourceBase.cs # 抽象基类
│ ├── XRayResult.cs # 结果封装类
│ ├── IRaySourceFactory.cs # 工厂接口
│ ├── Enums/ # 枚举定义
│ │ └── RaySourceStatus.cs # 三态枚举(Unavailable/Closed/Opened
│ └── Events/ # Prism 事件定义
│ ├── StatusUpdatedEvent.cs # 系统状态更新事件
│ ├── ErrorOccurredEvent.cs # 错误触发事件
│ ├── OperationResultEvent.cs # 操作结果事件
│ ├── RaySourceStatusChangedEvent.cs # 射线源状态变更事件
│ └── VariablesConnectedEvent.cs # PVI 变量连接状态事件
├── Implementations/ # 实现层 | Implementation Layer
│ ├── Comet225RaySource.cs # Comet 225kV 适配器(委托 IPC 客户端)
│ ├── CometHostManager.cs # Host 进程生命周期管理器
│ └── CometIpcClient.cs # Named Pipe IPC 客户端
├── Factories/ # 工厂层 | Factory Layer
│ └── RaySourceFactory.cs # 射线源工厂
├── Services/ # 业务服务层 | Service Layer
│ ├── IRaySourceService.cs # 服务接口
│ ├── RaySourceService.cs # 服务实现(单例)
│ ├── IFilamentLifetimeService.cs # 灯丝寿命服务接口
│ └── FilamentLifetimeService.cs # 灯丝寿命服务实现
├── Config/ # 配置层 | Configuration Layer
│ ├── RaySourceConfig.cs # 配置实体
│ └── ConfigLoader.cs # 配置加载器(支持保存)
├── Module/ # Prism 模块 | Prism Module
│ └── RaySourceModule.cs # 模块注册
├── ViewModels/ # 视图模型 | View Models
│ ├── RaySourceConfigViewModel.cs # 配置视图模型(初始化/连接/断开/设备状态监控/灯丝寿命)
│ └── RaySourceOperateViewModel.cs # 操作视图模型(开关/电压电流调节)
├── Views/ # WPF 视图 | WPF Views
│ ├── RaySourceConfigView.xaml # 配置视图(设备状态面板)
│ ├── RaySourceConfigWindow.xaml # 配置窗口(包裹 ConfigView
│ └── RaySourceOperateView.xaml # 操作视图(开关/滑块)
├── Converters/ # WPF 值转换器 | WPF Value Converters
│ ├── RaySourceOperateConverter.cs # 状态→颜色/边框色/启用状态
│ └── FilamentLifetimeColorConverter.cs # 灯丝寿命百分比→颜色
└── Documents/ # 文档 | Documentation
├── README.md # 项目说明(本文档)
├── ProjectStructure.md # 项目结构详解
├── GUIDENCE.md # 使用指南
├── README_RaySourceOperateView.md # 操作视图说明
└── App.config.example # 配置文件示例
```
### 设计模式 | Design Patterns
- **策略模式**`IXRaySource` 接口定义统一操作,支持多种设备实现
- **工厂模式**`IRaySourceFactory` 根据设备类型动态创建实例
- **适配器模式**`Comet225RaySource` 将 IPC 通信适配为 `IXRaySource` 接口
- **进程隔离**:通过 `CometHostManager` + `CometIpcClient` 实现 .NET 8 与 .NET Framework 4.8 的跨框架通信
- **模板方法模式**`XRaySourceBase` 提供基础实现框架
- **单例模式**`IRaySourceService``IFilamentLifetimeService` 作为全局单例
- **依赖注入**:通过 Prism 容器管理服务生命周期
- **事件聚合器**:使用 Prism `IEventAggregator` 实现松耦合通讯
### IPC 进程隔离架构 | IPC Process Isolation Architecture
```
┌──────────────────────────────────────────────────────────────────────┐
│ .NET 8 主进程 (XP.Hardware.RaySource) │
│ │
│ ViewModel → RaySourceService → Comet225RaySource │
│ │ │
│ CometHostManager(管理 Host 进程生命周期)│
│ CometIpcClientNamed Pipe 双管道通信) │
│ │ │
│ NamedPipe: Cmd(写入)/ Rsp(读取) │
└──────────────────────────────┬───────────────────────────────────────┘
│ Named Pipe IPC
┌──────────────────────────────┴───────────────────────────────────────┐
│ .NET Framework 4.8 Host 进程 (Comet.Host) │
│ │
│ CometPviClient ← BR.AN.PviServices.dll │
│ (PVI 通讯层,直接操作 PLC 变量) │
└──────────────────────────────────────────────────────────────────────┘
```
---
## 核心功能 | Core Features
### 1. 设备生命周期管理 | Device Lifecycle Management
```csharp
// 初始化射线源(类型从配置文件读取)
XRayResult result = _raySourceService.Initialize();
// 连接 PVI 变量并启动实时状态通讯
XRayResult connectResult = _raySourceService.ConnectVariables();
// 异步执行初始化 + 连接变量的完整流程
XRayResult autoResult = await _raySourceService.InitializeAndConnectAsync();
// 开启射线
_raySourceService.TurnOn();
// 关闭射线
_raySourceService.TurnOff();
// 紧急关闭(最高优先级)
_raySourceService.EmergencyShutdown();
// 断开连接(保留实例以便重连)
_raySourceService.Disconnect();
```
### 2. 电压电流控制 | Voltage and Current Control
```csharp
// 设置电压(20-225kV
_raySourceService.SetVoltage(100f);
// 设置电流(10-1000μA
_raySourceService.SetCurrent(500f);
// 读取实际电压
XRayResult voltageResult = _raySourceService.ReadVoltage();
float voltage = voltageResult.GetFloat();
// 读取实际电流
XRayResult currentResult = _raySourceService.ReadCurrent();
float current = currentResult.GetFloat();
```
### 3. 设备操作 | Device Operations
```csharp
// TXI 开启/关闭
_raySourceService.TxiOn();
_raySourceService.TxiOff();
// 暖机
_raySourceService.WarmUp();
// 训机
_raySourceService.Training();
// 灯丝校准
_raySourceService.FilamentCalibration();
// 全部电压自动定心
_raySourceService.AutoCenter();
// 设置功率模式(1=Micro Focus2=High Power
_raySourceService.SetPowerMode(1);
```
### 4. 状态监控 | Status Monitoring
```csharp
// 读取系统状态
_raySourceService.ReadSystemStatus();
// 检查错误
_raySourceService.CheckErrors();
// 清除错误
_raySourceService.ClearErrors();
// 检查状态属性
bool isInitialized = _raySourceService.IsInitialized;
bool isConnected = _raySourceService.IsConnected;
bool isXRayOn = _raySourceService.IsXRayOn;
RaySourceStatus status = _raySourceService.CurrentStatus;
```
### 5. 灯丝寿命管理 | Filament Lifetime Management
```csharp
// 初始化灯丝寿命服务(模块启动时自动调用)
_filamentLifetimeService.Initialize();
// 获取累计使用秒数
double totalSeconds = _filamentLifetimeService.GetCurrentTotalLifeSeconds();
// 获取寿命百分比
double percentage = _filamentLifetimeService.GetLifetimePercentage();
// 检查是否需要预警(≥90%
bool shouldWarn = _filamentLifetimeService.ShouldShowLifetimeWarning();
```
### 6. 事件通讯 | Event Communication
```csharp
// 订阅射线源状态变化事件(三态:Unavailable/Closed/Opened
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>()
.Subscribe(OnRaySourceStatusChanged, ThreadOption.UIThread);
// 订阅系统状态更新事件(电压/电流/各子系统状态)
_eventAggregator.GetEvent<StatusUpdatedEvent>()
.Subscribe(OnStatusUpdated, ThreadOption.UIThread);
// 订阅错误事件
_eventAggregator.GetEvent<ErrorOccurredEvent>()
.Subscribe(OnErrorOccurred, ThreadOption.UIThread);
// 订阅操作结果事件
_eventAggregator.GetEvent<OperationResultEvent>()
.Subscribe(OnOperationResult, ThreadOption.UIThread);
// 订阅 PVI 变量连接状态事件
_eventAggregator.GetEvent<VariablesConnectedEvent>()
.Subscribe(OnVariablesConnected, ThreadOption.UIThread);
```
---
## 技术要求 | Technical Requirements
### 运行环境 | Runtime Environment
- **.NET 8.0** (net8.0-windows7.0)
- **Windows 操作系统**WPF 依赖)
- **Visual Studio 2022** 或更高版本
### 核心依赖 | Core Dependencies
| 依赖库 | 版本 | 用途 |
|--------|------|------|
| **Prism.Wpf** | 9.0.537 | MVVM 框架和依赖注入 |
| **Telerik UI for WPF** | 2024.1.408 | UI 控件库 |
| **System.Configuration.ConfigurationManager** | 8.0.0 | 配置文件管理 |
| **XP.Common** | - | 日志、数据库、多语言基础设施 |
| **XP.Hardware.RaySource.Comet.Messages** | - | IPC 消息定义(netstandard2.0 共享库) |
### 关联项目 | Related Projects
| 项目 | 框架 | 用途 |
|------|------|------|
| **XP.Hardware.RaySource.Comet.Host** | .NET Framework 4.8 | Host 子进程,运行 B&R PVI 通讯 |
| **XP.Hardware.RaySource.Comet.Messages** | netstandard2.0 | IPC 命令/响应消息定义 |
---
## 快速开始 | Quick Start
### 1. 配置文件设置
参见 [App.config.example](./App.config.example)
### 2. 注册模块
`App.xaml.cs` 中:
```csharp
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule<RaySourceModule>();
}
```
### 3. 使用服务
在 ViewModel 中注入并使用:
```csharp
public class YourViewModel : BindableBase
{
private readonly IRaySourceService _raySourceService;
private readonly IEventAggregator _eventAggregator;
public YourViewModel(
IRaySourceService raySourceService,
IEventAggregator eventAggregator)
{
_raySourceService = raySourceService;
_eventAggregator = eventAggregator;
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>()
.Subscribe(OnStatusChanged, ThreadOption.UIThread);
}
public void Initialize()
{
XRayResult result = _raySourceService.Initialize();
if (result.Success)
{
_raySourceService.ConnectVariables();
}
}
}
```
---
## 配置参数说明 | Configuration Parameters
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| SourceType | string | Comet225 | 射线源类型 |
| PlcIpAddress | string | 192.168.12.10 | PLC IP 地址 |
| PlcPort | int | 11159 | PLC 端口号 |
| StationNumber | int | 1 | 源站号 |
| PortNumber | int | 11 | 源端口号 |
| CpuName | string | cpu | CPU 名称 |
| ConnectionTimeout | int | 5000 | 连接超时(毫秒)|
| MinVoltage | float | 20 | 最小电压(kV|
| MaxVoltage | float | 225 | 最大电压(kV|
| MinCurrent | float | 10 | 最小电流(μA|
| MaxCurrent | float | 1000 | 最大电流(μA|
| AdvanceExePath | string | FXEControl.exe 路径 | 高级设置外部程序路径 |
| HostExePath | string | 空(自动查找) | Host 进程可执行文件路径 |
| InitializationTimeout | int | 30000 | 初始化超时(毫秒)|
| WarmUpTimeout | int | 300000 | 暖机超时(5分钟)|
| StartUpTimeout | int | 180000 | 启动超时(3分钟)|
| AutoCenterTimeout | int | 120000 | 自动定心超时(2分钟)|
| FilamentAdjustTimeout | int | 120000 | 灯丝调整超时(2分钟)|
| GeneralOperationTimeout | int | 10000 | 一般操作超时(毫秒)|
| SerialNumber | string | 空 | 射线源序列号(灯丝寿命管理) |
| TotalLifeThreshold | int | 1000 | 灯丝总寿命阈值(小时) |
---
## 安全机制 | Safety Mechanisms
### 1. 参数范围验证
所有参数在设置前都会进行范围验证,超出范围返回错误结果。
### 2. 紧急关闭优先级
紧急关闭具有最高优先级,可以在任何状态下执行,会依次关闭射线、完全关闭设备。
### 3. 业务规则校验
- 未初始化禁止操作
- 防重复开启/关闭(双重检查锁定)
- 异常断联自动检测和状态重置
### 4. 灯丝寿命预警
灯丝使用时长达到阈值 90% 时,系统自动弹出预警对话框。
---
## 文档索引 | Documentation Index
- **[README.md](./README.md)** - 本文档,项目概述和快速参考
- **[ProjectStructure.md](./ProjectStructure.md)** - 项目结构详解和调用链路
- **[GUIDENCE.md](./GUIDENCE.md)** - 详细使用指南,包含完整代码示例
- **[README_RaySourceOperateView.md](./README_RaySourceOperateView.md)** - 操作视图使用说明
- **[App.config.example](./App.config.example)** - 配置文件示例
---
## 故障排查 | Troubleshooting
### 初始化失败
- 检查 Host 进程可执行文件是否存在(默认路径:`{主程序目录}/Host/XP.Hardware.RaySource.Comet.Host.exe`
- 检查 PLC IP 地址和端口配置
- 确认 PLC 网络连接正常
- 查看日志中 Named Pipe 连接状态
### 射线无法开启
- 确认已成功初始化并连接变量(`IsInitialized && IsConnected`
- 检查设备错误状态
- 验证互锁信号
### 电压电流设置失败
- 验证参数在有效范围内
- 确认射线源已初始化
- 检查 IPC 管道连接状态
### Host 进程异常
- 检查 Host 进程是否正常启动(查看日志中 PID 信息)
- 确认 BR.AN.PviServices.dll 已正确部署到 Host 目录
- 检查是否有残留的 Host 进程(CometHostManager 会自动清理)
详细故障排查请参考 [GUIDENCE.md](./GUIDENCE.md) 第 14 节。
---
## 许可证 | License
本模块是 XplorePlane 项目的一部分,遵循项目整体许可协议。
---
**最后更新 | Last Updated**: 2026-03-26
@@ -0,0 +1,378 @@
# RaySourceOperateView 使用说明
## 概述
`RaySourceOperateView` 是射线源操作和监控面板,提供射线源开关控制、电压电流实时调节和状态监控功能。
## 架构设计
### MVVM 模式
- **View**: `RaySourceOperateView.xaml` - WPF 用户控件界面
- **ViewModel**: `RaySourceOperateViewModel.cs` - 视图模型,处理业务逻辑
- **Model**: `RaySourceStatus.cs` - 射线源状态枚举(Unavailable / Closed / Opened
### 依赖注入
ViewModel 通过构造函数注入以下依赖:
- `IRaySourceService` - 射线源业务服务
- `IEventAggregator` - Prism 事件聚合器
- `RaySourceConfig` - 射线源配置
- `ILoggerService` - 日志服务
- `ILocalizationService` - 多语言服务
## 功能特性
### 1. 状态监控
- **腰圆状态指示器**
- 灰色径向渐变 = 射线源不可用(Unavailable
- 绿色径向渐变 (#8BC34A#4CAF50) = 射线源已关闭(Closed
- 红色径向渐变 (#FF8A80#F44336) = 射线源已开启(Opened
- **呼吸闪烁动画**:射线源开启时,状态指示器播放呼吸闪烁动画(透明度 1.0 → 0.6 循环,周期约 1.5 秒)
- **实时状态文本**:通过 `ILocalizationService` 获取多语言状态文字
### 2. 射线源控制
- **开启射线源按钮**
- 仅在 Closed 状态、已初始化且变量已连接时可用
- 调用 `IRaySourceService.TurnOn()`
- 按钮文字通过多语言绑定:`{loc:Localization RaySource_TurnOnButton}`
- **关闭射线源按钮**
- 仅在 Opened 状态、已初始化且变量已连接时可用
- 调用 `IRaySourceService.TurnOff()`
- 按钮文字通过多语言绑定:`{loc:Localization RaySource_TurnOffButton}`
- **配置按钮**
- 打开 `RaySourceConfigWindow`(射线源配置窗口)
- 窗口已存在时激活而非重复创建
- **高级/设置按钮**
- 启动外部高级设置程序(`FXEControl.exe`
- 如果程序已运行则将窗口置前
- 使用 `ProcessHelper.StartOrActivate()` 实现
### 3. 电压调节
- **滑块范围**:从配置文件读取(默认 20-225 kV)
- **双向绑定**:滑块和数值输入框绑定同一属性 `VoltageValue`,使用 `Mode=TwoWay`
- **延迟拖拽**:滑块使用 `IsDeferredDraggingEnabled="True"`,松手时才提交值
- **手动提交**:通过 `ApplyVoltageCommand` 提交电压值到硬件(滑块松手/输入框失焦时触发)
- **启用条件**:仅在服务已初始化且变量已连接时可调节(`IsSlidersEnabled`
- **范围显示**:滑块下方显示最小值和最大值
- **实际值显示**:右上角显示当前实际电压值
### 4. 电流调节
- **滑块范围**:从配置文件读取(默认 10-1000 μA)
- **双向绑定**:滑块和数值输入框绑定同一属性 `CurrentValue`,使用 `Mode=TwoWay`
- **延迟拖拽**:滑块使用 `IsDeferredDraggingEnabled="True"`,松手时才提交值
- **手动提交**:通过 `ApplyCurrentCommand` 提交电流值到硬件
- **启用条件**:仅在服务已初始化且变量已连接时可调节(`IsSlidersEnabled`
- **范围显示**:滑块下方显示最小值和最大值
- **实际值显示**:右上角显示当前实际电流值
### 5. 自动初始化
- ViewModel 提供 `AutoInitializeAsync()` 方法
- 由 View 的 `Loaded` 事件触发,仅执行一次
- 异步执行 `InitializeAndConnectAsync()`(初始化 + 连接变量)
- 如果已初始化且变量已连接则跳过
## 关联视图:RaySourceConfigView
`RaySourceConfigView` 是射线源配置和设备状态监控面板,通过 `RaySourceConfigWindow` 包裹为独立窗口。
### ConfigView 功能
- **设备信息**:显示射线源类型和连接状态(颜色编码:灰色=未连接,橙色=已初始化,绿色=变量已连接)
- **操作按钮**:初始化 / 连接变量 / 断开(三列等宽布局)
- **设备状态面板**:暖机/真空/启动/自动定心/灯丝校准/射线开启/连锁/看门狗/TXI/功率模式
- **TXI 控制**TXI ON / TXI OFF 按钮
- **功率模式切换**High Power / Micro Focus 按钮
- **功能设置按钮**:暖机设置 / 训机设置 / 灯丝校准 / 自动定心(四列等宽布局,带确认对话框和进度条窗口)
- **灯丝寿命进度条**:显示灯丝使用百分比(60 秒定时刷新),颜色随百分比变化(绿/黄/红)
### ConfigView 依赖注入
- `IRaySourceService` - 射线源业务服务
- `IEventAggregator` - Prism 事件聚合器
- `RaySourceConfig` - 射线源配置
- `ILoggerService` - 日志服务
- `ILocalizationService` - 多语言服务
- `IFilamentLifetimeService` - 灯丝寿命管理服务
## 多语言支持
### XAML 中使用
```xml
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
<TextBlock Text="{loc:Localization RaySource_VoltageLabel}"/>
<telerik:RadButton Content="{loc:Localization RaySource_TurnOnButton}"/>
```
### ViewModel 中使用
```csharp
_localizationService.GetString("RaySource_StatusClosed")
```
### 资源键列表
#### OperateView 资源键
| 资源键 | 中文 | 英文 |
|--------|------|------|
| RaySource_VoltageLabel | 电压(kV | Voltage (kV) |
| RaySource_CurrentLabel | 电流(μA | Current (μA) |
| RaySource_ActualValueLabel | 当前值: {0} | Actual: {0} |
| RaySource_TurnOnButton | 开启射线源 | Turn On X-Ray |
| RaySource_TurnOffButton | 关闭射线源 | Turn Off X-Ray |
| RaySource_AdvanceButton | 高级 | Advance |
| RaySource_ConfigButton | 配置 | Config |
| RaySource_StatusUnavailable | 射线源\n不可用 | X-Ray\nUnavailable |
| RaySource_StatusClosed | 射线源\n已关闭 | X-Ray\nClosed |
| RaySource_StatusOpened | 射线源\n已开启 | X-Ray\nOpened |
#### ConfigView 资源键
| 资源键 | 中文 | 英文 |
|--------|------|------|
| RaySource_SourceTypeLabel | 射线源类型 | Source Type |
| RaySource_InitializeButton | 初始化 | Initialize |
| RaySource_ConnectVariablesButton | 连接变量 | Connect Variables |
| RaySource_DisconnectButton | 断开 | Disconnect |
| RaySource_WarmUpLabel | 暖机 | Warm-up |
| RaySource_VacuumLabel | 真空 | Vacuum |
| RaySource_StartUpLabel | 启动 | Startup |
| RaySource_AutoCenterLabel | 自动定心 | Auto-center |
| RaySource_FilamentLabel | 灯丝校准 | Filament |
| RaySource_XRayOnLabel | 射线 | X-Ray |
| RaySource_InterlockLabel | 连锁 | Interlock |
| RaySource_WatchdogLabel | 看门狗 | Watchdog |
| RaySource_TxiStatusLabel | TXI | TXI |
| RaySource_TxiOnButton | TXI ON | TXI ON |
| RaySource_TxiOffButton | TXI OFF | TXI OFF |
| RaySource_PowerModeLabel | 功率模式 | Power Mode |
| RaySource_HighPowerButton | High Power | High Power |
| RaySource_MicroFocusButton | Micro Focus | Micro Focus |
| RaySource_WarmUpSettingButton | 暖机设置 | Warm-up |
| RaySource_TrainingSettingButton | 训机设置 | Training |
| RaySource_FilamentCalibrationButton | 灯丝校准 | Filament Cal. |
| RaySource_AutoCenterSettingButton | 自动定心 | Auto-center |
| RaySource_FilamentLifetimeLabel | 灯丝寿命 | Filament Life |
| RaySource_ConfigWindowTitle | 射线源配置 | X-Ray Source Config |
> 注意:多语言仅在应用启动时加载,无需运行时热切换。
## 呼吸闪烁动画
射线源开启时,状态指示器播放呼吸闪烁动画,增强视觉警示效果。
### 动画定义
```xml
<Storyboard x:Key="BreathingAnimation" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1.0" To="0.6" Duration="0:0:0.75"
AutoReverse="True"/>
</Storyboard>
```
### 触发机制
通过 `DataTrigger` 监听 `RaySourceStatus` 属性:
- 当状态变为 `Opened` 时,自动开始动画
- 当状态离开 `Opened` 时,自动停止动画
## 事件订阅
### OperateViewModel 订阅事件
1. **RaySourceStatusChangedEvent**:射线源状态变更(三态)
- 更新 `RaySourceStatus` 属性和按钮/滑块启用状态
- 异常断联时重置变量连接状态
2. **StatusUpdatedEvent**:系统状态更新
- 更新实际电压和电流值(仅在值真正变化时更新,避免无意义赋值)
- 同步服务层权威状态
3. **ErrorOccurredEvent**:错误发生
- 显示错误消息对话框
4. **OperationResultEvent**:操作结果
- 操作失败时显示警告消息
5. **VariablesConnectedEvent**:变量连接状态变更
- 更新 `IsVariablesConnected` 属性
- 连接成功时主动读取最新设备状态
### ConfigViewModel 订阅事件
1. **RaySourceStatusChangedEvent**:刷新初始化状态和命令可执行状态
2. **StatusUpdatedEvent**:刷新设备状态面板所有字段
3. **VariablesConnectedEvent**:刷新连接状态和命令可执行状态
## 数据绑定
### OperateView 状态绑定
```xml
<!-- 状态指示器背景色(径向渐变)-->
Background="{Binding RaySourceStatus, Converter={StaticResource StatusToColorConverter}}"
<!-- 状态指示器边框色 -->
BorderBrush="{Binding RaySourceStatus, Converter={StaticResource StatusToBorderColorConverter}}"
<!-- 状态文本(多语言)-->
Text="{Binding StatusText}"
```
### OperateView 命令绑定
```xml
<telerik:RadButton Content="{loc:Localization RaySource_TurnOnButton}" Command="{Binding TurnOnCommand}"/>
<telerik:RadButton Content="{loc:Localization RaySource_TurnOffButton}" Command="{Binding TurnOffCommand}"/>
<telerik:RadButton Content="{loc:Localization RaySource_AdvanceButton}" Command="{Binding SettingsCommand}"/>
<telerik:RadButton Content="{loc:Localization RaySource_ConfigButton}" Command="{Binding ConfigCommand}"/>
```
### OperateView 滑块绑定
```xml
<!-- 电压滑块(延迟拖拽模式)-->
<telerik:RadSlider Minimum="{Binding VoltageMin}" Maximum="{Binding VoltageMax}"
Value="{Binding VoltageValue, Mode=TwoWay}"
IsDeferredDraggingEnabled="True"
IsEnabled="{Binding IsSlidersEnabled}"/>
<!-- 电压数值输入框 -->
<telerik:RadNumericUpDown Minimum="{Binding VoltageMin}" Maximum="{Binding VoltageMax}"
Value="{Binding VoltageValue, Mode=TwoWay}"
IsEnabled="{Binding IsSlidersEnabled}" NumberDecimalDigits="1"/>
```
## 转换器
### 1. RaySourceStatusToColorConverter
`RaySourceStatus` 枚举转换为径向渐变背景色:
- `Unavailable` → 灰色渐变(#E0E0E0#BDBDBD
- `Closed` → 绿色渐变(#8BC34A#4CAF50
- `Opened` → 红色渐变(#FF8A80#F44336
### 2. RaySourceStatusToBorderColorConverter
`RaySourceStatus` 枚举转换为边框色:
- `Unavailable`#9E9E9E
- `Closed`#2E7D32
- `Opened`#C62828
### 3. FilamentLifetimeColorConverter
将灯丝寿命百分比转换为进度条颜色:
- < 80% → 绿色 (#4CAF50)
- 80%-89% → 黄色 (#FFC107)
- ≥ 90% → 红色 (#E53935)
## 业务规则
### 安全规则
1. **未初始化禁止操作**:所有操作前检查 `IsInitialized``IsVariablesConnected`
2. **参数范围校验**:电压和电流值必须在配置的范围内
3. **操作失败回滚**:设置失败时恢复滑块到实际值(仅当实际值在有效范围内时)
4. **连接丢失处理**:连接丢失时状态设为 `Unavailable`,重置变量连接状态,禁用所有操作
5. **设备反馈保护**`IsUpdatingFromDevice` 标志防止设备反馈更新时误触发写入
### 状态管理
- 状态变更通过事件驱动,确保 UI 与硬件状态同步
- 使用 `RaisePropertyChanged` 通知 UI 更新
- 命令的 `CanExecute` 自动监听状态变化
- 三态管理:Unavailable → Closed → Opened
## 使用示例
### 在独立窗口中使用
```csharp
var raySourceView = _containerProvider.Resolve<RaySourceOperateView>();
var window = new Window
{
Title = "射线源操作",
Content = raySourceView,
SizeToContent = SizeToContent.WidthAndHeight,
ResizeMode = ResizeMode.NoResize,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = Application.Current.MainWindow
};
window.ShowDialog();
```
### 在主窗口 Region 中加载
```xml
<ContentControl prism:RegionManager.RegionName="RaySourceOperateRegion" />
```
```csharp
_regionManager.RequestNavigate("RaySourceOperateRegion", "RaySourceOperateView");
```
## XAML 命名空间引用
```xml
xmlns:local="clr-namespace:XP.Hardware.RaySource.Views"
xmlns:converters="clr-namespace:XP.Hardware.RaySource.Converters"
xmlns:enums="clr-namespace:XP.Hardware.RaySource.Abstractions.Enums"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
xmlns:prism="http://prismlibrary.com/"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
```
## 注意事项
1. **线程安全**:所有事件处理使用 `ThreadOption.UIThread` 确保在 UI 线程执行
2. **资源释放**ViewModel 实现 `IDisposable`,在 Dispose 时取消事件订阅、断开射线源连接、关闭配置窗口
3. **同步操作**:硬件操作为同步方法,ViewModel 中直接调用(不阻塞 UI 因为操作通过 IPC 快速返回)
4. **错误处理**:所有操作包含 try-catch,确保异常不会导致程序崩溃
5. **多语言**:所有界面文字通过 `{loc:Localization}``ILocalizationService` 获取,无硬编码文字
6. **动画性能**:呼吸闪烁动画使用 WPF Storyboard,性能开销极低
7. **日志规范**:使用 `_logger.ForModule("RaySource.ViewModel")` 和结构化日志消息
8. **自动初始化**OperateView 加载时自动执行 `AutoInitializeAsync()`,仅执行一次
9. **配置窗口**:通过 ConfigCommand 打开,单例模式(已存在时激活而非重复创建)
## 故障排查
### 按钮不可用
- 检查 `IsInitialized``IsVariablesConnected` 状态
- 检查 `RaySourceStatus` 是否正确
- 查看命令的 `CanExecute` 逻辑
### 滑块不可用
- 确认服务已初始化且变量已连接(`IsSlidersEnabled`
### 实际值不更新
- 确认已订阅 `StatusUpdatedEvent`
- 检查 Host 进程是否正常推送状态
- 验证 `ThreadOption.UIThread` 设置
### 设置值不生效
- 检查 `ApplyVoltageCommand` / `ApplyCurrentCommand` 是否正确触发
- 查看业务规则校验逻辑
- 确认 IPC 管道连接正常
### 多语言文字不显示
- 确认 XP.Common 资源文件中已添加对应的资源键
- 检查 XAML 中 `xmlns:loc` 命名空间引用是否正确
### 动画不播放
- 确认 `RaySourceStatus` 已变为 `Opened`
- 检查 `enums` 命名空间引用是否正确
### 状态显示为"不可用"
- 检查射线源服务连接状态
- 确认 Host 进程是否正常运行
- 查看日志中是否有连接丢失的警告信息
---
**最后更新 | Last Updated**: 2026-03-26
@@ -0,0 +1,116 @@
using Prism.Events;
using System;
using System.Collections.Generic;
using System.IO;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Config;
using XP.Hardware.RaySource.Implementations;
namespace XP.Hardware.RaySource.Factories
{
/// <summary>
/// 射线源工厂实现类 | X-Ray Source Factory Implementation
/// 负责创建不同类型的射线源实例 | Responsible for creating different types of X-ray source instances
/// </summary>
public class RaySourceFactory : IRaySourceFactory
{
private readonly RaySourceConfig _config;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="config">配置对象 | Configuration object</param>
/// <param name="eventAggregator">事件聚合器 | Event aggregator</param>
/// <param name="logger">日志服务 | Logger service</param>
public RaySourceFactory(RaySourceConfig config, IEventAggregator eventAggregator, ILoggerService logger)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule("RaySource.Factory");
_logger.Info("射线源工厂已创建 | X-ray source factory created");
}
/// <summary>
/// 创建指定类型的射线源实例 | Create X-ray source instance of specified type
/// </summary>
/// <param name="sourceType">射线源类型名称 | X-ray source type name</param>
/// <returns>射线源实例 | X-ray source instance</returns>
public IXRaySource CreateRaySource(string sourceType)
{
if (string.IsNullOrWhiteSpace(sourceType))
{
_logger.Error(null, "射线源类型不能为空 | X-ray source type cannot be empty");
throw new ArgumentException("射线源类型不能为空", nameof(sourceType));
}
_logger.Info($"创建射线源实例,类型: {sourceType} | Creating X-ray source instance, type: {sourceType}");
try
{
var instance = sourceType.ToUpperInvariant() switch
{
"COMET225" => CreateComet225RaySource(),
_ => throw new NotSupportedException($"不支持的射线源类型: {sourceType}")
};
_logger.Info($"射线源实例创建成功: {sourceType} | X-ray source instance created successfully: {sourceType}");
return instance;
}
catch (NotSupportedException ex)
{
_logger.Error(ex, $"不支持的射线源类型: {sourceType} | Unsupported X-ray source type: {sourceType}");
throw;
}
catch (TypeLoadException ex)
{
_logger.Error(ex, "跨框架类型加载失败,请检查 BR.AN.PviServices.dll 是否正确部署:{Message}", ex.Message);
throw;
}
catch (FileNotFoundException ex)
{
_logger.Error(ex, "跨框架程序集未找到,请检查 XP.Hardware.RaySource.Comet.dll 和 BR.AN.PviServices.dll 是否存在:{Message}", ex.Message);
throw;
}
catch (Exception ex)
{
_logger.Error(ex, $"创建射线源实例失败: {sourceType} | Failed to create X-ray source instance: {sourceType}");
throw;
}
}
/// <summary>
/// 获取支持的射线源类型列表 | Get list of supported X-ray source types
/// </summary>
/// <returns>类型名称列表 | List of type names</returns>
public IEnumerable<string> GetSupportedSourceTypes()
{
return new List<string>
{
"Comet225"
};
}
/// <summary>
/// 创建 Comet225 射线源实例
/// 通过 CometHostManager 管理 Host 进程生命周期,CometIpcClient 负责 IPC 通信
/// </summary>
private IXRaySource CreateComet225RaySource()
{
try
{
var hostManager = new CometHostManager(_logger, _config);
var ipcClient = new CometIpcClient(_config, _eventAggregator, _logger);
return new Comet225RaySource(hostManager, ipcClient, _logger);
}
catch (Exception ex)
{
_logger.Error(ex, "创建 Comet225 射线源实例失败:{Message}", ex.Message);
throw;
}
}
}
}
@@ -0,0 +1,262 @@
using System;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
namespace XP.Hardware.RaySource.Implementations
{
/// <summary>
/// Comet 225kV 射线源适配器
/// 通过 CometIpcClient 和 CometHostManager 实现进程外隔离的 PVI 通信
/// 所有操作委托给 CometIpcClientHost 进程生命周期由 CometHostManager 管理
/// </summary>
public class Comet225RaySource : XRaySourceBase
{
#region
private readonly CometHostManager _hostManager;
private readonly CometIpcClient _ipcClient;
private readonly ILoggerService _logger;
#endregion
#region
/// <summary>
/// 构造函数,注入依赖
/// </summary>
/// <param name="hostManager">Host 进程生命周期管理器</param>
/// <param name="ipcClient">IPC 客户端,负责与 Host 进程通信</param>
/// <param name="loggerService">日志服务</param>
public Comet225RaySource(
CometHostManager hostManager,
CometIpcClient ipcClient,
ILoggerService loggerService)
{
_hostManager = hostManager ?? throw new ArgumentNullException(nameof(hostManager));
_ipcClient = ipcClient ?? throw new ArgumentNullException(nameof(ipcClient));
_logger = loggerService?.ForModule<Comet225RaySource>() ?? throw new ArgumentNullException(nameof(loggerService));
}
public override string SourceName => "Comet 225kV";
#endregion
#region IXRaySource - CometIpcClient
/// <summary>
/// 初始化射线源
/// 先确保 Host 进程运行,再通过 IPC 客户端初始化 PVI 连接
/// </summary>
public override XRayResult Initialize()
{
_logger.Info("初始化 Comet225 射线源(IPC 模式)");
try
{
// 先确保 Host 进程运行
_hostManager.EnsureRunning();
_logger.Info("Host 进程已就绪,开始 IPC 初始化");
// 再通过 IPC 客户端初始化
var result = _ipcClient.Initialize();
if (result.Success)
{
_isInitialized = true;
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "初始化 Comet225 射线源异常:{Message}", ex.Message);
return XRayResult.Error($"初始化异常:{ex.Message}");
}
}
/// <summary>
/// 创建和连接 PVI 变量
/// </summary>
public override XRayResult ConnectVariables()
{
var result = _ipcClient.ConnectVariables();
if (result.Success)
{
_isConnected = true;
}
return result;
}
/// <summary>
/// 开启射线源高压
/// </summary>
public override XRayResult TurnOn()
{
return _ipcClient.TurnOn();
}
/// <summary>
/// 关闭射线源高压
/// </summary>
public override XRayResult TurnOff()
{
return _ipcClient.TurnOff();
}
/// <summary>
/// 设置电压值
/// </summary>
public override XRayResult SetVoltage(float voltage)
{
return _ipcClient.SetVoltage(voltage);
}
/// <summary>
/// 设置电流值
/// </summary>
public override XRayResult SetCurrent(float current)
{
return _ipcClient.SetCurrent(current);
}
/// <summary>
/// 设置焦点 - Comet 225kV 不支持
/// </summary>
public override XRayResult SetFocus(float focus)
{
return _ipcClient.SetFocus(focus);
}
/// <summary>
/// 读取当前电压反馈值
/// </summary>
public override XRayResult ReadVoltage()
{
return _ipcClient.ReadVoltage();
}
/// <summary>
/// 读取当前电流反馈值
/// </summary>
public override XRayResult ReadCurrent()
{
return _ipcClient.ReadCurrent();
}
/// <summary>
/// 读取系统状态
/// </summary>
public override XRayResult ReadSystemStatus()
{
return _ipcClient.ReadSystemStatus();
}
/// <summary>
/// 检查错误状态
/// </summary>
public override XRayResult CheckErrors()
{
return _ipcClient.CheckErrors();
}
/// <summary>
/// TXI 开启
/// </summary>
public override XRayResult TxiOn()
{
return _ipcClient.TxiOn();
}
/// <summary>
/// TXI 关闭
/// </summary>
public override XRayResult TxiOff()
{
return _ipcClient.TxiOff();
}
/// <summary>
/// 暖机设置
/// </summary>
public override XRayResult WarmUp()
{
return _ipcClient.WarmUp();
}
/// <summary>
/// 训机设置
/// </summary>
public override XRayResult Training()
{
return _ipcClient.Training();
}
/// <summary>
/// 灯丝校准
/// </summary>
public override XRayResult FilamentCalibration()
{
return _ipcClient.FilamentCalibration();
}
/// <summary>
/// 全部电压自动定心
/// </summary>
public override XRayResult AutoCenter()
{
return _ipcClient.AutoCenter();
}
/// <summary>
/// 设置功率模式
/// </summary>
public override XRayResult SetPowerMode(int mode)
{
return _ipcClient.SetPowerMode(mode);
}
/// <summary>
/// 关闭射线源,释放 IPC 连接并关闭 Host 进程
/// </summary>
public override XRayResult CloseOff()
{
_logger.Info("执行 CloseOff 操作(IPC 模式)");
try
{
// 先通过 IPC 客户端发送断开命令
var result = _ipcClient.CloseOff();
// 再关闭 Host 进程
_hostManager.Shutdown();
_isInitialized = false;
_isConnected = false;
_logger.Info("CloseOff 操作完成");
return result;
}
catch (Exception ex)
{
_logger.Warn("CloseOff 过程中发生异常:{Message}", ex.Message);
_isInitialized = false;
_isConnected = false;
return XRayResult.Error($"CloseOff 异常:{ex.Message}");
}
}
#endregion
#region
/// <summary>
/// 释放资源,清理 IPC 客户端和 Host 进程管理器
/// </summary>
protected override void Dispose(bool disposing)
{
if (!_isDisposed && disposing)
{
_ipcClient?.Dispose();
_hostManager?.Dispose();
}
base.Dispose(disposing);
}
#endregion
}
}
@@ -0,0 +1,257 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Config;
namespace XP.Hardware.RaySource.Implementations
{
/// <summary>
/// Host 进程生命周期管理器
/// 负责启动、监控和关闭 .NET Framework 4.8 Host 子进程
/// </summary>
public class CometHostManager : IDisposable
{
private readonly ILoggerService _logger;
private readonly RaySourceConfig _config;
private Process _hostProcess;
private bool _isDisposed;
/// <summary>
/// Host 可执行文件名
/// </summary>
private const string HostExeName = "XP.Hardware.RaySource.Comet.Host.exe";
/// <summary>
/// 命令管道名称(客户端写 → Host 读)
/// </summary>
private const string CmdPipeName = "XP.Hardware.RaySource.Comet.Cmd";
/// <summary>
/// 响应管道名称(Host 写 → 客户端读)
/// </summary>
private const string RspPipeName = "XP.Hardware.RaySource.Comet.Rsp";
/// <summary>
/// 等待管道就绪的超时时间(毫秒)
/// </summary>
private const int PipeReadyTimeoutMs = 10000;
/// <summary>
/// 等待 Host 进程退出的超时时间(毫秒)
/// </summary>
private const int ShutdownTimeoutMs = 5000;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="loggerService">日志服务</param>
/// <param name="config">射线源配置</param>
public CometHostManager(ILoggerService loggerService, RaySourceConfig config)
{
_logger = loggerService?.ForModule<CometHostManager>() ?? throw new ArgumentNullException(nameof(loggerService));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// Host 进程是否正在运行
/// </summary>
public bool IsRunning => _hostProcess != null && !_hostProcess.HasExited;
/// <summary>
/// 确保 Host 进程正在运行
/// 优先检查是否已有同路径的 Host 进程在运行,有则直接关联;否则启动新进程
/// </summary>
public void EnsureRunning()
{
if (IsRunning)
{
_logger.Debug("Host 进程已在运行,PID={Pid}", _hostProcess.Id);
return;
}
var hostExePath = GetHostExePath();
// 检查是否已有 Host 进程在运行(例如上次异常退出后残留的进程),杀掉后重新启动
var hostProcessName = Path.GetFileNameWithoutExtension(HostExeName);
var existingProcesses = Process.GetProcessesByName(hostProcessName);
foreach (var proc in existingProcesses)
{
try
{
_logger.Info("发现残留的 Host 进程,正在终止,PID={Pid}", proc.Id);
proc.Kill();
proc.WaitForExit(1500);
}
catch (Exception ex)
{
_logger.Warn("终止残留 Host 进程失败,PID={Pid}{Message}", proc.Id, ex.Message);
}
}
_logger.Info("启动 Host 进程:{Path}", hostExePath);
if (!File.Exists(hostExePath))
{
throw new FileNotFoundException($"Host 可执行文件未找到:{hostExePath}", hostExePath);
}
var startInfo = new ProcessStartInfo
{
FileName = hostExePath,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardError = true
};
_hostProcess = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
_hostProcess.Exited += OnHostProcessExited;
_hostProcess.ErrorDataReceived += OnHostErrorDataReceived;
_hostProcess.Start();
_hostProcess.BeginErrorReadLine();
_logger.Info("Host 进程已启动,PID={Pid}", _hostProcess.Id);
// 等待 NamedPipe 就绪
WaitForPipeReady();
}
/// <summary>
/// 安全关闭 Host 进程
/// 等待进程退出,超时则强制终止(DisconnectCommand 已由 CometIpcClient 发送)
/// </summary>
public void Shutdown()
{
if (!IsRunning)
{
_logger.Debug("Host 进程未运行,无需关闭");
return;
}
_logger.Info("正在关闭 Host 进程,PID={Pid}", _hostProcess.Id);
try
{
// 等待进程退出(DisconnectCommand 已由 CometIpcClient 发送)
if (!_hostProcess.WaitForExit(ShutdownTimeoutMs))
{
_logger.Warn("Host 进程未在超时时间内退出,强制终止");
_hostProcess.Kill();
_hostProcess.WaitForExit(1000);
}
_logger.Info("Host 进程已关闭");
}
catch (Exception ex)
{
_logger.Warn("关闭 Host 进程时发生异常:{Message}", ex.Message);
}
finally
{
CleanupProcess();
}
}
/// <summary>
/// 获取 Host 可执行文件路径
/// 优先使用配置的路径,为空时默认在主程序输出目录下查找
/// </summary>
private string GetHostExePath()
{
// 优先使用配置的路径
if (!string.IsNullOrWhiteSpace(_config.HostExePath))
{
return _config.HostExePath;
}
// 默认在主程序输出目录的 Host 子目录下查找
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
return Path.Combine(baseDir, "Host", HostExeName);
}
/// <summary>
/// 等待 NamedPipe 就绪
/// 通过短暂延迟和进程存活检查确认 Host 进程已启动并创建了管道
/// 使用临时 NamedPipeClientStream 尝试连接来检测管道是否就绪
/// </summary>
private void WaitForPipeReady()
{
var startTime = DateTime.UtcNow;
while ((DateTime.UtcNow - startTime).TotalMilliseconds < PipeReadyTimeoutMs)
{
if (_hostProcess.HasExited)
{
throw new InvalidOperationException($"Host 进程启动后立即退出,退出码={_hostProcess.ExitCode}");
}
// 短暂等待,让 Host 进程有时间创建管道
Thread.Sleep(200);
// 进程仍在运行,认为管道即将就绪或已就绪
if (!_hostProcess.HasExited)
{
_logger.Info("Host 进程正在运行,管道应已就绪");
return;
}
}
// 超时但进程仍在运行,假设管道已就绪
if (!_hostProcess.HasExited)
{
_logger.Warn("等待管道就绪超时,但 Host 进程仍在运行,继续");
return;
}
throw new TimeoutException($"等待 Host 进程 NamedPipe 就绪超时({PipeReadyTimeoutMs}ms");
}
/// <summary>
/// Host 进程退出事件处理
/// </summary>
private void OnHostProcessExited(object sender, EventArgs e)
{
if (_hostProcess != null)
{
_logger.Warn("Host 进程已退出,退出码={ExitCode}", _hostProcess.ExitCode);
}
}
/// <summary>
/// Host 进程 stderr 输出事件处理,转发到主进程日志
/// </summary>
private void OnHostErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
{
_logger.Debug("[Host stderr] {Message}", e.Data);
}
}
/// <summary>
/// 清理进程资源
/// </summary>
private void CleanupProcess()
{
if (_hostProcess != null)
{
_hostProcess.Exited -= OnHostProcessExited;
_hostProcess.ErrorDataReceived -= OnHostErrorDataReceived;
_hostProcess.Dispose();
_hostProcess = null;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
if (!_isDisposed)
{
Shutdown();
_isDisposed = true;
}
}
}
}
@@ -0,0 +1,668 @@
using Prism.Events;
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using Telerik.Windows.Documents.Selection;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Abstractions.Enums;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Comet.Messages;
using XP.Hardware.RaySource.Comet.Messages.Commands;
using XP.Hardware.RaySource.Comet.Messages.Responses;
using XP.Hardware.RaySource.Config;
using XP.Hardware.RaySource.Services;
namespace XP.Hardware.RaySource.Implementations
{
/// <summary>
/// IPC 客户端
/// 通过 NamedPipe 与 Host 进程通信,实现 IXRaySource 接口
/// 上层代码无需感知进程隔离的存在
/// </summary>
public class CometIpcClient : XRaySourceBase
{
private readonly RaySourceConfig _config;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private NamedPipeClientStream _cmdPipe;
private NamedPipeClientStream _rspPipe;
private StreamReader _reader;
private Thread _receiveThread;
private volatile bool _isRunning;
/// <summary>
/// 管道连接状态标志位
/// 用于在 Host 进程崩溃或管道断开后,阻止后续 SendCommand 尝试写入已断开的管道
/// </summary>
private volatile bool _isPipeConnected;
/// <summary>
/// 用于等待命令响应的同步机制
/// </summary>
private TaskCompletionSource<RaySourceResponse> _pendingResponse;
private readonly object _sendLock = new object();
/// <summary>
/// 命令管道名称(客户端写 → Host 读)
/// </summary>
private const string CmdPipeName = "XP.Hardware.RaySource.Comet.Cmd";
/// <summary>
/// 响应管道名称(Host 写 → 客户端读)
/// </summary>
private const string RspPipeName = "XP.Hardware.RaySource.Comet.Rsp";
/// <summary>
/// 命令超时时间(毫秒)
/// 需要大于 Host 端 PVI 连接超时时间(30s),因为 Initialize 命令会在 Host 端阻塞等待 PVI 回调链完成
/// </summary>
private const int CommandTimeoutMs = 35000;
public override string SourceName => "Comet 225kV (IPC)";
/// <summary>
/// 构造函数
/// </summary>
public CometIpcClient(
RaySourceConfig config,
IEventAggregator eventAggregator,
ILoggerService loggerService)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = loggerService?.ForModule<CometIpcClient>() ?? throw new ArgumentNullException(nameof(loggerService));
}
#region
/// <summary>
/// 连接到 Host 进程的 NamedPipe
/// </summary>
public void Connect()
{
try
{
// 命令管道:客户端写 → Host 读
_cmdPipe = new NamedPipeClientStream(".", CmdPipeName, PipeDirection.Out);
_cmdPipe.Connect(_config.ConnectionTimeout);
// 响应管道:Host 写 → 客户端读
_rspPipe = new NamedPipeClientStream(".", RspPipeName, PipeDirection.In);
_rspPipe.Connect(_config.ConnectionTimeout);
_reader = new StreamReader(_rspPipe);
// 启动后台消息接收线程
_isRunning = true;
_receiveThread = new Thread(ReceiveLoop)
{
IsBackground = true,
Name = "CometIpcClient-Receiver"
};
_receiveThread.Start();
_isPipeConnected = true;
_logger.Info("已连接到 Host 进程 NamedPipe(双管道模式)");
}
catch (Exception ex)
{
_logger.Error(ex, "连接 Host 进程 NamedPipe 失败:{Message}", ex.Message);
throw;
}
}
/// <summary>
/// 直接写入一行文本到命令管道
/// </summary>
private void WriteLineToHost(string text)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(text + "\n");
_cmdPipe.Write(bytes, 0, bytes.Length);
}
#endregion
#region
/// <summary>
/// 发送命令并等待响应
/// </summary>
private RaySourceResponse SendCommand(RaySourceCommand command)
{
// 检查管道连接状态,如果已断开则直接返回 null
if (!_isPipeConnected)
{
_logger.Error(null, "管道未连接,无法发送命令:{CommandType}", command.CommandType);
return null;
}
lock (_sendLock)
{
try
{
_pendingResponse = new TaskCompletionSource<RaySourceResponse>();
var json = MessageSerializer.Serialize(command);
_logger.Debug("发送命令:{CommandType}JSON 长度={Length}", command.CommandType, json.Length);
WriteLineToHost(json);
_logger.Debug("命令已写入管道,开始等待响应:{CommandType}", command.CommandType);
// 等待响应,带超时
using (var cts = new CancellationTokenSource(CommandTimeoutMs))
{
cts.Token.Register(() => _pendingResponse?.TrySetCanceled());
try
{
_pendingResponse.Task.Wait();
var result = _pendingResponse.Task.Result;
_logger.Debug("收到命令响应:{CommandType}Success={Success}", command.CommandType, result?.Success);
return result;
}
catch (AggregateException ae) when (ae.InnerException is TaskCanceledException)
{
_logger.Error(null, "命令超时({TimeoutMs}ms):{CommandType}", CommandTimeoutMs, command.CommandType);
return null;
}
}
}
catch (IOException ex)
{
_logger.Error(ex, "管道通信异常(Host 进程可能已崩溃):{Message}", ex.Message);
return null;
}
catch (Exception ex)
{
_logger.Error(ex, "发送命令异常:{Message}", ex.Message);
return null;
}
}
}
#endregion
#region 线
/// <summary>
/// 后台消息接收循环
/// 持续读取管道消息,区分命令响应和主动推送
/// </summary>
private void ReceiveLoop()
{
try
{
_logger.Debug("接收线程已启动");
while (_isRunning)
{
var line = _reader.ReadLine();
if (line == null)
{
// 管道断开,设置标志位阻止后续 SendCommand 写入
_isPipeConnected = false;
_logger.Warn("Host 进程管道已断开");
_pendingResponse?.TrySetResult(null);
break;
}
_logger.Debug("收到管道消息,长度={Length}", line.Length);
var response = MessageSerializer.DeserializeResponse(line);
if (response == null)
{
_logger.Warn("收到无法反序列化的消息,原始内容:{Raw}", line.Length > 200 ? line.Substring(0, 200) + "..." : line);
continue;
}
if (response.IsPush)
{
_logger.Debug("收到推送消息:{PushType}", response.PushType);
// 主动推送消息,路由到对应的 Prism 事件
HandlePushMessage(response);
}
else
{
_logger.Debug("收到命令响应,Success={Success}", response.Success);
// 命令响应,通知等待的 SendCommand 调用
_pendingResponse?.TrySetResult(response);
}
}
}
catch (IOException ex)
{
_isPipeConnected = false;
_logger.Warn("接收线程 IO 异常(管道可能已断开):{Message}", ex.Message);
_pendingResponse?.TrySetResult(null);
}
catch (ObjectDisposedException)
{
// 管道已释放,正常退出
}
catch (Exception ex)
{
_logger.Error(ex, "接收线程异常:{Message}", ex.Message);
_pendingResponse?.TrySetResult(null);
}
}
#endregion
#region
/// <summary>
/// 处理推送消息,根据 PushType 路由到对应的 Prism 事件
/// </summary>
private void HandlePushMessage(RaySourceResponse response)
{
try
{
switch (response.PushType)
{
case "StatusChanged":
HandleStatusPush(response as StatusResponse);
break;
case "XRayStateChanged":
HandleXRayStatePush(response as OperationResponse);
break;
case "ErrorOccurred":
HandleErrorPush(response);
break;
case "ConnectionStateChanged":
HandleConnectionStatePush(response as OperationResponse);
break;
case "Log":
HandleLogPush(response as LogResponse);
break;
default:
_logger.Warn("收到未知推送类型:{PushType}", response.PushType);
break;
}
}
catch (Exception ex)
{
_logger.Error(ex, "处理推送消息异常:{Message}", ex.Message);
}
}
private void HandleStatusPush(StatusResponse status)
{
if (status == null) return;
var statusData = new SystemStatusData
{
SetVoltage = status.SetVoltage,
ActualVoltage = status.ActualVoltage,
SetCurrent = status.SetCurrent,
ActualCurrent = status.ActualCurrent,
IsXRayOn = status.IsXRayOn,
WarmUpStatus = status.WarmUpStatus,
VacuumStatus = status.VacuumStatus,
StartUpStatus = status.StartUpStatus,
AutoCenterStatus = status.AutoCenterStatus,
FilamentAdjustStatus = status.FilamentAdjustStatus,
IsInterlockActive = status.IsInterlockActive,
WatchdogStatus = status.WatchdogStatus,
PowerMode = status.PowerMode,
TxiStatus = status.TxiStatus
};
_eventAggregator.GetEvent<StatusUpdatedEvent>().Publish(statusData);
}
private void HandleXRayStatePush(OperationResponse response)
{
if (response?.Data == null) return;
var isXRayOn = Convert.ToBoolean(response.Data);
var rayStatus = isXRayOn ? RaySourceStatus.Opened : RaySourceStatus.Closed;
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(rayStatus);
}
private void HandleErrorPush(RaySourceResponse response)
{
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(response.ErrorMessage);
}
private void HandleConnectionStatePush(OperationResponse response)
{
if (response?.Data == null) return;
var stateStr = response.Data.ToString();
if (stateStr == "RaySourceConnected")
{
_isConnected = true;
_logger.Info("收到 RaySourceConnected 推送,射线源已完成全部连接流程,准备就绪");
_eventAggregator.GetEvent<VariablesConnectedEvent>().Publish(true);
_logger.Info("射线源连接成功: {SourceName},状态更新为 Closed | X-ray source connectzed successfully: {SourceName}, status updated to Closed", SourceName);
// 通知状态变更 | Notify status changed
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(RaySourceStatus.Closed);
}
else if (stateStr == "VariablesConnected")
{
_isInitialized = true;
_logger.Info("收到 VariablesConnected 推送,PVI 变量已创建、激活并绑定");
}
else if (stateStr == "ServiceConnected")
{
_serviceConnectedEvent.Set();
_logger.Info("收到 ServiceConnected 推送,PVI Service 和 CPU 已连接");
}
else if (stateStr == "Disconnected")
{
_isConnected = false;
_isInitialized = false;
_logger.Info("收到 Disconnected 推送,PVI 未连接或已断开");
_eventAggregator.GetEvent<VariablesConnectedEvent>().Publish(false);
}
}
#endregion
#region
/// <summary>
/// 处理日志推送消息
/// 根据 Level 字段映射到 ILoggerService 对应方法
/// </summary>
private void HandleLogPush(LogResponse logResponse)
{
if (logResponse == null) return;
var message = logResponse.Message ?? "";
// 将 string[] 转换为 object[] 以匹配 ILoggerService 的 params object[] 签名
var args = logResponse.Args != null ? (object[])logResponse.Args : Array.Empty<object>();
switch (logResponse.Level)
{
case "Debug":
_logger.Debug(message, args);
break;
case "Info":
_logger.Info(message, args);
break;
case "Warn":
_logger.Warn(message, args);
break;
case "Error":
_logger.Error(null, message, args);
break;
case "Fatal":
_logger.Fatal(null, message, args);
break;
default:
_logger.Debug(message, args);
break;
}
}
#endregion
#region IXRaySource
/// <summary>
/// 等待 ServiceConnected 推送的信号量
/// Host 端 HandleInitialize 会等待 PVI 回调链完成后才返回响应
/// 但作为双重保险,客户端也等待 ServiceConnected 推送到达
/// </summary>
private readonly ManualResetEventSlim _serviceConnectedEvent = new ManualResetEventSlim(false);
/// <summary>
/// 等待 ServiceConnected 推送的超时时间(毫秒)
/// 应略大于 Host 端的 PVI 连接超时时间,因为还有管道通信延迟
/// </summary>
private const int ServiceConnectedTimeoutMs = 35000;
public override XRayResult Initialize()
{
_logger.Info("初始化 Comet225 射线源(IPC 模式)");
try
{
// 重置等待信号
_serviceConnectedEvent.Reset();
// 连接管道
Connect();
var command = new InitializeCommand
{
IpAddress = _config.PlcIpAddress,
Port = _config.PlcPort,
CpuName = _config.CpuName,
SourcePort = _config.PortNumber,
StationNumber = _config.StationNumber
};
// Host 端 HandleInitialize 会等待 PVI 回调链完成后才返回响应
var response = SendCommand(command);
var result = ProcessResponse(response, "Initialize");
if (!result.Success)
{
return result;
}
// 双重保险:等待 ServiceConnected 推送到达
// Host 返回成功意味着 PVI 回调链已完成,推送应该很快到达
if (!_serviceConnectedEvent.Wait(ServiceConnectedTimeoutMs))
{
_logger.Warn("Initialize 命令成功但未收到 ServiceConnected 推送,可能推送丢失");
// Host 已确认成功,即使推送未到达也设置初始化标志
_isInitialized = true;
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "Initialize 异常:{Message}", ex.Message);
return XRayResult.Error($"Initialize 异常:{ex.Message}");
}
}
public override XRayResult ConnectVariables()
{
var response = SendCommand(new ConnectVariablesCommand());
return ProcessResponse(response, "ConnectVariables");
}
public override XRayResult TurnOn()
{
var response = SendCommand(new TurnOnCommand());
return ProcessResponse(response, "TurnOn");
}
public override XRayResult TurnOff()
{
var response = SendCommand(new TurnOffCommand());
return ProcessResponse(response, "TurnOff");
}
public override XRayResult SetVoltage(float voltage)
{
var response = SendCommand(new SetVoltageCommand { Voltage = voltage });
return ProcessResponse(response, "SetVoltage");
}
public override XRayResult SetCurrent(float current)
{
var response = SendCommand(new SetCurrentCommand { Current = current });
return ProcessResponse(response, "SetCurrent");
}
public override XRayResult SetFocus(float focus)
{
_logger.Info("SetFocus 被调用,Comet 225kV 射线源不支持焦点设置");
return XRayResult.Ok("Comet 225kV 射线源不支持焦点设置");
}
public override XRayResult ReadVoltage()
{
var response = SendCommand(new ReadVoltageCommand());
if (response is OperationResponse opResp && opResp.Success)
{
return XRayResult.Ok(opResp.Data);
}
return ProcessResponse(response, "ReadVoltage");
}
public override XRayResult ReadCurrent()
{
var response = SendCommand(new ReadCurrentCommand());
if (response is OperationResponse opResp && opResp.Success)
{
return XRayResult.Ok(opResp.Data);
}
return ProcessResponse(response, "ReadCurrent");
}
public override XRayResult ReadSystemStatus()
{
var response = SendCommand(new ReadSystemStatusCommand());
if (response is StatusResponse statusResp && statusResp.Success)
{
var statusData = new SystemStatusData
{
SetVoltage = statusResp.SetVoltage,
ActualVoltage = statusResp.ActualVoltage,
SetCurrent = statusResp.SetCurrent,
ActualCurrent = statusResp.ActualCurrent,
IsXRayOn = statusResp.IsXRayOn,
WarmUpStatus = statusResp.WarmUpStatus,
VacuumStatus = statusResp.VacuumStatus,
StartUpStatus = statusResp.StartUpStatus,
AutoCenterStatus = statusResp.AutoCenterStatus,
FilamentAdjustStatus = statusResp.FilamentAdjustStatus,
IsInterlockActive = statusResp.IsInterlockActive,
WatchdogStatus = statusResp.WatchdogStatus,
PowerMode = statusResp.PowerMode,
TxiStatus = statusResp.TxiStatus
};
return XRayResult.Ok(statusData);
}
return ProcessResponse(response, "ReadSystemStatus");
}
public override XRayResult CheckErrors()
{
var response = SendCommand(new ReadErrorsCommand());
if (response is ErrorDataResponse errorResp && errorResp.Success)
{
return XRayResult.Ok(errorResp);
}
return ProcessResponse(response, "CheckErrors");
}
public override XRayResult TxiOn()
{
var response = SendCommand(new TxiOnCommand());
return ProcessResponse(response, "TxiOn");
}
public override XRayResult TxiOff()
{
var response = SendCommand(new TxiOffCommand());
return ProcessResponse(response, "TxiOff");
}
public override XRayResult WarmUp()
{
var response = SendCommand(new WarmUpCommand());
return ProcessResponse(response, "WarmUp");
}
public override XRayResult Training()
{
var response = SendCommand(new TrainingCommand());
return ProcessResponse(response, "Training");
}
public override XRayResult FilamentCalibration()
{
var response = SendCommand(new FilamentCalibrationCommand());
return ProcessResponse(response, "FilamentCalibration");
}
public override XRayResult AutoCenter()
{
var response = SendCommand(new AutoCenterCommand());
return ProcessResponse(response, "AutoCenter");
}
public override XRayResult SetPowerMode(int mode)
{
var response = SendCommand(new SetPowerModeCommand { Mode = mode });
return ProcessResponse(response, "SetPowerMode");
}
public override XRayResult CloseOff()
{
_logger.Info("执行 CloseOff 操作(IPC 模式)");
try
{
var response = SendCommand(new DisconnectCommand());
_isInitialized = false;
_isConnected = false;
return ProcessResponse(response, "CloseOff");
}
catch (Exception ex)
{
_logger.Warn("CloseOff 过程中发生异常:{Message}", ex.Message);
_isInitialized = false;
_isConnected = false;
return XRayResult.Error($"CloseOff 异常:{ex.Message}");
}
}
#endregion
#region
/// <summary>
/// 处理响应,将 Host 返回的响应转换为 XRayResult
/// </summary>
private XRayResult ProcessResponse(RaySourceResponse response, string operationName)
{
if (response == null)
{
var errorMsg = $"{operationName} 操作失败:未收到 Host 响应(管道可能已断开或超时)";
_logger.Error(null, errorMsg);
return XRayResult.Error(errorMsg);
}
if (!response.Success)
{
_logger.Error(null, "{Operation} 操作失败:{ErrorMessage}", operationName, response.ErrorMessage);
return XRayResult.Error(response.ErrorMessage);
}
return XRayResult.Ok();
}
#endregion
#region
protected override void Dispose(bool disposing)
{
if (!_isDisposed && disposing)
{
_isRunning = false;
_isPipeConnected = false;
_serviceConnectedEvent?.Dispose();
_reader?.Dispose();
_cmdPipe?.Dispose();
_rspPipe?.Dispose();
_reader = null;
_cmdPipe = null;
_rspPipe = null;
}
base.Dispose(disposing);
}
#endregion
}
}
@@ -0,0 +1,102 @@
using Prism.Ioc;
using Prism.Modularity;
using System.Resources;
using XP.Common.Localization;
using XP.Common.Localization.Interfaces;
using XP.Common.Logging.Implementations;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Config;
using XP.Hardware.RaySource.Factories;
using XP.Hardware.RaySource.Services;
namespace XP.Hardware.RaySource.Module
{
/// <summary>
/// 射线源模块 | X-Ray Source Module
/// Prism模块入口,注册服务/工厂到Unity容器 | Prism module entry, register services/factories to Unity container
/// </summary>
[Module(ModuleName = "RaySourceModule")]
public class RaySourceModule : IModule
{
/// <summary>
/// 模块初始化 | Module initialization
/// </summary>
public void OnInitialized(IContainerProvider containerProvider)
{
// 注册模块级多语言资源到 Fallback Chain | Register module-level localization resources to Fallback Chain
var localizationService = containerProvider.Resolve<ILocalizationService>();
var resourceManager = new ResourceManager(
"XP.Hardware.RaySource.Resources.Resources",
typeof(RaySourceModule).Assembly);
localizationService.RegisterResourceSource("XP.Hardware.RaySource", resourceManager);
// 初始化 LocalizationHelper,使其通过 ILocalizationService 获取字符串(支持 Fallback Chain
// Initialize LocalizationHelper to use ILocalizationService for string lookup (supports Fallback Chain)
LocalizationHelper.Initialize(localizationService);
// 初始化灯丝寿命服务(建表、异常恢复)| Initialize filament lifetime service (create tables, recover anomalies)
var filamentService = containerProvider.Resolve<IFilamentLifetimeService>();
var initialized = filamentService.Initialize();
if (initialized && filamentService.ShouldShowLifetimeWarning())
{
// 获取预警数据 | Get warning data
var totalSeconds = filamentService.GetCurrentTotalLifeSeconds();
var thresholdSeconds = filamentService.GetThresholdSeconds();
var percentage = filamentService.GetLifetimePercentage();
// 转换为小时,保留一位小数 | Convert to hours, keep one decimal place
var totalHours = (totalSeconds / 3600.0).ToString("F1");
var thresholdHours = (thresholdSeconds / 3600.0).ToString("F1");
var percentageText = percentage.ToString("F1");
// 使用 LocalizationHelper 获取本地化文本(OnInitialized 中不方便注入)
// Use LocalizationHelper for localized text (injection not convenient in OnInitialized)
var title = LocalizationHelper.Get("RaySource_FilamentLifetimeWarningTitle");
var message = LocalizationHelper.Get("RaySource_FilamentLifetimeWarningMessage",
totalHours, thresholdHours, percentageText);
// 在 UI 线程弹出模态提醒窗口 | Show modal warning dialog on UI thread
System.Windows.Application.Current?.Dispatcher?.Invoke(() =>
{
System.Windows.MessageBox.Show(
message,
title,
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
});
}
System.Console.WriteLine("[RaySourceModule] 模块已初始化");
}
/// <summary>
/// 注册类型到DI容器 | Register types to DI container
/// </summary>
public void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注册日志服务(单例)| Register logger service (singleton)
if (!containerRegistry.IsRegistered<ILoggerService>())
{
containerRegistry.RegisterSingleton<ILoggerService, SerilogLoggerService>();
System.Console.WriteLine("[RaySourceModule] 日志服务已注册 | Logger service registered");
}
// 注册配置对象(单例)| Register configuration object (singleton)
var config = ConfigLoader.LoadConfig();
containerRegistry.RegisterInstance(config);
// 注册工厂层(单例)| Register factory layer (singleton)
containerRegistry.RegisterSingleton<IRaySourceFactory, RaySourceFactory>();
// 注册服务层(单例)| Register service layer (singleton)
containerRegistry.RegisterSingleton<IRaySourceService, RaySourceService>();
// 注册灯丝寿命服务(单例)| Register filament lifetime service (singleton)
containerRegistry.RegisterSingleton<IFilamentLifetimeService, FilamentLifetimeService>();
System.Console.WriteLine("[RaySourceModule] 类型注册完成 | Type registration completed");
}
}
}
@@ -0,0 +1,2 @@
Telerik.Windows.Controls.RadButton, Telerik.Windows.Controls, Version=2024.1.408.310, Culture=neutral, PublicKeyToken=5803cfa389c90ce7
Telerik.Windows.Controls.RadPropertyGrid, Telerik.Windows.Controls.Data, Version=2024.1.408.310, Culture=neutral, PublicKeyToken=5803cfa389c90ce7
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

+885
View File
@@ -0,0 +1,885 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace XP.Hardware.RaySource.Resources {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XP.Hardware.RaySource.Resources.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 查找类似 实际 {0} 的本地化字符串。
/// </summary>
internal static string RaySource_ActualValueLabel {
get {
return ResourceManager.GetString("RaySource_ActualValueLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 高级 的本地化字符串。
/// </summary>
internal static string RaySource_AdvanceButton {
get {
return ResourceManager.GetString("RaySource_AdvanceButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 是否确认执行全部电压自动定心操作? 的本地化字符串。
/// </summary>
internal static string RaySource_AutoCenter_Confirm {
get {
return ResourceManager.GetString("RaySource_AutoCenter_Confirm", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在执行全部电压自动定心操作,请稍候... 的本地化字符串。
/// </summary>
internal static string RaySource_AutoCenter_Message {
get {
return ResourceManager.GetString("RaySource_AutoCenter_Message", resourceCulture);
}
}
/// <summary>
/// 查找类似 自动定心 的本地化字符串。
/// </summary>
internal static string RaySource_AutoCenter_Title {
get {
return ResourceManager.GetString("RaySource_AutoCenter_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 自动定心: 的本地化字符串。
/// </summary>
internal static string RaySource_AutoCenterLabel {
get {
return ResourceManager.GetString("RaySource_AutoCenterLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 自动定心 的本地化字符串。
/// </summary>
internal static string RaySource_AutoCenterSettingButton {
get {
return ResourceManager.GetString("RaySource_AutoCenterSettingButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 配置 的本地化字符串。
/// </summary>
internal static string RaySource_ConfigButton {
get {
return ResourceManager.GetString("RaySource_ConfigButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线源配置 的本地化字符串。
/// </summary>
internal static string RaySource_ConfigWindowTitle {
get {
return ResourceManager.GetString("RaySource_ConfigWindowTitle", resourceCulture);
}
}
/// <summary>
/// 查找类似 确认 的本地化字符串。
/// </summary>
internal static string RaySource_Confirm_Title {
get {
return ResourceManager.GetString("RaySource_Confirm_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 连接射线源 的本地化字符串。
/// </summary>
internal static string RaySource_ConnectButton {
get {
return ResourceManager.GetString("RaySource_ConnectButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 已连接 的本地化字符串。
/// </summary>
internal static string RaySource_Connected {
get {
return ResourceManager.GetString("RaySource_Connected", resourceCulture);
}
}
/// <summary>
/// 查找类似 连接射线源设备 的本地化字符串。
/// </summary>
internal static string RaySource_ConnectTooltip {
get {
return ResourceManager.GetString("RaySource_ConnectTooltip", resourceCulture);
}
}
/// <summary>
/// 查找类似 连接变量 的本地化字符串。
/// </summary>
internal static string RaySource_ConnectVariablesButton {
get {
return ResourceManager.GetString("RaySource_ConnectVariablesButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 电流 (μA) 的本地化字符串。
/// </summary>
internal static string RaySource_CurrentLabel {
get {
return ResourceManager.GetString("RaySource_CurrentLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 断开射线源 的本地化字符串。
/// </summary>
internal static string RaySource_DisconnectButton {
get {
return ResourceManager.GetString("RaySource_DisconnectButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 未连接 的本地化字符串。
/// </summary>
internal static string RaySource_Disconnected {
get {
return ResourceManager.GetString("RaySource_Disconnected", resourceCulture);
}
}
/// <summary>
/// 查找类似 断开射线源设备 的本地化字符串。
/// </summary>
internal static string RaySource_DisconnectTooltip {
get {
return ResourceManager.GetString("RaySource_DisconnectTooltip", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源紧急停止 的本地化字符串。
/// </summary>
internal static string RaySource_EmergencyStop {
get {
return ResourceManager.GetString("RaySource_EmergencyStop", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线开启时无法调整参数 的本地化字符串。
/// </summary>
internal static string RaySource_Error_CannotAdjustWhileXrayOn {
get {
return ResourceManager.GetString("RaySource_Error_CannotAdjustWhileXrayOn", resourceCulture);
}
}
/// <summary>
/// 查找类似 电流超出范围 (10-1000 μA) 的本地化字符串。
/// </summary>
internal static string RaySource_Error_CurrentOutOfRange {
get {
return ResourceManager.GetString("RaySource_Error_CurrentOutOfRange", resourceCulture);
}
}
/// <summary>
/// 查找类似 电压超出范围 (20-225 kV) 的本地化字符串。
/// </summary>
internal static string RaySource_Error_VoltageOutOfRange {
get {
return ResourceManager.GetString("RaySource_Error_VoltageOutOfRange", resourceCulture);
}
}
/// <summary>
/// 查找类似 是否确认执行灯丝校准操作? 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentCalibration_Confirm {
get {
return ResourceManager.GetString("RaySource_FilamentCalibration_Confirm", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在执行灯丝校准操作,请稍候... 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentCalibration_Message {
get {
return ResourceManager.GetString("RaySource_FilamentCalibration_Message", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在执行灯丝校准操作,请稍候... 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentCalibration_Title {
get {
return ResourceManager.GetString("RaySource_FilamentCalibration_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 灯丝校准 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentCalibrationButton {
get {
return ResourceManager.GetString("RaySource_FilamentCalibrationButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 灯丝校准: 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentLabel {
get {
return ResourceManager.GetString("RaySource_FilamentLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 灯丝寿命: 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentLifetimeLabel {
get {
return ResourceManager.GetString("RaySource_FilamentLifetimeLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 确认 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentLifetimeWarningConfirm {
get {
return ResourceManager.GetString("RaySource_FilamentLifetimeWarningConfirm", resourceCulture);
}
}
/// <summary>
/// 查找类似 灯丝累计使用 {0} 小时,寿命阈值 {1} 小时,已使用 {2}%。建议尽快更换灯丝。 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentLifetimeWarningMessage {
get {
return ResourceManager.GetString("RaySource_FilamentLifetimeWarningMessage", resourceCulture);
}
}
/// <summary>
/// 查找类似 灯丝寿命预警 的本地化字符串。
/// </summary>
internal static string RaySource_FilamentLifetimeWarningTitle {
get {
return ResourceManager.GetString("RaySource_FilamentLifetimeWarningTitle", resourceCulture);
}
}
/// <summary>
/// 查找类似 High Power 的本地化字符串。
/// </summary>
internal static string RaySource_HighPowerButton {
get {
return ResourceManager.GetString("RaySource_HighPowerButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源初始化失败 的本地化字符串。
/// </summary>
internal static string RaySource_InitFailed {
get {
return ResourceManager.GetString("RaySource_InitFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 初始化 的本地化字符串。
/// </summary>
internal static string RaySource_InitializeButton {
get {
return ResourceManager.GetString("RaySource_InitializeButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源初始化成功 的本地化字符串。
/// </summary>
internal static string RaySource_InitSuccess {
get {
return ResourceManager.GetString("RaySource_InitSuccess", resourceCulture);
}
}
/// <summary>
/// 查找类似 激活 的本地化字符串。
/// </summary>
internal static string RaySource_InterlockActive {
get {
return ResourceManager.GetString("RaySource_InterlockActive", resourceCulture);
}
}
/// <summary>
/// 查找类似 连锁: 的本地化字符串。
/// </summary>
internal static string RaySource_InterlockLabel {
get {
return ResourceManager.GetString("RaySource_InterlockLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 正常 的本地化字符串。
/// </summary>
internal static string RaySource_InterlockNormal {
get {
return ResourceManager.GetString("RaySource_InterlockNormal", resourceCulture);
}
}
/// <summary>
/// 查找类似 最大值 的本地化字符串。
/// </summary>
internal static string RaySource_MaxValueLabel {
get {
return ResourceManager.GetString("RaySource_MaxValueLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 Micro Focus 的本地化字符串。
/// </summary>
internal static string RaySource_MicroFocusButton {
get {
return ResourceManager.GetString("RaySource_MicroFocusButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 最小值 的本地化字符串。
/// </summary>
internal static string RaySource_MinValueLabel {
get {
return ResourceManager.GetString("RaySource_MinValueLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源操作 的本地化字符串。
/// </summary>
internal static string RaySource_OperateWindowTitle {
get {
return ResourceManager.GetString("RaySource_OperateWindowTitle", resourceCulture);
}
}
/// <summary>
/// 查找类似 操作指令已发送,等待设备执行... 的本地化字符串。
/// </summary>
internal static string RaySource_Operation_Sent {
get {
return ResourceManager.GetString("RaySource_Operation_Sent", resourceCulture);
}
}
/// <summary>
/// 查找类似 功率 (W) 的本地化字符串。
/// </summary>
internal static string RaySource_PowerLabel {
get {
return ResourceManager.GetString("RaySource_PowerLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 功率模式: 的本地化字符串。
/// </summary>
internal static string RaySource_PowerModeLabel {
get {
return ResourceManager.GetString("RaySource_PowerModeLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 设置 的本地化字符串。
/// </summary>
internal static string RaySource_SettingsButton {
get {
return ResourceManager.GetString("RaySource_SettingsButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线源类型: 的本地化字符串。
/// </summary>
internal static string RaySource_SourceTypeLabel {
get {
return ResourceManager.GetString("RaySource_SourceTypeLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源启动失败 的本地化字符串。
/// </summary>
internal static string RaySource_StartFailed {
get {
return ResourceManager.GetString("RaySource_StartFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源启动成功 的本地化字符串。
/// </summary>
internal static string RaySource_StartSuccess {
get {
return ResourceManager.GetString("RaySource_StartSuccess", resourceCulture);
}
}
/// <summary>
/// 查找类似 启动: 的本地化字符串。
/// </summary>
internal static string RaySource_StartUpLabel {
get {
return ResourceManager.GetString("RaySource_StartUpLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 错误 的本地化字符串。
/// </summary>
internal static string RaySource_Status_Error {
get {
return ResourceManager.GetString("RaySource_Status_Error", resourceCulture);
}
}
/// <summary>
/// 查找类似 故障 的本地化字符串。
/// </summary>
internal static string RaySource_Status_Fault {
get {
return ResourceManager.GetString("RaySource_Status_Fault", resourceCulture);
}
}
/// <summary>
/// 查找类似 空闲 的本地化字符串。
/// </summary>
internal static string RaySource_Status_Idle {
get {
return ResourceManager.GetString("RaySource_Status_Idle", resourceCulture);
}
}
/// <summary>
/// 查找类似 初始化中 的本地化字符串。
/// </summary>
internal static string RaySource_Status_Initializing {
get {
return ResourceManager.GetString("RaySource_Status_Initializing", resourceCulture);
}
}
/// <summary>
/// 查找类似 就绪 的本地化字符串。
/// </summary>
internal static string RaySource_Status_Ready {
get {
return ResourceManager.GetString("RaySource_Status_Ready", resourceCulture);
}
}
/// <summary>
/// 查找类似 预热中 的本地化字符串。
/// </summary>
internal static string RaySource_Status_Warmup {
get {
return ResourceManager.GetString("RaySource_Status_Warmup", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线开启 的本地化字符串。
/// </summary>
internal static string RaySource_Status_XrayOn {
get {
return ResourceManager.GetString("RaySource_Status_XrayOn", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线源
///已关闭 的本地化字符串。
/// </summary>
internal static string RaySource_StatusClosed {
get {
return ResourceManager.GetString("RaySource_StatusClosed", resourceCulture);
}
}
/// <summary>
/// 查找类似 状态 的本地化字符串。
/// </summary>
internal static string RaySource_StatusLabel {
get {
return ResourceManager.GetString("RaySource_StatusLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线源
///已开启 的本地化字符串。
/// </summary>
internal static string RaySource_StatusOpened {
get {
return ResourceManager.GetString("RaySource_StatusOpened", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线源
///不可用 的本地化字符串。
/// </summary>
internal static string RaySource_StatusUnavailable {
get {
return ResourceManager.GetString("RaySource_StatusUnavailable", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源关闭失败 的本地化字符串。
/// </summary>
internal static string RaySource_StopFailed {
get {
return ResourceManager.GetString("RaySource_StopFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源关闭成功 的本地化字符串。
/// </summary>
internal static string RaySource_StopSuccess {
get {
return ResourceManager.GetString("RaySource_StopSuccess", resourceCulture);
}
}
/// <summary>
/// 查找类似 系统 的本地化字符串。
/// </summary>
internal static string RaySource_SystemButton {
get {
return ResourceManager.GetString("RaySource_SystemButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 温度 (°C) 的本地化字符串。
/// </summary>
internal static string RaySource_TemperatureLabel {
get {
return ResourceManager.GetString("RaySource_TemperatureLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源控制 的本地化字符串。
/// </summary>
internal static string RaySource_Title {
get {
return ResourceManager.GetString("RaySource_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 是否确认执行训机操作? 的本地化字符串。
/// </summary>
internal static string RaySource_Training_Confirm {
get {
return ResourceManager.GetString("RaySource_Training_Confirm", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在执行训机操作,请稍候... 的本地化字符串。
/// </summary>
internal static string RaySource_Training_Message {
get {
return ResourceManager.GetString("RaySource_Training_Message", resourceCulture);
}
}
/// <summary>
/// 查找类似 训机设置 的本地化字符串。
/// </summary>
internal static string RaySource_Training_Title {
get {
return ResourceManager.GetString("RaySource_Training_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 训机设置 的本地化字符串。
/// </summary>
internal static string RaySource_TrainingSettingButton {
get {
return ResourceManager.GetString("RaySource_TrainingSettingButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 关闭射线源 的本地化字符串。
/// </summary>
internal static string RaySource_TurnOffButton {
get {
return ResourceManager.GetString("RaySource_TurnOffButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 开启射线源 的本地化字符串。
/// </summary>
internal static string RaySource_TurnOnButton {
get {
return ResourceManager.GetString("RaySource_TurnOnButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 TXI OFF 的本地化字符串。
/// </summary>
internal static string RaySource_TxiOffButton {
get {
return ResourceManager.GetString("RaySource_TxiOffButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 TXI ON 的本地化字符串。
/// </summary>
internal static string RaySource_TxiOnButton {
get {
return ResourceManager.GetString("RaySource_TxiOnButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 TXI状态: 的本地化字符串。
/// </summary>
internal static string RaySource_TxiStatusLabel {
get {
return ResourceManager.GetString("RaySource_TxiStatusLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 真空度 的本地化字符串。
/// </summary>
internal static string RaySource_VacuumLabel {
get {
return ResourceManager.GetString("RaySource_VacuumLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 变量已连接 的本地化字符串。
/// </summary>
internal static string RaySource_VariablesConnected {
get {
return ResourceManager.GetString("RaySource_VariablesConnected", resourceCulture);
}
}
/// <summary>
/// 查找类似 电压 (kV) 的本地化字符串。
/// </summary>
internal static string RaySource_VoltageLabel {
get {
return ResourceManager.GetString("RaySource_VoltageLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 是否确认执行暖机操作? 的本地化字符串。
/// </summary>
internal static string RaySource_WarmUp_Confirm {
get {
return ResourceManager.GetString("RaySource_WarmUp_Confirm", resourceCulture);
}
}
/// <summary>
/// 查找类似 正在执行暖机操作,请稍候... 的本地化字符串。
/// </summary>
internal static string RaySource_WarmUp_Message {
get {
return ResourceManager.GetString("RaySource_WarmUp_Message", resourceCulture);
}
}
/// <summary>
/// 查找类似 暖机设置 的本地化字符串。
/// </summary>
internal static string RaySource_WarmUp_Title {
get {
return ResourceManager.GetString("RaySource_WarmUp_Title", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源预热完成 的本地化字符串。
/// </summary>
internal static string RaySource_WarmupComplete {
get {
return ResourceManager.GetString("RaySource_WarmupComplete", resourceCulture);
}
}
/// <summary>
/// 查找类似 X射线源预热中... 的本地化字符串。
/// </summary>
internal static string RaySource_WarmupInProgress {
get {
return ResourceManager.GetString("RaySource_WarmupInProgress", resourceCulture);
}
}
/// <summary>
/// 查找类似 暖机: 的本地化字符串。
/// </summary>
internal static string RaySource_WarmUpLabel {
get {
return ResourceManager.GetString("RaySource_WarmUpLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 暖机设置 的本地化字符串。
/// </summary>
internal static string RaySource_WarmUpSettingButton {
get {
return ResourceManager.GetString("RaySource_WarmUpSettingButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 温度过高警告 的本地化字符串。
/// </summary>
internal static string RaySource_Warning_HighTemperature {
get {
return ResourceManager.GetString("RaySource_Warning_HighTemperature", resourceCulture);
}
}
/// <summary>
/// 查找类似 真空度过低警告 的本地化字符串。
/// </summary>
internal static string RaySource_Warning_LowVacuum {
get {
return ResourceManager.GetString("RaySource_Warning_LowVacuum", resourceCulture);
}
}
/// <summary>
/// 查找类似 看门狗: 的本地化字符串。
/// </summary>
internal static string RaySource_WatchdogLabel {
get {
return ResourceManager.GetString("RaySource_WatchdogLabel", resourceCulture);
}
}
/// <summary>
/// 查找类似 关闭 的本地化字符串。
/// </summary>
internal static string RaySource_XRayOff {
get {
return ResourceManager.GetString("RaySource_XRayOff", resourceCulture);
}
}
/// <summary>
/// 查找类似 开启 的本地化字符串。
/// </summary>
internal static string RaySource_XRayOn {
get {
return ResourceManager.GetString("RaySource_XRayOn", resourceCulture);
}
}
/// <summary>
/// 查找类似 射线状态: 的本地化字符串。
/// </summary>
internal static string RaySource_XRayOnLabel {
get {
return ResourceManager.GetString("RaySource_XRayOnLabel", resourceCulture);
}
}
}
}
@@ -0,0 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="RaySource_Title" xml:space="preserve">
<value>X-Ray Source Control</value>
</data>
<data name="RaySource_OperateWindowTitle" xml:space="preserve">
<value>X-Ray Source Operation</value>
</data>
<data name="RaySource_StartSuccess" xml:space="preserve">
<value>X-Ray source started successfully</value>
</data>
<data name="RaySource_StartFailed" xml:space="preserve">
<value>X-Ray source start failed</value>
</data>
<data name="RaySource_StopSuccess" xml:space="preserve">
<value>X-Ray source stopped successfully</value>
</data>
<data name="RaySource_StopFailed" xml:space="preserve">
<value>X-Ray source stop failed</value>
</data>
<data name="RaySource_InitSuccess" xml:space="preserve">
<value>X-Ray source initialized successfully</value>
</data>
<data name="RaySource_InitFailed" xml:space="preserve">
<value>X-Ray source initialization failed</value>
</data>
<data name="RaySource_WarmupInProgress" xml:space="preserve">
<value>X-Ray source warming up...</value>
</data>
<data name="RaySource_WarmupComplete" xml:space="preserve">
<value>X-Ray source warmup complete</value>
</data>
<data name="RaySource_EmergencyStop" xml:space="preserve">
<value>X-Ray source emergency stop</value>
</data>
<data name="RaySource_VoltageLabel" xml:space="preserve">
<value>Voltage (kV)</value>
</data>
<data name="RaySource_CurrentLabel" xml:space="preserve">
<value>Current (μA)</value>
</data>
<data name="RaySource_StatusLabel" xml:space="preserve">
<value>Status</value>
</data>
<data name="RaySource_PowerLabel" xml:space="preserve">
<value>Power (W)</value>
</data>
<data name="RaySource_TemperatureLabel" xml:space="preserve">
<value>Temperature (°C)</value>
</data>
<data name="RaySource_VacuumLabel" xml:space="preserve">
<value>Vacuum</value>
</data>
<data name="RaySource_Status_Idle" xml:space="preserve">
<value>Idle</value>
</data>
<data name="RaySource_Status_Initializing" xml:space="preserve">
<value>Initializing</value>
</data>
<data name="RaySource_Status_Warmup" xml:space="preserve">
<value>Warming Up</value>
</data>
<data name="RaySource_Status_Ready" xml:space="preserve">
<value>Ready</value>
</data>
<data name="RaySource_Status_XrayOn" xml:space="preserve">
<value>X-Ray On</value>
</data>
<data name="RaySource_Status_Error" xml:space="preserve">
<value>Error</value>
</data>
<data name="RaySource_Status_Fault" xml:space="preserve">
<value>Fault</value>
</data>
<data name="RaySource_Error_VoltageOutOfRange" xml:space="preserve">
<value>Voltage out of range (20-225 kV)</value>
</data>
<data name="RaySource_Error_CurrentOutOfRange" xml:space="preserve">
<value>Current out of range (10-1000 μA)</value>
</data>
<data name="RaySource_Error_CannotAdjustWhileXrayOn" xml:space="preserve">
<value>Cannot adjust parameters while X-Ray is on</value>
</data>
<data name="RaySource_Warning_HighTemperature" xml:space="preserve">
<value>High temperature warning</value>
</data>
<data name="RaySource_Warning_LowVacuum" xml:space="preserve">
<value>Low vacuum warning</value>
</data>
<data name="RaySource_ActualValueLabel" xml:space="preserve">
<value>Actual {0}</value>
</data>
<data name="RaySource_MinValueLabel" xml:space="preserve">
<value>Min</value>
</data>
<data name="RaySource_MaxValueLabel" xml:space="preserve">
<value>Max</value>
</data>
<data name="RaySource_TurnOnButton" xml:space="preserve">
<value>Turn On X-Ray</value>
</data>
<data name="RaySource_TurnOffButton" xml:space="preserve">
<value>Turn Off X-Ray</value>
</data>
<data name="RaySource_SettingsButton" xml:space="preserve">
<value>Settings</value>
</data>
<data name="RaySource_AdvanceButton" xml:space="preserve">
<value>Advance</value>
</data>
<data name="RaySource_SystemButton" xml:space="preserve">
<value>System</value>
</data>
<data name="RaySource_StatusClosed" xml:space="preserve">
<value>X-Ray
Closed</value>
</data>
<data name="RaySource_StatusOpened" xml:space="preserve">
<value>X-Ray
Opened</value>
</data>
<data name="RaySource_StatusUnavailable" xml:space="preserve">
<value>X-Ray
Unavailable</value>
</data>
<data name="RaySource_ConnectButton" xml:space="preserve">
<value>Connect Ray Source</value>
</data>
<data name="RaySource_DisconnectButton" xml:space="preserve">
<value>Disconnect Ray Source</value>
</data>
<data name="RaySource_ConnectTooltip" xml:space="preserve">
<value>Connect ray source device</value>
</data>
<data name="RaySource_DisconnectTooltip" xml:space="preserve">
<value>Disconnect ray source device</value>
</data>
<data name="RaySource_ConnectVariablesButton" xml:space="preserve">
<value>Connect Variables</value>
</data>
<data name="RaySource_InitializeButton" xml:space="preserve">
<value>Initialize</value>
</data>
<data name="RaySource_SourceTypeLabel" xml:space="preserve">
<value>Source Type:</value>
</data>
<data name="RaySource_Connected" xml:space="preserve">
<value>Connected</value>
</data>
<data name="RaySource_Disconnected" xml:space="preserve">
<value>Disconnected</value>
</data>
<data name="RaySource_VariablesConnected" xml:space="preserve">
<value>Variables Connected</value>
</data>
<data name="RaySource_ConfigButton" xml:space="preserve">
<value>Config</value>
</data>
<data name="RaySource_ConfigWindowTitle" xml:space="preserve">
<value>X-Ray Source Configuration</value>
</data>
<data name="RaySource_WarmUpLabel" xml:space="preserve">
<value>Warm-Up:</value>
</data>
<data name="RaySource_StartUpLabel" xml:space="preserve">
<value>Startup:</value>
</data>
<data name="RaySource_AutoCenterLabel" xml:space="preserve">
<value>Auto-Center:</value>
</data>
<data name="RaySource_FilamentLabel" xml:space="preserve">
<value>Filament Adj:</value>
</data>
<data name="RaySource_PowerModeLabel" xml:space="preserve">
<value>Power Mode:</value>
</data>
<data name="RaySource_InterlockLabel" xml:space="preserve">
<value>Interlock:</value>
</data>
<data name="RaySource_WatchdogLabel" xml:space="preserve">
<value>Watchdog:</value>
</data>
<data name="RaySource_InterlockActive" xml:space="preserve">
<value>Active</value>
</data>
<data name="RaySource_InterlockNormal" xml:space="preserve">
<value>Normal</value>
</data>
<data name="RaySource_XRayOnLabel" xml:space="preserve">
<value>X-Ray:</value>
</data>
<data name="RaySource_XRayOn" xml:space="preserve">
<value>On</value>
</data>
<data name="RaySource_XRayOff" xml:space="preserve">
<value>Off</value>
</data>
<data name="RaySource_TxiStatusLabel" xml:space="preserve">
<value>TXI Status:</value>
</data>
<data name="RaySource_TxiOnButton" xml:space="preserve">
<value>TXI ON</value>
</data>
<data name="RaySource_TxiOffButton" xml:space="preserve">
<value>TXI OFF</value>
</data>
<data name="RaySource_HighPowerButton" xml:space="preserve">
<value>High Power</value>
</data>
<data name="RaySource_MicroFocusButton" xml:space="preserve">
<value>Micro Focus</value>
</data>
<data name="RaySource_WarmUpSettingButton" xml:space="preserve">
<value>Warm-Up Setting</value>
</data>
<data name="RaySource_TrainingSettingButton" xml:space="preserve">
<value>Training Setting</value>
</data>
<data name="RaySource_FilamentCalibrationButton" xml:space="preserve">
<value>Filament Calibration</value>
</data>
<data name="RaySource_AutoCenterSettingButton" xml:space="preserve">
<value>Auto-Center</value>
</data>
<data name="RaySource_FilamentLifetimeLabel" xml:space="preserve">
<value>Filament Lifetime:</value>
</data>
<data name="RaySource_FilamentLifetimeWarningTitle" xml:space="preserve">
<value>Filament Lifetime Warning</value>
</data>
<data name="RaySource_FilamentLifetimeWarningMessage" xml:space="preserve">
<value>Filament has been used for {0} hours, lifetime threshold is {1} hours, {2}% used. Please replace the filament as soon as possible.</value>
</data>
<data name="RaySource_FilamentLifetimeWarningConfirm" xml:space="preserve">
<value>OK</value>
</data>
<data name="RaySource_Confirm_Title" xml:space="preserve">
<value>Confirmation</value>
</data>
<data name="RaySource_WarmUp_Confirm" xml:space="preserve">
<value>Are you sure you want to perform the warm-up operation?</value>
</data>
<data name="RaySource_WarmUp_Title" xml:space="preserve">
<value>Warm-Up Setting</value>
</data>
<data name="RaySource_WarmUp_Message" xml:space="preserve">
<value>Performing warm-up operation, please wait...</value>
</data>
<data name="RaySource_Training_Confirm" xml:space="preserve">
<value>Are you sure you want to perform the training operation?</value>
</data>
<data name="RaySource_Training_Title" xml:space="preserve">
<value>Training Setting</value>
</data>
<data name="RaySource_Training_Message" xml:space="preserve">
<value>Performing training operation, please wait...</value>
</data>
<data name="RaySource_FilamentCalibration_Confirm" xml:space="preserve">
<value>Are you sure you want to perform the filament calibration?</value>
</data>
<data name="RaySource_FilamentCalibration_Title" xml:space="preserve">
<value>Filament Calibration</value>
</data>
<data name="RaySource_FilamentCalibration_Message" xml:space="preserve">
<value>Performing filament calibration, please wait...</value>
</data>
<data name="RaySource_AutoCenter_Confirm" xml:space="preserve">
<value>Are you sure you want to perform the full voltage auto-center operation?</value>
</data>
<data name="RaySource_AutoCenter_Title" xml:space="preserve">
<value>Auto-Center</value>
</data>
<data name="RaySource_AutoCenter_Message" xml:space="preserve">
<value>Performing full voltage auto-center operation, please wait...</value>
</data>
<data name="RaySource_Operation_Sent" xml:space="preserve">
<value>Operation command sent, waiting for device execution...</value>
</data>
</root>
@@ -0,0 +1,386 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="RaySource_Title" xml:space="preserve">
<value>X射线源控制</value>
</data>
<data name="RaySource_OperateWindowTitle" xml:space="preserve">
<value>X射线源操作</value>
<comment>RaySourceOperateWindow - 窗口标题 | Window title</comment>
</data>
<data name="RaySource_StartSuccess" xml:space="preserve">
<value>X射线源启动成功</value>
</data>
<data name="RaySource_StartFailed" xml:space="preserve">
<value>X射线源启动失败</value>
</data>
<data name="RaySource_StopSuccess" xml:space="preserve">
<value>X射线源关闭成功</value>
</data>
<data name="RaySource_StopFailed" xml:space="preserve">
<value>X射线源关闭失败</value>
</data>
<data name="RaySource_InitSuccess" xml:space="preserve">
<value>X射线源初始化成功</value>
</data>
<data name="RaySource_InitFailed" xml:space="preserve">
<value>X射线源初始化失败</value>
</data>
<data name="RaySource_WarmupInProgress" xml:space="preserve">
<value>X射线源预热中...</value>
</data>
<data name="RaySource_WarmupComplete" xml:space="preserve">
<value>X射线源预热完成</value>
</data>
<data name="RaySource_EmergencyStop" xml:space="preserve">
<value>X射线源紧急停止</value>
</data>
<data name="RaySource_VoltageLabel" xml:space="preserve">
<value>电压 (kV)</value>
</data>
<data name="RaySource_CurrentLabel" xml:space="preserve">
<value>电流 (μA)</value>
</data>
<data name="RaySource_StatusLabel" xml:space="preserve">
<value>状态</value>
</data>
<data name="RaySource_PowerLabel" xml:space="preserve">
<value>功率 (W)</value>
</data>
<data name="RaySource_TemperatureLabel" xml:space="preserve">
<value>温度 (°C)</value>
</data>
<data name="RaySource_VacuumLabel" xml:space="preserve">
<value>真空度</value>
</data>
<data name="RaySource_Status_Idle" xml:space="preserve">
<value>空闲</value>
</data>
<data name="RaySource_Status_Initializing" xml:space="preserve">
<value>初始化中</value>
</data>
<data name="RaySource_Status_Warmup" xml:space="preserve">
<value>预热中</value>
</data>
<data name="RaySource_Status_Ready" xml:space="preserve">
<value>就绪</value>
</data>
<data name="RaySource_Status_XrayOn" xml:space="preserve">
<value>射线开启</value>
</data>
<data name="RaySource_Status_Error" xml:space="preserve">
<value>错误</value>
</data>
<data name="RaySource_Status_Fault" xml:space="preserve">
<value>故障</value>
</data>
<data name="RaySource_Error_VoltageOutOfRange" xml:space="preserve">
<value>电压超出范围 (20-225 kV)</value>
</data>
<data name="RaySource_Error_CurrentOutOfRange" xml:space="preserve">
<value>电流超出范围 (10-1000 μA)</value>
</data>
<data name="RaySource_Error_CannotAdjustWhileXrayOn" xml:space="preserve">
<value>射线开启时无法调整参数</value>
</data>
<data name="RaySource_Warning_HighTemperature" xml:space="preserve">
<value>温度过高警告</value>
</data>
<data name="RaySource_Warning_LowVacuum" xml:space="preserve">
<value>真空度过低警告</value>
</data>
<data name="RaySource_ActualValueLabel" xml:space="preserve">
<value>实际 {0}</value>
<comment>RaySourceOperateView - 实际值标签({0}为数值)| Actual value label ({0} is the value)</comment>
</data>
<data name="RaySource_MinValueLabel" xml:space="preserve">
<value>最小值</value>
<comment>RaySourceOperateView - 最小值标签 | Minimum value label</comment>
</data>
<data name="RaySource_MaxValueLabel" xml:space="preserve">
<value>最大值</value>
<comment>RaySourceOperateView - 最大值标签 | Maximum value label</comment>
</data>
<data name="RaySource_TurnOnButton" xml:space="preserve">
<value>开启射线源</value>
<comment>RaySourceOperateView - 开启射线源按钮 | Turn on X-Ray button</comment>
</data>
<data name="RaySource_TurnOffButton" xml:space="preserve">
<value>关闭射线源</value>
<comment>RaySourceOperateView - 关闭射线源按钮 | Turn off X-Ray button</comment>
</data>
<data name="RaySource_SettingsButton" xml:space="preserve">
<value>设置</value>
<comment>RaySourceOperateView - 设置按钮 | Settings button</comment>
</data>
<data name="RaySource_AdvanceButton" xml:space="preserve">
<value>高级</value>
<comment>RaySourceOperateView - 高级按钮 | Advance button</comment>
</data>
<data name="RaySource_SystemButton" xml:space="preserve">
<value>系统</value>
<comment>RaySourceOperateView - 系统按钮 | System button</comment>
</data>
<data name="RaySource_StatusClosed" xml:space="preserve">
<value>射线源
已关闭</value>
<comment>RaySourceOperateView - 射线源关闭状态文本 | X-Ray closed status text</comment>
</data>
<data name="RaySource_StatusOpened" xml:space="preserve">
<value>射线源
已开启</value>
<comment>RaySourceOperateView - 射线源开启状态文本 | X-Ray opened status text</comment>
</data>
<data name="RaySource_StatusUnavailable" xml:space="preserve">
<value>射线源
不可用</value>
<comment>RaySourceOperateView - 射线源不可用状态文本 | X-Ray unavailable status text</comment>
</data>
<data name="RaySource_ConnectButton" xml:space="preserve">
<value>连接射线源</value>
<comment>RaySourceOperateWindow - 连接射线源按钮 | Connect ray source button</comment>
</data>
<data name="RaySource_DisconnectButton" xml:space="preserve">
<value>断开射线源</value>
<comment>RaySourceOperateWindow - 断开射线源按钮 | Disconnect ray source button</comment>
</data>
<data name="RaySource_ConnectTooltip" xml:space="preserve">
<value>连接射线源设备</value>
<comment>RaySourceOperateWindow - 连接射线源提示 | Connect ray source tooltip</comment>
</data>
<data name="RaySource_DisconnectTooltip" xml:space="preserve">
<value>断开射线源设备</value>
<comment>RaySourceOperateWindow - 断开射线源提示 | Disconnect ray source tooltip</comment>
</data>
<data name="RaySource_ConnectVariablesButton" xml:space="preserve">
<value>连接变量</value>
<comment>RaySourceOperateView - 连接 PVI 变量按钮 | Connect PVI variables button</comment>
</data>
<data name="RaySource_InitializeButton" xml:space="preserve">
<value>初始化</value>
<comment>RaySourceConfigView - 初始化射线源按钮 | Initialize ray source button</comment>
</data>
<data name="RaySource_SourceTypeLabel" xml:space="preserve">
<value>射线源类型:</value>
<comment>RaySourceConfigView - 射线源类型标签 | Ray source type label</comment>
</data>
<data name="RaySource_Connected" xml:space="preserve">
<value>已连接</value>
<comment>RaySourceConfigView - 已连接状态文本 | Connected status text</comment>
</data>
<data name="RaySource_Disconnected" xml:space="preserve">
<value>未连接</value>
<comment>RaySourceConfigView - 未连接状态文本 | Disconnected status text</comment>
</data>
<data name="RaySource_VariablesConnected" xml:space="preserve">
<value>变量已连接</value>
<comment>RaySourceConfigView - 变量已连接状态文本 | Variables connected status text</comment>
</data>
<data name="RaySource_ConfigButton" xml:space="preserve">
<value>配置</value>
<comment>RaySourceOperateView - 配置按钮 | Config button</comment>
</data>
<data name="RaySource_ConfigWindowTitle" xml:space="preserve">
<value>射线源配置</value>
<comment>RaySourceConfigWindow - 窗口标题 | Window title</comment>
</data>
<data name="RaySource_WarmUpLabel" xml:space="preserve">
<value>暖机:</value>
<comment>RaySourceConfigView - 暖机状态标签 | Warm-up status label</comment>
</data>
<data name="RaySource_StartUpLabel" xml:space="preserve">
<value>启动:</value>
<comment>RaySourceConfigView - 启动状态标签 | Startup status label</comment>
</data>
<data name="RaySource_AutoCenterLabel" xml:space="preserve">
<value>自动定心:</value>
<comment>RaySourceConfigView - 自动定心状态标签 | Auto-center status label</comment>
</data>
<data name="RaySource_FilamentLabel" xml:space="preserve">
<value>灯丝校准:</value>
<comment>RaySourceConfigView - 灯丝校准状态标签 | Filament adjust status label</comment>
</data>
<data name="RaySource_PowerModeLabel" xml:space="preserve">
<value>功率模式:</value>
<comment>RaySourceConfigView - 功率模式标签 | Power mode label</comment>
</data>
<data name="RaySource_InterlockLabel" xml:space="preserve">
<value>连锁:</value>
<comment>RaySourceConfigView - 连锁状态标签 | Interlock status label</comment>
</data>
<data name="RaySource_WatchdogLabel" xml:space="preserve">
<value>看门狗:</value>
<comment>RaySourceConfigView - 看门狗状态标签 | Watchdog status label</comment>
</data>
<data name="RaySource_InterlockActive" xml:space="preserve">
<value>激活</value>
<comment>RaySourceConfigView - 连锁激活状态 | Interlock active status</comment>
</data>
<data name="RaySource_InterlockNormal" xml:space="preserve">
<value>正常</value>
<comment>RaySourceConfigView - 连锁正常状态 | Interlock normal status</comment>
</data>
<data name="RaySource_XRayOnLabel" xml:space="preserve">
<value>射线状态:</value>
<comment>RaySourceConfigView - 射线开启状态标签 | X-ray on status label</comment>
</data>
<data name="RaySource_XRayOn" xml:space="preserve">
<value>开启</value>
<comment>RaySourceConfigView - 射线已开启 | X-ray on</comment>
</data>
<data name="RaySource_XRayOff" xml:space="preserve">
<value>关闭</value>
<comment>RaySourceConfigView - 射线已关闭 | X-ray off</comment>
</data>
<data name="RaySource_TxiStatusLabel" xml:space="preserve">
<value>TXI状态:</value>
<comment>RaySourceConfigView - TXI状态标签 | TXI status label</comment>
</data>
<data name="RaySource_TxiOnButton" xml:space="preserve">
<value>TXI ON</value>
<comment>RaySourceConfigView - TXI开启按钮 | TXI on button</comment>
</data>
<data name="RaySource_TxiOffButton" xml:space="preserve">
<value>TXI OFF</value>
<comment>RaySourceConfigView - TXI关闭按钮 | TXI off button</comment>
</data>
<data name="RaySource_HighPowerButton" xml:space="preserve">
<value>High Power</value>
<comment>RaySourceConfigView - 高功率模式按钮 | High power mode button</comment>
</data>
<data name="RaySource_MicroFocusButton" xml:space="preserve">
<value>Micro Focus</value>
<comment>RaySourceConfigView - 微焦点模式按钮 | Micro focus mode button</comment>
</data>
<data name="RaySource_WarmUpSettingButton" xml:space="preserve">
<value>暖机设置</value>
<comment>RaySourceConfigView - 暖机设置按钮 | Warm-up setting button</comment>
</data>
<data name="RaySource_TrainingSettingButton" xml:space="preserve">
<value>训机设置</value>
<comment>RaySourceConfigView - 训机设置按钮 | Training setting button</comment>
</data>
<data name="RaySource_FilamentCalibrationButton" xml:space="preserve">
<value>灯丝校准</value>
<comment>RaySourceConfigView - 灯丝校准按钮 | Filament calibration button</comment>
</data>
<data name="RaySource_AutoCenterSettingButton" xml:space="preserve">
<value>自动定心</value>
<comment>RaySourceConfigView - 自动定心按钮 | Auto-center setting button</comment>
</data>
<data name="RaySource_FilamentLifetimeLabel" xml:space="preserve">
<value>灯丝寿命:</value>
<comment>RaySourceConfigView - 灯丝寿命标签 | Filament lifetime label</comment>
</data>
<data name="RaySource_FilamentLifetimeWarningTitle" xml:space="preserve">
<value>灯丝寿命预警</value>
<comment>灯丝寿命预警弹窗标题 | Filament lifetime warning dialog title</comment>
</data>
<data name="RaySource_FilamentLifetimeWarningMessage" xml:space="preserve">
<value>灯丝累计使用 {0} 小时,寿命阈值 {1} 小时,已使用 {2}%。建议尽快更换灯丝。</value>
<comment>灯丝寿命预警弹窗内容({0}=累计时长,{1}=阈值,{2}=百分比)| Filament lifetime warning message</comment>
</data>
<data name="RaySource_FilamentLifetimeWarningConfirm" xml:space="preserve">
<value>确认</value>
<comment>灯丝寿命预警弹窗确认按钮 | Filament lifetime warning confirm button</comment>
</data>
<data name="RaySource_Confirm_Title" xml:space="preserve">
<value>确认</value>
</data>
<data name="RaySource_WarmUp_Confirm" xml:space="preserve">
<value>是否确认执行暖机操作?</value>
</data>
<data name="RaySource_WarmUp_Title" xml:space="preserve">
<value>暖机设置</value>
</data>
<data name="RaySource_WarmUp_Message" xml:space="preserve">
<value>正在执行暖机操作,请稍候...</value>
</data>
<data name="RaySource_Training_Confirm" xml:space="preserve">
<value>是否确认执行训机操作?</value>
</data>
<data name="RaySource_Training_Title" xml:space="preserve">
<value>训机设置</value>
</data>
<data name="RaySource_Training_Message" xml:space="preserve">
<value>正在执行训机操作,请稍候...</value>
</data>
<data name="RaySource_FilamentCalibration_Confirm" xml:space="preserve">
<value>是否确认执行灯丝校准操作?</value>
</data>
<data name="RaySource_FilamentCalibration_Title" xml:space="preserve">
<value>正在执行灯丝校准操作,请稍候...</value>
</data>
<data name="RaySource_FilamentCalibration_Message" xml:space="preserve">
<value>正在执行灯丝校准操作,请稍候...</value>
</data>
<data name="RaySource_AutoCenter_Confirm" xml:space="preserve">
<value>是否确认执行全部电压自动定心操作?</value>
</data>
<data name="RaySource_AutoCenter_Title" xml:space="preserve">
<value>自动定心</value>
</data>
<data name="RaySource_AutoCenter_Message" xml:space="preserve">
<value>正在执行全部电压自动定心操作,请稍候...</value>
</data>
<data name="RaySource_Operation_Sent" xml:space="preserve">
<value>操作指令已发送,等待设备执行...</value>
</data>
</root>
@@ -0,0 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="RaySource_Title" xml:space="preserve">
<value>X射线源控制</value>
</data>
<data name="RaySource_OperateWindowTitle" xml:space="preserve">
<value>X射线源操作</value>
</data>
<data name="RaySource_StartSuccess" xml:space="preserve">
<value>X射线源启动成功</value>
</data>
<data name="RaySource_StartFailed" xml:space="preserve">
<value>X射线源启动失败</value>
</data>
<data name="RaySource_StopSuccess" xml:space="preserve">
<value>X射线源关闭成功</value>
</data>
<data name="RaySource_StopFailed" xml:space="preserve">
<value>X射线源关闭失败</value>
</data>
<data name="RaySource_InitSuccess" xml:space="preserve">
<value>X射线源初始化成功</value>
</data>
<data name="RaySource_InitFailed" xml:space="preserve">
<value>X射线源初始化失败</value>
</data>
<data name="RaySource_WarmupInProgress" xml:space="preserve">
<value>X射线源预热中...</value>
</data>
<data name="RaySource_WarmupComplete" xml:space="preserve">
<value>X射线源预热完成</value>
</data>
<data name="RaySource_EmergencyStop" xml:space="preserve">
<value>X射线源紧急停止</value>
</data>
<data name="RaySource_VoltageLabel" xml:space="preserve">
<value>电压 (kV)</value>
</data>
<data name="RaySource_CurrentLabel" xml:space="preserve">
<value>电流 (μA)</value>
</data>
<data name="RaySource_StatusLabel" xml:space="preserve">
<value>状态</value>
</data>
<data name="RaySource_PowerLabel" xml:space="preserve">
<value>功率 (W)</value>
</data>
<data name="RaySource_TemperatureLabel" xml:space="preserve">
<value>温度 (°C)</value>
</data>
<data name="RaySource_VacuumLabel" xml:space="preserve">
<value>真空度</value>
</data>
<data name="RaySource_Status_Idle" xml:space="preserve">
<value>空闲</value>
</data>
<data name="RaySource_Status_Initializing" xml:space="preserve">
<value>初始化中</value>
</data>
<data name="RaySource_Status_Warmup" xml:space="preserve">
<value>预热中</value>
</data>
<data name="RaySource_Status_Ready" xml:space="preserve">
<value>就绪</value>
</data>
<data name="RaySource_Status_XrayOn" xml:space="preserve">
<value>射线开启</value>
</data>
<data name="RaySource_Status_Error" xml:space="preserve">
<value>错误</value>
</data>
<data name="RaySource_Status_Fault" xml:space="preserve">
<value>故障</value>
</data>
<data name="RaySource_Error_VoltageOutOfRange" xml:space="preserve">
<value>电压超出范围 (20-225 kV)</value>
</data>
<data name="RaySource_Error_CurrentOutOfRange" xml:space="preserve">
<value>电流超出范围 (10-1000 μA)</value>
</data>
<data name="RaySource_Error_CannotAdjustWhileXrayOn" xml:space="preserve">
<value>射线开启时无法调整参数</value>
</data>
<data name="RaySource_Warning_HighTemperature" xml:space="preserve">
<value>温度过高警告</value>
</data>
<data name="RaySource_Warning_LowVacuum" xml:space="preserve">
<value>真空度过低警告</value>
</data>
<data name="RaySource_ActualValueLabel" xml:space="preserve">
<value>实际 {0}</value>
</data>
<data name="RaySource_MinValueLabel" xml:space="preserve">
<value>最小值</value>
</data>
<data name="RaySource_MaxValueLabel" xml:space="preserve">
<value>最大值</value>
</data>
<data name="RaySource_TurnOnButton" xml:space="preserve">
<value>开启射线源</value>
</data>
<data name="RaySource_TurnOffButton" xml:space="preserve">
<value>关闭射线源</value>
</data>
<data name="RaySource_SettingsButton" xml:space="preserve">
<value>设置</value>
</data>
<data name="RaySource_AdvanceButton" xml:space="preserve">
<value>高级</value>
</data>
<data name="RaySource_SystemButton" xml:space="preserve">
<value>系统</value>
</data>
<data name="RaySource_StatusClosed" xml:space="preserve">
<value>射线源
已关闭</value>
</data>
<data name="RaySource_StatusOpened" xml:space="preserve">
<value>射线源
已开启</value>
</data>
<data name="RaySource_StatusUnavailable" xml:space="preserve">
<value>射线源
不可用</value>
</data>
<data name="RaySource_ConnectButton" xml:space="preserve">
<value>连接射线源</value>
</data>
<data name="RaySource_DisconnectButton" xml:space="preserve">
<value>断开射线源</value>
</data>
<data name="RaySource_ConnectTooltip" xml:space="preserve">
<value>连接射线源设备</value>
</data>
<data name="RaySource_DisconnectTooltip" xml:space="preserve">
<value>断开射线源设备</value>
</data>
<data name="RaySource_ConnectVariablesButton" xml:space="preserve">
<value>连接变量</value>
</data>
<data name="RaySource_InitializeButton" xml:space="preserve">
<value>初始化</value>
</data>
<data name="RaySource_SourceTypeLabel" xml:space="preserve">
<value>射线源类型:</value>
</data>
<data name="RaySource_Connected" xml:space="preserve">
<value>已连接</value>
</data>
<data name="RaySource_Disconnected" xml:space="preserve">
<value>未连接</value>
</data>
<data name="RaySource_VariablesConnected" xml:space="preserve">
<value>变量已连接</value>
</data>
<data name="RaySource_ConfigButton" xml:space="preserve">
<value>配置</value>
</data>
<data name="RaySource_ConfigWindowTitle" xml:space="preserve">
<value>射线源配置</value>
</data>
<data name="RaySource_WarmUpLabel" xml:space="preserve">
<value>暖机:</value>
</data>
<data name="RaySource_StartUpLabel" xml:space="preserve">
<value>启动:</value>
</data>
<data name="RaySource_AutoCenterLabel" xml:space="preserve">
<value>自动定心:</value>
</data>
<data name="RaySource_FilamentLabel" xml:space="preserve">
<value>灯丝校准:</value>
</data>
<data name="RaySource_PowerModeLabel" xml:space="preserve">
<value>功率模式:</value>
</data>
<data name="RaySource_InterlockLabel" xml:space="preserve">
<value>连锁:</value>
</data>
<data name="RaySource_WatchdogLabel" xml:space="preserve">
<value>看门狗:</value>
</data>
<data name="RaySource_InterlockActive" xml:space="preserve">
<value>激活</value>
</data>
<data name="RaySource_InterlockNormal" xml:space="preserve">
<value>正常</value>
</data>
<data name="RaySource_XRayOnLabel" xml:space="preserve">
<value>射线状态:</value>
</data>
<data name="RaySource_XRayOn" xml:space="preserve">
<value>开启</value>
</data>
<data name="RaySource_XRayOff" xml:space="preserve">
<value>关闭</value>
</data>
<data name="RaySource_TxiStatusLabel" xml:space="preserve">
<value>TXI状态:</value>
</data>
<data name="RaySource_TxiOnButton" xml:space="preserve">
<value>TXI ON</value>
</data>
<data name="RaySource_TxiOffButton" xml:space="preserve">
<value>TXI OFF</value>
</data>
<data name="RaySource_HighPowerButton" xml:space="preserve">
<value>High Power</value>
</data>
<data name="RaySource_MicroFocusButton" xml:space="preserve">
<value>Micro Focus</value>
</data>
<data name="RaySource_WarmUpSettingButton" xml:space="preserve">
<value>暖机设置</value>
</data>
<data name="RaySource_TrainingSettingButton" xml:space="preserve">
<value>训机设置</value>
</data>
<data name="RaySource_FilamentCalibrationButton" xml:space="preserve">
<value>灯丝校准</value>
</data>
<data name="RaySource_AutoCenterSettingButton" xml:space="preserve">
<value>自动定心</value>
</data>
<data name="RaySource_FilamentLifetimeLabel" xml:space="preserve">
<value>灯丝寿命:</value>
</data>
<data name="RaySource_FilamentLifetimeWarningTitle" xml:space="preserve">
<value>灯丝寿命预警</value>
</data>
<data name="RaySource_FilamentLifetimeWarningMessage" xml:space="preserve">
<value>灯丝累计使用 {0} 小时,寿命阈值 {1} 小时,已使用 {2}%。建议尽快更换灯丝。</value>
</data>
<data name="RaySource_FilamentLifetimeWarningConfirm" xml:space="preserve">
<value>确认</value>
</data>
<data name="RaySource_Confirm_Title" xml:space="preserve">
<value>确认</value>
</data>
<data name="RaySource_WarmUp_Confirm" xml:space="preserve">
<value>是否确认执行暖机操作?</value>
</data>
<data name="RaySource_WarmUp_Title" xml:space="preserve">
<value>暖机设置</value>
</data>
<data name="RaySource_WarmUp_Message" xml:space="preserve">
<value>正在执行暖机操作,请稍候...</value>
</data>
<data name="RaySource_Training_Confirm" xml:space="preserve">
<value>是否确认执行训机操作?</value>
</data>
<data name="RaySource_Training_Title" xml:space="preserve">
<value>训机设置</value>
</data>
<data name="RaySource_Training_Message" xml:space="preserve">
<value>正在执行训机操作,请稍候...</value>
</data>
<data name="RaySource_FilamentCalibration_Confirm" xml:space="preserve">
<value>是否确认执行灯丝校准操作?</value>
</data>
<data name="RaySource_FilamentCalibration_Title" xml:space="preserve">
<value>灯丝校准</value>
</data>
<data name="RaySource_FilamentCalibration_Message" xml:space="preserve">
<value>正在执行灯丝校准操作,请稍候...</value>
</data>
<data name="RaySource_AutoCenter_Confirm" xml:space="preserve">
<value>是否确认执行全部电压自动定心操作?</value>
</data>
<data name="RaySource_AutoCenter_Title" xml:space="preserve">
<value>自动定心</value>
</data>
<data name="RaySource_AutoCenter_Message" xml:space="preserve">
<value>正在执行全部电压自动定心操作,请稍候...</value>
</data>
<data name="RaySource_Operation_Sent" xml:space="preserve">
<value>操作指令已发送,等待设备执行...</value>
</data>
</root>
@@ -0,0 +1,337 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="RaySource_Title" xml:space="preserve">
<value>X射線源控制</value>
</data>
<data name="RaySource_OperateWindowTitle" xml:space="preserve">
<value>X射線源操作</value>
</data>
<data name="RaySource_StartSuccess" xml:space="preserve">
<value>X射線源啟動成功</value>
</data>
<data name="RaySource_StartFailed" xml:space="preserve">
<value>X射線源啟動失敗</value>
</data>
<data name="RaySource_StopSuccess" xml:space="preserve">
<value>X射線源關閉成功</value>
</data>
<data name="RaySource_StopFailed" xml:space="preserve">
<value>X射線源關閉失敗</value>
</data>
<data name="RaySource_InitSuccess" xml:space="preserve">
<value>X射線源初始化成功</value>
</data>
<data name="RaySource_InitFailed" xml:space="preserve">
<value>X射線源初始化失敗</value>
</data>
<data name="RaySource_WarmupInProgress" xml:space="preserve">
<value>X射線源預熱中...</value>
</data>
<data name="RaySource_WarmupComplete" xml:space="preserve">
<value>X射線源預熱完成</value>
</data>
<data name="RaySource_EmergencyStop" xml:space="preserve">
<value>X射線源緊急停止</value>
</data>
<data name="RaySource_VoltageLabel" xml:space="preserve">
<value>電壓 (kV)</value>
</data>
<data name="RaySource_CurrentLabel" xml:space="preserve">
<value>電流 (μA)</value>
</data>
<data name="RaySource_StatusLabel" xml:space="preserve">
<value>狀態</value>
</data>
<data name="RaySource_PowerLabel" xml:space="preserve">
<value>功率 (W)</value>
</data>
<data name="RaySource_TemperatureLabel" xml:space="preserve">
<value>溫度 (°C)</value>
</data>
<data name="RaySource_VacuumLabel" xml:space="preserve">
<value>真空度</value>
</data>
<data name="RaySource_Status_Idle" xml:space="preserve">
<value>閒置</value>
</data>
<data name="RaySource_Status_Initializing" xml:space="preserve">
<value>初始化中</value>
</data>
<data name="RaySource_Status_Warmup" xml:space="preserve">
<value>預熱中</value>
</data>
<data name="RaySource_Status_Ready" xml:space="preserve">
<value>就緒</value>
</data>
<data name="RaySource_Status_XrayOn" xml:space="preserve">
<value>射線開啟</value>
</data>
<data name="RaySource_Status_Error" xml:space="preserve">
<value>錯誤</value>
</data>
<data name="RaySource_Status_Fault" xml:space="preserve">
<value>故障</value>
</data>
<data name="RaySource_Error_VoltageOutOfRange" xml:space="preserve">
<value>電壓超出範圍 (20-225 kV)</value>
</data>
<data name="RaySource_Error_CurrentOutOfRange" xml:space="preserve">
<value>電流超出範圍 (10-1000 μA)</value>
</data>
<data name="RaySource_Error_CannotAdjustWhileXrayOn" xml:space="preserve">
<value>射線開啟時無法調整參數</value>
</data>
<data name="RaySource_Warning_HighTemperature" xml:space="preserve">
<value>溫度過高警告</value>
</data>
<data name="RaySource_Warning_LowVacuum" xml:space="preserve">
<value>真空度過低警告</value>
</data>
<data name="RaySource_ActualValueLabel" xml:space="preserve">
<value>實際 {0}</value>
</data>
<data name="RaySource_MinValueLabel" xml:space="preserve">
<value>最小值</value>
</data>
<data name="RaySource_MaxValueLabel" xml:space="preserve">
<value>最大值</value>
</data>
<data name="RaySource_TurnOnButton" xml:space="preserve">
<value>開啟射線源</value>
</data>
<data name="RaySource_TurnOffButton" xml:space="preserve">
<value>關閉射線源</value>
</data>
<data name="RaySource_SettingsButton" xml:space="preserve">
<value>設定</value>
</data>
<data name="RaySource_AdvanceButton" xml:space="preserve">
<value>進階</value>
</data>
<data name="RaySource_SystemButton" xml:space="preserve">
<value>系統</value>
</data>
<data name="RaySource_StatusClosed" xml:space="preserve">
<value>射線源
已關閉</value>
</data>
<data name="RaySource_StatusOpened" xml:space="preserve">
<value>射線源
已開啟</value>
</data>
<data name="RaySource_StatusUnavailable" xml:space="preserve">
<value>射線源
不可用</value>
</data>
<data name="RaySource_ConnectButton" xml:space="preserve">
<value>連接射線源</value>
</data>
<data name="RaySource_DisconnectButton" xml:space="preserve">
<value>斷開射線源</value>
</data>
<data name="RaySource_ConnectTooltip" xml:space="preserve">
<value>連接射線源設備</value>
</data>
<data name="RaySource_DisconnectTooltip" xml:space="preserve">
<value>斷開射線源設備</value>
</data>
<data name="RaySource_ConnectVariablesButton" xml:space="preserve">
<value>連接變量</value>
</data>
<data name="RaySource_InitializeButton" xml:space="preserve">
<value>初始化</value>
</data>
<data name="RaySource_SourceTypeLabel" xml:space="preserve">
<value>射線源類型:</value>
</data>
<data name="RaySource_Connected" xml:space="preserve">
<value>已連接</value>
</data>
<data name="RaySource_Disconnected" xml:space="preserve">
<value>未連接</value>
</data>
<data name="RaySource_VariablesConnected" xml:space="preserve">
<value>變量已連接</value>
</data>
<data name="RaySource_ConfigButton" xml:space="preserve">
<value>配置</value>
</data>
<data name="RaySource_ConfigWindowTitle" xml:space="preserve">
<value>射線源配置</value>
</data>
<data name="RaySource_WarmUpLabel" xml:space="preserve">
<value>暖機:</value>
</data>
<data name="RaySource_StartUpLabel" xml:space="preserve">
<value>啟動:</value>
</data>
<data name="RaySource_AutoCenterLabel" xml:space="preserve">
<value>自動定心:</value>
</data>
<data name="RaySource_FilamentLabel" xml:space="preserve">
<value>燈絲校準:</value>
</data>
<data name="RaySource_PowerModeLabel" xml:space="preserve">
<value>功率模式:</value>
</data>
<data name="RaySource_InterlockLabel" xml:space="preserve">
<value>連鎖:</value>
</data>
<data name="RaySource_WatchdogLabel" xml:space="preserve">
<value>看門狗:</value>
</data>
<data name="RaySource_InterlockActive" xml:space="preserve">
<value>啟動</value>
</data>
<data name="RaySource_InterlockNormal" xml:space="preserve">
<value>正常</value>
</data>
<data name="RaySource_XRayOnLabel" xml:space="preserve">
<value>射線狀態:</value>
</data>
<data name="RaySource_XRayOn" xml:space="preserve">
<value>開啟</value>
</data>
<data name="RaySource_XRayOff" xml:space="preserve">
<value>關閉</value>
</data>
<data name="RaySource_TxiStatusLabel" xml:space="preserve">
<value>TXI狀態:</value>
</data>
<data name="RaySource_TxiOnButton" xml:space="preserve">
<value>TXI ON</value>
</data>
<data name="RaySource_TxiOffButton" xml:space="preserve">
<value>TXI OFF</value>
</data>
<data name="RaySource_HighPowerButton" xml:space="preserve">
<value>High Power</value>
</data>
<data name="RaySource_MicroFocusButton" xml:space="preserve">
<value>Micro Focus</value>
</data>
<data name="RaySource_WarmUpSettingButton" xml:space="preserve">
<value>暖機設置</value>
</data>
<data name="RaySource_TrainingSettingButton" xml:space="preserve">
<value>訓機設置</value>
</data>
<data name="RaySource_FilamentCalibrationButton" xml:space="preserve">
<value>燈絲校準</value>
</data>
<data name="RaySource_AutoCenterSettingButton" xml:space="preserve">
<value>自動定心</value>
</data>
<data name="RaySource_FilamentLifetimeLabel" xml:space="preserve">
<value>燈絲壽命:</value>
</data>
<data name="RaySource_FilamentLifetimeWarningTitle" xml:space="preserve">
<value>燈絲壽命預警</value>
</data>
<data name="RaySource_FilamentLifetimeWarningMessage" xml:space="preserve">
<value>燈絲累計使用 {0} 小時,壽命閾值 {1} 小時,已使用 {2}%。建議盡快更換燈絲。</value>
</data>
<data name="RaySource_FilamentLifetimeWarningConfirm" xml:space="preserve">
<value>確認</value>
</data>
<data name="RaySource_Confirm_Title" xml:space="preserve">
<value>確認</value>
</data>
<data name="RaySource_WarmUp_Confirm" xml:space="preserve">
<value>是否確認執行暖機操作?</value>
</data>
<data name="RaySource_WarmUp_Title" xml:space="preserve">
<value>暖機設置</value>
</data>
<data name="RaySource_WarmUp_Message" xml:space="preserve">
<value>正在執行暖機操作,請稍候...</value>
</data>
<data name="RaySource_Training_Confirm" xml:space="preserve">
<value>是否確認執行訓機操作?</value>
</data>
<data name="RaySource_Training_Title" xml:space="preserve">
<value>訓機設置</value>
</data>
<data name="RaySource_Training_Message" xml:space="preserve">
<value>正在執行訓機操作,請稍候...</value>
</data>
<data name="RaySource_FilamentCalibration_Confirm" xml:space="preserve">
<value>是否確認執行燈絲校準操作?</value>
</data>
<data name="RaySource_FilamentCalibration_Title" xml:space="preserve">
<value>燈絲校準</value>
</data>
<data name="RaySource_FilamentCalibration_Message" xml:space="preserve">
<value>正在執行燈絲校準操作,請稍候...</value>
</data>
<data name="RaySource_AutoCenter_Confirm" xml:space="preserve">
<value>是否確認執行全部電壓自動定心操作?</value>
</data>
<data name="RaySource_AutoCenter_Title" xml:space="preserve">
<value>自動定心</value>
</data>
<data name="RaySource_AutoCenter_Message" xml:space="preserve">
<value>正在執行全部電壓自動定心操作,請稍候...</value>
</data>
<data name="RaySource_Operation_Sent" xml:space="preserve">
<value>操作指令已發送,等待設備執行...</value>
</data>
</root>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

@@ -0,0 +1,577 @@
using System;
using System.Collections.Generic;
using System.Data;
using XP.Common.Database.Interfaces;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Config;
namespace XP.Hardware.RaySource.Services
{
/// <summary>
/// 灯丝寿命管理服务实现 | Filament Lifetime Management Service Implementation
/// 负责灯丝使用时长的记录、计算、异常恢复和预警判断
/// Responsible for filament usage duration recording, calculation, anomaly recovery, and warning evaluation
/// </summary>
public class FilamentLifetimeService : IFilamentLifetimeService
{
private readonly IDbContext _dbContext;
private readonly RaySourceConfig _config;
private readonly ILoggerService _logger;
// 内存状态 | In-memory state
private DateTime? _filamentStartTime;
private string _sourceType = "";
private string _serialNumber = "";
private double _thresholdSeconds;
private readonly object _filamentLock = new object();
/// <inheritdoc/>
public bool IsInitialized { get; private set; }
/// <inheritdoc/>
public bool IsFilamentOn => _filamentStartTime != null;
#region | Constructor
/// <summary>
/// 构造函数,注入依赖 | Constructor with dependency injection
/// </summary>
/// <param name="dbContext">数据库上下文 | Database context</param>
/// <param name="config">射线源配置 | Ray source configuration</param>
/// <param name="logger">日志服务 | Logger service</param>
public FilamentLifetimeService(IDbContext dbContext, RaySourceConfig config, ILoggerService logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = logger?.ForModule<FilamentLifetimeService>() ?? throw new ArgumentNullException(nameof(logger));
}
#endregion
#region | Initialization
/// <inheritdoc/>
public bool Initialize()
{
try
{
// 1. 读取配置 | Read configuration
_sourceType = _config.SourceType;
_serialNumber = _config.SerialNumber;
_thresholdSeconds = _config.TotalLifeThreshold * 3600;
// 2. 验证配置(SourceType 或 SerialNumber 为空时阻止初始化)
// Validate configuration (block initialization if SourceType or SerialNumber is empty)
if (string.IsNullOrWhiteSpace(_sourceType) || string.IsNullOrWhiteSpace(_serialNumber))
{
_logger.Error(null, "灯丝寿命服务初始化失败:SourceType 或 SerialNumber 为空,SourceType={SourceType}SerialNumber={SerialNumber} | " +
"Filament lifetime service initialization failed: SourceType or SerialNumber is empty, SourceType={SourceType}, SerialNumber={SerialNumber}",
_sourceType ?? "", _serialNumber ?? "");
IsInitialized = false;
return false;
}
// 3. 建表(CREATE TABLE IF NOT EXISTS| Create tables
if (!CreateTables())
{
IsInitialized = false;
return false;
}
// 4. 异常恢复(关闭未结束记录)| Anomaly recovery (close unclosed records)
RecoverUnclosedRecords();
// 5. 重算累计寿命 | Recalculate accumulated lifetime
RecalculateTotalLifeSeconds();
IsInitialized = true;
_logger.Info("灯丝寿命服务初始化成功,SourceType={SourceType}SerialNumber={SerialNumber},阈值={Threshold}小时 | " +
"Filament lifetime service initialized successfully, SourceType={SourceType}, SerialNumber={SerialNumber}, threshold={Threshold} hours",
_sourceType, _serialNumber, _config.TotalLifeThreshold);
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "灯丝寿命服务初始化异常 | Filament lifetime service initialization exception: {Message}", ex.Message);
IsInitialized = false;
return false;
}
}
/// <summary>
/// 创建数据库表 | Create database tables
/// </summary>
private bool CreateTables()
{
try
{
// 创建累计统计表 | Create lifetime statistics table
var createStatsSql = @"
CREATE TABLE IF NOT EXISTS RaySourceFilamentLifetimeStatistics (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SourceType TEXT NOT NULL,
SerialNumber TEXT NOT NULL,
TotalLifeSeconds REAL NOT NULL DEFAULT 0,
LastUpdateTime TEXT NOT NULL DEFAULT '1970-01-01T00:00:00Z'
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_lifetime_serial ON RaySourceFilamentLifetimeStatistics(SerialNumber);
CREATE INDEX IF NOT EXISTS idx_lifetime_source_type ON RaySourceFilamentLifetimeStatistics(SourceType);";
var statsResult = _dbContext.ExecuteNonQuery(createStatsSql);
if (!statsResult.IsSuccess)
{
_logger.Error(statsResult.Exception, "创建统计表失败:{Message} | Failed to create statistics table: {Message}", statsResult.Message);
return false;
}
// 创建使用流水表 | Create usage logs table
var createLogsSql = @"
CREATE TABLE IF NOT EXISTS RaySourceFilamentUsageLogs (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SourceType TEXT NOT NULL,
SerialNumber TEXT NOT NULL,
StartTime TEXT NOT NULL,
EndTime TEXT,
DurationSeconds REAL NOT NULL DEFAULT 0,
Status INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_usage_serial ON RaySourceFilamentUsageLogs(SerialNumber);";
var logsResult = _dbContext.ExecuteNonQuery(createLogsSql);
if (!logsResult.IsSuccess)
{
_logger.Error(logsResult.Exception, "创建流水表失败:{Message} | Failed to create usage logs table: {Message}", logsResult.Message);
return false;
}
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "建表异常 | Exception creating tables: {Message}", ex.Message);
return false;
}
}
/// <summary>
/// 异常恢复:关闭未结束的流水记录 | Anomaly recovery: close unclosed usage log records
/// </summary>
private void RecoverUnclosedRecords()
{
try
{
var querySql = @"
SELECT Id, StartTime FROM RaySourceFilamentUsageLogs
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber AND EndTime IS NULL";
var queryParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (queryResult, dataTable) = _dbContext.ExecuteDataTable(querySql, queryParams);
if (!queryResult.IsSuccess || dataTable == null || dataTable.Rows.Count == 0)
{
return;
}
_logger.Warn("检测到 {Count} 条未关闭的灯丝使用记录,开始异常恢复 | " +
"Detected {Count} unclosed filament usage records, starting anomaly recovery",
dataTable.Rows.Count);
foreach (DataRow row in dataTable.Rows)
{
var id = Convert.ToInt64(row["Id"]);
var startTimeStr = row["StartTime"]?.ToString() ?? "";
// 将未关闭记录的 EndTime 设为 StartTimeDurationSeconds 设为 0Status 设为 1(异常中断)
// Set unclosed record's EndTime to StartTime, DurationSeconds to 0, Status to 1 (abnormal interruption)
var updateSql = @"
UPDATE RaySourceFilamentUsageLogs
SET EndTime = StartTime, DurationSeconds = 0, Status = 1
WHERE Id = @Id";
var updateParams = new Dictionary<string, object>
{
{ "Id", id }
};
_dbContext.ExecuteNonQuery(updateSql, updateParams);
_logger.Warn("异常恢复:关闭未结束记录,SourceType={SourceType}SerialNumber={SerialNumber}StartTime={StartTime} | " +
"Anomaly recovery: closed unclosed record, SourceType={SourceType}, SerialNumber={SerialNumber}, StartTime={StartTime}",
_sourceType, _serialNumber, startTimeStr);
}
}
catch (Exception ex)
{
_logger.Error(ex, "异常恢复失败 | Anomaly recovery failed: {Message}", ex.Message);
}
}
/// <summary>
/// 基于流水表重算累计寿命 | Recalculate accumulated lifetime based on usage logs
/// </summary>
private void RecalculateTotalLifeSeconds()
{
try
{
var sumSql = @"
SELECT COALESCE(SUM(DurationSeconds), 0) FROM RaySourceFilamentUsageLogs
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber";
var sumParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (sumResult, totalSeconds) = _dbContext.ExecuteScalar<double>(sumSql, sumParams);
if (!sumResult.IsSuccess)
{
_logger.Error(sumResult.Exception, "重算累计寿命失败:查询 DurationSeconds 总和出错,{Message} | " +
"Failed to recalculate lifetime: error querying DurationSeconds sum, {Message}", sumResult.Message);
return;
}
var nowUtc = DateTime.UtcNow.ToString("o");
// 更新或插入统计表 | Update or insert statistics table
var upsertSql = @"
INSERT INTO RaySourceFilamentLifetimeStatistics (SourceType, SerialNumber, TotalLifeSeconds, LastUpdateTime)
VALUES (@SourceType, @SerialNumber, @TotalLifeSeconds, @LastUpdateTime)
ON CONFLICT(SerialNumber) DO UPDATE SET TotalLifeSeconds = @TotalLifeSeconds, LastUpdateTime = @LastUpdateTime";
var updateParams = new Dictionary<string, object>
{
{ "TotalLifeSeconds", totalSeconds },
{ "LastUpdateTime", nowUtc },
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var updateResult = _dbContext.ExecuteNonQuery(upsertSql, updateParams);
if (!updateResult.IsSuccess)
{
_logger.Error(updateResult.Exception, "重算累计寿命失败:更新统计表出错,{Message} | " +
"Failed to recalculate lifetime: error updating statistics table, {Message}", updateResult.Message);
}
}
catch (Exception ex)
{
_logger.Error(ex, "重算累计寿命异常 | Exception recalculating lifetime: {Message}", ex.Message);
}
}
#endregion
#region | Filament Start and Stop
/// <inheritdoc/>
public bool StartFilamentUsage()
{
if (!IsInitialized)
{
_logger.Warn("灯丝寿命服务未初始化,无法开始记录 | Filament lifetime service not initialized, cannot start recording");
return false;
}
lock (_filamentLock)
{
// 锁内再次检查,防止并发重复开始 | Double-check inside lock to prevent concurrent duplicate start
if (_filamentStartTime != null)
{
_logger.Warn("灯丝已处于开启状态,忽略重复开始请求 | Filament already on, ignoring duplicate start request");
return false;
}
try
{
// 1. 确保统计表中存在当前设备记录 | Ensure current device record exists in statistics table
var ensureStatsSql = @"
INSERT OR IGNORE INTO RaySourceFilamentLifetimeStatistics (SourceType, SerialNumber, TotalLifeSeconds, LastUpdateTime)
VALUES (@SourceType, @SerialNumber, 0, @LastUpdateTime)";
var ensureParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber },
{ "LastUpdateTime", DateTime.UtcNow.ToString("o") }
};
var ensureResult = _dbContext.ExecuteNonQuery(ensureStatsSql, ensureParams);
if (!ensureResult.IsSuccess)
{
_logger.Error(ensureResult.Exception, "确保统计记录存在失败:{Message}SourceType={SourceType}SerialNumber={SerialNumber} | " +
"Failed to ensure statistics record exists: {Message}, SourceType={SourceType}, SerialNumber={SerialNumber}",
ensureResult.Message, _sourceType, _serialNumber);
return false;
}
// 2. 向流水表插入新记录 | Insert new usage log record
var startTime = DateTime.UtcNow;
var startTimeStr = startTime.ToString("o");
var insertLogSql = @"
INSERT INTO RaySourceFilamentUsageLogs (SourceType, SerialNumber, StartTime, EndTime, DurationSeconds, Status)
VALUES (@SourceType, @SerialNumber, @StartTime, NULL, 0, 0)";
var insertParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber },
{ "StartTime", startTimeStr }
};
var insertResult = _dbContext.ExecuteNonQuery(insertLogSql, insertParams);
if (!insertResult.IsSuccess)
{
_logger.Error(insertResult.Exception, "插入灯丝使用流水记录失败:{Message}SourceType={SourceType}SerialNumber={SerialNumber} | " +
"Failed to insert filament usage log: {Message}, SourceType={SourceType}, SerialNumber={SerialNumber}",
insertResult.Message, _sourceType, _serialNumber);
return false;
}
// 3. 记录内存起始时间(仅在数据库插入成功后更新)| Record in-memory start time
_filamentStartTime = startTime;
_logger.Info("灯丝使用开始,SourceType={SourceType}SerialNumber={SerialNumber}StartTime={StartTime} | " +
"Filament usage started, SourceType={SourceType}, SerialNumber={SerialNumber}, StartTime={StartTime}",
_sourceType, _serialNumber, startTimeStr);
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "开始灯丝使用记录异常,SourceType={SourceType}SerialNumber={SerialNumber} | " +
"Exception starting filament usage recording, SourceType={SourceType}, SerialNumber={SerialNumber}",
_sourceType, _serialNumber);
return false;
}
} // end lock
}
/// <inheritdoc/>
public bool StopFilamentUsage()
{
if (!IsInitialized)
{
_logger.Warn("灯丝寿命服务未初始化,无法停止记录 | Filament lifetime service not initialized, cannot stop recording");
return false;
}
lock (_filamentLock)
{
// 锁内再次检查,防止并发重复停止 | Double-check inside lock to prevent concurrent duplicate stop
if (_filamentStartTime == null)
{
_logger.Warn("灯丝已处于关闭状态,忽略重复停止请求 | Filament already off, ignoring duplicate stop request");
return false;
}
try
{
// 1. 查找当前设备最近一条未关闭的记录 | Find the most recent unclosed record
var querySql = @"
SELECT Id, StartTime FROM RaySourceFilamentUsageLogs
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber AND EndTime IS NULL
ORDER BY Id DESC LIMIT 1";
var queryParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (queryResult, dataTable) = _dbContext.ExecuteDataTable(querySql, queryParams);
if (!queryResult.IsSuccess || dataTable == null || dataTable.Rows.Count == 0)
{
_logger.Warn("未找到当前设备的未结束流水记录,SourceType={SourceType}SerialNumber={SerialNumber} | " +
"No unclosed usage log found, SourceType={SourceType}, SerialNumber={SerialNumber}",
_sourceType, _serialNumber);
return false;
}
var row = dataTable.Rows[0];
var recordId = Convert.ToInt64(row["Id"]);
var startTimeStr = row["StartTime"]?.ToString() ?? "";
// 2. 计算 DurationSeconds | Calculate DurationSeconds
var endTime = DateTime.UtcNow;
var endTimeStr = endTime.ToString("o");
double durationSeconds = 0;
if (DateTime.TryParse(startTimeStr, null, System.Globalization.DateTimeStyles.RoundtripKind, out var startTime))
{
durationSeconds = (endTime - startTime).TotalSeconds;
if (durationSeconds < 0) durationSeconds = 0;
}
// 3. 顺序更新流水表和统计表(ExecuteNonQuery 不支持外部事务参数,改为顺序执行)
// Sequential update: usage log then statistics
// 3.1 更新流水记录 | Update usage log
var updateLogSql = @"
UPDATE RaySourceFilamentUsageLogs
SET EndTime = @EndTime, DurationSeconds = @DurationSeconds, Status = 1
WHERE Id = @Id";
var updateLogParams = new Dictionary<string, object>
{
{ "EndTime", endTimeStr },
{ "DurationSeconds", durationSeconds },
{ "Id", recordId }
};
var updateLogResult = _dbContext.ExecuteNonQuery(updateLogSql, updateLogParams);
if (!updateLogResult.IsSuccess)
{
_logger.Error(updateLogResult.Exception, "更新流水记录失败:{Message} | Failed to update usage log: {Message}", updateLogResult.Message);
return false;
}
// 3.2 累加统计表 | Accumulate statistics
var updateStatsSql = @"
UPDATE RaySourceFilamentLifetimeStatistics
SET TotalLifeSeconds = TotalLifeSeconds + @DurationSeconds, LastUpdateTime = @LastUpdateTime
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber";
var updateStatsParams = new Dictionary<string, object>
{
{ "DurationSeconds", durationSeconds },
{ "LastUpdateTime", endTimeStr },
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var updateStatsResult = _dbContext.ExecuteNonQuery(updateStatsSql, updateStatsParams);
if (!updateStatsResult.IsSuccess)
{
_logger.Error(updateStatsResult.Exception, "更新统计记录失败:{Message} | Failed to update statistics: {Message}", updateStatsResult.Message);
// 流水已更新但统计失败,记录警告(下次 RecalculateTotalLifeSeconds 可恢复)
_logger.Warn("统计表更新失败但流水记录已关闭,可通过重算恢复 | Statistics update failed but usage log closed, recoverable via recalculation");
}
// 4. 清除内存起始时间 | Clear in-memory start time
_filamentStartTime = null;
// 5. 查询更新后的 TotalLifeSeconds 用于日志 | Query updated TotalLifeSeconds for logging
var totalLifeSeconds = 0.0;
var totalSql = @"
SELECT TotalLifeSeconds FROM RaySourceFilamentLifetimeStatistics
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber";
var totalParams = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (totalResult, totalData) = _dbContext.ExecuteScalar<double>(totalSql, totalParams);
if (totalResult.IsSuccess)
{
totalLifeSeconds = totalData;
}
_logger.Info("灯丝使用结束,SourceType={SourceType}SerialNumber={SerialNumber}DurationSeconds={DurationSeconds}TotalLifeSeconds={TotalLifeSeconds} | " +
"Filament usage stopped, SourceType={SourceType}, SerialNumber={SerialNumber}, DurationSeconds={DurationSeconds}, TotalLifeSeconds={TotalLifeSeconds}",
_sourceType, _serialNumber, durationSeconds, totalLifeSeconds);
return true;
}
catch (Exception ex)
{
_logger.Error(ex, "停止灯丝使用记录异常,SourceType={SourceType}SerialNumber={SerialNumber} | " +
"Exception stopping filament usage recording, SourceType={SourceType}, SerialNumber={SerialNumber}",
_sourceType, _serialNumber);
return false;
}
} // end lock
}
#endregion
#region | Query and Calculation
/// <inheritdoc/>
public double GetTotalLifeSeconds()
{
if (!IsInitialized)
{
_logger.Warn("服务未初始化,返回默认值 0 | Service not initialized, returning default value 0");
return 0;
}
try
{
var sql = @"
SELECT COALESCE(TotalLifeSeconds, 0) FROM RaySourceFilamentLifetimeStatistics
WHERE SourceType = @SourceType AND SerialNumber = @SerialNumber";
var parameters = new Dictionary<string, object>
{
{ "SourceType", _sourceType },
{ "SerialNumber", _serialNumber }
};
var (result, totalSeconds) = _dbContext.ExecuteScalar<double>(sql, parameters);
if (!result.IsSuccess)
{
_logger.Error(result.Exception, "查询累计使用秒数失败,{Message} | Failed to query total life seconds, {Message}", result.Message);
return 0;
}
return totalSeconds;
}
catch (Exception ex)
{
_logger.Error(ex, "查询累计使用秒数异常,SourceType={SourceType}SerialNumber={SerialNumber} | " +
"Exception querying total life seconds, SourceType={SourceType}, SerialNumber={SerialNumber}",
_sourceType, _serialNumber);
return 0;
}
}
/// <inheritdoc/>
public double GetCurrentTotalLifeSeconds()
{
var dbSeconds = GetTotalLifeSeconds();
// 如果灯丝正在运行,加上本次运行时长 | If filament is on, add current session duration
if (_filamentStartTime.HasValue)
{
var currentSessionSeconds = (DateTime.UtcNow - _filamentStartTime.Value).TotalSeconds;
dbSeconds += currentSessionSeconds;
}
return dbSeconds;
}
/// <inheritdoc/>
public double GetThresholdSeconds()
{
return _thresholdSeconds;
}
/// <inheritdoc/>
public double GetLifetimePercentage()
{
// 处理阈值为 0 的边界情况,避免除零 | Handle zero threshold to avoid division by zero
if (_thresholdSeconds <= 0)
{
return 100;
}
var totalSeconds = GetCurrentTotalLifeSeconds();
return Math.Min(totalSeconds / _thresholdSeconds * 100, 100);
}
/// <inheritdoc/>
public bool ShouldShowLifetimeWarning()
{
return GetLifetimePercentage() >= 90;
}
#endregion
}
}
@@ -0,0 +1,72 @@
namespace XP.Hardware.RaySource.Services
{
/// <summary>
/// 灯丝寿命管理服务接口 | Filament Lifetime Management Service Interface
/// 负责灯丝使用时长的记录、计算、异常恢复和预警判断
/// Responsible for filament usage duration recording, calculation, anomaly recovery, and warning evaluation
/// </summary>
public interface IFilamentLifetimeService
{
/// <summary>
/// 是否已成功初始化 | Whether the service has been successfully initialized
/// </summary>
bool IsInitialized { get; }
/// <summary>
/// 灯丝是否处于开启状态 | Whether the filament is currently on
/// </summary>
bool IsFilamentOn { get; }
/// <summary>
/// 初始化服务(建表、异常恢复、重算累计寿命)
/// Initialize service (create tables, recover anomalies, recalculate accumulated lifetime)
/// </summary>
/// <returns>是否初始化成功 | Whether initialization succeeded</returns>
bool Initialize();
/// <summary>
/// 开始灯丝使用记录 | Start filament usage recording
/// </summary>
/// <returns>是否成功 | Whether the operation succeeded</returns>
bool StartFilamentUsage();
/// <summary>
/// 停止灯丝使用记录 | Stop filament usage recording
/// </summary>
/// <returns>是否成功 | Whether the operation succeeded</returns>
bool StopFilamentUsage();
/// <summary>
/// 获取数据库中的累计使用秒数 | Get accumulated usage seconds from database
/// </summary>
/// <returns>累计使用秒数 | Accumulated usage seconds</returns>
double GetTotalLifeSeconds();
/// <summary>
/// 获取当前实时累计使用秒数(含本次运行时长)
/// Get real-time total seconds (including current session duration)
/// </summary>
/// <returns>实时累计秒数 | Real-time accumulated seconds</returns>
double GetCurrentTotalLifeSeconds();
/// <summary>
/// 获取寿命阈值(秒)| Get lifetime threshold in seconds
/// </summary>
/// <returns>阈值秒数 | Threshold in seconds</returns>
double GetThresholdSeconds();
/// <summary>
/// 获取当前寿命使用百分比(0-100)
/// Get current lifetime usage percentage (0-100)
/// </summary>
/// <returns>寿命百分比 | Lifetime percentage</returns>
double GetLifetimePercentage();
/// <summary>
/// 检查是否需要弹出寿命预警(百分比 >= 90%)
/// Check if lifetime warning should be shown (percentage >= 90%)
/// </summary>
/// <returns>是否需要预警 | Whether warning should be shown</returns>
bool ShouldShowLifetimeWarning();
}
}
@@ -0,0 +1,148 @@
using System;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Abstractions.Enums;
namespace XP.Hardware.RaySource.Services
{
/// <summary>
/// 射线源业务服务接口 | X-Ray Source Business Service Interface
/// 封装初始化、操作调度、状态管理和业务规则校验 | Encapsulates initialization, operation dispatch, status management, and business rule validation
/// </summary>
public interface IRaySourceService : IDisposable
{
/// <summary>
/// 当前射线源实例 | Current X-ray source instance
/// </summary>
IXRaySource CurrentSource { get; }
/// <summary>
/// 是否已初始化 | Is initialized
/// </summary>
bool IsInitialized { get; }
/// <summary>
/// PVI 变量是否已连接 | Whether PVI variables are connected
/// </summary>
bool IsConnected { get; }
/// <summary>
/// 是否射线开启 | Is X-ray on
/// </summary>
bool IsXRayOn { get; }
/// <summary>
/// 当前射线源状态 | Current X-ray source status
/// </summary>
RaySourceStatus CurrentStatus { get; }
/// <summary>
/// 初始化射线源(射线源类型从配置文件读取)| Initialize X-ray source (source type read from configuration)
/// </summary>
XRayResult Initialize();
/// <summary>
/// 连接变量并启动实时状态通讯 | Connect variables and start real-time status communication
/// </summary>
XRayResult ConnectVariables();
/// <summary>
/// 断开射线源连接 | Disconnect X-ray source
/// </summary>
XRayResult Disconnect();
/// <summary>
/// 开启射线 | Turn on X-ray
/// </summary>
XRayResult TurnOn();
/// <summary>
/// 关闭射线 | Turn off X-ray
/// </summary>
XRayResult TurnOff();
/// <summary>
/// 紧急关闭(最高优先级)| Emergency shutdown (highest priority)
/// </summary>
XRayResult EmergencyShutdown();
/// <summary>
/// 设置电压 | Set voltage
/// </summary>
/// <param name="voltage">电压值(kV| Voltage value (kV)</param>
XRayResult SetVoltage(float voltage);
/// <summary>
/// 设置电流 | Set current
/// </summary>
/// <param name="current">电流值(μA| Current value (μA)</param>
XRayResult SetCurrent(float current);
/// <summary>
/// 读取实际电压 | Read actual voltage
/// </summary>
XRayResult ReadVoltage();
/// <summary>
/// 读取实际电流 | Read actual current
/// </summary>
XRayResult ReadCurrent();
/// <summary>
/// 读取系统状态 | Read system status
/// </summary>
XRayResult ReadSystemStatus();
/// <summary>
/// 检查错误 | Check errors
/// </summary>
XRayResult CheckErrors();
/// <summary>
/// 清除错误 | Clear errors
/// </summary>
XRayResult ClearErrors();
/// <summary>
/// TXI 开启 | TXI On
/// </summary>
XRayResult TxiOn();
/// <summary>
/// TXI 关闭 | TXI Off
/// </summary>
XRayResult TxiOff();
/// <summary>
/// 暖机设置 | Warm-up setting
/// </summary>
XRayResult WarmUp();
/// <summary>
/// 训机设置 | Training setting
/// </summary>
XRayResult Training();
/// <summary>
/// 灯丝校准 | Filament calibration
/// </summary>
XRayResult FilamentCalibration();
/// <summary>
/// 全部电压自动定心 | Auto-center all voltages
/// </summary>
XRayResult AutoCenter();
/// <summary>
/// 设置功率模式 | Set power mode
/// </summary>
/// <param name="mode">功率模式值:1=Micro Focus2=High Power</param>
XRayResult SetPowerMode(int mode);
/// <summary>
/// 异步执行初始化 + 连接变量的完整流程 | Async full sequence: Initialize + ConnectVariables
/// 供 ViewModel 在界面加载时调用,避免阻塞 UI 线程 | For ViewModel to call on view loaded without blocking UI
/// </summary>
/// <returns>最终操作结果 | Final operation result</returns>
System.Threading.Tasks.Task<XRayResult> InitializeAndConnectAsync();
}
}
@@ -0,0 +1,213 @@
using System;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Config;
namespace XP.Hardware.RaySource.Services
{
/// <summary>
/// 操作类型枚举 | Operation type enum
/// </summary>
public enum OperationType
{
WarmUp,
Training,
FilamentCalibration,
AutoCenter
}
/// <summary>
/// 操作进度计算结果 | Operation progress calculation result
/// </summary>
public class ProgressResult
{
/// <summary>
/// 进度百分比(0-100| Progress percentage (0-100)
/// </summary>
public int Progress { get; set; }
/// <summary>
/// 操作是否已完成 | Whether operation is completed
/// </summary>
public bool IsCompleted { get; set; }
}
/// <summary>
/// 操作进度计算器 | Operation Progress Calculator
/// 根据实时电压/电流/状态字符串估算暖机、训机、灯丝校准、自动定心的进度
/// Estimates progress for warm-up, training, filament calibration, auto-center based on real-time voltage/current/status strings
/// </summary>
public class OperationProgressCalculator
{
private readonly OperationType _operationType;
private readonly RaySourceConfig _config;
private double _maxRatio;
private int _trainingPhase = 1;
public OperationProgressCalculator(OperationType operationType, RaySourceConfig config)
{
_operationType = operationType;
_config = config ?? throw new ArgumentNullException(nameof(config));
_maxRatio = 0;
_trainingPhase = 1;
}
/// <summary>
/// 根据最新状态数据计算进度 | Calculate progress based on latest status data
/// </summary>
public ProgressResult Calculate(SystemStatusData data)
{
if (data == null) return new ProgressResult { Progress = (int)_maxRatio, IsCompleted = false };
return _operationType switch
{
OperationType.WarmUp => CalculateWarmUp(data),
OperationType.Training => CalculateTraining(data),
OperationType.FilamentCalibration => CalculateFilamentCalibration(data),
OperationType.AutoCenter => CalculateAutoCenter(data),
_ => new ProgressResult { Progress = 0, IsCompleted = false }
};
}
/// <summary>
/// 暖机进度计算 | Warm-up progress calculation
/// 参照老项目:根据电压/电流阶段性变化估算,完成条件为 WarmUpStatus 包含"完成"
/// </summary>
private ProgressResult CalculateWarmUp(SystemStatusData data)
{
double ratio = 0;
float voltage = data.ActualVoltage;
float current = data.ActualCurrent;
float maxVoltage = _config.MaxVoltage; // 225
if (current < 10 && voltage <= 50)
{
// 第一阶段:电压升到 50kV,占 25%
ratio = voltage / 50.0 * 25.0;
}
else if (current >= 10 && current <= 100 && voltage >= 49)
{
// 第二阶段:电流升到 100μA,占 25%-75%
ratio = current / 100.0 * 50.0 + 25.0;
}
else if (current >= 98 && voltage >= 55)
{
// 第三阶段:电压升到最大值,占 75%-100%
ratio = voltage / maxVoltage * 100.0;
}
if (ratio > _maxRatio) _maxRatio = ratio;
// 完成条件:code = "2"(暖机完成)且电压超过最大值的 90% | Complete: code "2" and voltage > 90% max
bool isCompleted = data.WarmUpStatus == "2" && voltage > maxVoltage * 0.9f;
if (isCompleted) _maxRatio = 100;
return new ProgressResult
{
Progress = Math.Min((int)_maxRatio, 100),
IsCompleted = isCompleted
};
}
/// <summary>
/// 训机进度计算 | Training progress calculation
/// 参照老项目:分 6 个阶段,根据电压/电流和阶段号计算进度
/// </summary>
private ProgressResult CalculateTraining(SystemStatusData data)
{
double ratio = 0;
float voltage = data.ActualVoltage;
float current = data.ActualCurrent;
float maxVoltage = _config.MaxVoltage;
float maxCurrent = _config.MaxCurrent;
switch (_trainingPhase)
{
case 1:
if (current < 10 && voltage <= 50)
ratio = voltage / 50.0 * 20.0;
if (ratio > 17) _trainingPhase = 2;
break;
case 2:
if (current >= 10 && current <= 100 && voltage >= 45)
ratio = current / 100.0 * 30.0;
if (ratio > 27) _trainingPhase = 3;
break;
case 3:
if (current >= 98 && voltage >= 60)
ratio = voltage / maxVoltage * 40.0;
if (ratio > 37) _trainingPhase = 4;
break;
case 4:
if (current >= 98 && voltage >= 98)
ratio = current / maxCurrent * 60.0;
if (ratio > 57) _trainingPhase = 5;
break;
case 5:
if (current >= 45 && voltage >= 30)
ratio = voltage / maxVoltage * 80.0;
if (ratio > 77) _trainingPhase = 6;
break;
case 6:
if (current >= 45 && voltage >= 30)
ratio = voltage / maxVoltage * 100.0;
break;
}
if (ratio > _maxRatio) _maxRatio = ratio;
// 完成条件:code = "2"(训机完成)且处于最后阶段 | Complete: code "2" and final phase
bool isCompleted = data.StartUpStatus == "2" && _trainingPhase >= 6;
if (isCompleted) _maxRatio = 100;
return new ProgressResult
{
Progress = Math.Min((int)_maxRatio, 100),
IsCompleted = isCompleted
};
}
/// <summary>
/// 灯丝校准进度计算 | Filament calibration progress calculation
/// 参照老项目:电流 / 880 * 100,完成条件为状态包含"完成"且电流 > 800
/// </summary>
private ProgressResult CalculateFilamentCalibration(SystemStatusData data)
{
double ratio = data.ActualCurrent / 880.0 * 100.0;
if (ratio > _maxRatio) _maxRatio = ratio;
// 完成条件:code = "2"(灯丝校准完成)且电流 > 800 | Complete: code "2" and current > 800
bool isCompleted = data.FilamentAdjustStatus == "2" && data.ActualCurrent > 800;
if (isCompleted) _maxRatio = 100;
return new ProgressResult
{
Progress = Math.Min((int)_maxRatio, 100),
IsCompleted = isCompleted
};
}
/// <summary>
/// 自动定心进度计算 | Auto-center progress calculation
/// 参照老项目:电压 / 220 * 100,完成条件为状态包含"完成"且电压 > 200
/// </summary>
private ProgressResult CalculateAutoCenter(SystemStatusData data)
{
double ratio = data.ActualVoltage / 220.0 * 100.0;
if (ratio > _maxRatio) _maxRatio = ratio;
// 完成条件:code = "2"(自动定心完成)且电压 > 200 | Complete: code "2" and voltage > 200
bool isCompleted = data.AutoCenterStatus == "2" && data.ActualVoltage > 200;
if (isCompleted) _maxRatio = 100;
return new ProgressResult
{
Progress = Math.Min((int)_maxRatio, 100),
IsCompleted = isCompleted
};
}
}
}
@@ -0,0 +1,996 @@
using Prism.Events;
using System;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Abstractions.Enums;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Config;
namespace XP.Hardware.RaySource.Services
{
/// <summary>
/// 射线源业务服务实现类 | X-Ray Source Business Service Implementation
/// 单例模式,调度工厂+业务规则校验+事件驱动 | Singleton pattern, factory dispatch + business rule validation + event-driven
/// </summary>
public class RaySourceService : IRaySourceService
{
private readonly IRaySourceFactory _factory;
private readonly RaySourceConfig _config;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
private readonly IFilamentLifetimeService _filamentLifetimeService;
private IXRaySource _currentSource;
private bool _isDisposed = false;
private readonly object _lockObj = new object();
/// <summary>
/// 上一次灯丝记录对应的射线状态(用于去重,防止设备推送抖动)| Last filament recording status (for dedup, prevents device push jitter)
/// </summary>
private RaySourceStatus? _lastFilamentRecordedStatus;
/// <summary>
/// 当前射线源实例 | Current X-ray source instance
/// </summary>
public IXRaySource CurrentSource => _currentSource;
/// <summary>
/// 是否已初始化(且连接正常)| Is initialized (and connected)
/// </summary>
public bool IsInitialized => _isInitializedFlag && _currentSource != null;
private bool _isInitializedFlag = false;
/// <summary>
/// 是否射线开启(由 CurrentStatus 派生,Opened 即为 true| Is X-ray on (derived from CurrentStatus, true when Opened)
/// </summary>
public bool IsXRayOn => CurrentStatus == RaySourceStatus.Opened;
/// <summary>
/// 射线源是否已建立完整连接(由设备层 RaySourceConnected 推送确认)| Whether RaySource has established full connection (confirmed by device RaySourceConnected push)
/// </summary>
public bool IsConnected => _currentSource?.IsConnected ?? false;
/// <summary>
/// 当前射线源状态 | Current X-ray source status
/// </summary>
public RaySourceStatus CurrentStatus { get; private set; } = RaySourceStatus.Unavailable;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public RaySourceService(
IRaySourceFactory factory,
RaySourceConfig config,
IEventAggregator eventAggregator,
ILoggerService logger,
IFilamentLifetimeService filamentLifetimeService)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_config = config ?? throw new ArgumentNullException(nameof(config));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule("RaySource.Service");
_filamentLifetimeService = filamentLifetimeService ?? throw new ArgumentNullException(nameof(filamentLifetimeService));
_logger.Info("射线源服务已创建 | RaySource service created");
// 订阅射线源状态变更事件(处理状态同步和异常断联)| Subscribe to ray source status changed event
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Subscribe(OnRaySourceStatusChangedFromDevice);
}
/// <summary>
/// 初始化射线源(射线源类型从配置文件读取)| Initialize X-ray source (source type read from configuration)
/// </summary>
public XRayResult Initialize()
{
var sourceType = _config.SourceType;
lock (_lockObj)
{
try
{
_logger.Info("开始初始化射线源,类型: {SourceType} | Starting X-ray source initialization, type: {SourceType}", sourceType);
if (IsInitialized)
{
_logger.Warn("射线源已初始化,请勿重复初始化 | X-ray source already initialized");
return XRayResult.Error("射线源已初始化,请勿重复初始化");
}
// 验证配置 | Validate configuration
_logger.Debug("验证配置参数 | Validating configuration parameters");
var (isValid, errorMessage) = _config.Validate();
if (!isValid)
{
_logger.Error(null, "配置验证失败: {ErrorMessage} | Configuration validation failed: {ErrorMessage}", errorMessage);
return XRayResult.Error($"配置验证失败: {errorMessage}");
}
// 复用已有实例或创建新实例 | Reuse existing instance or create new one
if (_currentSource == null)
{
_logger.Debug("创建射线源实例: {SourceType} | Creating X-ray source instance: {SourceType}", sourceType);
_currentSource = _factory.CreateRaySource(sourceType);
if (_currentSource == null)
{
_logger.Error(null, "创建射线源实例失败: {SourceType} | Failed to create X-ray source instance: {SourceType}", sourceType);
return XRayResult.Error($"创建射线源实例失败: {sourceType}");
}
}
else
{
_logger.Debug("复用已有射线源实例 | Reusing existing X-ray source instance");
}
// 初始化射线源 | Initialize X-ray source
_logger.Info("调用射线源初始化方法 | Calling X-ray source initialization method");
var result = _currentSource.Initialize();
if (result.Success)
{
_isInitializedFlag = true;
_logger.Info("射线源初始化成功: {SourceType} | X-ray source initialized successfully: {SourceType}", sourceType);
}
else
{
_logger.Error(null, "射线源初始化失败: {ErrorMessage} | X-ray source initialization failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
var errorMsg = $"初始化异常: {ex.Message}";
_logger.Error(ex, "射线源初始化异常: {Message} | X-ray source initialization exception: {Message}", ex.Message);
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(errorMsg);
return XRayResult.Error(errorMsg);
}
}
}
/// <summary>
/// 连接 PVI 变量并启动实时状态通讯 | Connect PVI variables and start real-time status communication
/// 必须在 Initialize 成功后、TurnOn 之前调用 | Must be called after Initialize succeeds and before TurnOn
/// </summary>
public XRayResult ConnectVariables()
{
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法连接变量 | X-ray source not initialized, cannot connect variables");
return XRayResult.Error("射线源未初始化,无法连接变量");
}
lock (_lockObj)
{
try
{
_logger.Info("开始连接 PVI 变量 | Starting PVI variable connection");
var result = _currentSource.ConnectVariables();
if (result.Success)
{
_logger.Info("PVI 变量连接命令已发送,等待设备层确认 | PVI variable connection command sent, waiting for device confirmation");
}
else
{
_logger.Error(null, "PVI 变量连接失败: {ErrorMessage} | PVI variable connection failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
var errorMsg = $"连接变量异常: {ex.Message}";
_logger.Error(ex, "连接变量异常: {Message} | Connect variables exception: {Message}", ex.Message);
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(errorMsg);
return XRayResult.Error(errorMsg);
}
}
}
/// <summary>
/// 开启射线 | Turn on X-ray
/// </summary>
public XRayResult TurnOn()
{
// 业务规则校验:未初始化禁止操作 | Business rule: operation forbidden if not initialized
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法开启射线 | X-ray source not initialized, cannot turn on");
return XRayResult.Error("射线源未初始化,无法开启射线");
}
// 快速检查:已开启则直接返回,避免不必要的锁竞争 | Quick check: return early if already opened
if (CurrentStatus == RaySourceStatus.Opened)
{
_logger.Warn("射线已处于开启状态,忽略重复请求 | X-ray already opened, ignoring duplicate request");
return XRayResult.Ok("射线已开启");
}
lock (_lockObj)
{
try
{
// 获取锁后再次检查状态,防止重复开启 | Double-check status after acquiring lock
if (CurrentStatus == RaySourceStatus.Opened)
{
_logger.Warn("射线已处于开启状态,忽略重复请求 | X-ray already opened, ignoring duplicate request");
return XRayResult.Ok("射线已开启");
}
_logger.Info("开始开启射线 | Starting to turn on X-ray");
var result = _currentSource.TurnOn();
if (result.Success)
{
CurrentStatus = RaySourceStatus.Opened;
_logger.Info("射线开启成功,状态更新为 Opened | X-ray turned on successfully, status updated to Opened");
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(CurrentStatus);
// 灯丝记录由 OnRaySourceStatusChangedFromDevice 统一处理,避免重复调用
}
else
{
_logger.Error(null, "射线开启失败: {ErrorMessage} | X-ray turn on failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "开启射线异常: {Message} | Exception turning on X-ray: {Message}", ex.Message);
throw;
}
}
}
/// <summary>
/// 关闭射线 | Turn off X-ray
/// </summary>
public XRayResult TurnOff()
{
// 业务规则校验:未初始化禁止操作 | Business rule: operation forbidden if not initialized
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法关闭射线 | X-ray source not initialized, cannot turn off");
return XRayResult.Error("射线源未初始化,无法关闭射线");
}
// 快速检查:已关闭则直接返回,避免不必要的锁竞争 | Quick check: return early if already closed
if (CurrentStatus == RaySourceStatus.Closed)
{
_logger.Warn("射线已处于关闭状态,忽略重复请求 | X-ray already closed, ignoring duplicate request");
return XRayResult.Ok("射线已关闭");
}
lock (_lockObj)
{
try
{
// 获取锁后再次检查状态,防止重复关闭 | Double-check status after acquiring lock
if (CurrentStatus == RaySourceStatus.Closed)
{
_logger.Warn("射线已处于关闭状态,忽略重复请求 | X-ray already closed, ignoring duplicate request");
return XRayResult.Ok("射线已关闭");
}
_logger.Info("开始关闭射线 | Starting to turn off X-ray");
var result = _currentSource.TurnOff();
if (result.Success)
{
CurrentStatus = RaySourceStatus.Closed;
_logger.Info("射线关闭成功,状态更新为 Closed | X-ray turned off successfully, status updated to Closed");
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(CurrentStatus);
// 灯丝记录由 OnRaySourceStatusChangedFromDevice 统一处理,避免重复调用
}
else
{
_logger.Error(null, "射线关闭失败: {ErrorMessage} | X-ray turn off failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "关闭射线异常: {Message} | Exception turning off X-ray: {Message}", ex.Message);
throw;
}
}
}
/// <summary>
/// 断开射线源连接(保留实例以便重连)| Disconnect X-ray source (retain instance for reconnection)
/// </summary>
public XRayResult Disconnect()
{
lock (_lockObj)
{
try
{
_logger.Info("开始断开射线源连接 | Starting X-ray source disconnection");
if (_currentSource == null)
{
_logger.Warn("射线源实例不存在,无需断开 | X-ray source instance does not exist, no need to disconnect");
_isInitializedFlag = false;
CurrentStatus = RaySourceStatus.Unavailable;
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(CurrentStatus);
return XRayResult.Ok("射线源已断开");
}
// 同步调用 CloseOff 进行优雅关闭 | Call CloseOff for graceful shutdown
try
{
var closeResult = _currentSource.CloseOff();
if (!closeResult.Success)
{
_logger.Warn("优雅关闭返回失败: {ErrorMessage} | Graceful shutdown returned failure: {ErrorMessage}", closeResult.ErrorMessage);
}
}
catch (Exception ex)
{
_logger.Warn("优雅关闭异常: {Message} | Graceful shutdown exception: {Message}", ex.Message);
}
// 不 Dispose 实例,保留 PVI Service 以便重连 | Don't dispose instance, retain PVI Service for reconnection
_isInitializedFlag = false;
CurrentStatus = RaySourceStatus.Unavailable;
// 断开连接时停止灯丝使用记录 | Stop filament usage recording on disconnect
if (_filamentLifetimeService.IsInitialized && _filamentLifetimeService.IsFilamentOn)
{
_filamentLifetimeService.StopFilamentUsage();
_lastFilamentRecordedStatus = RaySourceStatus.Unavailable;
}
_logger.Info("射线源连接已断开,状态更新为 Unavailable | X-ray source disconnected, status updated to Unavailable");
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(CurrentStatus);
return XRayResult.Ok();
}
catch (Exception ex)
{
var errorMsg = $"断开连接异常: {ex.Message}";
_logger.Error(ex, "射线源断开连接异常: {Message} | X-ray source disconnection exception: {Message}", ex.Message);
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(errorMsg);
return XRayResult.Error(errorMsg);
}
}
}
/// <summary>
/// 紧急关闭(最高优先级,强制中断所有操作)| Emergency shutdown (highest priority, force interrupt all operations)
/// </summary>
public XRayResult EmergencyShutdown()
{
try
{
_logger.Warn("执行紧急关闭程序 | Executing emergency shutdown");
if (_currentSource != null)
{
// 先关闭射线 | Turn off X-ray first
_logger.Info("紧急关闭:关闭射线 | Emergency shutdown: turning off X-ray");
_currentSource.TurnOff();
// 完全关闭设备 | Fully shut down device
_logger.Info("紧急关闭:完全关闭设备 | Emergency shutdown: fully shutting down device");
_currentSource.CloseOff();
}
_isInitializedFlag = false;
CurrentStatus = RaySourceStatus.Unavailable;
// 紧急关闭时停止灯丝使用记录 | Stop filament usage recording on emergency shutdown
if (_filamentLifetimeService.IsInitialized && _filamentLifetimeService.IsFilamentOn)
{
_filamentLifetimeService.StopFilamentUsage();
_lastFilamentRecordedStatus = RaySourceStatus.Unavailable;
}
_logger.Info("紧急关闭完成,状态更新为 Unavailable | Emergency shutdown completed, status updated to Unavailable");
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Publish(CurrentStatus);
_eventAggregator.GetEvent<OperationResultEvent>()
.Publish(new OperationResultData("紧急关闭", true, "紧急关闭完成"));
return XRayResult.Ok("紧急关闭完成");
}
catch (Exception ex)
{
var errorMsg = $"紧急关闭异常: {ex.Message}";
_logger.Error(ex, "紧急关闭异常: {Message} | Emergency shutdown exception: {Message}", ex.Message);
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(errorMsg);
return XRayResult.Error(errorMsg);
}
}
/// <summary>
/// 设置电压 | Set voltage
/// </summary>
public XRayResult SetVoltage(float voltage)
{
// 业务规则校验:未初始化禁止操作 | Business rule: operation forbidden if not initialized
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法设置电压 | X-ray source not initialized, cannot set voltage");
return XRayResult.Error("射线源未初始化,无法设置电压");
}
// 参数范围校验 | Parameter range validation
if (voltage < _config.MinVoltage || voltage > _config.MaxVoltage)
{
_logger.Warn("电压参数超出范围: {Voltage}kV,有效范围: {Min}-{Max}kV | Voltage out of range: {Voltage}kV, valid range: {Min}-{Max}kV", voltage, _config.MinVoltage, _config.MaxVoltage);
return XRayResult.Error($"电压参数超出范围:{_config.MinVoltage}-{_config.MaxVoltage}kV");
}
lock (_lockObj)
{
try
{
_logger.Info("设置电压: {Voltage}kV | Setting voltage: {Voltage}kV", voltage);
var result = _currentSource.SetVoltage(voltage);
if (result.Success)
{
_logger.Info("电压设置成功: {Voltage}kV | Voltage set successfully: {Voltage}kV", voltage);
}
else
{
_logger.Error(null, "电压设置失败: {ErrorMessage} | Voltage setting failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "设置电压异常: {Message} | Exception setting voltage: {Message}", ex.Message);
throw;
}
}
}
/// <summary>
/// 设置电流 | Set current
/// </summary>
public XRayResult SetCurrent(float current)
{
// 业务规则校验:未初始化禁止操作 | Business rule: operation forbidden if not initialized
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法设置电流 | X-ray source not initialized, cannot set current");
return XRayResult.Error("射线源未初始化,无法设置电流");
}
// 参数范围校验 | Parameter range validation
if (current < _config.MinCurrent || current > _config.MaxCurrent)
{
_logger.Warn("电流参数超出范围: {Current}μA,有效范围: {Min}-{Max}μA | Current out of range: {Current}μA, valid range: {Min}-{Max}μA", current, _config.MinCurrent, _config.MaxCurrent);
return XRayResult.Error($"电流参数超出范围:{_config.MinCurrent}-{_config.MaxCurrent}μA");
}
lock (_lockObj)
{
try
{
_logger.Info("设置电流: {Current}μA | Setting current: {Current}μA", current);
var result = _currentSource.SetCurrent(current);
if (result.Success)
{
_logger.Info("电流设置成功: {Current}μA | Current set successfully: {Current}μA", current);
}
else
{
_logger.Error(null, "电流设置失败: {ErrorMessage} | Current setting failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "设置电流异常: {Message} | Exception setting current: {Message}", ex.Message);
throw;
}
}
}
/// <summary>
/// 读取实际电压 | Read actual voltage
/// </summary>
public XRayResult ReadVoltage()
{
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法读取电压 | X-ray source not initialized, cannot read voltage");
return XRayResult.Error("射线源未初始化");
}
try
{
_logger.Debug("读取实际电压 | Reading actual voltage");
var result = _currentSource.ReadVoltage();
if (result.Success)
{
_logger.Debug("电压读取成功: {Data}kV | Voltage read successfully: {Data}kV", result.Data);
}
else
{
_logger.Warn("电压读取失败: {ErrorMessage} | Voltage reading failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "读取电压异常: {Message} | Exception reading voltage: {Message}", ex.Message);
throw;
}
}
/// <summary>
/// 读取实际电流 | Read actual current
/// </summary>
public XRayResult ReadCurrent()
{
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法读取电流 | X-ray source not initialized, cannot read current");
return XRayResult.Error("射线源未初始化");
}
try
{
_logger.Debug("读取实际电流 | Reading actual current");
var result = _currentSource.ReadCurrent();
if (result.Success)
{
_logger.Debug("电流读取成功: {Data}μA | Current read successfully: {Data}μA", result.Data);
}
else
{
_logger.Warn("电流读取失败: {ErrorMessage} | Current reading failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "读取电流异常: {Message} | Exception reading current: {Message}", ex.Message);
throw;
}
}
/// <summary>
/// 读取系统状态 | Read system status
/// </summary>
public XRayResult ReadSystemStatus()
{
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法读取系统状态 | X-ray source not initialized, cannot read system status");
return XRayResult.Error("射线源未初始化");
}
try
{
_logger.Debug("读取系统状态 | Reading system status");
return _currentSource.ReadSystemStatus();
}
catch (Exception ex)
{
_logger.Error(ex, "读取系统状态异常: {Message} | Exception reading system status: {Message}", ex.Message);
throw;
}
}
/// <summary>
/// 检查错误 | Check errors
/// </summary>
public XRayResult CheckErrors()
{
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法检查错误 | X-ray source not initialized, cannot check errors");
return XRayResult.Error("射线源未初始化");
}
try
{
_logger.Debug("检查错误状态 | Checking error status");
return _currentSource.CheckErrors();
}
catch (Exception ex)
{
_logger.Error(ex, "检查错误异常: {Message} | Exception checking errors: {Message}", ex.Message);
throw;
}
}
/// <summary>
/// 清除错误 | Clear errors
/// </summary>
public XRayResult ClearErrors()
{
if (!IsInitialized || _currentSource == null)
{
_logger.Warn("射线源未初始化,无法清除错误 | X-ray source not initialized, cannot clear errors");
return XRayResult.Error("射线源未初始化");
}
try
{
_logger.Info("开始清除错误 | Starting to clear errors");
_eventAggregator.GetEvent<OperationResultEvent>()
.Publish(new OperationResultData("清除错误", true));
_logger.Info("错误清除完成 | Errors cleared successfully");
return XRayResult.Ok("错误已清除");
}
catch (Exception ex)
{
var errorMsg = $"清除错误失败: {ex.Message}";
_logger.Error(ex, "清除错误异常: {Message} | Exception clearing errors: {Message}", ex.Message);
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(errorMsg);
return XRayResult.Error(errorMsg);
}
}
/// <summary>
/// TXI 开启 | TXI On
/// </summary>
public XRayResult TxiOn()
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法执行 TXI ON | X-ray source not connected, cannot execute TXI ON");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始执行 TXI ON | Starting TXI ON");
var result = _currentSource.TxiOn();
if (result.Success)
{
_logger.Info("TXI ON 执行成功 | TXI ON executed successfully");
}
else
{
_logger.Error(null, "TXI ON 执行失败: {ErrorMessage} | TXI ON failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "TXI ON 异常: {Message} | TXI ON exception: {Message}", ex.Message);
return XRayResult.Error($"TXI ON 异常:{ex.Message}");
}
}
}
/// <summary>
/// TXI 关闭 | TXI Off
/// </summary>
public XRayResult TxiOff()
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法执行 TXI OFF | X-ray source not connected, cannot execute TXI OFF");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始执行 TXI OFF | Starting TXI OFF");
var result = _currentSource.TxiOff();
if (result.Success)
{
_logger.Info("TXI OFF 执行成功 | TXI OFF executed successfully");
}
else
{
_logger.Error(null, "TXI OFF 执行失败: {ErrorMessage} | TXI OFF failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "TXI OFF 异常: {Message} | TXI OFF exception: {Message}", ex.Message);
return XRayResult.Error($"TXI OFF 异常:{ex.Message}");
}
}
}
/// <summary>
/// 暖机设置 | Warm-up setting
/// </summary>
public XRayResult WarmUp()
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法执行暖机 | X-ray source not connected, cannot execute warm-up");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始执行暖机设置 | Starting warm-up setting");
var result = _currentSource.WarmUp();
if (result.Success)
{
_logger.Info("暖机设置已发送 | Warm-up setting sent successfully");
}
else
{
_logger.Error(null, "暖机设置失败: {ErrorMessage} | Warm-up setting failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "暖机设置异常: {Message} | Warm-up setting exception: {Message}", ex.Message);
return XRayResult.Error($"暖机设置异常:{ex.Message}");
}
}
}
/// <summary>
/// 训机设置 | Training setting
/// </summary>
public XRayResult Training()
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法执行训机 | X-ray source not connected, cannot execute training");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始执行训机设置 | Starting training setting");
var result = _currentSource.Training();
if (result.Success)
{
_logger.Info("训机设置已发送 | Training setting sent successfully");
}
else
{
_logger.Error(null, "训机设置失败: {ErrorMessage} | Training setting failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "训机设置异常: {Message} | Training setting exception: {Message}", ex.Message);
return XRayResult.Error($"训机设置异常:{ex.Message}");
}
}
}
/// <summary>
/// 灯丝校准 | Filament calibration
/// </summary>
public XRayResult FilamentCalibration()
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法执行灯丝校准 | X-ray source not connected, cannot execute filament calibration");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始执行灯丝校准 | Starting filament calibration");
var result = _currentSource.FilamentCalibration();
if (result.Success)
{
_logger.Info("灯丝校准已发送 | Filament calibration sent successfully");
}
else
{
_logger.Error(null, "灯丝校准失败: {ErrorMessage} | Filament calibration failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "灯丝校准异常: {Message} | Filament calibration exception: {Message}", ex.Message);
return XRayResult.Error($"灯丝校准异常:{ex.Message}");
}
}
}
/// <summary>
/// 全部电压自动定心 | Auto-center all voltages
/// </summary>
public XRayResult AutoCenter()
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法执行自动定心 | X-ray source not connected, cannot execute auto-center");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始执行全部电压自动定心 | Starting auto-center all voltages");
var result = _currentSource.AutoCenter();
if (result.Success)
{
_logger.Info("自动定心已发送 | Auto-center sent successfully");
}
else
{
_logger.Error(null, "自动定心失败: {ErrorMessage} | Auto-center failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "自动定心异常: {Message} | Auto-center exception: {Message}", ex.Message);
return XRayResult.Error($"自动定心异常:{ex.Message}");
}
}
}
/// <summary>
/// 设置功率模式 | Set power mode
/// </summary>
/// <param name="mode">功率模式值:1=Micro Focus2=High Power</param>
public XRayResult SetPowerMode(int mode)
{
if (!IsConnected || _currentSource == null)
{
_logger.Warn("射线源未连接,无法设置功率模式 | X-ray source not connected, cannot set power mode");
return XRayResult.Error("射线源未连接");
}
lock (_lockObj)
{
try
{
_logger.Info("开始设置功率模式: Mode={Mode} | Starting to set power mode: Mode={Mode}", mode);
var result = _currentSource.SetPowerMode(mode);
if (result.Success)
{
_logger.Info("功率模式设置成功: Mode={Mode} | Power mode set successfully: Mode={Mode}", mode);
}
else
{
_logger.Error(null, "功率模式设置失败: {ErrorMessage} | Power mode setting failed: {ErrorMessage}", result.ErrorMessage);
}
return result;
}
catch (Exception ex)
{
_logger.Error(ex, "功率模式设置异常: {Message} | Power mode setting exception: {Message}", ex.Message);
return XRayResult.Error($"功率模式设置异常:{ex.Message}");
}
}
}
/// <summary>
/// 异步执行初始化 + 连接变量的完整流程 | Async full sequence: Initialize + ConnectVariables
/// </summary>
public async Task<XRayResult> InitializeAndConnectAsync()
{
return await Task.Run(() =>
{
try
{
// 步骤1:初始化 | Step 1: Initialize
if (!IsInitialized)
{
_logger.Info("自动初始化:执行 Initialize | Auto-init: executing Initialize");
var initResult = Initialize();
if (!initResult.Success)
{
_logger.Warn("自动初始化失败: {Error} | Auto-initialization failed: {Error}", initResult.ErrorMessage);
return initResult;
}
_logger.Info("自动初始化成功 | Auto-initialization succeeded");
}
// 步骤2:连接变量 | Step 2: Connect Variables
if (!IsConnected)
{
_logger.Info("自动连接变量:执行 ConnectVariables | Auto-init: executing ConnectVariables");
var connectResult = ConnectVariables();
if (!connectResult.Success)
{
_logger.Warn("自动连接变量失败: {Error} | Auto connect variables failed: {Error}", connectResult.ErrorMessage);
return connectResult;
}
_logger.Info("自动连接变量成功 | Auto connect variables succeeded");
}
_logger.Info("自动初始化流程完成 | Auto-initialization sequence completed");
return XRayResult.Ok();
}
catch (Exception ex)
{
_logger.Error(ex, "自动初始化流程异常: {Message} | Auto-initialization sequence exception: {Message}", ex.Message);
return XRayResult.Error($"自动初始化异常: {ex.Message}");
}
});
}
/// <summary>
/// 射线源状态变更事件处理(设备层回调)| Ray source status changed event handler (device callback)
/// </summary>
private void OnRaySourceStatusChangedFromDevice(RaySourceStatus newStatus)
{
// 更新当前状态(IsXRayOn 由 CurrentStatus 自动派生)| Update current status (IsXRayOn is auto-derived from CurrentStatus)
CurrentStatus = newStatus;
_logger.Info("服务层状态更新: {Status} | Service layer status updated: {Status}", newStatus);
// 根据射线源状态变更记录灯丝使用(带去重,防止设备推送抖动)| Record filament usage with dedup
if (_filamentLifetimeService.IsInitialized && newStatus != _lastFilamentRecordedStatus)
{
if (newStatus == RaySourceStatus.Opened && !_filamentLifetimeService.IsFilamentOn)
{
if (_filamentLifetimeService.StartFilamentUsage())
_lastFilamentRecordedStatus = newStatus;
}
else if (newStatus != RaySourceStatus.Opened && _filamentLifetimeService.IsFilamentOn)
{
if (_filamentLifetimeService.StopFilamentUsage())
_lastFilamentRecordedStatus = newStatus;
}
}
if (newStatus == RaySourceStatus.Unavailable && _isInitializedFlag)
{
// 异常断联处理 | Handle unexpected disconnection
_logger.Error(null, "服务层检测到连接丢失(状态变为 Unavailable| Service layer detected connection lost (status changed to Unavailable)");
_isInitializedFlag = false;
// 通知 UI 更新状态 | Notify UI to update status
_eventAggregator.GetEvent<StatusUpdatedEvent>().Publish(new SystemStatusData
{
IsXRayOn = false,
ActualVoltage = 0,
ActualCurrent = 0
});
}
}
/// <summary>
/// 释放资源 | Dispose resources
/// </summary>
public void Dispose()
{
if (_isDisposed)
return;
try
{
// 先执行紧急关闭 | Execute emergency shutdown first
EmergencyShutdown();
// 释放射线源实例 | Dispose X-ray source instance
_currentSource?.Dispose();
_currentSource = null;
_isDisposed = true;
_logger.Info("服务层资源已释放 | Service layer resources disposed");
}
catch (Exception ex)
{
_logger.Warn("服务层资源释放异常: {Message} | Service layer resource disposal exception: {Message}", ex.Message);
}
}
}
}
@@ -0,0 +1,123 @@
using XP.Common.Localization.Interfaces;
namespace XP.Hardware.RaySource.Services
{
/// <summary>
/// 设备状态码多语言映射工具 | Device status code localization mapper
/// 将 Comet.Host 传递的原始 code 码映射为多语言显示文本
/// </summary>
public static class StatusCodeMapper
{
/// <summary>
/// 暖机状态码映射 | Warm-up status code mapping
/// </summary>
public static string MapWarmUpStatus(string code, ILocalizationService loc)
{
return code switch
{
"0" => loc.GetString("RaySource_Status_WarmUp_0") ?? "初始状态",
"1" => loc.GetString("RaySource_Status_WarmUp_1") ?? "暖机中",
"2" => loc.GetString("RaySource_Status_WarmUp_2") ?? "暖机完成",
"3" => loc.GetString("RaySource_Status_WarmUp_3") ?? "暖机失败",
"33" => loc.GetString("RaySource_Status_WarmUp_33") ?? "需要暖机",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
/// <summary>
/// 真空状态码映射 | Vacuum status code mapping
/// </summary>
public static string MapVacuumStatus(string code, ILocalizationService loc)
{
return code switch
{
"0" => loc.GetString("RaySource_Status_Vacuum_0") ?? "初始状态",
"1" => loc.GetString("RaySource_Status_Vacuum_1") ?? "抽真空中",
"2" => loc.GetString("RaySource_Status_Vacuum_2") ?? "真空准备好",
"22" => loc.GetString("RaySource_Status_Vacuum_22") ?? "真空值<1e-5mbar",
"30" => loc.GetString("RaySource_Status_Vacuum_30") ?? "真空泵未使能",
"40" => loc.GetString("RaySource_Status_Vacuum_40") ?? "真空泵通风",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
/// <summary>
/// 训机状态码映射 | Training status code mapping
/// </summary>
public static string MapStartUpStatus(string code, ILocalizationService loc)
{
return code switch
{
"0" => loc.GetString("RaySource_Status_StartUp_0") ?? "初始状态",
"1" => loc.GetString("RaySource_Status_StartUp_1") ?? "训机中",
"2" => loc.GetString("RaySource_Status_StartUp_2") ?? "训机完成",
"3" => loc.GetString("RaySource_Status_StartUp_3") ?? "训机失败",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
/// <summary>
/// 自动定心状态码映射 | Auto-center status code mapping
/// </summary>
public static string MapAutoCenterStatus(string code, ILocalizationService loc)
{
return code switch
{
"0" => loc.GetString("RaySource_Status_AutoCenter_0") ?? "初始状态",
"1" => loc.GetString("RaySource_Status_AutoCenter_1") ?? "自动定心中",
"2" => loc.GetString("RaySource_Status_AutoCenter_2") ?? "自动定心完成",
"3" => loc.GetString("RaySource_Status_AutoCenter_3") ?? "定心失败",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
/// <summary>
/// 灯丝调整状态码映射 | Filament adjust status code mapping
/// </summary>
public static string MapFilamentAdjustStatus(string code, ILocalizationService loc)
{
return code switch
{
"0" => loc.GetString("RaySource_Status_Filament_0") ?? "初始状态",
"1" => loc.GetString("RaySource_Status_Filament_1") ?? "灯丝校准中",
"2" => loc.GetString("RaySource_Status_Filament_2") ?? "灯丝校准完成",
"3" => loc.GetString("RaySource_Status_Filament_3") ?? "灯丝校准失败",
"33" => loc.GetString("RaySource_Status_Filament_33") ?? "需要灯丝校准",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
/// <summary>
/// 功率模式码映射 | Power mode code mapping
/// </summary>
public static string MapPowerMode(string code, ILocalizationService loc)
{
var trimmed = (code ?? "").Trim();
if (int.TryParse(trimmed, out var intVal))
{
return intVal switch
{
1 => loc.GetString("RaySource_Status_PowerMode_1") ?? "Micro Focus",
2 => loc.GetString("RaySource_Status_PowerMode_2") ?? "High Power",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
if (string.IsNullOrEmpty(trimmed))
return "--";
return loc.GetString("RaySource_Status_Unknown") ?? "未知状态";
}
/// <summary>
/// TXI 状态码映射 | TXI status code mapping
/// </summary>
public static string MapTxiStatus(string code, ILocalizationService loc)
{
return code switch
{
"True" => loc.GetString("RaySource_Status_TXI_On") ?? "TXI 开启",
"False" => loc.GetString("RaySource_Status_TXI_Off") ?? "TXI 关闭",
_ => loc.GetString("RaySource_Status_Unknown") ?? "未知状态"
};
}
}
}
@@ -0,0 +1,919 @@
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using System.Threading;
using XP.Common.GeneralForm.Views;
using XP.Common.Localization.Interfaces;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Abstractions.Enums;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Config;
using XP.Hardware.RaySource.Services;
namespace XP.Hardware.RaySource.ViewModels
{
/// <summary>
/// 射线源配置视图模型 | X-Ray Source Config ViewModel
/// 负责射线源初始化、连接、断开及设备状态监控 | Responsible for X-ray source init, connect, disconnect and device status monitoring
/// </summary>
public class RaySourceConfigViewModel : BindableBase, IDisposable
{
#region | Fields
private readonly IRaySourceService _raySourceService;
private readonly IEventAggregator _eventAggregator;
private readonly RaySourceConfig _config;
private readonly ILoggerService _logger;
private readonly ILocalizationService _localizationService;
private readonly IFilamentLifetimeService _filamentLifetimeService;
private bool _isOperating = false;
private bool _isDisposed = false;
/// <summary>
/// 灯丝寿命定时刷新定时器(60 秒间隔)| Filament lifetime refresh timer (60-second interval)
/// </summary>
private DispatcherTimer _lifetimeRefreshTimer;
#endregion
#region | Connection Properties
/// <summary>
/// 是否已初始化(代理到服务层)| Whether initialized (delegated to service layer)
/// </summary>
public bool IsInitialized => _raySourceService.IsInitialized;
/// <summary>
/// 通知 IsInitialized 变更并刷新相关命令状态 | Notify IsInitialized changed and refresh related command states
/// </summary>
private void NotifyInitializedChanged()
{
RaisePropertyChanged(nameof(IsInitialized));
InitializeCommand?.RaiseCanExecuteChanged();
ConnectVariablesCommand?.RaiseCanExecuteChanged();
DisconnectCommand?.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(ConnectionStatusText));
}
/// <summary>
/// 射线源是否已建立完整连接(代理到服务层)| Whether fully connected (delegated to service layer)
/// </summary>
public bool IsConnected => _raySourceService.IsConnected;
/// <summary>
/// 通知 IsConnected 变更并刷新相关命令状态 | Notify IsConnected changed and refresh related command states
/// </summary>
private void NotifyConnectedChanged()
{
RaisePropertyChanged(nameof(IsConnected));
ConnectVariablesCommand?.RaiseCanExecuteChanged();
DisconnectCommand?.RaiseCanExecuteChanged();
RaisePropertyChanged(nameof(ConnectionStatusText));
}
/// <summary>
/// 连接状态文本 | Connection status text
/// </summary>
public string ConnectionStatusText
{
get
{
if (IsConnected)
return _localizationService.GetString("RaySource_VariablesConnected") ?? "变量已连接";
if (IsInitialized)
return _localizationService.GetString("RaySource_Connected") ?? "已连接";
return _localizationService.GetString("RaySource_Disconnected") ?? "未连接";
}
}
/// <summary>
/// 射线源类型显示 | Ray source type display
/// </summary>
public string SourceTypeText => _config.SourceType;
#endregion
#region | Device Status Properties
private string _warmUpStatus = "--";
/// <summary>
/// 暖机状态 | Warm-up status
/// </summary>
public string WarmUpStatus
{
get => _warmUpStatus;
private set => SetProperty(ref _warmUpStatus, value);
}
private string _vacuumStatus = "--";
/// <summary>
/// 真空状态 | Vacuum status
/// </summary>
public string VacuumStatus
{
get => _vacuumStatus;
private set => SetProperty(ref _vacuumStatus, value);
}
private string _startUpStatus = "--";
/// <summary>
/// 启动状态 | Startup status
/// </summary>
public string StartUpStatus
{
get => _startUpStatus;
private set => SetProperty(ref _startUpStatus, value);
}
private string _autoCenterStatus = "--";
/// <summary>
/// 自动定心状态 | Auto-center status
/// </summary>
public string AutoCenterStatus
{
get => _autoCenterStatus;
private set => SetProperty(ref _autoCenterStatus, value);
}
private string _filamentAdjustStatus = "--";
/// <summary>
/// 灯丝校准状态 | Filament adjust status
/// </summary>
public string FilamentAdjustStatus
{
get => _filamentAdjustStatus;
private set => SetProperty(ref _filamentAdjustStatus, value);
}
private string _powerMode = "--";
/// <summary>
/// 功率模式 | Power mode
/// </summary>
public string PowerMode
{
get => _powerMode;
private set => SetProperty(ref _powerMode, value);
}
private bool _isInterlockActive;
/// <summary>
/// 连锁是否激活 | Whether interlock is active
/// </summary>
public bool IsInterlockActive
{
get => _isInterlockActive;
private set
{
if (SetProperty(ref _isInterlockActive, value))
{
RaisePropertyChanged(nameof(InterlockStatusText));
RefreshCommandStates();
}
}
}
/// <summary>
/// 连锁状态文本 | Interlock status text
/// </summary>
public string InterlockStatusText => IsInterlockActive
? (_localizationService.GetString("RaySource_InterlockActive") ?? "激活")
: (_localizationService.GetString("RaySource_InterlockNormal") ?? "未激活");
private string _watchdogStatus = "--";
/// <summary>
/// 看门狗状态 | Watchdog status
/// </summary>
public string WatchdogStatus
{
get => _watchdogStatus;
private set => SetProperty(ref _watchdogStatus, value);
}
private bool _isXRayOn;
/// <summary>
/// 射线开启状态 | X-ray on status
/// </summary>
public bool IsXRayOn
{
get => _isXRayOn;
private set
{
if (SetProperty(ref _isXRayOn, value))
RaisePropertyChanged(nameof(XRayOnStatusText));
}
}
/// <summary>
/// 射线开启状态文本 | X-ray on status text
/// </summary>
public string XRayOnStatusText => IsXRayOn
? (_localizationService.GetString("RaySource_XRayOn") ?? "开启")
: (_localizationService.GetString("RaySource_XRayOff") ?? "关闭");
private string _txiStatus = "--";
/// <summary>
/// TXI状态 | TXI status
/// </summary>
public string TxiStatus
{
get => _txiStatus;
private set => SetProperty(ref _txiStatus, value);
}
private double _filamentLifetime;
/// <summary>
/// 灯丝寿命百分比(0-100| Filament lifetime percentage (0-100)
/// </summary>
public double FilamentLifetime
{
get => _filamentLifetime;
private set => SetProperty(ref _filamentLifetime, value);
}
private string _filamentLifetimeText = "";
/// <summary>
/// 灯丝寿命显示文本(如 "123.4h / 1000.0h (12.3%)"
/// Filament lifetime display text (e.g. "123.4h / 1000.0h (12.3%)")
/// </summary>
public string FilamentLifetimeText
{
get => _filamentLifetimeText;
private set => SetProperty(ref _filamentLifetimeText, value);
}
#endregion
#region | Commands
public DelegateCommand InitializeCommand { get; }
public DelegateCommand ConnectVariablesCommand { get; }
public DelegateCommand DisconnectCommand { get; }
public DelegateCommand TxiOnCommand { get; }
public DelegateCommand TxiOffCommand { get; }
public DelegateCommand HighPowerCommand { get; }
public DelegateCommand MicroFocusCommand { get; }
public DelegateCommand WarmUpSettingCommand { get; }
public DelegateCommand TrainingSettingCommand { get; }
public DelegateCommand FilamentCalibrationCommand { get; }
public DelegateCommand AutoCenterSettingCommand { get; }
#endregion
#region | Constructor
public RaySourceConfigViewModel(
IRaySourceService raySourceService,
IEventAggregator eventAggregator,
RaySourceConfig config,
ILoggerService logger,
ILocalizationService localizationService,
IFilamentLifetimeService filamentLifetimeService)
{
_raySourceService = raySourceService ?? throw new ArgumentNullException(nameof(raySourceService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule("RaySource.ConfigVM");
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
_filamentLifetimeService = filamentLifetimeService ?? throw new ArgumentNullException(nameof(filamentLifetimeService));
InitializeCommand = new DelegateCommand(ExecuteInitialize, CanExecuteInitialize);
ConnectVariablesCommand = new DelegateCommand(ExecuteConnectVariables, CanExecuteConnectVariables);
DisconnectCommand = new DelegateCommand(ExecuteDisconnect, CanExecuteDisconnect);
TxiOnCommand = new DelegateCommand(ExecuteTxiOn, CanExecuteDeviceCommand);
TxiOffCommand = new DelegateCommand(ExecuteTxiOff, CanExecuteDeviceCommand);
HighPowerCommand = new DelegateCommand(ExecuteHighPower, CanExecuteDeviceCommand);
MicroFocusCommand = new DelegateCommand(ExecuteMicroFocus, CanExecuteDeviceCommand);
WarmUpSettingCommand = new DelegateCommand(ExecuteWarmUpSetting, CanExecuteDeviceCommand);
TrainingSettingCommand = new DelegateCommand(ExecuteTrainingSetting, CanExecuteDeviceCommand);
FilamentCalibrationCommand = new DelegateCommand(ExecuteFilamentCalibration, CanExecuteDeviceCommand);
AutoCenterSettingCommand = new DelegateCommand(ExecuteAutoCenterSetting, CanExecuteDeviceCommand);
// 订阅事件 | Subscribe events
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Subscribe(OnRaySourceStatusChanged, ThreadOption.UIThread);
_eventAggregator.GetEvent<StatusUpdatedEvent>().Subscribe(OnStatusUpdated, ThreadOption.UIThread);
_eventAggregator.GetEvent<VariablesConnectedEvent>().Subscribe(OnVariablesConnectedChanged, ThreadOption.UIThread);
// 同步当前状态 | Sync current state
NotifyInitializedChanged();
NotifyConnectedChanged();
// 初始刷新灯丝寿命数据 | Initial refresh of filament lifetime data
RefreshFilamentLifetime();
// 启动灯丝寿命定时刷新(60 秒间隔)| Start filament lifetime periodic refresh (60-second interval)
StartLifetimeRefreshTimer();
_logger.Info("射线源配置视图模型已创建 | X-ray source config view model created");
}
#endregion
#region | Event Handlers
private void OnRaySourceStatusChanged(RaySourceStatus newStatus)
{
NotifyInitializedChanged();
if (newStatus == RaySourceStatus.Unavailable && IsConnected)
{
NotifyConnectedChanged();
ResetDeviceStatus();
}
}
/// <summary>
/// 状态更新事件处理:刷新设备状态面板 | Status updated event handler: refresh device status panel
/// </summary>
private void OnStatusUpdated(SystemStatusData data)
{
if (data == null) return;
// 使用 StatusCodeMapper 将原始 code 映射为多语言文本 | Map raw codes to localized text
WarmUpStatus = StatusCodeMapper.MapWarmUpStatus(data.WarmUpStatus, _localizationService);
VacuumStatus = StatusCodeMapper.MapVacuumStatus(data.VacuumStatus, _localizationService);
StartUpStatus = StatusCodeMapper.MapStartUpStatus(data.StartUpStatus, _localizationService);
AutoCenterStatus = StatusCodeMapper.MapAutoCenterStatus(data.AutoCenterStatus, _localizationService);
FilamentAdjustStatus = StatusCodeMapper.MapFilamentAdjustStatus(data.FilamentAdjustStatus, _localizationService);
PowerMode = StatusCodeMapper.MapPowerMode(data.PowerMode, _localizationService);
IsInterlockActive = data.IsInterlockActive;
WatchdogStatus = data.WatchdogStatus ?? "--";
IsXRayOn = data.IsXRayOn;
TxiStatus = StatusCodeMapper.MapTxiStatus(data.TxiStatus, _localizationService);
}
/// <summary>
/// 重置设备状态为默认值 | Reset device status to defaults
/// </summary>
private void ResetDeviceStatus()
{
WarmUpStatus = "--";
VacuumStatus = "--";
StartUpStatus = "--";
AutoCenterStatus = "--";
FilamentAdjustStatus = "--";
PowerMode = "--";
IsInterlockActive = false;
WatchdogStatus = "--";
IsXRayOn = false;
TxiStatus = "--";
FilamentLifetime = 0;
FilamentLifetimeText = "";
}
/// <summary>
/// 变量连接状态变更事件处理 | Variables connected state changed event handler
/// </summary>
private void OnVariablesConnectedChanged(bool isConnected)
{
NotifyConnectedChanged();
// 同步初始化状态(自动初始化后服务状态可能已变)| Sync init state (service state may have changed after auto-init)
NotifyInitializedChanged();
_logger.Info("ConfigVM 收到变量连接状态变更: {IsConnected} | ConfigVM received variables connection state change: {IsConnected}", isConnected);
}
#endregion
#region | Command Execution Logic
/// <summary>
/// 初始化命令执行逻辑 | Initialize command execution logic
/// </summary>
private async void ExecuteInitialize()
{
_isOperating = true;
RefreshCommandStates();
try
{
_logger.Info("用户请求初始化射线源 | User requested to initialize X-ray source");
var result = await Task.Run(() => _raySourceService.Initialize());
if (result.Success)
{
NotifyInitializedChanged();
_logger.Info("射线源初始化成功 | X-ray source initialized successfully");
}
else
{
_logger.Warn("射线源初始化失败: {Message} | X-ray source initialization failed: {Message}", result.ErrorMessage);
MessageBox.Show($"初始化失败: {result.ErrorMessage}", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "初始化射线源异常: {Message} | Exception initializing X-ray source: {Message}", ex.Message);
MessageBox.Show($"初始化异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_isOperating = false;
RefreshCommandStates();
}
}
/// <summary>
/// 初始化命令是否可执行 | Can execute initialize command
/// </summary>
private bool CanExecuteInitialize() => !_isOperating && !IsInitialized;
/// <summary>
/// 连接变量命令执行逻辑 | Connect variables command execution logic
/// </summary>
private async void ExecuteConnectVariables()
{
_isOperating = true;
RefreshCommandStates();
try
{
_logger.Info("用户请求连接 PVI 变量 | User requested to connect PVI variables");
var result = await Task.Run(() => _raySourceService.ConnectVariables());
if (result.Success)
{
// 不在此处设置 IsConnected,等待设备层 RaySourceConnected 推送通过 VariablesConnectedEvent 事件确认
// IsConnected will be set by VariablesConnectedEvent from device layer RaySourceConnected push
_logger.Info("PVI 变量连接命令已发送,等待设备确认 | PVI variable connection command sent, waiting for device confirmation");
}
else
{
_logger.Warn("PVI 变量连接失败: {Message} | PVI variables connection failed: {Message}", result.ErrorMessage);
MessageBox.Show($"连接变量失败: {result.ErrorMessage}", "错误", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "连接变量异常: {Message} | Exception connecting variables: {Message}", ex.Message);
MessageBox.Show($"连接变量异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_isOperating = false;
RefreshCommandStates();
}
}
/// <summary>
/// 连接变量命令是否可执行 | Can execute connect variables command
/// </summary>
private bool CanExecuteConnectVariables() => !_isOperating && IsInitialized && !IsConnected;
/// <summary>
/// 断开命令执行逻辑 | Disconnect command execution logic
/// </summary>
private async void ExecuteDisconnect()
{
_isOperating = true;
RefreshCommandStates();
try
{
_logger.Info("用户请求断开射线源 | User requested to disconnect X-ray source");
var result = await Task.Run(() => _raySourceService.Disconnect());
if (result.Success)
{
NotifyConnectedChanged();
NotifyInitializedChanged();
_eventAggregator.GetEvent<VariablesConnectedEvent>().Publish(false);
ResetDeviceStatus();
_logger.Info("射线源已断开 | X-ray source disconnected");
}
else
{
_logger.Warn("断开射线源失败: {Error} | Disconnect X-ray source failed: {Error}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage,
_localizationService.GetString("Message_OperationFailed") ?? "操作失败",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
_logger.Error(ex, "断开射线源异常: {Message} | Exception disconnecting X-ray source: {Message}", ex.Message);
MessageBox.Show($"断开异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_isOperating = false;
RefreshCommandStates();
}
}
/// <summary>
/// 断开命令是否可执行 | Can execute disconnect command
/// </summary>
private bool CanExecuteDisconnect() => !_isOperating && IsInitialized;
/// <summary>
/// 刷新所有命令的可执行状态 | Refresh all command CanExecute states
/// </summary>
private void RefreshCommandStates()
{
InitializeCommand?.RaiseCanExecuteChanged();
ConnectVariablesCommand?.RaiseCanExecuteChanged();
DisconnectCommand?.RaiseCanExecuteChanged();
TxiOnCommand?.RaiseCanExecuteChanged();
TxiOffCommand?.RaiseCanExecuteChanged();
HighPowerCommand?.RaiseCanExecuteChanged();
MicroFocusCommand?.RaiseCanExecuteChanged();
WarmUpSettingCommand?.RaiseCanExecuteChanged();
TrainingSettingCommand?.RaiseCanExecuteChanged();
FilamentCalibrationCommand?.RaiseCanExecuteChanged();
AutoCenterSettingCommand?.RaiseCanExecuteChanged();
}
/// <summary>
/// 设备命令是否可执行(需已连接变量)| Can execute device command (requires variables connected)
/// </summary>
private bool CanExecuteDeviceCommand() => !_isOperating && IsConnected && IsInitialized && IsInterlockActive;
/// <summary>
/// TXI 开启 | TXI On
/// </summary>
private void ExecuteTxiOn()
{
_logger.Info("用户请求 TXI ON | User requested TXI ON");
try
{
var result = _raySourceService.TxiOn();
if (!result.Success)
{
_logger.Error(null, "TXI ON 失败: {ErrorMessage}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage,
_localizationService.GetString("RaySource_OperationFailed") ?? "操作失败",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "TXI ON 异常: {Message}", ex.Message);
}
}
/// <summary>
/// TXI 关闭 | TXI Off
/// </summary>
private void ExecuteTxiOff()
{
_logger.Info("用户请求 TXI OFF | User requested TXI OFF");
try
{
var result = _raySourceService.TxiOff();
if (!result.Success)
{
_logger.Error(null, "TXI OFF 失败: {ErrorMessage}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage,
_localizationService.GetString("RaySource_OperationFailed") ?? "操作失败",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "TXI OFF 异常: {Message}", ex.Message);
}
}
/// <summary>
/// 切换到高功率模式 | Switch to High Power mode
/// </summary>
private void ExecuteHighPower()
{
_logger.Info("用户请求切换到 High Power 模式 | User requested switch to High Power mode");
try
{
var result = _raySourceService.SetPowerMode(2);
if (!result.Success)
{
_logger.Error(null, "切换 High Power 模式失败: {ErrorMessage}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage,
_localizationService.GetString("RaySource_OperationFailed") ?? "操作失败",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "切换 High Power 模式异常: {Message}", ex.Message);
}
}
/// <summary>
/// 切换到微焦点模式 | Switch to Micro Focus mode
/// </summary>
private void ExecuteMicroFocus()
{
_logger.Info("用户请求切换到 Micro Focus 模式 | User requested switch to Micro Focus mode");
try
{
var result = _raySourceService.SetPowerMode(1);
if (!result.Success)
{
_logger.Error(null, "切换 Micro Focus 模式失败: {ErrorMessage}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage,
_localizationService.GetString("RaySource_OperationFailed") ?? "操作失败",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
_logger.Error(ex, "切换 Micro Focus 模式异常: {Message}", ex.Message);
}
}
/// <summary>
/// 暖机设置 | Warm-up setting
/// 参照旧代码:确认对话框 → 显示进度条 → 发送暖机命令
/// </summary>
private async void ExecuteWarmUpSetting()
{
_logger.Info("用户打开暖机设置 | User opened warm-up setting");
var confirmResult = MessageBox.Show(
_localizationService.GetString("RaySource_WarmUp_Confirm") ?? "是否确认执行暖机操作?",
_localizationService.GetString("RaySource_Confirm_Title") ?? "确认",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (confirmResult != MessageBoxResult.Yes)
{
_logger.Info("用户取消暖机操作 | User cancelled warm-up operation");
return;
}
await ExecuteLongRunningOperation(
_localizationService.GetString("RaySource_WarmUp_Title") ?? "暖机设置",
_localizationService.GetString("RaySource_WarmUp_Message") ?? "正在执行暖机操作,请稍候...",
() => _raySourceService.WarmUp(),
OperationType.WarmUp,
_config.WarmUpTimeout);
}
/// <summary>
/// 训机设置 | Training setting
/// 参照旧代码:确认对话框 → 显示进度条 → 发送训机命令
/// </summary>
private async void ExecuteTrainingSetting()
{
_logger.Info("用户打开训机设置 | User opened training setting");
var confirmResult = MessageBox.Show(
_localizationService.GetString("RaySource_Training_Confirm") ?? "是否确认执行训机操作?",
_localizationService.GetString("RaySource_Confirm_Title") ?? "确认",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (confirmResult != MessageBoxResult.Yes)
{
_logger.Info("用户取消训机操作 | User cancelled training operation");
return;
}
await ExecuteLongRunningOperation(
_localizationService.GetString("RaySource_Training_Title") ?? "训机设置",
_localizationService.GetString("RaySource_Training_Message") ?? "正在执行训机操作,请稍候...",
() => _raySourceService.Training(),
OperationType.Training,
_config.StartUpTimeout);
}
/// <summary>
/// 灯丝校准 | Filament calibration
/// 参照旧代码:确认对话框 → 显示进度条 → 发送灯丝校准命令
/// </summary>
private async void ExecuteFilamentCalibration()
{
_logger.Info("用户请求灯丝校准 | User requested filament calibration");
var confirmResult = MessageBox.Show(
_localizationService.GetString("RaySource_FilamentCalibration_Confirm") ?? "是否确认执行灯丝校准操作?",
_localizationService.GetString("RaySource_Confirm_Title") ?? "确认",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (confirmResult != MessageBoxResult.Yes)
{
_logger.Info("用户取消灯丝校准操作 | User cancelled filament calibration operation");
return;
}
await ExecuteLongRunningOperation(
_localizationService.GetString("RaySource_FilamentCalibration_Title") ?? "灯丝校准",
_localizationService.GetString("RaySource_FilamentCalibration_Message") ?? "正在执行灯丝校准操作,请稍候...",
() => _raySourceService.FilamentCalibration(),
OperationType.FilamentCalibration,
_config.FilamentAdjustTimeout);
}
/// <summary>
/// 自动定心设置 | Auto-center setting
/// 参照旧代码:确认对话框 → 显示进度条 → 发送全部电压自动定心命令
/// </summary>
private async void ExecuteAutoCenterSetting()
{
_logger.Info("用户请求自动定心 | User requested auto-center");
var confirmResult = MessageBox.Show(
_localizationService.GetString("RaySource_AutoCenter_Confirm") ?? "是否确认执行全部电压自动定心操作?",
_localizationService.GetString("RaySource_Confirm_Title") ?? "确认",
MessageBoxButton.YesNo, MessageBoxImage.Question);
if (confirmResult != MessageBoxResult.Yes)
{
_logger.Info("用户取消自动定心操作 | User cancelled auto-center operation");
return;
}
await ExecuteLongRunningOperation(
_localizationService.GetString("RaySource_AutoCenter_Title") ?? "自动定心",
_localizationService.GetString("RaySource_AutoCenter_Message") ?? "正在执行全部电压自动定心操作,请稍候...",
() => _raySourceService.AutoCenter(),
OperationType.AutoCenter,
_config.AutoCenterTimeout);
}
/// <summary>
/// 执行长时间运行操作的通用方法(显示进度条窗口,实时跟踪设备进度)
/// 发送指令后订阅 StatusUpdatedEvent,根据实时电压/电流/状态字符串计算进度
/// </summary>
/// <param name="title">进度窗口标题</param>
/// <param name="message">进度窗口提示信息</param>
/// <param name="operation">要执行的操作(发送指令)</param>
/// <param name="operationType">操作类型(用于选择进度计算策略)</param>
/// <param name="timeoutMs">超时时间(毫秒)</param>
private async Task ExecuteLongRunningOperation(
string title, string message, Func<XRayResult> operation,
OperationType operationType, int timeoutMs)
{
var progressWindow = new ProgressWindow(
title: title,
message: message,
isCancelable: false,
logger: _logger);
var calculator = new OperationProgressCalculator(operationType, _config);
var completionSource = new TaskCompletionSource<bool>();
var cts = new CancellationTokenSource(timeoutMs);
// 状态更新事件处理:计算进度并更新窗口 | Status update handler: calculate progress and update window
void OnProgressStatusUpdated(SystemStatusData data)
{
if (data == null) return;
var result = calculator.Calculate(data);
progressWindow.UpdateProgress(message, result.Progress);
if (result.IsCompleted && !completionSource.Task.IsCompleted)
{
completionSource.TrySetResult(true);
}
}
try
{
_isOperating = true;
RefreshCommandStates();
progressWindow.Show();
progressWindow.UpdateProgress(message, 0);
// 订阅状态更新事件以跟踪进度 | Subscribe to status update event for progress tracking
_eventAggregator.GetEvent<StatusUpdatedEvent>().Subscribe(OnProgressStatusUpdated, ThreadOption.UIThread);
// 在后台线程发送操作指令 | Send operation command on background thread
var cmdResult = await Task.Run(() => operation());
if (!cmdResult.Success)
{
_logger.Error(null, "{Title} 操作失败: {ErrorMessage}", title, cmdResult.ErrorMessage);
MessageBox.Show(cmdResult.ErrorMessage,
_localizationService.GetString("RaySource_OperationFailed") ?? "操作失败",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
_logger.Info("{Title} 操作指令已发送,等待设备完成 | {Title} command sent, waiting for device completion", title);
// 等待完成或超时 | Wait for completion or timeout
using (cts.Token.Register(() => completionSource.TrySetResult(false)))
{
var completed = await completionSource.Task;
if (completed)
{
progressWindow.UpdateProgress(
_localizationService.GetString("RaySource_Operation_Completed") ?? "操作已完成", 100);
_logger.Info("{Title} 操作已完成 | {Title} operation completed", title);
await Task.Delay(1500); // 短暂停留让用户看到完成状态 | Brief pause for user to see completion
}
else
{
_logger.Warn("{Title} 操作超时({Timeout}ms| {Title} operation timed out ({Timeout}ms)", title, timeoutMs);
MessageBox.Show(
_localizationService.GetString("RaySource_Operation_Timeout") ?? "操作超时,请检查设备状态。",
_localizationService.GetString("RaySource_Warning") ?? "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
catch (Exception ex)
{
_logger.Error(ex, "{Title} 操作异常: {Message}", title, ex.Message);
}
finally
{
// 取消订阅状态更新事件 | Unsubscribe from status update event
_eventAggregator.GetEvent<StatusUpdatedEvent>().Unsubscribe(OnProgressStatusUpdated);
cts.Dispose();
_isOperating = false;
RefreshCommandStates();
progressWindow.Close();
}
}
/// <summary>
/// 从灯丝寿命服务获取最新数据并刷新 UI 属性(纯展示,不含计算逻辑)
/// Refresh filament lifetime UI properties from service (display only, no calculation logic)
/// </summary>
private void RefreshFilamentLifetime()
{
try
{
if (!_filamentLifetimeService.IsInitialized)
return;
// 从服务获取数据 | Get data from service
var percentage = _filamentLifetimeService.GetLifetimePercentage();
var totalSeconds = _filamentLifetimeService.GetCurrentTotalLifeSeconds();
var thresholdSeconds = _filamentLifetimeService.GetThresholdSeconds();
// 转换为小时(保留一位小数)| Convert to hours (one decimal place)
var totalHours = totalSeconds / 3600.0;
var thresholdHours = thresholdSeconds / 3600.0;
// 更新 UI 属性 | Update UI properties
FilamentLifetime = percentage;
FilamentLifetimeText = $"{totalHours:F1}h / {thresholdHours:F1}h ({percentage:F1}%)";
}
catch (Exception ex)
{
_logger.Error(ex, "刷新灯丝寿命数据失败 | Failed to refresh filament lifetime data: {Message}", ex.Message);
}
}
/// <summary>
/// 启动灯丝寿命定时刷新定时器 | Start filament lifetime periodic refresh timer
/// </summary>
private void StartLifetimeRefreshTimer()
{
_lifetimeRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(60)
};
_lifetimeRefreshTimer.Tick += OnLifetimeRefreshTimerTick;
_lifetimeRefreshTimer.Start();
_logger.Debug("灯丝寿命定时刷新已启动(间隔 60 秒)| Filament lifetime refresh timer started (60s interval)");
}
/// <summary>
/// 停止灯丝寿命定时刷新定时器 | Stop filament lifetime periodic refresh timer
/// </summary>
private void StopLifetimeRefreshTimer()
{
if (_lifetimeRefreshTimer != null)
{
_lifetimeRefreshTimer.Stop();
_lifetimeRefreshTimer.Tick -= OnLifetimeRefreshTimerTick;
_lifetimeRefreshTimer = null;
_logger.Debug("灯丝寿命定时刷新已停止 | Filament lifetime refresh timer stopped");
}
}
/// <summary>
/// 定时器 Tick 事件处理:刷新灯丝寿命数据 | Timer tick handler: refresh filament lifetime data
/// </summary>
private void OnLifetimeRefreshTimerTick(object sender, EventArgs e)
{
RefreshFilamentLifetime();
}
#endregion
#region IDisposable | IDisposable Implementation
/// <summary>
/// 释放资源:停止定时器、取消事件订阅 | Dispose resources: stop timer, unsubscribe events
/// </summary>
public void Dispose()
{
if (!_isDisposed)
{
// 停止灯丝寿命定时刷新 | Stop filament lifetime refresh timer
StopLifetimeRefreshTimer();
// 取消事件订阅 | Unsubscribe events
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Unsubscribe(OnRaySourceStatusChanged);
_eventAggregator.GetEvent<StatusUpdatedEvent>().Unsubscribe(OnStatusUpdated);
_eventAggregator.GetEvent<VariablesConnectedEvent>().Unsubscribe(OnVariablesConnectedChanged);
_isDisposed = true;
_logger.Info("射线源配置视图模型已释放 | X-ray source config view model disposed");
}
}
#endregion
}
}
@@ -0,0 +1,771 @@
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Threading.Tasks;
using System.Windows;
using XP.Common.Helpers;
using XP.Common.Localization.Interfaces;
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
using XP.Hardware.RaySource.Abstractions.Enums;
using XP.Hardware.RaySource.Abstractions.Events;
using XP.Hardware.RaySource.Config;
using XP.Hardware.RaySource.Services;
namespace XP.Hardware.RaySource.ViewModels
{
/// <summary>
/// 射线源操作视图模型 | X-Ray Source Operate ViewModel
/// 负责射线源开关控制、电压电流调节、状态监控 | Responsible for X-ray on/off control, voltage/current adjustment, status monitoring
/// </summary>
public class RaySourceOperateViewModel : BindableBase, IDisposable
{
#region | Fields
private readonly IRaySourceService _raySourceService;
private readonly IEventAggregator _eventAggregator;
private readonly RaySourceConfig _config;
private readonly ILoggerService _logger;
private readonly ILocalizationService _localizationService;
private bool _isDisposed = false;
private bool _isOperating = false;
private bool _isVariablesConnected = false;
private bool _autoInitialized = false;
private Views.RaySourceConfigWindow _configWindow;
/// <summary>
/// 标记当前是否正在从设备反馈更新值(防止 code-behind 误触发写入)
/// Flag indicating whether values are being updated from device feedback (prevents code-behind from triggering writes)
/// </summary>
public bool IsUpdatingFromDevice { get; private set; } = false;
#endregion
#region | Status Properties
private RaySourceStatus _raySourceStatus = RaySourceStatus.Unavailable;
/// <summary>
/// 射线源状态(不可用/关闭/开启)| X-ray source status (Unavailable/Closed/Opened)
/// </summary>
public RaySourceStatus RaySourceStatus
{
get => _raySourceStatus;
set
{
if (SetProperty(ref _raySourceStatus, value))
{
RaisePropertyChanged(nameof(StatusText));
RaisePropertyChanged(nameof(IsSlidersEnabled));
TurnOnCommand?.RaiseCanExecuteChanged();
TurnOffCommand?.RaiseCanExecuteChanged();
}
}
}
/// <summary>
/// 状态文本(绑定到腰圆内的文字)| Status text (bound to capsule text)
/// </summary>
public string StatusText
{
get
{
return RaySourceStatus switch
{
RaySourceStatus.Unavailable => $"{_localizationService.GetString("RaySource_StatusUnavailable")}",
RaySourceStatus.Closed => _localizationService.GetString("RaySource_StatusClosed"),
RaySourceStatus.Opened => _localizationService.GetString("RaySource_StatusOpened"),
_ => _localizationService.GetString("RaySource_StatusUnavailable")
};
}
}
/// <summary>
/// 滑块是否启用(仅服务已初始化时可调节)| Sliders enabled (only when service is initialized)
/// </summary>
public bool IsSlidersEnabled => _raySourceService.IsInitialized && _isVariablesConnected;
/// <summary>
/// PVI 变量是否已连接 | Whether PVI variables are connected
/// </summary>
public bool IsVariablesConnected
{
get => _isVariablesConnected;
private set
{
if (SetProperty(ref _isVariablesConnected, value))
{
RaisePropertyChanged(nameof(IsSlidersEnabled));
TurnOnCommand?.RaiseCanExecuteChanged();
TurnOffCommand?.RaiseCanExecuteChanged();
}
}
}
private bool _isInterlockActive;
/// <summary>
/// 连锁是否激活 | Whether interlock is active
/// </summary>
public bool IsInterlockActive
{
get => _isInterlockActive;
private set
{
if (SetProperty(ref _isInterlockActive, value))
{
TurnOnCommand?.RaiseCanExecuteChanged();
}
}
}
#endregion
#region | Voltage Properties
private double _voltageMin;
/// <summary>
/// 电压最小值(kV| Voltage minimum (kV)
/// </summary>
public double VoltageMin
{
get => _voltageMin;
set => SetProperty(ref _voltageMin, value);
}
private double _voltageMax;
/// <summary>
/// 电压最大值(kV| Voltage maximum (kV)
/// </summary>
public double VoltageMax
{
get => _voltageMax;
set => SetProperty(ref _voltageMax, value);
}
private double _voltageValue;
/// <summary>
/// 电压滑块值(kV),仅更新UI显示,不自动写入硬件 | Voltage slider value (kV), UI display only, no auto write to hardware
/// </summary>
public double VoltageValue
{
get => _voltageValue;
set => SetProperty(ref _voltageValue, Math.Round(value, 0));
}
private double _voltageActual;
/// <summary>
/// 电压实际值(kV| Voltage actual value (kV)
/// </summary>
public double VoltageActual
{
get => _voltageActual;
set => SetProperty(ref _voltageActual, value);
}
/// <summary>
/// 电压实际值显示文本 | Voltage actual value display text
/// </summary>
public string VoltageActualText => string.Format(_localizationService.GetString("RaySource_ActualValueLabel"), VoltageActual.ToString("F1"));
#endregion
#region | Current Properties
private double _currentMin;
/// <summary>
/// 电流最小值(μA| Current minimum (μA)
/// </summary>
public double CurrentMin
{
get => _currentMin;
set => SetProperty(ref _currentMin, value);
}
private double _currentMax;
/// <summary>
/// 电流最大值(μA| Current maximum (μA)
/// </summary>
public double CurrentMax
{
get => _currentMax;
set => SetProperty(ref _currentMax, value);
}
private double _currentValue;
/// <summary>
/// 电流滑块值(μA),仅更新UI显示,不自动写入硬件 | Current slider value (μA), UI display only, no auto write to hardware
/// </summary>
public double CurrentValue
{
get => _currentValue;
set => SetProperty(ref _currentValue, Math.Round(value, 0));
}
private double _currentActual;
/// <summary>
/// 电流实际值(μA| Current actual value (μA)
/// </summary>
public double CurrentActual
{
get => _currentActual;
set => SetProperty(ref _currentActual, value);
}
/// <summary>
/// 电流实际值显示文本 | Current actual value display text
/// </summary>
public string CurrentActualText => string.Format(_localizationService.GetString("RaySource_ActualValueLabel"), CurrentActual.ToString("F1"));
#endregion
#region | Commands
/// <summary>
/// 开启射线源命令 | Turn on X-ray command
/// </summary>
public DelegateCommand TurnOnCommand { get; }
/// <summary>
/// 关闭射线源命令 | Turn off X-ray command
/// </summary>
public DelegateCommand TurnOffCommand { get; }
/// <summary>
/// 提交电压值命令(滑块松手/输入框失焦时触发)| Apply voltage command (triggered on slider drag complete / input lost focus)
/// </summary>
public DelegateCommand ApplyVoltageCommand { get; }
/// <summary>
/// 提交电流值命令(滑块松手/输入框失焦时触发)| Apply current command (triggered on slider drag complete / input lost focus)
/// </summary>
public DelegateCommand ApplyCurrentCommand { get; }
/// <summary>
/// 设置命令(预留接口)| Settings command (reserved interface)
/// </summary>
public DelegateCommand SettingsCommand { get; }
/// <summary>
/// 配置命令(打开射线源配置窗口)| Config command (open ray source config window)
/// </summary>
public DelegateCommand ConfigCommand { get; }
#endregion
#region | Constructor
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public RaySourceOperateViewModel(
IRaySourceService raySourceService,
IEventAggregator eventAggregator,
RaySourceConfig config,
ILoggerService logger,
ILocalizationService localizationService)
{
_raySourceService = raySourceService ?? throw new ArgumentNullException(nameof(raySourceService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_config = config ?? throw new ArgumentNullException(nameof(config));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule("RaySource.ViewModel");
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
_logger.Info("射线源操作视图模型已创建 | X-ray source operate view model created");
// 初始化极值 | Initialize min/max values
VoltageMin = _config.MinVoltage;
VoltageMax = _config.MaxVoltage;
CurrentMin = _config.MinCurrent;
CurrentMax = _config.MaxCurrent;
// 初始化滑块默认值 | Initialize slider default values
VoltageValue = _config.MinVoltage; //中间值:(_config.MinVoltage + _config.MaxVoltage) / 2;
CurrentValue = _config.MinCurrent; //中间值:(_config.MinCurrent + _config.MaxCurrent) / 2;
// 初始化命令 | Initialize commands
TurnOnCommand = new DelegateCommand(ExecuteTurnOn, CanExecuteTurnOn);
TurnOffCommand = new DelegateCommand(ExecuteTurnOff, CanExecuteTurnOff);
ApplyVoltageCommand = new DelegateCommand(ApplyVoltage);
ApplyCurrentCommand = new DelegateCommand(ApplyCurrent);
SettingsCommand = new DelegateCommand(ExecuteSettings, CanExecuteSettings);
ConfigCommand = new DelegateCommand(ExecuteConfig);
// 订阅事件 | Subscribe to events
SubscribeEvents();
// 初始化状态 | Initialize status
UpdateStatusFromService();
}
#endregion
#region | Event Subscription
/// <summary>
/// 订阅事件 | Subscribe to events
/// </summary>
private void SubscribeEvents()
{
// 订阅射线源状态变更事件(三态)| Subscribe to ray source status changed event (tri-state)
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Subscribe(OnRaySourceStatusChanged, ThreadOption.UIThread);
// 订阅状态更新事件 | Subscribe to status updated event
_eventAggregator.GetEvent<StatusUpdatedEvent>().Subscribe(OnStatusUpdated, ThreadOption.UIThread);
// 订阅错误事件 | Subscribe to error event
_eventAggregator.GetEvent<ErrorOccurredEvent>().Subscribe(OnErrorOccurred, ThreadOption.UIThread);
// 订阅操作结果事件 | Subscribe to operation result event
_eventAggregator.GetEvent<OperationResultEvent>().Subscribe(OnOperationResult, ThreadOption.UIThread);
// 订阅变量连接状态变更事件 | Subscribe to variables connected event
_eventAggregator.GetEvent<VariablesConnectedEvent>().Subscribe(OnVariablesConnectedChanged, ThreadOption.UIThread);
}
/// <summary>
/// 射线源状态变更事件处理(三态)| Ray source status changed event handler (tri-state)
/// </summary>
private void OnRaySourceStatusChanged(RaySourceStatus newStatus)
{
var previousStatus = RaySourceStatus;
RaySourceStatus = newStatus;
RaisePropertyChanged(nameof(IsSlidersEnabled));
// 连接恢复处理:从不可用变为已连接,主动读取最新状态刷新UI | Connection restored: from unavailable to connected, actively read latest status to refresh UI
if (previousStatus == RaySourceStatus.Unavailable &&
(newStatus == RaySourceStatus.Opened || newStatus == RaySourceStatus.Closed))
{
_logger.Info("射线源连接已建立 | Ray Source Connected");
}
// 异常断联处理:从已连接状态变为不可用 | Handle unexpected disconnection: from connected to unavailable
if (newStatus == RaySourceStatus.Unavailable &&
(previousStatus == RaySourceStatus.Opened || previousStatus == RaySourceStatus.Closed))
{
// 断联时重置变量连接状态 | Reset variable connection state on disconnection
IsVariablesConnected = false;
_logger.Warn("ViewModel 检测到连接丢失(状态变为 Unavailable| ViewModel detected connection lost (status changed to Unavailable)");
//MessageBox.Show("射线源连接丢失,请检查设备连接后重新初始化。", "连接丢失", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 状态更新事件处理 | Status updated event handler
/// 仅在实际值真正变化时更新滑块/输入框,避免无意义赋值触发事件链
/// Only update slider/input when actual value truly changed, avoiding unnecessary assignment triggering event chain
/// </summary>
private void OnStatusUpdated(SystemStatusData statusData)
{
if (statusData == null) return;
IsUpdatingFromDevice = true;
try
{
// 电压实际值变化时,仅更新实际值标签显示 | When actual voltage changes, only update actual value label
if (Math.Abs(statusData.ActualVoltage - VoltageActual) > 0.01)
{
VoltageActual = statusData.ActualVoltage;
RaisePropertyChanged(nameof(VoltageActualText));
}
// 电流实际值变化时,仅更新实际值标签显示 | When actual current changes, only update actual value label
if (Math.Abs(statusData.ActualCurrent - CurrentActual) > 0.01)
{
CurrentActual = statusData.ActualCurrent;
RaisePropertyChanged(nameof(CurrentActualText));
}
// 设定电压反馈同步到滑块(仅设备反馈时更新,人工操作时 IsUpdatingFromDevice=false 不会进入此处)
// Sync set voltage feedback to slider (only during device feedback, not during manual operation)
if (statusData.SetVoltage >= _config.MinVoltage && Math.Abs(statusData.SetVoltage - VoltageValue) > 0.5)
{
VoltageValue = Math.Round(statusData.SetVoltage, 0);
}
// 设定电流反馈同步到滑块 | Sync set current feedback to slider
if (statusData.SetCurrent >= _config.MinCurrent && Math.Abs(statusData.SetCurrent - CurrentValue) > 0.5)
{
CurrentValue = Math.Round(statusData.SetCurrent, 0);
}
// 使用服务层的权威状态 | Use authoritative status from service layer
RaySourceStatus = _raySourceService.CurrentStatus;
// 同步连锁状态 | Sync interlock status
IsInterlockActive = statusData.IsInterlockActive;
}
finally
{
IsUpdatingFromDevice = false;
}
}
/// <summary>
/// 错误事件处理 | Error event handler
/// </summary>
private void OnErrorOccurred(string errorMessage)
{
// 显示错误消息 | Show error message
MessageBox.Show(errorMessage, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
/// <summary>
/// 操作结果事件处理 | Operation result event handler
/// </summary>
private void OnOperationResult(OperationResultData resultData)
{
if (resultData == null) return;
if (!resultData.IsSuccess)
{
// 操作失败时显示消息 | Show message when operation fails
MessageBox.Show(resultData.Message, "操作失败", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
/// <summary>
/// 变量连接状态变更事件处理 | Variables connected state changed event handler
/// </summary>
private void OnVariablesConnectedChanged(bool isConnected)
{
IsVariablesConnected = isConnected;
_logger.Info("收到变量连接状态变更: {IsConnected} | Received variables connection state change: {IsConnected}", isConnected);
if (isConnected)
{
_logger.Info("变量已连接,主动读取最新状态 | Variables Connected, actively reading latest status");
RefreshStatusFromDevice();
}
}
#endregion
#region | Command Execution Logic
/// <summary>
/// 开启射线源执行逻辑 | Turn on X-ray execution logic
/// </summary>
private void ExecuteTurnOn()
{
_isOperating = true;
TurnOnCommand.RaiseCanExecuteChanged();
TurnOffCommand.RaiseCanExecuteChanged();
try
{
_logger.Info("用户请求开启射线 | User requested to turn on X-ray");
var result = _raySourceService.TurnOn();
if (result.Success)
{
_logger.Info("射线开启指令发送成功 | X-ray turned on successfully");
}
else
{
_logger.Warn("射线开启指令发送失败: {Error} | X-ray turn on failed: {Error}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage, "开启失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
_logger.Error(ex, "开启射线源异常: {Message} | Exception turning on X-ray: {Message}", ex.Message);
MessageBox.Show($"开启射线源异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_isOperating = false;
TurnOnCommand.RaiseCanExecuteChanged();
TurnOffCommand.RaiseCanExecuteChanged();
}
}
/// <summary>
/// 开启命令是否可执行(仅关闭状态且连锁激活时可执行)| Can execute turn on command (only when closed and interlock active)
/// </summary>
private bool CanExecuteTurnOn()
{
return !_isOperating && _isVariablesConnected && IsInterlockActive && RaySourceStatus == RaySourceStatus.Closed && _raySourceService.IsInitialized;
}
/// <summary>
/// 关闭射线源执行逻辑 | Turn off X-ray execution logic
/// </summary>
private void ExecuteTurnOff()
{
_isOperating = true;
TurnOnCommand.RaiseCanExecuteChanged();
TurnOffCommand.RaiseCanExecuteChanged();
try
{
_logger.Info("用户请求关闭射线 | User requested to turn off X-ray");
var result = _raySourceService.TurnOff();
if (result.Success)
{
_logger.Info("射线关闭指令发送成功 | X-ray turned off successfully");
}
else
{
_logger.Warn("射线关闭指令发送失败: {Error} | X-ray turn off failed: {Error}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage, "关闭失败", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
catch (Exception ex)
{
_logger.Error(ex, "关闭射线源异常: {Message} | Exception turning off X-ray: {Message}", ex.Message);
MessageBox.Show($"关闭射线源异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
_isOperating = false;
TurnOnCommand.RaiseCanExecuteChanged();
TurnOffCommand.RaiseCanExecuteChanged();
}
}
/// <summary>
/// 关闭命令是否可执行(仅开启状态可执行)| Can execute turn off command (only when opened)
/// </summary>
private bool CanExecuteTurnOff()
{
return !_isOperating && _isVariablesConnected && RaySourceStatus == RaySourceStatus.Opened && _raySourceService.IsInitialized;
}
/// <summary>
/// 设置命令执行逻辑:打开外部高级设置程序 | Settings command: launch external advance settings program
/// 如果程序已运行则将窗口置前 | If program is already running, bring its window to front
/// </summary>
private void ExecuteSettings()
{
var exePath = _config.AdvanceExePath;
_logger.Info("用户点击高级按钮,目标程序: {ExePath} | User clicked advance button, target program: {ExePath}", exePath);
var result = ProcessHelper.StartOrActivate(exePath, true);
if (result.Success)
{
var action = result.IsNewlyStarted ? "已启动 | started" : "已激活 | activated";
_logger.Info("外部程序{Action} | External program {Action}", action, action);
}
else
{
_logger.Warn("外部程序操作失败: {Error} | External program operation failed: {Error}", result.ErrorMessage);
MessageBox.Show(result.ErrorMessage,
_localizationService.GetString("RaySource_Warning") ?? "警告",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
/// <summary>
/// 设置命令是否可执行 | Can execute settings command
/// </summary>
private bool CanExecuteSettings()
{
return true; // 始终可用 | Always available
}
/// <summary>
/// 配置命令执行逻辑:打开射线源配置窗口 | Config command: open ray source config window
/// </summary>
private void ExecuteConfig()
{
_logger.Info("用户点击配置按钮 | User clicked config button");
// 如果窗口已存在且未关闭,激活它而不是重复创建
if (_configWindow != null && _configWindow.IsLoaded)
{
_configWindow.Activate();
return;
}
_configWindow = new Views.RaySourceConfigWindow
{
Owner = Application.Current.MainWindow,
ShowInTaskbar = false
};
_configWindow.Closed += (s, e) => _configWindow = null;
_configWindow.Show();
}
/// <summary>
/// 提交电压值到硬件(滑块松手/输入框失焦时调用)| Apply voltage to hardware (called on slider drag complete / input lost focus)
/// </summary>
private void ApplyVoltage()
{
var voltage = (float)VoltageValue;
if (!IsSlidersEnabled) return;
// 范围校验 | Range validation
if (voltage < _config.MinVoltage || voltage > _config.MaxVoltage)
{
_logger.Warn("电压值超出范围: {Voltage}kV,忽略提交 | Voltage out of range: {Voltage}kV, ignoring apply", voltage);
return;
}
try
{
_logger.Debug("用户确认电压值: {Voltage}kV | User confirmed voltage: {Voltage}kV", voltage);
var result = _raySourceService.SetVoltage(voltage);
if (!result.Success)
{
_logger.Warn("电压设置失败: {Error} | Voltage setting failed: {Error}", result.ErrorMessage);
// 设置失败时恢复滑块值(仅当实际值在有效范围内时)| Restore slider value when setting fails (only when actual value is in valid range)
if (VoltageActual >= _config.MinVoltage)
VoltageValue = VoltageActual;
}
}
catch (Exception ex)
{
_logger.Error(ex, "设置电压异常: {Message} | Exception setting voltage: {Message}", ex.Message);
MessageBox.Show($"设置电压异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
if (VoltageActual >= _config.MinVoltage)
VoltageValue = VoltageActual;
}
}
/// <summary>
/// 提交电流值到硬件(滑块松手/输入框失焦时调用)| Apply current to hardware (called on slider drag complete / input lost focus)
/// </summary>
private void ApplyCurrent()
{
var current = (float)CurrentValue;
if (!IsSlidersEnabled) return;
// 范围校验 | Range validation
if (current < _config.MinCurrent || current > _config.MaxCurrent)
{
_logger.Warn("电流值超出范围: {Current}μA,忽略提交 | Current out of range: {Current}μA, ignoring apply", current);
return;
}
try
{
_logger.Debug("用户确认电流值: {Current}μA | User confirmed current: {Current}μA", current);
var result = _raySourceService.SetCurrent(current);
if (!result.Success)
{
_logger.Warn("电流设置失败: {Error} | Current setting failed: {Error}", result.ErrorMessage);
// 设置失败时恢复滑块值(仅当实际值在有效范围内时)| Restore slider value when setting fails (only when actual value is in valid range)
if (CurrentActual >= _config.MinCurrent)
CurrentValue = CurrentActual;
}
}
catch (Exception ex)
{
_logger.Error(ex, "设置电流异常: {Message} | Exception setting current: {Message}", ex.Message);
MessageBox.Show($"设置电流异常: {ex.Message}", "异常", MessageBoxButton.OK, MessageBoxImage.Error);
if (CurrentActual >= _config.MinCurrent)
CurrentValue = CurrentActual;
}
}
/// <summary>
/// 从服务更新状态 | Update status from service
/// </summary>
private void UpdateStatusFromService()
{
RaySourceStatus = _raySourceService.CurrentStatus;
}
/// <summary>
/// 从设备主动读取最新状态并刷新UI | Actively read latest status from device and refresh UI
/// </summary>
private void RefreshStatusFromDevice()
{
try
{
var result = _raySourceService.ReadSystemStatus();
if (result.Success && result.Data is SystemStatusData statusData)
{
OnStatusUpdated(statusData);
_logger.Info("已从设备刷新最新状态 | Refreshed latest status from device");
}
else
{
_logger.Warn("读取设备状态失败: {Error} | Failed to read device status: {Error}", result.ErrorMessage);
}
}
catch (Exception ex)
{
_logger.Error(ex, "刷新设备状态异常: {Message} | Exception refreshing device status: {Message}", ex.Message);
}
}
/// <summary>
/// 界面加载时自动执行初始化和连接变量流程(仅执行一次)
/// Auto-execute Initialize and ConnectVariables on view loaded (runs only once)
/// </summary>
/// <summary>
/// 自动初始化:异步执行初始化 + 连接变量 | Auto-initialize: async Initialize + ConnectVariables
/// 由 View 的 Loaded 事件触发,仅执行一次 | Triggered by View's Loaded event, runs only once
/// </summary>
public async Task AutoInitializeAsync()
{
// 防止重复执行 | Prevent duplicate execution
if (_autoInitialized) return;
_autoInitialized = true;
// 如果已经初始化并连接,跳过 | Skip if already initialized and connected
if (_raySourceService.IsInitialized && _isVariablesConnected)
{
_logger.Info("射线源已初始化且变量已连接,跳过自动初始化 | Ray source already initialized and variables connected, skipping auto-init");
return;
}
_logger.Info("开始自动初始化流程 | Starting auto-initialization sequence");
var result = await _raySourceService.InitializeAndConnectAsync();
if (result.Success)
{
_logger.Info("自动初始化命令已发送,等待设备确认 | Auto-initialization command sent, waiting for device confirmation");
}
else
{
_logger.Warn("自动初始化流程失败: {Error} | Auto-initialization sequence failed: {Error}", result.ErrorMessage);
}
}
#endregion
#region IDisposable Implementation
/// <summary>
/// 释放资源 | Dispose resources
/// </summary>
public void Dispose()
{
if (!_isDisposed)
{
// 断开射线源连接 | Disconnect X-ray source
if (_raySourceService.IsInitialized)
{
_logger.Info("界面关闭,执行断开操作 | View closing, executing disconnect");
try
{
Task.Run(() => _raySourceService.Disconnect()).Wait(5000);
}
catch (Exception ex)
{
_logger.Warn("断开射线源异常: {Message} | Disconnect exception: {Message}", ex.Message);
}
}
// 关闭配置窗口 | Close config window
if (_configWindow != null && _configWindow.IsLoaded)
{
_configWindow.Close();
_configWindow = null;
}
// 取消订阅事件 | Unsubscribe events
_eventAggregator.GetEvent<RaySourceStatusChangedEvent>().Unsubscribe(OnRaySourceStatusChanged);
_eventAggregator.GetEvent<StatusUpdatedEvent>().Unsubscribe(OnStatusUpdated);
_eventAggregator.GetEvent<ErrorOccurredEvent>().Unsubscribe(OnErrorOccurred);
_eventAggregator.GetEvent<OperationResultEvent>().Unsubscribe(OnOperationResult);
_eventAggregator.GetEvent<VariablesConnectedEvent>().Unsubscribe(OnVariablesConnectedChanged);
_isDisposed = true;
}
}
#endregion
}
}
@@ -0,0 +1,299 @@
<UserControl x:Class="XP.Hardware.RaySource.Views.RaySourceConfigView"
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:prism="http://prismlibrary.com/"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
xmlns:converters="clr-namespace:XP.Hardware.RaySource.Converters"
mc:Ignorable="d"
d:DesignHeight="420" d:DesignWidth="500"
prism:ViewModelLocator.AutoWireViewModel="True"
Background="White">
<UserControl.Resources>
<converters:FilamentLifetimeColorConverter x:Key="LifetimeColorConverter"/>
</UserControl.Resources>
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 第1行:设备信息 | Row 1: Device Info -->
<Grid Grid.Row="0" Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{loc:Localization RaySource_SourceTypeLabel}"
FontSize="13" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding SourceTypeText}"
FontSize="13" FontWeight="Bold" VerticalAlignment="Center" Margin="8 0 0 0"/>
<TextBlock Grid.Column="2" Text="{Binding ConnectionStatusText}"
FontSize="12" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<!-- 默认灰色:未连接 -->
<Setter Property="Foreground" Value="#FF9E9E9E"/>
<Style.Triggers>
<!-- 已初始化但变量未连接:橙色 -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsInitialized}" Value="True"/>
<Condition Binding="{Binding IsVariablesConnected}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="#FFFF9800"/>
</MultiDataTrigger>
<!-- 变量已连接:绿色 -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsInitialized}" Value="True"/>
<Condition Binding="{Binding IsVariablesConnected}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="#FF4CAF50"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<!-- 第2行:操作按钮(3列等宽均布)| Row 2: Action Buttons (3 equal-width columns) -->
<Grid Grid.Row="1" Margin="0 0 0 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<telerik:RadButton Grid.Column="0" Content="{loc:Localization RaySource_InitializeButton}"
Command="{Binding InitializeCommand}"
Height="32" Margin="0 0 4 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FF62B8E0" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
<telerik:RadButton Grid.Column="1" Content="{loc:Localization RaySource_ConnectVariablesButton}"
Command="{Binding ConnectVariablesCommand}"
Height="32" Margin="4 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FF77E062" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
<telerik:RadButton Grid.Column="2" Content="{loc:Localization RaySource_DisconnectButton}"
Command="{Binding DisconnectCommand}"
Height="32" Margin="4 0 0 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FFFFA1A1" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
</Grid>
<!-- 第3行:分隔线 | Row 3: Separator -->
<Separator Grid.Row="2" Margin="0 0 0 8"/>
<!-- 第4行:设备状态监控面板 | Row 4: Device Status Monitor Panel -->
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 暖机状态 | Warm-up Status -->
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="0 2">
<TextBlock Text="{loc:Localization RaySource_WarmUpLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding WarmUpStatus}" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
<!-- 真空状态 | Vacuum Status -->
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="8 2">
<TextBlock Text="{loc:Localization RaySource_VacuumLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding VacuumStatus}" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
<!-- 启动状态 | Startup Status -->
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Margin="0 2">
<TextBlock Text="{loc:Localization RaySource_StartUpLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding StartUpStatus}" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
<!-- 自动定心 | Auto-center Status -->
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Margin="8 2">
<TextBlock Text="{loc:Localization RaySource_AutoCenterLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding AutoCenterStatus}" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
<!-- 灯丝校准 | Filament Adjust Status -->
<StackPanel Grid.Row="2" Grid.Column="0" Orientation="Horizontal" Margin="0 2">
<TextBlock Text="{loc:Localization RaySource_FilamentLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding FilamentAdjustStatus}" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
<!-- 射线开启状态 | X-Ray On Status -->
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" Margin="8 2">
<TextBlock Text="{loc:Localization RaySource_XRayOnLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding XRayOnStatusText}" FontSize="12" FontWeight="SemiBold">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsXRayOn}" Value="True">
<Setter Property="Foreground" Value="#FF4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<!-- 连锁状态 | Interlock Status -->
<StackPanel Grid.Row="3" Grid.Column="0" Orientation="Horizontal" Margin="0 2">
<TextBlock Text="{loc:Localization RaySource_InterlockLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding InterlockStatusText}" FontSize="12" FontWeight="SemiBold">
<TextBlock.Style>
<Style TargetType="TextBlock">
<!-- 默认未激活:红色 | Default inactive: red -->
<Setter Property="Foreground" Value="#FFE53935"/>
<Style.Triggers>
<!-- 激活时:绿色 | Active: green -->
<DataTrigger Binding="{Binding IsInterlockActive}" Value="True">
<Setter Property="Foreground" Value="#FF4CAF50"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<!-- 看门狗 | Watchdog Status -->
<StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Margin="8 2">
<TextBlock Text="{loc:Localization RaySource_WatchdogLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding WatchdogStatus}" FontSize="12" FontWeight="SemiBold"/>
</StackPanel>
<!-- TXI状态 + TXI ON/OFF 按钮(同一行)| TXI Status + TXI ON/OFF Buttons (same row) -->
<StackPanel Grid.Row="4" Grid.Column="0" Orientation="Horizontal" Margin="0 4 0 2" VerticalAlignment="Center">
<TextBlock Text="{loc:Localization RaySource_TxiStatusLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding TxiStatus}" FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="4" Grid.Column="1" Margin="8 2 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<telerik:RadButton Grid.Column="0" Content="{loc:Localization RaySource_TxiOnButton}"
Command="{Binding TxiOnCommand}"
Height="20" Padding="4 0" FontSize="11"
Margin="0 0 2 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
<telerik:RadButton Grid.Column="1" Content="{loc:Localization RaySource_TxiOffButton}"
Command="{Binding TxiOffCommand}"
Height="20" Padding="4 0" FontSize="11"
Margin="2 0 0 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
</Grid>
<!-- 功率模式 + High Power/Micro Focus 按钮(同一行)| Power Mode + HP/MF Buttons (same row) -->
<StackPanel Grid.Row="5" Grid.Column="0" Orientation="Horizontal" Margin="0 4 0 2" VerticalAlignment="Center">
<TextBlock Text="{loc:Localization RaySource_PowerModeLabel}" FontSize="12" Foreground="#616161" Width="80"/>
<TextBlock Text="{Binding PowerMode}" FontSize="12" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="5" Grid.Column="1" Margin="8 2 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<telerik:RadButton Grid.Column="0" Content="{loc:Localization RaySource_HighPowerButton}"
Command="{Binding HighPowerCommand}"
Height="20" Padding="4 0" FontSize="11"
Margin="0 0 2 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
<telerik:RadButton Grid.Column="1" Content="{loc:Localization RaySource_MicroFocusButton}"
Command="{Binding MicroFocusCommand}"
Height="20" Padding="4 0" FontSize="11"
Margin="2 0 0 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
</Grid>
</Grid>
<!-- 第5行:分隔线 | Row 5: Separator -->
<Separator Grid.Row="4" Margin="0 8 0 8"/>
<!-- 第6行:功能设置按钮 | Row 6: Function Setting Buttons -->
<Grid Grid.Row="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<telerik:RadButton Grid.Column="0" Content="{loc:Localization RaySource_WarmUpSettingButton}"
Command="{Binding WarmUpSettingCommand}"
Height="30" Margin="0 0 4 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
<telerik:RadButton Grid.Column="1" Content="{loc:Localization RaySource_TrainingSettingButton}"
Command="{Binding TrainingSettingCommand}"
Height="30" Margin="4 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
<telerik:RadButton Grid.Column="2" Content="{loc:Localization RaySource_FilamentCalibrationButton}"
Command="{Binding FilamentCalibrationCommand}"
Height="30" Margin="4 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
<telerik:RadButton Grid.Column="3" Content="{loc:Localization RaySource_AutoCenterSettingButton}"
Command="{Binding AutoCenterSettingCommand}"
Height="30" Margin="4 0 0 0" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal"/>
</Grid>
<!-- 第7行:分隔线 | Row 7: Separator -->
<Separator Grid.Row="6" Margin="0 8 0 8"/>
<!-- 第8行:灯丝寿命进度条 | Row 8: Filament Lifetime Progress Bar -->
<Grid Grid.Row="7">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{loc:Localization RaySource_FilamentLifetimeLabel}"
FontSize="12" Foreground="#616161" VerticalAlignment="Center" Margin="0 0 8 0"/>
<telerik:RadProgressBar Grid.Column="1"
Minimum="0" Maximum="100"
Value="{Binding FilamentLifetime, Mode=OneWay}"
Foreground="{Binding FilamentLifetime, Converter={StaticResource LifetimeColorConverter}}"
Height="8" VerticalAlignment="Center"
telerik:StyleManager.Theme="Crystal"/>
<TextBlock Grid.Column="2"
Text="{Binding FilamentLifetimeText}"
FontSize="11" Foreground="#616161" VerticalAlignment="Center" Margin="8 0 0 0"/>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace XP.Hardware.RaySource.Views
{
/// <summary>
/// Interaction logic for ViewA.xaml
/// </summary>
public partial class RaySourceConfigView : UserControl
{
public RaySourceConfigView()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,14 @@
<Window x:Class="XP.Hardware.RaySource.Views.RaySourceConfigWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
xmlns:raySourceViews="clr-namespace:XP.Hardware.RaySource.Views"
Title="{loc:Localization RaySource_ConfigWindowTitle}"
Height="350" Width="600"
MinHeight="350" MinWidth="400"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize">
<Grid>
<raySourceViews:RaySourceConfigView />
</Grid>
</Window>
@@ -0,0 +1,24 @@
using System.Windows;
namespace XP.Hardware.RaySource.Views
{
/// <summary>
/// 射线源配置窗口 | Ray source config window
/// </summary>
public partial class RaySourceConfigWindow : Window
{
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public RaySourceConfigWindow()
{
InitializeComponent();
// 继承主窗口图标
if (Application.Current?.MainWindow != null)
{
Icon = Application.Current.MainWindow.Icon;
}
}
}
}
@@ -0,0 +1,279 @@
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:XP.Hardware.RaySource.Converters"
xmlns:enums="clr-namespace:XP.Hardware.RaySource.Abstractions.Enums"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:prism="http://prismlibrary.com/"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" x:Class="XP.Hardware.RaySource.Views.RaySourceOperateView"
mc:Ignorable="d" d:DesignWidth="350"
prism:ViewModelLocator.AutoWireViewModel="True" Height="249" Background="White" >
<UserControl.Resources>
<!-- 转换器 | Converters -->
<converters:RaySourceStatusToColorConverter x:Key="StatusToColorConverter"/>
<converters:RaySourceStatusToBorderColorConverter x:Key="StatusToBorderColorConverter"/>
<!-- 呼吸闪烁动画 | Breathing Flash Animation -->
<Storyboard x:Key="BreathingAnimation" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1.0" To="0.6" Duration="0:0:0.75"
AutoReverse="True"/>
</Storyboard>
<!-- 按钮样式 | Button Style -->
<Style x:Key="RaySourceButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="#E0E0E0"/>
<Setter Property="BorderBrush" Value="#BDBDBD"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Padding" Value="8"/>
<Setter Property="Width" Value="85"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="4" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#BDBDBD"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#9E9E9E"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#BDBDBD"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" MinHeight="70" />
<RowDefinition Height="Auto" MinHeight="70" />
</Grid.RowDefinitions>
<!-- 1. 状态区:腰圆指示器 + 按钮 | Status Area: Capsule Indicator + Buttons -->
<Grid Grid.Row="0" Margin="5,5,5,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 腰圆状态指示器 | Capsule Status Indicator -->
<Grid Grid.Column="0" Width="200" Height="80" Margin="20 0 0 5">
<Border x:Name="StatusIndicator"
Width="200" Height="80"
Background="{Binding RaySourceStatus, Converter={StaticResource StatusToColorConverter}}"
BorderBrush="{Binding RaySourceStatus, Converter={StaticResource StatusToBorderColorConverter}}"
BorderThickness="2"
CornerRadius="40">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding RaySourceStatus}"
Value="{x:Static enums:RaySourceStatus.Opened}">
<DataTrigger.EnterActions>
<BeginStoryboard Name="BreathingStoryboard" Storyboard="{StaticResource BreathingAnimation}"/>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="BreathingStoryboard"/>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock Text="{Binding StatusText}"
Foreground="White" FontSize="18" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" LineHeight="20"/>
<!-- 连锁状态图标(腰圆右侧圆弧区域)| Interlock status icon (right arc area of capsule) -->
<Image Width="22" Height="22"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,20,0"
RenderOptions.BitmapScalingMode="HighQuality">
<Image.Style>
<Style TargetType="Image">
<!-- 默认未激活:解锁图标 | Default inactive: unlocked icon -->
<Setter Property="Source" Value="/XP.Hardware.RaySource;component/Resources/Unlocked.png"/>
<Setter Property="ToolTip" Value="{loc:Localization RaySource_InterlockNormal}"/>
<Style.Triggers>
<!-- 激活时:锁定图标 | Active: locked icon -->
<DataTrigger Binding="{Binding IsInterlockActive}" Value="True">
<Setter Property="Source" Value="/XP.Hardware.RaySource;component/Resources/Locked.png"/>
<Setter Property="ToolTip" Value="{loc:Localization RaySource_InterlockActive}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</Grid>
<!-- 按钮区 | Button Area -->
<StackPanel Grid.Column="1" Orientation="Vertical"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
Margin="20 0 20 0">
<telerik:RadButton Content="{loc:Localization RaySource_TurnOnButton}"
Command="{Binding TurnOnCommand}"
Margin="0 0 0 8" Height="35" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FF77E062" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
<telerik:RadButton Content="{loc:Localization RaySource_TurnOffButton}"
Command="{Binding TurnOffCommand}"
Margin="0 0 0 0" Height="35" HorizontalAlignment="Stretch"
telerik:StyleManager.Theme="Crystal">
<telerik:RadButton.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FFFFA1A1" Offset="1"/>
</LinearGradientBrush>
</telerik:RadButton.Background>
</telerik:RadButton>
</StackPanel>
</Grid>
<!-- 2. 电压区 | Voltage Area -->
<Grid Grid.Row="1" Margin="20,0,20,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标签+实际值 | Label + Actual Value -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{loc:Localization RaySource_VoltageLabel}"
FontSize="14" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding VoltageActualText}"
FontSize="10" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<telerik:RadSlider x:Name="VoltageSlider" Grid.Column="0" HorizontalAlignment="Stretch" Margin="0,5,10,0" VerticalAlignment="Center"
telerik:StyleManager.Theme="Crystal"
Minimum="{Binding VoltageMin}"
Maximum="{Binding VoltageMax}"
Value="{Binding VoltageValue, Mode=TwoWay}"
SmallChange="1" LargeChange="10"
IsDeferredDraggingEnabled="True"
IsEnabled="{Binding IsSlidersEnabled}"/>
<telerik:RadNumericUpDown x:Name="VoltageNumeric" Grid.Column="1" HorizontalAlignment="Right" Margin="0,0,0,0" VerticalAlignment="Center" Width="70"
Minimum="{Binding VoltageMin}"
Maximum="{Binding VoltageMax}"
Value="{Binding VoltageValue, Mode=TwoWay}"
SmallChange="1" LargeChange="10"
IsEnabled="{Binding IsSlidersEnabled}"
NumberDecimalDigits="0" telerik:StyleManager.Theme="Crystal" FontSize="11"/>
<!-- 最小值 | Minimum Value -->
<TextBlock Grid.Column="0" Text="{Binding VoltageMin, StringFormat={}{0:F0}}"
FontSize="12" Foreground="#616161"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="0 0 0 -17"/>
<!-- 最大值 | Maximum Value -->
<TextBlock Grid.Column="0" Text="{Binding VoltageMax, StringFormat={}{0:F0}}"
FontSize="12" Foreground="#616161"
HorizontalAlignment="Right" VerticalAlignment="Bottom" TextAlignment="Right"
Margin="0,0,10,-17"/>
</Grid>
<!-- 按钮行:配置 | Button Row: Config -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,0,0">
<telerik:RadButton Content="{loc:Localization RaySource_ConfigButton}"
Command="{Binding ConfigCommand}"
Width="70"
telerik:StyleManager.Theme="Crystal" FontSize="10"/>
</StackPanel>
</Grid>
<!-- 3. 电流区 | Current Area -->
<Grid Grid.Row="1" Margin="20,75,20,0" Grid.RowSpan="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标签+实际值 | Label + Actual Value -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{loc:Localization RaySource_CurrentLabel}"
FontSize="14" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding CurrentActualText}"
FontSize="10" VerticalAlignment="Center" HorizontalAlignment="Right"/>
</Grid>
<!-- 滑块 | Slider -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<telerik:RadSlider x:Name="CurrentSlider" Grid.Column="0" HorizontalAlignment="Stretch" Margin="0,6,10,0" VerticalAlignment="Top"
telerik:StyleManager.Theme="Crystal"
Minimum="{Binding CurrentMin}"
Maximum="{Binding CurrentMax}"
Value="{Binding CurrentValue, Mode=TwoWay}"
SmallChange="10" LargeChange="100"
IsDeferredDraggingEnabled="True"
IsEnabled="{Binding IsSlidersEnabled}"/>
<telerik:RadNumericUpDown x:Name="CurrentNumeric" Grid.Column="1" HorizontalAlignment="Right" Margin="0,0,0,0" VerticalAlignment="Center" Width="70"
Minimum="{Binding CurrentMin}"
Maximum="{Binding CurrentMax}"
Value="{Binding CurrentValue, Mode=TwoWay}"
SmallChange="10" LargeChange="100"
IsEnabled="{Binding IsSlidersEnabled}"
NumberDecimalDigits="0" telerik:StyleManager.Theme="Crystal" FontSize="11"/>
<!-- 最小值 | Minimum Value -->
<TextBlock Grid.Column="0" Text="{Binding CurrentMin, StringFormat={}{0:F0}}"
FontSize="12" Foreground="#616161"
HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="0 0 0 -17"/>
<!-- 最大值 | Maximum Value -->
<TextBlock Grid.Column="0" Text="{Binding CurrentMax, StringFormat={}{0:F0}}"
FontSize="12" Foreground="#616161"
HorizontalAlignment="Right" VerticalAlignment="Bottom" TextAlignment="Right"
Margin="0,0,10,-17"/>
</Grid>
<!-- 按钮行:高级 | Button Row: Advance -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,0,0">
<telerik:RadButton Content="{loc:Localization RaySource_AdvanceButton}"
Command="{Binding SettingsCommand}"
Width="70"
telerik:StyleManager.Theme="Crystal" FontSize="10"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,186 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Telerik.Windows.Controls;
using XP.Hardware.RaySource.ViewModels;
namespace XP.Hardware.RaySource.Views
{
/// <summary>
/// 射线源操作视图 code-behind | X-Ray Source Operate View code-behind
/// 负责将滑块 DragCompleted / 输入框确认操作桥接到 ViewModel 命令
/// Bridges slider DragCompleted / numeric confirm actions to ViewModel commands
/// </summary>
public partial class RaySourceOperateView : UserControl
{
// 防止初始化阶段触发写入 | Prevent writes during initialization
private bool _isLoaded = false;
// 标记输入框是否正在键盘输入中(区分手动输入和箭头点击)
// Flag whether numeric input is in keyboard editing (distinguish manual input from arrow click)
private bool _isVoltageEditing = false;
private bool _isCurrentEditing = false;
public RaySourceOperateView()
{
InitializeComponent();
// 滑块拖动完成时提交值到硬件 | Apply value to hardware when slider drag completes
VoltageSlider.DragCompleted += VoltageSlider_DragCompleted;
CurrentSlider.DragCompleted += CurrentSlider_DragCompleted;
// 输入框事件 | Numeric input events
VoltageNumeric.GotKeyboardFocus += (s, e) => _isVoltageEditing = true;
VoltageNumeric.LostFocus += VoltageNumeric_LostFocus;
VoltageNumeric.KeyDown += VoltageNumeric_KeyDown;
VoltageNumeric.ValueChanged += VoltageNumeric_ValueChanged;
CurrentNumeric.GotKeyboardFocus += (s, e) => _isCurrentEditing = true;
CurrentNumeric.LostFocus += CurrentNumeric_LostFocus;
CurrentNumeric.KeyDown += CurrentNumeric_KeyDown;
CurrentNumeric.ValueChanged += CurrentNumeric_ValueChanged;
Loaded += OnViewLoaded;
Unloaded += OnViewUnloaded;
}
/// <summary>
/// 界面加载完成:标记已加载并触发自动初始化流程
/// View loaded: mark as loaded and trigger auto-initialization sequence
/// 参考旧代码 AreaDetector_Load 模式 | Follows AreaDetector_Load pattern from legacy code
/// </summary>
private async void OnViewLoaded(object sender, RoutedEventArgs e)
{
_isLoaded = true;
if (DataContext is RaySourceOperateViewModel vm)
{
await vm.AutoInitializeAsync();
}
}
/// <summary>
/// 界面卸载:触发 ViewModel 的 Dispose 执行断开操作
/// View unloaded: trigger ViewModel Dispose to execute disconnect
/// </summary>
private void OnViewUnloaded(object sender, RoutedEventArgs e)
{
if (DataContext is RaySourceOperateViewModel vm)
{
vm.Dispose();
}
}
/// <summary>
/// 判断是否可以提交(已加载、非设备反馈更新中)| Check if can apply (loaded and not updating from device)
/// </summary>
private bool CanApply(out RaySourceOperateViewModel vm)
{
vm = null;
if (!_isLoaded) return false;
if (DataContext is not RaySourceOperateViewModel viewModel) return false;
if (viewModel.IsUpdatingFromDevice) return false;
vm = viewModel;
return true;
}
#region | Slider Events
/// <summary>
/// 电压滑块拖动完成 | Voltage slider drag completed
/// </summary>
private void VoltageSlider_DragCompleted(object sender, RadDragCompletedEventArgs e)
{
if (CanApply(out var vm))
vm.ApplyVoltageCommand.Execute();
}
/// <summary>
/// 电流滑块拖动完成 | Current slider drag completed
/// </summary>
private void CurrentSlider_DragCompleted(object sender, RadDragCompletedEventArgs e)
{
if (CanApply(out var vm))
vm.ApplyCurrentCommand.Execute();
}
#endregion
#region | Voltage Numeric Events
/// <summary>
/// 电压输入框值变更:仅在非键盘编辑时提交(即箭头按钮点击)
/// Voltage numeric value changed: only apply when not keyboard editing (i.e. arrow button click)
/// </summary>
private void VoltageNumeric_ValueChanged(object sender, RadRangeBaseValueChangedEventArgs e)
{
if (!_isVoltageEditing && CanApply(out var vm))
vm.ApplyVoltageCommand.Execute();
}
/// <summary>
/// 电压输入框回车确认 | Voltage numeric Enter key confirm
/// </summary>
private void VoltageNumeric_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && CanApply(out var vm))
{
_isVoltageEditing = false;
vm.ApplyVoltageCommand.Execute();
}
}
/// <summary>
/// 电压输入框失焦确认 | Voltage numeric lost focus confirm
/// </summary>
private void VoltageNumeric_LostFocus(object sender, RoutedEventArgs e)
{
if (_isVoltageEditing && CanApply(out var vm))
{
_isVoltageEditing = false;
vm.ApplyVoltageCommand.Execute();
}
}
#endregion
#region | Current Numeric Events
/// <summary>
/// 电流输入框值变更:仅在非键盘编辑时提交(即箭头按钮点击)
/// Current numeric value changed: only apply when not keyboard editing (i.e. arrow button click)
/// </summary>
private void CurrentNumeric_ValueChanged(object sender, RadRangeBaseValueChangedEventArgs e)
{
if (!_isCurrentEditing && CanApply(out var vm))
vm.ApplyCurrentCommand.Execute();
}
/// <summary>
/// 电流输入框回车确认 | Current numeric Enter key confirm
/// </summary>
private void CurrentNumeric_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && CanApply(out var vm))
{
_isCurrentEditing = false;
vm.ApplyCurrentCommand.Execute();
}
}
/// <summary>
/// 电流输入框失焦确认 | Current numeric lost focus confirm
/// </summary>
private void CurrentNumeric_LostFocus(object sender, RoutedEventArgs e)
{
if (_isCurrentEditing && CanApply(out var vm))
{
_isCurrentEditing = false;
vm.ApplyCurrentCommand.Execute();
}
}
#endregion
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
<PackageReference Include="Telerik.UI.for.Wpf.NetCore.Xaml" Version="2024.1.408" />
</ItemGroup>
<ItemGroup>
<Folder Include="ViewModels\" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Locked.png" />
<Resource Include="Resources\Unlocked.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\XP.Common\XP.Common.csproj" />
<ProjectReference Include="..\XP.Hardware.RaySource.Comet.Messages\XP.Hardware.RaySource.Comet.Messages.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>