using System; using System.Collections.ObjectModel; using System.Linq; using Prism.Commands; using Prism.Events; using Prism.Mvvm; using XP.Common.GeneralForm.Views; using XP.Common.Logging.Interfaces; using XP.Hardware.Detector.Abstractions.Events; using XP.Hardware.Detector.Abstractions.Enums; using XP.Hardware.Detector.Config; using XP.Hardware.Detector.Services; namespace XP.Hardware.Detector.ViewModels { /// /// 面阵探测器配置 ViewModel | Area detector configuration ViewModel /// 根据探测器类型动态加载参数选项,包含校正参数一致性校验和扫描期间 UI 锁定 /// public class DetectorConfigViewModel : BindableBase { private readonly IDetectorService _detectorService; private readonly IEventAggregator _eventAggregator; private readonly ILoggerService _logger; private DetectorConfig _config; #region 连接状态 | Connection status private bool _isConnected; /// /// 探测器是否已连接 | Whether detector is connected /// public bool IsConnected { get => _isConnected; private set { if (SetProperty(ref _isConnected, value)) RaiseAllCommandsCanExecuteChanged(); } } #endregion #region 暗场校正时记录的参数快照 | Parameter snapshot at dark correction time private int _darkCorrectionBinningIndex = -1; private int _darkCorrectionPga = -1; private decimal _darkCorrectionFrameRate = -1m; #endregion #region 灵敏度(PGA)| Sensitivity (PGA) /// /// PGA 可选项列表 | PGA selectable items /// public ObservableCollection PgaItems { get; } = new ObservableCollection(); private int _selectedPga; /// /// 当前选中的 PGA 值 | Currently selected PGA value /// public int SelectedPga { get => _selectedPga; set => SetProperty(ref _selectedPga, value); } #endregion #region 像素合并(Binning)| Pixel binning /// /// Binning 可选项列表 | Binning selectable items /// public ObservableCollection BinningItems { get; } = new ObservableCollection(); private int _selectedBinningIndex; /// /// 当前选中的 Binning 索引 | Currently selected binning index /// public int SelectedBinningIndex { get => _selectedBinningIndex; set { if (SetProperty(ref _selectedBinningIndex, value)) { ClampFrameRate(); UpdateImageSpec(); } } } #endregion #region 帧率 | Frame rate private decimal _frameRate = 5m; /// /// 帧率 | Frame rate /// public decimal FrameRate { get => _frameRate; set { if (value < FrameRateMinimum) value = FrameRateMinimum; var max = GetMaxFrameRate(_selectedBinningIndex); if (value > max) { _logger?.Warn("帧率超出当前 Binning 模式上限 {Max},已自动修正 | Frame rate exceeds max {Max} for current binning, auto corrected", max); value = max; } SetProperty(ref _frameRate, value); } } /// /// 帧率最小值 | Frame rate minimum /// public decimal FrameRateMinimum => 0.1m; private decimal _frameRateMaximum = 15m; /// /// 帧率最大值(随 Binning 变化)| Frame rate maximum (varies with binning) /// public decimal FrameRateMaximum { get => _frameRateMaximum; private set => SetProperty(ref _frameRateMaximum, value); } #endregion #region 帧合并 | Average frames private int _avgFrames = 1; /// /// 帧合并数 | Average frame count /// public int AvgFrames { get => _avgFrames; set { if (value < 1) value = 1; SetProperty(ref _avgFrames, value); } } #endregion #region 图像规格(只读,随 Binning 变化)| Image spec (read-only, varies with binning) private string _imageSpecText = ""; /// /// 当前 Binning 模式下的图像规格文本 | Image spec text for current binning mode /// public string ImageSpecText { get => _imageSpecText; private set => SetProperty(ref _imageSpecText, value); } #endregion #region 命令 | Commands public DelegateCommand DarkCorrectionCommand { get; } public DelegateCommand LightCorrectionCommand { get; } public DelegateCommand BadPixelCorrectionCommand { get; } public DelegateCommand ApplyParametersCommand { get; } #endregion #region 状态 | Status private bool _isBusy; /// /// 是否正在执行校正操作 | Whether a correction operation is in progress /// public bool IsBusy { get => _isBusy; private set { SetProperty(ref _isBusy, value); RaiseAllCommandsCanExecuteChanged(); } } private bool _darkCorrectionDone; /// /// 暗场校正是否已完成 | Whether dark correction is done /// public bool DarkCorrectionDone { get => _darkCorrectionDone; private set { SetProperty(ref _darkCorrectionDone, value); LightCorrectionCommand.RaiseCanExecuteChanged(); } } private bool _isParametersLocked; /// /// 参数是否被锁定(扫描采集期间禁止修改)| Whether parameters are locked (during acquisition) /// public bool IsParametersLocked { get => _isParametersLocked; private set { SetProperty(ref _isParametersLocked, value); RaisePropertyChanged(nameof(IsParametersEditable)); RaiseAllCommandsCanExecuteChanged(); } } /// /// 参数是否可编辑(已连接、未锁定且不忙)| Whether parameters are editable /// public bool IsParametersEditable => _isConnected && !_isParametersLocked && !_isBusy; #endregion #region 构造函数 | Constructor public DetectorConfigViewModel( IDetectorService detectorService, IEventAggregator eventAggregator, ILoggerService logger) { _detectorService = detectorService ?? throw new ArgumentNullException(nameof(detectorService)); _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); DarkCorrectionCommand = new DelegateCommand(ExecuteDarkCorrectionAsync, CanExecuteCorrection); LightCorrectionCommand = new DelegateCommand(ExecuteLightCorrectionAsync, CanExecuteLightCorrection); BadPixelCorrectionCommand = new DelegateCommand(ExecuteBadPixelCorrectionAsync, CanExecuteCorrection); ApplyParametersCommand = new DelegateCommand(ExecuteApplyParametersAsync, CanExecuteCorrection); // 订阅探测器状态变更事件,用于扫描期间锁定参数 | Subscribe to status changed event for parameter locking _eventAggregator.GetEvent() .Subscribe(OnDetectorStatusChanged, ThreadOption.UIThread); // 从配置加载 UI 选项 | Load UI options from config LoadOptionsFromConfig(); } #endregion #region 公共方法 | Public methods /// /// 锁定参数(外部调用,如扫描开始时)| Lock parameters (called externally, e.g. when scan starts) /// public void LockParameters() { IsParametersLocked = true; _logger?.Info("探测器参数已锁定 | Detector parameters locked"); } /// /// 解锁参数(外部调用,如扫描结束时)| Unlock parameters (called externally, e.g. when scan ends) /// public void UnlockParameters() { IsParametersLocked = false; _logger?.Info("探测器参数已解锁 | Detector parameters unlocked"); } /// /// 获取当前参数的图像规格(供扫描配置导出)| Get current image spec for scan config export /// public BinningImageSpec GetCurrentImageSpec() { return _config?.GetImageSpec(_selectedBinningIndex); } /// /// 导出当前探测器参数为字典(供扫描配置保存)| Export current parameters as dictionary /// public System.Collections.Generic.Dictionary ExportParameters() { var dict = new System.Collections.Generic.Dictionary(); var spec = GetCurrentImageSpec(); var binningName = _selectedBinningIndex < BinningItems.Count ? BinningItems[_selectedBinningIndex].DisplayName : "1×1"; dict["Det_Binning"] = binningName; dict["Det_PGA"] = _selectedPga.ToString(); dict["Det_Frame_rate"] = _frameRate.ToString(System.Globalization.CultureInfo.InvariantCulture); dict["Det_Avg_Frames"] = _avgFrames.ToString(); if (spec != null) { dict["Pixel_X"] = spec.PixelX.ToString(System.Globalization.CultureInfo.InvariantCulture); dict["Pixel_Y"] = spec.PixelY.ToString(System.Globalization.CultureInfo.InvariantCulture); dict["Image_Size_Width"] = spec.ImageWidth.ToString(); dict["Image_Size_Height"] = spec.ImageHeight.ToString(); } return dict; } #endregion #region 私有方法 | Private methods /// /// 从探测器配置加载下拉框选项 | Load combo box options from detector config /// private void LoadOptionsFromConfig() { _config = _detectorService.GetCurrentConfig(); // 加载 Binning 选项 | Load binning options BinningItems.Clear(); var binnings = _config?.GetSupportedBinnings(); if (binnings != null && binnings.Count > 0) { foreach (var b in binnings) BinningItems.Add(b); } else { BinningItems.Add(new BinningOption("1×1", 0)); BinningItems.Add(new BinningOption("2×2", 1)); } // 加载 PGA 选项 | Load PGA options PgaItems.Clear(); var pgas = _config?.GetSupportedPgaValues(); if (pgas != null && pgas.Count > 0) { foreach (var p in pgas) PgaItems.Add(p); } else { foreach (var p in new[] { 2, 3, 4, 5, 6, 7 }) PgaItems.Add(p); } // 尝试从持久化配置恢复参数 | Try to restore parameters from saved config var saved = ConfigLoader.LoadSavedParameters(); if (saved.HasValue) { var (binIdx, pga, fr, avg) = saved.Value; SelectedBinningIndex = binIdx < BinningItems.Count ? binIdx : 0; SelectedPga = PgaItems.Contains(pga) ? pga : PgaItems.FirstOrDefault(); FrameRate = fr > 0 ? fr : 5m; AvgFrames = avg > 0 ? avg : 1; _logger?.Info("从持久化配置恢复参数,Binning={Binning},PGA={PGA},帧率={FrameRate},帧合并={AvgFrames} | Restored parameters from saved config", binIdx, pga, fr, avg); } else { SelectedBinningIndex = 0; SelectedPga = PgaItems.FirstOrDefault(); } // 初始化帧率上限 | Initialize frame rate maximum FrameRateMaximum = GetMaxFrameRate(_selectedBinningIndex); UpdateImageSpec(); _logger?.Info("探测器配置选项已加载,类型={Type},Binning选项数={BinCount},PGA选项数={PgaCount} | Detector config options loaded", _config?.Type.ToString() ?? "未知", BinningItems.Count, PgaItems.Count); } /// /// 获取最大帧率(优先从配置获取)| Get max frame rate (prefer from config) /// private decimal GetMaxFrameRate(int binningIndex) { return _config?.GetMaxFrameRate(binningIndex) ?? 15m; } private void ClampFrameRate() { var max = GetMaxFrameRate(_selectedBinningIndex); FrameRateMaximum = max; if (_frameRate > max) { _logger?.Warn("Binning 变更,帧率已从 {Old} 调整为上限 {Max} | Binning changed, frame rate adjusted from {Old} to max {Max}", _frameRate, max); FrameRate = max; } } /// /// 更新图像规格显示文本 | Update image spec display text /// private void UpdateImageSpec() { var spec = _config?.GetImageSpec(_selectedBinningIndex); if (spec != null) { ImageSpecText = $"{spec.ImageWidth}×{spec.ImageHeight} 像素尺寸 {spec.PixelX}×{spec.PixelY} mm"; } else { ImageSpecText = ""; } } /// /// 探测器状态变更回调,用于扫描期间自动锁定/解锁参数 | Detector status changed callback /// private void OnDetectorStatusChanged(DetectorStatus status) { // 同步连接状态:非 Uninitialized 即视为已连接 | Sync connection status: connected if not Uninitialized IsConnected = status != DetectorStatus.Uninitialized; if (status == DetectorStatus.Acquiring) { IsParametersLocked = true; _logger?.Debug("探测器进入采集状态,参数已自动锁定 | Detector acquiring, parameters auto-locked"); } else if (status == DetectorStatus.Ready) { IsParametersLocked = false; _logger?.Debug("探测器就绪,参数已自动解锁 | Detector ready, parameters auto-unlocked"); } } /// /// 校验亮场校正前参数是否与暗场校正时一致 | Validate parameters consistency before light correction /// /// 参数是否一致 | Whether parameters are consistent private bool ValidateCorrectionParametersConsistency() { if (_darkCorrectionBinningIndex < 0) { _logger?.Warn("未执行暗场校正,无法进行亮场校正 | Dark correction not done, cannot perform light correction"); return false; } if (_selectedBinningIndex != _darkCorrectionBinningIndex || _selectedPga != _darkCorrectionPga || _frameRate != _darkCorrectionFrameRate) { _logger?.Warn("暗场校正与亮场校正参数不一致:暗场 Binning={DarkBin},PGA={DarkPga},帧率={DarkFr};当前 Binning={CurBin},PGA={CurPga},帧率={CurFr} | Parameter mismatch between dark and light correction", _darkCorrectionBinningIndex, _darkCorrectionPga, _darkCorrectionFrameRate, _selectedBinningIndex, _selectedPga, _frameRate); return false; } return true; } /// /// 记录暗场校正时的参数快照 | Record parameter snapshot at dark correction time /// private void RecordDarkCorrectionParameters() { _darkCorrectionBinningIndex = _selectedBinningIndex; _darkCorrectionPga = _selectedPga; _darkCorrectionFrameRate = _frameRate; } #endregion #region 命令执行 | Command execution private bool CanExecuteCorrection() => _isConnected && !_isBusy && !_isParametersLocked; private bool CanExecuteLightCorrection() => _isConnected && !_isBusy && !_isParametersLocked && _darkCorrectionDone; private void RaiseAllCommandsCanExecuteChanged() { DarkCorrectionCommand.RaiseCanExecuteChanged(); LightCorrectionCommand.RaiseCanExecuteChanged(); BadPixelCorrectionCommand.RaiseCanExecuteChanged(); ApplyParametersCommand.RaiseCanExecuteChanged(); RaisePropertyChanged(nameof(IsParametersEditable)); } /// /// 执行暗场校正 | Execute dark correction /// private async void ExecuteDarkCorrectionAsync() { var binningName = _selectedBinningIndex < BinningItems.Count ? BinningItems[_selectedBinningIndex].DisplayName : "?"; _logger?.Info("开始暗场校正,Binning={Binning},PGA={PGA},帧率={FrameRate} | Starting dark correction", binningName, _selectedPga, _frameRate); // 显示进度条窗口 | Show progress window var progressWindow = new ProgressWindow( title: "暗场校正 | Dark Correction", message: "正在应用参数... | Applying parameters...", isCancelable: false, logger: _logger); progressWindow.Show(); IsBusy = true; try { // 1. 应用参数到硬件 | Apply parameters to hardware progressWindow.UpdateProgress("正在应用参数... | Applying parameters...", 10); var applyResult = await _detectorService.ApplyParametersAsync(_selectedBinningIndex, _selectedPga, _frameRate); if (!applyResult.IsSuccess) { _logger?.Error(applyResult.Exception, "应用参数失败,暗场校正中止:{Message} | Apply parameters failed, dark correction aborted: {Message}", applyResult.ErrorMessage); return; } // 2. 执行暗场校正 | Execute dark correction progressWindow.UpdateProgress("正在采集暗场数据... | Acquiring dark field data...", 30); var result = await _detectorService.DarkCorrectionAsync(_avgFrames); if (result.IsSuccess) { progressWindow.UpdateProgress("暗场校正完成 | Dark correction completed", 100); RecordDarkCorrectionParameters(); DarkCorrectionDone = true; _logger?.Info("暗场校正完成 | Dark correction completed"); _detectorService.SaveParameters(_selectedBinningIndex, _selectedPga, _frameRate, _avgFrames); } else { _logger?.Error(result.Exception, "暗场校正失败:{Message} | Dark correction failed: {Message}", result.ErrorMessage); } } catch (Exception ex) { _logger?.Error(ex, "暗场校正异常:{Message} | Dark correction exception: {Message}", ex.Message); } finally { IsBusy = false; progressWindow.Close(); } } /// /// 执行亮场校正(含参数一致性校验)| Execute light correction (with parameter consistency check) /// private async void ExecuteLightCorrectionAsync() { // 校验参数一致性 | Validate parameter consistency if (!ValidateCorrectionParametersConsistency()) { _logger?.Warn("暗场校正与亮场校正参数不一致,请重新进行暗场校正 | Parameter mismatch, please redo dark correction"); DarkCorrectionDone = false; return; } var binningName = _selectedBinningIndex < BinningItems.Count ? BinningItems[_selectedBinningIndex].DisplayName : "?"; _logger?.Info("开始亮场校正,Binning={Binning},PGA={PGA},帧率={FrameRate} | Starting light correction", binningName, _selectedPga, _frameRate); // 显示进度条窗口 | Show progress window var progressWindow = new ProgressWindow( title: "亮场校正 | Light Correction", message: "正在采集亮场数据... | Acquiring light field data...", isCancelable: false, logger: _logger); progressWindow.Show(); IsBusy = true; try { progressWindow.UpdateProgress("正在采集亮场数据... | Acquiring light field data...", 30); var result = await _detectorService.GainCorrectionAsync(_avgFrames); if (result.IsSuccess) { progressWindow.UpdateProgress("亮场校正完成 | Light correction completed", 100); _logger?.Info("亮场校正完成 | Light correction completed"); } else { _logger?.Error(result.Exception, "亮场校正失败:{Message} | Light correction failed: {Message}", result.ErrorMessage); } } catch (Exception ex) { _logger?.Error(ex, "亮场校正异常:{Message} | Light correction exception: {Message}", ex.Message); } finally { IsBusy = false; progressWindow.Close(); } } /// /// 执行坏像素校正 | Execute bad pixel correction /// private async void ExecuteBadPixelCorrectionAsync() { _logger?.Info("开始坏像素校正 | Starting bad pixel correction"); // 显示进度条窗口 | Show progress window var progressWindow = new ProgressWindow( title: "坏像素校正 | Bad Pixel Correction", message: "正在检测坏像素... | Detecting bad pixels...", isCancelable: false, logger: _logger); progressWindow.Show(); IsBusy = true; try { progressWindow.UpdateProgress("正在检测坏像素... | Detecting bad pixels...", 30); var result = await _detectorService.BadPixelCorrectionAsync(); if (result.IsSuccess) { progressWindow.UpdateProgress("坏像素校正完成 | Bad pixel correction completed", 100); _logger?.Info("坏像素校正完成 | Bad pixel correction completed"); } else { _logger?.Error(result.Exception, "坏像素校正失败:{Message} | Bad pixel correction failed: {Message}", result.ErrorMessage); } } catch (Exception ex) { _logger?.Error(ex, "坏像素校正异常:{Message} | Bad pixel correction exception: {Message}", ex.Message); } finally { IsBusy = false; progressWindow.Close(); } } /// /// 应用参数到探测器硬件 | Apply parameters to detector hardware /// private async void ExecuteApplyParametersAsync() { var binningName = _selectedBinningIndex < BinningItems.Count ? BinningItems[_selectedBinningIndex].DisplayName : "?"; _logger?.Info("应用探测器参数,Binning={Binning},PGA={PGA},帧率={FrameRate} | Applying detector parameters", binningName, _selectedPga, _frameRate); IsBusy = true; try { var result = await _detectorService.ApplyParametersAsync(_selectedBinningIndex, _selectedPga, _frameRate); if (result.IsSuccess) { _logger?.Info("参数应用成功 | Parameters applied successfully"); // 持久化参数 | Persist parameters _detectorService.SaveParameters(_selectedBinningIndex, _selectedPga, _frameRate, _avgFrames); } else { _logger?.Error(result.Exception, "参数应用失败:{Message} | Parameters apply failed: {Message}", result.ErrorMessage); } } catch (Exception ex) { _logger?.Error(ex, "参数应用异常:{Message} | Parameters apply exception: {Message}", ex.Message); } finally { IsBusy = false; } } #endregion } }