将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -0,0 +1,670 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Prism.Commands;
|
||||
using Prism.Events;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.Detector.Abstractions;
|
||||
using XP.Hardware.Detector.Abstractions.Enums;
|
||||
using XP.Hardware.Detector.Abstractions.Events;
|
||||
using XP.Hardware.Detector.Services;
|
||||
|
||||
namespace XP.Hardware.Detector.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器操作 ViewModel | Detector operation ViewModel
|
||||
/// 负责连接/断开、连续采集、单帧采集、图像显示及状态跟踪
|
||||
/// </summary>
|
||||
public class DetectorImageWindowViewModel : BindableBase
|
||||
{
|
||||
private readonly IDetectorService _detectorService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IImageService _imageSaveService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 帧处理节流标志,防止后台线程堆积 | Frame processing throttle flag to prevent thread pile-up
|
||||
/// 0 = 空闲,1 = 正在处理
|
||||
/// </summary>
|
||||
private int _isProcessingFrame;
|
||||
|
||||
#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();
|
||||
RaisePropertyChanged(nameof(ConnectionStatusText));
|
||||
RaisePropertyChanged(nameof(ConnectionStatusColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接状态文本 | Connection status text
|
||||
/// </summary>
|
||||
public string ConnectionStatusText => _isConnected ? "已连接 | Connected" : "未连接 | Disconnected";
|
||||
|
||||
/// <summary>
|
||||
/// 连接状态指示颜色 | Connection status indicator color
|
||||
/// </summary>
|
||||
public Brush ConnectionStatusColor => _isConnected
|
||||
? new SolidColorBrush(Color.FromRgb(0x4C, 0xAF, 0x50)) // 绿色 | Green
|
||||
: new SolidColorBrush(Color.FromRgb(0xF4, 0x43, 0x36)); // 红色 | Red
|
||||
|
||||
#endregion
|
||||
|
||||
#region 探测器状态 | Detector status
|
||||
|
||||
private DetectorStatus _detectorStatus = DetectorStatus.Uninitialized;
|
||||
/// <summary>
|
||||
/// 当前探测器状态 | Current detector status
|
||||
/// </summary>
|
||||
public DetectorStatus DetectorStatus
|
||||
{
|
||||
get => _detectorStatus;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _detectorStatus, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(DetectorStatusText));
|
||||
RaiseAllCommandsCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 探测器状态文本 | Detector status text
|
||||
/// </summary>
|
||||
public string DetectorStatusText => _detectorStatus switch
|
||||
{
|
||||
Abstractions.Enums.DetectorStatus.Uninitialized => "未初始化 | Uninitialized",
|
||||
Abstractions.Enums.DetectorStatus.Initializing => "初始化中 | Initializing",
|
||||
Abstractions.Enums.DetectorStatus.Ready => "就绪 | Ready",
|
||||
Abstractions.Enums.DetectorStatus.Acquiring => "采集中 | Acquiring",
|
||||
Abstractions.Enums.DetectorStatus.Correcting => "校正中 | Correcting",
|
||||
Abstractions.Enums.DetectorStatus.Error => "错误 | Error",
|
||||
_ => "未知 | Unknown"
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region 探测器信息 | Detector info
|
||||
|
||||
private string _detectorModel = "--";
|
||||
/// <summary>
|
||||
/// 探测器型号 | Detector model
|
||||
/// </summary>
|
||||
public string DetectorModel
|
||||
{
|
||||
get => _detectorModel;
|
||||
private set => SetProperty(ref _detectorModel, value);
|
||||
}
|
||||
|
||||
private string _detectorResolution = "--";
|
||||
/// <summary>
|
||||
/// 探测器分辨率 | Detector resolution
|
||||
/// </summary>
|
||||
public string DetectorResolution
|
||||
{
|
||||
get => _detectorResolution;
|
||||
private set => SetProperty(ref _detectorResolution, value);
|
||||
}
|
||||
|
||||
private string _detectorPixelSize = "--";
|
||||
/// <summary>
|
||||
/// 探测器像素尺寸 | Detector pixel size
|
||||
/// </summary>
|
||||
public string DetectorPixelSize
|
||||
{
|
||||
get => _detectorPixelSize;
|
||||
private set => SetProperty(ref _detectorPixelSize, value);
|
||||
}
|
||||
|
||||
private string _detectorType = "--";
|
||||
/// <summary>
|
||||
/// 探测器类型 | Detector type
|
||||
/// </summary>
|
||||
public string DetectorTypeText
|
||||
{
|
||||
get => _detectorType;
|
||||
private set => SetProperty(ref _detectorType, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 图像属性 | Image properties
|
||||
|
||||
private BitmapSource _imageSource;
|
||||
/// <summary>
|
||||
/// 当前显示的图像 | Currently displayed image
|
||||
/// </summary>
|
||||
public BitmapSource ImageSource
|
||||
{
|
||||
get => _imageSource;
|
||||
private set => SetProperty(ref _imageSource, value);
|
||||
}
|
||||
|
||||
private int _frameNumber;
|
||||
/// <summary>
|
||||
/// 当前帧号 | Current frame number
|
||||
/// </summary>
|
||||
public int FrameNumber
|
||||
{
|
||||
get => _frameNumber;
|
||||
private set => SetProperty(ref _frameNumber, value);
|
||||
}
|
||||
|
||||
private string _imageInfo = "等待采集... | Waiting for acquisition...";
|
||||
/// <summary>
|
||||
/// 图像信息文本 | Image info text
|
||||
/// </summary>
|
||||
public string ImageInfo
|
||||
{
|
||||
get => _imageInfo;
|
||||
private set => SetProperty(ref _imageInfo, value);
|
||||
}
|
||||
|
||||
private bool _isAcquiring;
|
||||
/// <summary>
|
||||
/// 是否正在连续采集 | Whether continuous acquisition is in progress
|
||||
/// </summary>
|
||||
public bool IsAcquiring
|
||||
{
|
||||
get => _isAcquiring;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isAcquiring, value))
|
||||
RaiseAllCommandsCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 操作忙碌状态 | Operation busy state
|
||||
|
||||
private bool _isBusy;
|
||||
/// <summary>
|
||||
/// 是否正在执行连接/断开操作 | Whether connect/disconnect is in progress
|
||||
/// </summary>
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _isBusy, value))
|
||||
RaiseAllCommandsCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _statusMessage = "";
|
||||
/// <summary>
|
||||
/// 操作状态消息 | Operation status message
|
||||
/// </summary>
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
private set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 命令 | Commands
|
||||
|
||||
/// <summary>
|
||||
/// 连接探测器命令 | Connect detector command
|
||||
/// </summary>
|
||||
public DelegateCommand ConnectCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 断开探测器命令 | Disconnect detector command
|
||||
/// </summary>
|
||||
public DelegateCommand DisconnectCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动连续采集命令 | Start continuous acquisition command
|
||||
/// </summary>
|
||||
public DelegateCommand StartAcquisitionCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 停止采集命令 | Stop acquisition command
|
||||
/// </summary>
|
||||
public DelegateCommand StopAcquisitionCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 单帧采集命令 | Single frame acquisition command
|
||||
/// </summary>
|
||||
public DelegateCommand SingleFrameCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 保存图像命令 | Save image command
|
||||
/// </summary>
|
||||
public DelegateCommand SaveImageCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数 | Constructor
|
||||
|
||||
public DetectorImageWindowViewModel(
|
||||
IDetectorService detectorService,
|
||||
IEventAggregator eventAggregator,
|
||||
IImageService imageSaveService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_detectorService = detectorService ?? throw new ArgumentNullException(nameof(detectorService));
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_imageSaveService = imageSaveService ?? throw new ArgumentNullException(nameof(imageSaveService));
|
||||
_logger = logger?.ForModule<DetectorImageWindowViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
ConnectCommand = new DelegateCommand(ExecuteConnectAsync, CanExecuteConnect);
|
||||
DisconnectCommand = new DelegateCommand(ExecuteDisconnectAsync, CanExecuteDisconnect);
|
||||
StartAcquisitionCommand = new DelegateCommand(ExecuteStartAcquisitionAsync, CanExecuteStartAcquisition);
|
||||
StopAcquisitionCommand = new DelegateCommand(ExecuteStopAcquisitionAsync, CanExecuteStopAcquisition);
|
||||
SingleFrameCommand = new DelegateCommand(ExecuteSingleFrameAsync, CanExecuteSingleFrame);
|
||||
SaveImageCommand = new DelegateCommand(ExecuteSaveImageAsync, CanExecuteSaveImage);
|
||||
|
||||
// 订阅图像采集事件(后台线程)| Subscribe to image captured event (background thread)
|
||||
// 图像转换在后台线程执行,避免阻塞 UI | Image conversion runs on background thread to avoid blocking UI
|
||||
_eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||
|
||||
// 订阅探测器状态变更事件 | Subscribe to detector status changed event
|
||||
_eventAggregator.GetEvent<StatusChangedEvent>()
|
||||
.Subscribe(OnDetectorStatusChanged, ThreadOption.UIThread);
|
||||
|
||||
// 同步初始状态 | Sync initial state
|
||||
SyncStatusFromService();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CanExecute
|
||||
|
||||
private bool CanExecuteConnect() => !_isBusy && !_isConnected;
|
||||
private bool CanExecuteDisconnect() => !_isBusy && _isConnected && !_isAcquiring;
|
||||
private bool CanExecuteStartAcquisition() => !_isBusy && _isConnected &&
|
||||
_detectorStatus == Abstractions.Enums.DetectorStatus.Ready &&
|
||||
!_isAcquiring;
|
||||
private bool CanExecuteStopAcquisition() => _isConnected && _isAcquiring;
|
||||
private bool CanExecuteSingleFrame() => !_isBusy && _isConnected &&
|
||||
_detectorStatus == Abstractions.Enums.DetectorStatus.Ready &&
|
||||
!_isAcquiring;
|
||||
private bool CanExecuteSaveImage() => _isConnected && _imageSaveService.LatestFrame != null;
|
||||
|
||||
private void RaiseAllCommandsCanExecuteChanged()
|
||||
{
|
||||
ConnectCommand.RaiseCanExecuteChanged();
|
||||
DisconnectCommand.RaiseCanExecuteChanged();
|
||||
StartAcquisitionCommand.RaiseCanExecuteChanged();
|
||||
StopAcquisitionCommand.RaiseCanExecuteChanged();
|
||||
SingleFrameCommand.RaiseCanExecuteChanged();
|
||||
SaveImageCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 命令执行 | Command execution
|
||||
|
||||
/// <summary>
|
||||
/// 连接探测器 | Connect detector
|
||||
/// </summary>
|
||||
private async void ExecuteConnectAsync()
|
||||
{
|
||||
_logger?.Info("开始连接探测器 | Connecting detector");
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在连接探测器... | Connecting...";
|
||||
try
|
||||
{
|
||||
var result = await _detectorService.InitializeAsync();
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
IsConnected = true;
|
||||
StatusMessage = "探测器连接成功,正在应用参数... | Detector connected, applying parameters...";
|
||||
_logger?.Info("探测器连接成功 | Detector connected successfully");
|
||||
RefreshDetectorInfo();
|
||||
|
||||
// 连接成功后自动应用持久化的探测器参数 | Auto apply saved detector parameters after connection
|
||||
var saved = Config.ConfigLoader.LoadSavedParameters();
|
||||
if (saved.HasValue)
|
||||
{
|
||||
var (binIdx, pga, fr, _) = saved.Value;
|
||||
var applyResult = await _detectorService.ApplyParametersAsync(binIdx, pga, fr);
|
||||
if (applyResult.IsSuccess)
|
||||
{
|
||||
StatusMessage = "探测器就绪 | Detector ready";
|
||||
_logger?.Info("探测器参数已自动应用,Binning={Binning},PGA={PGA},帧率={FrameRate} | Detector parameters auto-applied", binIdx, pga, fr);
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"参数应用失败:{applyResult.ErrorMessage}";
|
||||
_logger?.Warn("自动应用参数失败:{Message} | Auto apply parameters failed: {Message}", applyResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = "探测器就绪(使用默认参数)| Detector ready (default parameters)";
|
||||
_logger?.Info("未找到持久化参数,使用默认配置 | No saved parameters found, using default config");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"连接失败:{result.ErrorMessage}";
|
||||
_logger?.Error(result.Exception, "探测器连接失败:{Message} | Detector connection failed: {Message}", result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"连接异常:{ex.Message}";
|
||||
_logger?.Error(ex, "探测器连接异常:{Message} | Detector connection exception: {Message}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开探测器 | Disconnect detector
|
||||
/// </summary>
|
||||
private async void ExecuteDisconnectAsync()
|
||||
{
|
||||
_logger?.Info("开始断开探测器 | Disconnecting detector");
|
||||
IsBusy = true;
|
||||
StatusMessage = "正在断开探测器... | Disconnecting...";
|
||||
try
|
||||
{
|
||||
var result = await _detectorService.DisconnectAsync();
|
||||
IsConnected = false;
|
||||
IsAcquiring = false;
|
||||
ClearDetectorInfo();
|
||||
ImageInfo = "等待采集... | Waiting for acquisition...";
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
StatusMessage = "探测器已断开 | Detector disconnected";
|
||||
_logger?.Info("探测器已断开 | Detector disconnected");
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusMessage = $"断开时发生错误:{result.ErrorMessage}";
|
||||
_logger?.Warn("断开探测器时发生错误:{Message} | Error during disconnect: {Message}", result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsConnected = false;
|
||||
IsAcquiring = false;
|
||||
ClearDetectorInfo();
|
||||
StatusMessage = $"断开异常:{ex.Message}";
|
||||
_logger?.Error(ex, "断开探测器异常:{Message} | Disconnect exception: {Message}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动连续采集 | Start continuous acquisition
|
||||
/// </summary>
|
||||
private async void ExecuteStartAcquisitionAsync()
|
||||
{
|
||||
_logger?.Info("启动连续采集 | Starting continuous acquisition");
|
||||
IsAcquiring = true;
|
||||
StatusMessage = "采集中... | Acquiring...";
|
||||
try
|
||||
{
|
||||
var result = await _detectorService.StartAcquisitionAsync();
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
IsAcquiring = false;
|
||||
StatusMessage = $"启动采集失败:{result.ErrorMessage}";
|
||||
_logger?.Error(result.Exception, "启动采集失败:{Message} | Start acquisition failed: {Message}", result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsAcquiring = false;
|
||||
StatusMessage = $"启动采集异常:{ex.Message}";
|
||||
_logger?.Error(ex, "启动采集异常:{Message} | Start acquisition exception: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止采集 | Stop acquisition
|
||||
/// </summary>
|
||||
private async void ExecuteStopAcquisitionAsync()
|
||||
{
|
||||
_logger?.Info("停止采集 | Stopping acquisition");
|
||||
StatusMessage = "正在停止采集... | Stopping...";
|
||||
try
|
||||
{
|
||||
var result = await _detectorService.StopAcquisitionAsync();
|
||||
IsAcquiring = false;
|
||||
StatusMessage = result.IsSuccess ? "采集已停止 | Acquisition stopped" : $"停止采集失败:{result.ErrorMessage}";
|
||||
if (!result.IsSuccess)
|
||||
_logger?.Error(result.Exception, "停止采集失败:{Message} | Stop acquisition failed: {Message}", result.ErrorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsAcquiring = false;
|
||||
StatusMessage = $"停止采集异常:{ex.Message}";
|
||||
_logger?.Error(ex, "停止采集异常:{Message} | Stop acquisition exception: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单帧采集 | Single frame acquisition
|
||||
/// </summary>
|
||||
private async void ExecuteSingleFrameAsync()
|
||||
{
|
||||
_logger?.Info("单帧采集 | Single frame acquisition");
|
||||
IsBusy = true;
|
||||
StatusMessage = "单帧采集中... | Acquiring single frame...";
|
||||
try
|
||||
{
|
||||
var result = await _detectorService.AcquireSingleFrameAsync();
|
||||
StatusMessage = result.IsSuccess ? "单帧采集完成 | Single frame acquired" : $"单帧采集失败:{result.ErrorMessage}";
|
||||
if (!result.IsSuccess)
|
||||
_logger?.Error(result.Exception, "单帧采集失败:{Message} | Single frame failed: {Message}", result.ErrorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"单帧采集异常:{ex.Message}";
|
||||
_logger?.Error(ex, "单帧采集异常:{Message} | Single frame exception: {Message}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前图像为 16 位 TIFF | Save current image as 16-bit TIFF
|
||||
/// </summary>
|
||||
private async void ExecuteSaveImageAsync()
|
||||
{
|
||||
var frame = _imageSaveService.LatestFrame;
|
||||
if (frame?.ImageData == null)
|
||||
{
|
||||
StatusMessage = "无可保存的图像 | No image to save";
|
||||
return;
|
||||
}
|
||||
|
||||
_logger?.Info("保存图像,帧号:{FrameNumber} | Saving image, frame: {FrameNumber}", frame.FrameNumber);
|
||||
StatusMessage = "正在保存图像... | Saving image...";
|
||||
try
|
||||
{
|
||||
var saveDir = _imageSaveService.GetDefaultSaveDirectory();
|
||||
var result = await _imageSaveService.SaveLatestFrameAsync(saveDir, "IMG");
|
||||
|
||||
StatusMessage = result.IsSuccess
|
||||
? $"图像已保存 | Image saved"
|
||||
: $"保存失败:{result.ErrorMessage}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"保存异常:{ex.Message}";
|
||||
_logger?.Error(ex, "保存图像异常:{Message} | Save image exception: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件回调 | Event callbacks
|
||||
|
||||
/// <summary>
|
||||
/// 图像采集回调,将 16 位原始数据归一化为 8 位灰度图 | Image captured callback
|
||||
/// </summary>
|
||||
private void OnImageCaptured(ImageCapturedEventArgs args)
|
||||
{
|
||||
if (args?.ImageData == null || args.Width == 0 || args.Height == 0) return;
|
||||
|
||||
// 帧节流:上一帧尚未被 UI 线程消费完毕时,跳过当前帧
|
||||
// Frame throttle: skip current frame if previous frame hasn't been consumed by UI thread yet
|
||||
if (Interlocked.CompareExchange(ref _isProcessingFrame, 1, 0) != 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 在后台线程执行图像转换 | Perform image conversion on background thread
|
||||
var bitmap = ConvertToBitmapSource(args.ImageData, (int)args.Width, (int)args.Height);
|
||||
bitmap.Freeze(); // Freeze 后可跨线程访问 | Freeze allows cross-thread access
|
||||
|
||||
var frameNumber = args.FrameNumber;
|
||||
var imageInfo = $"{args.Width}×{args.Height} 帧#{args.FrameNumber} {args.CaptureTime:HH:mm:ss.fff}";
|
||||
|
||||
// 将属性赋值调度到 UI 线程,完成后才释放节流标志
|
||||
// Dispatch property assignment to UI thread; release throttle flag only after UI completes
|
||||
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ImageSource = bitmap;
|
||||
FrameNumber = frameNumber;
|
||||
ImageInfo = imageInfo;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// UI 线程完成渲染后才允许下一帧进入 | Allow next frame only after UI thread finishes rendering
|
||||
Interlocked.Exchange(ref _isProcessingFrame, 0);
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 异常时释放标志,避免永久锁死 | Release flag on exception to avoid permanent lock
|
||||
Interlocked.Exchange(ref _isProcessingFrame, 0);
|
||||
_logger?.Error(ex, "图像转换失败:{Message} | Image conversion failed: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 探测器状态变更回调 | Detector status changed callback
|
||||
/// </summary>
|
||||
private void OnDetectorStatusChanged(DetectorStatus status)
|
||||
{
|
||||
DetectorStatus = status;
|
||||
|
||||
// 根据状态同步 IsAcquiring | Sync IsAcquiring from status
|
||||
if (status == Abstractions.Enums.DetectorStatus.Acquiring)
|
||||
IsAcquiring = true;
|
||||
else if (status == Abstractions.Enums.DetectorStatus.Ready ||
|
||||
status == Abstractions.Enums.DetectorStatus.Error)
|
||||
IsAcquiring = false;
|
||||
|
||||
// 探测器错误时同步连接状态 | Sync connection state on error
|
||||
if (status == Abstractions.Enums.DetectorStatus.Uninitialized)
|
||||
{
|
||||
IsConnected = false;
|
||||
ClearDetectorInfo();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有辅助方法 | Private helpers
|
||||
|
||||
/// <summary>
|
||||
/// 从服务层同步当前状态(ViewModel 初始化时调用)| Sync state from service (called on init)
|
||||
/// </summary>
|
||||
private void SyncStatusFromService()
|
||||
{
|
||||
IsConnected = _detectorService.IsConnected;
|
||||
DetectorStatus = _detectorService.Status;
|
||||
if (IsConnected)
|
||||
RefreshDetectorInfo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新探测器信息显示 | Refresh detector info display
|
||||
/// </summary>
|
||||
private void RefreshDetectorInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = _detectorService.GetInfo();
|
||||
if (info == null) return;
|
||||
|
||||
DetectorModel = string.IsNullOrEmpty(info.Model) ? "--" : info.Model;
|
||||
DetectorResolution = (info.MaxWidth > 0 && info.MaxHeight > 0)
|
||||
? $"{info.MaxWidth}×{info.MaxHeight}"
|
||||
: "--";
|
||||
DetectorPixelSize = info.PixelSize > 0
|
||||
? $"{info.PixelSize:F3} mm"
|
||||
: "--";
|
||||
DetectorTypeText = _detectorService.Type?.ToString() ?? "--";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Warn("获取探测器信息失败:{Message} | Failed to get detector info: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空探测器信息 | Clear detector info
|
||||
/// </summary>
|
||||
private void ClearDetectorInfo()
|
||||
{
|
||||
DetectorModel = "--";
|
||||
DetectorResolution = "--";
|
||||
DetectorPixelSize = "--";
|
||||
DetectorTypeText = "--";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 16 位灰度数据转换为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
|
||||
/// Convert 16-bit grayscale data to 8-bit BitmapSource (delegates to XP.Common common converter)
|
||||
/// </summary>
|
||||
private static BitmapSource ConvertToBitmapSource(ushort[] data, int width, int height)
|
||||
{
|
||||
return XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(data, width, height);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user