Files
XplorePlane/XP.Hardware.Detector/ViewModels/DetectorConfigViewModel.cs
T

671 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 面阵探测器配置 ViewModel | Area detector configuration ViewModel
/// 根据探测器类型动态加载参数选项,包含校正参数一致性校验和扫描期间 UI 锁定
/// </summary>
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;
/// <summary>
/// 探测器是否已连接 | Whether detector is connected
/// </summary>
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)
/// <summary>
/// PGA 可选项列表 | PGA selectable items
/// </summary>
public ObservableCollection<int> PgaItems { get; } = new ObservableCollection<int>();
private int _selectedPga;
/// <summary>
/// 当前选中的 PGA 值 | Currently selected PGA value
/// </summary>
public int SelectedPga
{
get => _selectedPga;
set => SetProperty(ref _selectedPga, value);
}
#endregion
#region Binning| Pixel binning
/// <summary>
/// Binning 可选项列表 | Binning selectable items
/// </summary>
public ObservableCollection<BinningOption> BinningItems { get; } = new ObservableCollection<BinningOption>();
private int _selectedBinningIndex;
/// <summary>
/// 当前选中的 Binning 索引 | Currently selected binning index
/// </summary>
public int SelectedBinningIndex
{
get => _selectedBinningIndex;
set
{
if (SetProperty(ref _selectedBinningIndex, value))
{
ClampFrameRate();
UpdateImageSpec();
}
}
}
#endregion
#region | Frame rate
private decimal _frameRate = 5m;
/// <summary>
/// 帧率 | Frame rate
/// </summary>
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);
}
}
/// <summary>
/// 帧率最小值 | Frame rate minimum
/// </summary>
public decimal FrameRateMinimum => 0.1m;
private decimal _frameRateMaximum = 15m;
/// <summary>
/// 帧率最大值(随 Binning 变化)| Frame rate maximum (varies with binning)
/// </summary>
public decimal FrameRateMaximum
{
get => _frameRateMaximum;
private set => SetProperty(ref _frameRateMaximum, value);
}
#endregion
#region | Average frames
private int _avgFrames = 1;
/// <summary>
/// 帧合并数 | Average frame count
/// </summary>
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 = "";
/// <summary>
/// 当前 Binning 模式下的图像规格文本 | Image spec text for current binning mode
/// </summary>
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;
/// <summary>
/// 是否正在执行校正操作 | Whether a correction operation is in progress
/// </summary>
public bool IsBusy
{
get => _isBusy;
private set
{
SetProperty(ref _isBusy, value);
RaiseAllCommandsCanExecuteChanged();
}
}
private bool _darkCorrectionDone;
/// <summary>
/// 暗场校正是否已完成 | Whether dark correction is done
/// </summary>
public bool DarkCorrectionDone
{
get => _darkCorrectionDone;
private set
{
SetProperty(ref _darkCorrectionDone, value);
LightCorrectionCommand.RaiseCanExecuteChanged();
}
}
private bool _isParametersLocked;
/// <summary>
/// 参数是否被锁定(扫描采集期间禁止修改)| Whether parameters are locked (during acquisition)
/// </summary>
public bool IsParametersLocked
{
get => _isParametersLocked;
private set
{
SetProperty(ref _isParametersLocked, value);
RaisePropertyChanged(nameof(IsParametersEditable));
RaiseAllCommandsCanExecuteChanged();
}
}
/// <summary>
/// 参数是否可编辑(已连接、未锁定且不忙)| Whether parameters are editable
/// </summary>
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<DetectorConfigViewModel>() ?? 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<StatusChangedEvent>()
.Subscribe(OnDetectorStatusChanged, ThreadOption.UIThread);
// 从配置加载 UI 选项 | Load UI options from config
LoadOptionsFromConfig();
}
#endregion
#region | Public methods
/// <summary>
/// 锁定参数(外部调用,如扫描开始时)| Lock parameters (called externally, e.g. when scan starts)
/// </summary>
public void LockParameters()
{
IsParametersLocked = true;
_logger?.Info("探测器参数已锁定 | Detector parameters locked");
}
/// <summary>
/// 解锁参数(外部调用,如扫描结束时)| Unlock parameters (called externally, e.g. when scan ends)
/// </summary>
public void UnlockParameters()
{
IsParametersLocked = false;
_logger?.Info("探测器参数已解锁 | Detector parameters unlocked");
}
/// <summary>
/// 获取当前参数的图像规格(供扫描配置导出)| Get current image spec for scan config export
/// </summary>
public BinningImageSpec GetCurrentImageSpec()
{
return _config?.GetImageSpec(_selectedBinningIndex);
}
/// <summary>
/// 导出当前探测器参数为字典(供扫描配置保存)| Export current parameters as dictionary
/// </summary>
public System.Collections.Generic.Dictionary<string, string> ExportParameters()
{
var dict = new System.Collections.Generic.Dictionary<string, string>();
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
/// <summary>
/// 从探测器配置加载下拉框选项 | Load combo box options from detector config
/// </summary>
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);
}
/// <summary>
/// 获取最大帧率(优先从配置获取)| Get max frame rate (prefer from config)
/// </summary>
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;
}
}
/// <summary>
/// 更新图像规格显示文本 | Update image spec display text
/// </summary>
private void UpdateImageSpec()
{
var spec = _config?.GetImageSpec(_selectedBinningIndex);
if (spec != null)
{
ImageSpecText = $"{spec.ImageWidth}×{spec.ImageHeight} 像素尺寸 {spec.PixelX}×{spec.PixelY} mm";
}
else
{
ImageSpecText = "";
}
}
/// <summary>
/// 探测器状态变更回调,用于扫描期间自动锁定/解锁参数 | Detector status changed callback
/// </summary>
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");
}
}
/// <summary>
/// 校验亮场校正前参数是否与暗场校正时一致 | Validate parameters consistency before light correction
/// </summary>
/// <returns>参数是否一致 | Whether parameters are consistent</returns>
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;
}
/// <summary>
/// 记录暗场校正时的参数快照 | Record parameter snapshot at dark correction time
/// </summary>
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));
}
/// <summary>
/// 执行暗场校正 | Execute dark correction
/// </summary>
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();
}
}
/// <summary>
/// 执行亮场校正(含参数一致性校验)| Execute light correction (with parameter consistency check)
/// </summary>
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();
}
}
/// <summary>
/// 执行坏像素校正 | Execute bad pixel correction
/// </summary>
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();
}
}
/// <summary>
/// 应用参数到探测器硬件 | Apply parameters to detector hardware
/// </summary>
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
}
}