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
}
}