feat: 基础版本
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
tmp/
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
@@ -16,9 +18,42 @@ public partial class App : Application
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
int simulateIndex = ParseSimulateArg(desktop.Args);
|
||||
var noiseRange = ParseRangeArg(desktop.Args);
|
||||
desktop.MainWindow = new MainWindow(simulateIndex, noiseRange);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private static int ParseSimulateArg(string[]? args)
|
||||
{
|
||||
if (args == null) return -1;
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (string.Equals(args[i], "--simulate", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int idx) && idx >= 1 && idx <= 4)
|
||||
return idx;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static (double Lo, double Hi)? ParseRangeArg(string[]? args)
|
||||
{
|
||||
if (args == null) return null;
|
||||
for (int i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (string.Equals(args[i], "--range", StringComparison.OrdinalIgnoreCase)
|
||||
&& i + 2 < args.Length
|
||||
&& double.TryParse(args[i + 1], System.Globalization.CultureInfo.InvariantCulture, out double lo)
|
||||
&& double.TryParse(args[i + 2], System.Globalization.CultureInfo.InvariantCulture, out double hi))
|
||||
{
|
||||
return (lo, hi);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
|
||||
|
||||
<!-- 快速保存 -->
|
||||
<Button Classes="tool-btn">
|
||||
<Button Classes="tool-btn" Click="OnQuickSaveClick">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#2196F3"
|
||||
Data="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
|
||||
@@ -215,7 +215,7 @@
|
||||
</Button>
|
||||
|
||||
<!-- 导出光谱数据 -->
|
||||
<Button Classes="tool-btn">
|
||||
<Button Classes="tool-btn" Click="OnExportClick">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#FF5722"
|
||||
Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M12,19L8,15H10.5V12H13.5V15H16L12,19Z" />
|
||||
@@ -224,7 +224,7 @@
|
||||
</Button>
|
||||
|
||||
<!-- 导入光谱数据 -->
|
||||
<Button Classes="tool-btn">
|
||||
<Button Classes="tool-btn" Click="OnImportClick">
|
||||
<StackPanel Spacing="2">
|
||||
<PathIcon Foreground="#3F51B5"
|
||||
Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M12,12L16,16H13.5V19H10.5V16H8L12,12Z" />
|
||||
@@ -253,6 +253,9 @@
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="2,1" />
|
||||
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
|
||||
Text="{Binding DeviceSettings.Spectrometer1.AverageCount, StringFormat=平均: {0}次}" />
|
||||
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="2,1" />
|
||||
<TextBlock FontSize="10" Foreground="#FF9800" VerticalAlignment="Center"
|
||||
Text="{Binding CalibrationStatus}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
|
||||
@@ -305,6 +308,9 @@
|
||||
<TabItem Header="合并光谱">
|
||||
<views:SpectrumChartView DataContext="{Binding MergedChart}" />
|
||||
</TabItem>
|
||||
<TabItem Header="吸光度">
|
||||
<views:SpectrumChartView DataContext="{Binding AbsorbanceChart}" />
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- GridSplitter between center and right panel -->
|
||||
|
||||
@@ -12,10 +12,12 @@ namespace RamanApp;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
public MainWindow() : this(-1, null) { }
|
||||
|
||||
public MainWindow(int simulateIndex, (double Lo, double Hi)? noiseRange = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = new MainWindowViewModel();
|
||||
DataContext = new MainWindowViewModel(simulateIndex, noiseRange);
|
||||
}
|
||||
|
||||
private MainWindowViewModel Vm => (MainWindowViewModel)DataContext!;
|
||||
@@ -181,6 +183,11 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
private void OnQuickSaveClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Vm.QuickSave();
|
||||
}
|
||||
|
||||
private void OnAutoScaleClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var avaPlot = FindActiveAvaPlot();
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<!--<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="SampleData\sample_spectra.csv" LogicalName="RamanApp.SampleData.sample_spectra.csv" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.3.17" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.17" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using RamanApp.Models;
|
||||
|
||||
namespace RamanApp.Services;
|
||||
|
||||
public class CalibrationStore
|
||||
{
|
||||
private readonly string _baseDir;
|
||||
|
||||
public CalibrationStore(string? baseDir = null)
|
||||
{
|
||||
_baseDir = baseDir ?? Path.Combine(AppContext.BaseDirectory, "calibration");
|
||||
}
|
||||
|
||||
public void SaveDark(int integrationTimeMs, string deviceId, SpectrumData spectrum)
|
||||
{
|
||||
var dir = GetDeviceDir(integrationTimeMs, deviceId);
|
||||
Directory.CreateDirectory(dir);
|
||||
SpectrumFileService.ExportCsv(Path.Combine(dir, "dark.csv"), spectrum, 0);
|
||||
}
|
||||
|
||||
public void SaveReference(int integrationTimeMs, string deviceId, SpectrumData spectrum)
|
||||
{
|
||||
var dir = GetDeviceDir(integrationTimeMs, deviceId);
|
||||
Directory.CreateDirectory(dir);
|
||||
SpectrumFileService.ExportCsv(Path.Combine(dir, "reference.csv"), spectrum, 0);
|
||||
}
|
||||
|
||||
public SpectrumData? LoadDark(int integrationTimeMs, string deviceId)
|
||||
{
|
||||
var path = Path.Combine(GetDeviceDir(integrationTimeMs, deviceId), "dark.csv");
|
||||
if (!File.Exists(path)) return null;
|
||||
return SpectrumFileService.ImportCsv(path);
|
||||
}
|
||||
|
||||
public SpectrumData? LoadReference(int integrationTimeMs, string deviceId)
|
||||
{
|
||||
var path = Path.Combine(GetDeviceDir(integrationTimeMs, deviceId), "reference.csv");
|
||||
if (!File.Exists(path)) return null;
|
||||
return SpectrumFileService.ImportCsv(path);
|
||||
}
|
||||
|
||||
public bool HasCalibration(int integrationTimeMs, string deviceId)
|
||||
{
|
||||
var dir = GetDeviceDir(integrationTimeMs, deviceId);
|
||||
return File.Exists(Path.Combine(dir, "dark.csv"))
|
||||
&& File.Exists(Path.Combine(dir, "reference.csv"));
|
||||
}
|
||||
|
||||
public bool HasDark(int integrationTimeMs, string deviceId)
|
||||
{
|
||||
return File.Exists(Path.Combine(GetDeviceDir(integrationTimeMs, deviceId), "dark.csv"));
|
||||
}
|
||||
|
||||
public bool HasReference(int integrationTimeMs, string deviceId)
|
||||
{
|
||||
return File.Exists(Path.Combine(GetDeviceDir(integrationTimeMs, deviceId), "reference.csv"));
|
||||
}
|
||||
|
||||
public List<int> GetAvailableIntegrationTimes()
|
||||
{
|
||||
if (!Directory.Exists(_baseDir))
|
||||
return [];
|
||||
|
||||
return Directory.GetDirectories(_baseDir)
|
||||
.Select(d => Path.GetFileName(d))
|
||||
.Where(name => name.EndsWith("ms"))
|
||||
.Select(name => name[..^2])
|
||||
.Where(n => int.TryParse(n, out _))
|
||||
.Select(int.Parse)
|
||||
.OrderBy(t => t)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public List<string> GetCalibratedDevices(int integrationTimeMs)
|
||||
{
|
||||
var itDir = Path.Combine(_baseDir, $"{integrationTimeMs}ms");
|
||||
if (!Directory.Exists(itDir))
|
||||
return [];
|
||||
|
||||
return Directory.GetDirectories(itDir)
|
||||
.Where(d => File.Exists(Path.Combine(d, "dark.csv"))
|
||||
&& File.Exists(Path.Combine(d, "reference.csv")))
|
||||
.Select(Path.GetFileName)
|
||||
.Where(n => n != null)
|
||||
.Cast<string>()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private string GetDeviceDir(int integrationTimeMs, string deviceId)
|
||||
{
|
||||
var safeName = string.Join("_", deviceId.Split(Path.GetInvalidFileNameChars()));
|
||||
return Path.Combine(_baseDir, $"{integrationTimeMs}ms", safeName);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@ using RamanApp.Models;
|
||||
|
||||
namespace RamanApp.Services;
|
||||
|
||||
internal enum AcquisitionTarget
|
||||
{
|
||||
Sample,
|
||||
Dark,
|
||||
Reference,
|
||||
}
|
||||
|
||||
public class FakeSpectrometerDevice : ISpectrometerDevice
|
||||
{
|
||||
public string Name { get; }
|
||||
@@ -14,27 +21,22 @@ public class FakeSpectrometerDevice : ISpectrometerDevice
|
||||
|
||||
private readonly double _startWavelength;
|
||||
private readonly double _endWavelength;
|
||||
private readonly double _laserWavelength;
|
||||
private readonly Random _rng = new();
|
||||
private readonly (double Wavenumber, double Intensity, double Width)[] _peaks;
|
||||
|
||||
private static readonly (double Wavenumber, double Intensity, double Width)[] SubstratePeaks =
|
||||
[
|
||||
(430, 2000, 40), // SiO2 broad band
|
||||
(800, 1500, 30), // SiO2 ring breathing
|
||||
(1060, 1000, 35), // SiO2 asymmetric stretch
|
||||
];
|
||||
private readonly int _sampleIndex;
|
||||
private readonly double _noiseLo;
|
||||
private readonly double _noiseHi;
|
||||
|
||||
public FakeSpectrometerDevice(string name, string serial,
|
||||
double startNm, double endNm, double laserNm,
|
||||
(double Wavenumber, double Intensity, double Width)[] peaks)
|
||||
double startNm, double endNm, int sampleIndex = 0,
|
||||
double noiseLo = 0, double noiseHi = 0)
|
||||
{
|
||||
Name = name;
|
||||
SerialNumber = serial;
|
||||
_startWavelength = startNm;
|
||||
_endWavelength = endNm;
|
||||
_laserWavelength = laserNm;
|
||||
_peaks = peaks;
|
||||
_sampleIndex = sampleIndex;
|
||||
_noiseLo = noiseLo;
|
||||
_noiseHi = noiseHi;
|
||||
}
|
||||
|
||||
public double[] GetWavelengths()
|
||||
@@ -46,68 +48,81 @@ public class FakeSpectrometerDevice : ISpectrometerDevice
|
||||
return w;
|
||||
}
|
||||
|
||||
public async Task<SpectrumData> AcquireAsync(SpectrometerSettings settings, CancellationToken ct = default)
|
||||
public Task<SpectrumData> AcquireAsync(SpectrometerSettings settings, CancellationToken ct = default)
|
||||
=> AcquireCoreAsync(settings, AcquisitionTarget.Sample, ct);
|
||||
|
||||
public Task<SpectrumData> AcquireDarkAsync(SpectrometerSettings settings, CancellationToken ct = default)
|
||||
=> AcquireCoreAsync(settings, AcquisitionTarget.Dark, ct);
|
||||
|
||||
public Task<SpectrumData> AcquireReferenceAsync(SpectrometerSettings settings, CancellationToken ct = default)
|
||||
=> AcquireCoreAsync(settings, AcquisitionTarget.Reference, ct);
|
||||
|
||||
private async Task<SpectrumData> AcquireCoreAsync(SpectrometerSettings settings,
|
||||
AcquisitionTarget target, CancellationToken ct)
|
||||
{
|
||||
int delayMs = Math.Max(settings.IntegrationTimeMs * settings.AverageCount / 10, 100);
|
||||
int delayMs = Math.Max(settings.IntegrationTimeMs * settings.AverageCount / 10, 50);
|
||||
await Task.Delay(delayMs, ct);
|
||||
|
||||
var wavelengths = GetWavelengths();
|
||||
var intensities = GenerateSpectrum(wavelengths, settings);
|
||||
return new SpectrumData(wavelengths, intensities, Name);
|
||||
}
|
||||
var provider = SampleDataProvider.Instance;
|
||||
double itScale = settings.IntegrationTimeMs / 100.0;
|
||||
|
||||
double[] source = target switch
|
||||
{
|
||||
AcquisitionTarget.Dark => provider.Dark,
|
||||
AcquisitionTarget.Reference => provider.Reference,
|
||||
_ => GetSampleArray(provider, _sampleIndex),
|
||||
};
|
||||
|
||||
private double[] GenerateSpectrum(double[] wavelengths, SpectrometerSettings settings)
|
||||
{
|
||||
double scale = settings.IntegrationTimeMs / 100.0;
|
||||
double noiseReduction = Math.Sqrt(Math.Max(1, settings.AverageCount));
|
||||
var intensities = new double[PixelCount];
|
||||
|
||||
for (int i = 0; i < PixelCount; i++)
|
||||
{
|
||||
double signal;
|
||||
if (settings.DarkAcquisition)
|
||||
{
|
||||
double darkCurrent = 50 * scale;
|
||||
double readNoise = 8.0 / noiseReduction;
|
||||
signal = darkCurrent + (_rng.NextDouble() * 2 - 1) * readNoise;
|
||||
}
|
||||
else if (settings.LightAcquisition)
|
||||
{
|
||||
double wn = WavelengthToWavenumber(wavelengths[i]);
|
||||
double bg = (250 + 0.12 * wn - 2e-5 * wn * wn + 1.2e-9 * wn * wn * wn) * scale;
|
||||
double substratePeak = 0;
|
||||
foreach (var (center, intensity, width) in SubstratePeaks)
|
||||
{
|
||||
double delta = wn - center;
|
||||
substratePeak += intensity * scale * (width * width) / (delta * delta + width * width);
|
||||
}
|
||||
signal = bg + substratePeak;
|
||||
double noise = Math.Sqrt(Math.Max(1, signal)) / noiseReduction;
|
||||
signal += (_rng.NextDouble() * 2 - 1) * noise;
|
||||
}
|
||||
else
|
||||
{
|
||||
double wn = WavelengthToWavenumber(wavelengths[i]);
|
||||
double bg = (300 + 0.08 * wn - 1.5e-5 * wn * wn + 1e-9 * wn * wn * wn) * scale;
|
||||
double baseVal = Interpolate(provider.Wavelengths, source, wavelengths[i]) * itScale;
|
||||
|
||||
double peakSignal = 0;
|
||||
foreach (var (center, intensity, width) in _peaks)
|
||||
{
|
||||
double delta = wn - center;
|
||||
peakSignal += intensity * scale * (width * width) / (delta * delta + width * width);
|
||||
}
|
||||
|
||||
signal = bg + peakSignal;
|
||||
double noise = Math.Sqrt(Math.Max(1, signal)) / noiseReduction;
|
||||
signal += (_rng.NextDouble() * 2 - 1) * noise;
|
||||
if (_noiseHi > 0)
|
||||
{
|
||||
double sigma = _noiseLo + (_noiseHi - _noiseLo) * _rng.NextDouble();
|
||||
baseVal += NextGaussian() * sigma;
|
||||
}
|
||||
|
||||
intensities[i] = Math.Max(0, signal);
|
||||
intensities[i] = Math.Max(0, baseVal);
|
||||
}
|
||||
|
||||
if (settings.BoxcarWidth > 0)
|
||||
ApplyBoxcar(intensities, settings.BoxcarWidth);
|
||||
|
||||
return intensities;
|
||||
return new SpectrumData(wavelengths, intensities, Name);
|
||||
}
|
||||
|
||||
private double NextGaussian()
|
||||
{
|
||||
double u1 = 1.0 - _rng.NextDouble();
|
||||
double u2 = _rng.NextDouble();
|
||||
return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2);
|
||||
}
|
||||
|
||||
private static double[] GetSampleArray(SampleDataProvider p, int index) => index switch
|
||||
{
|
||||
0 => p.SampleOD04,
|
||||
1 => p.SampleOD02,
|
||||
2 => p.SampleOD01,
|
||||
3 => p.SampleOD0025,
|
||||
_ => p.SampleOD04,
|
||||
};
|
||||
|
||||
private static double Interpolate(double[] xs, double[] ys, double x)
|
||||
{
|
||||
if (x <= xs[0]) return ys[0];
|
||||
if (x >= xs[^1]) return ys[^1];
|
||||
|
||||
int idx = Array.BinarySearch(xs, x);
|
||||
if (idx >= 0) return ys[idx];
|
||||
idx = ~idx;
|
||||
if (idx == 0) return ys[0];
|
||||
if (idx >= xs.Length) return ys[^1];
|
||||
|
||||
double t = (x - xs[idx - 1]) / (xs[idx] - xs[idx - 1]);
|
||||
return ys[idx - 1] + t * (ys[idx] - ys[idx - 1]);
|
||||
}
|
||||
|
||||
private static void ApplyBoxcar(double[] data, int width)
|
||||
@@ -123,44 +138,30 @@ public class FakeSpectrometerDevice : ISpectrometerDevice
|
||||
data[i] = sum / (2 * half + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private double WavelengthToWavenumber(double wavelength)
|
||||
{
|
||||
return (1.0 / _laserWavelength - 1.0 / wavelength) * 1e7;
|
||||
}
|
||||
}
|
||||
|
||||
public class FakeSpectrometerDiscovery : ISpectrometerDiscovery
|
||||
{
|
||||
private readonly int _sampleIndex;
|
||||
private readonly double _noiseLo;
|
||||
private readonly double _noiseHi;
|
||||
|
||||
public FakeSpectrometerDiscovery(int sampleIndex = 0,
|
||||
double noiseLo = 0, double noiseHi = 0)
|
||||
{
|
||||
_sampleIndex = sampleIndex;
|
||||
_noiseLo = noiseLo;
|
||||
_noiseHi = noiseHi;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ISpectrometerDevice> DiscoverDevices()
|
||||
{
|
||||
var lowRangePeaks = new (double, double, double)[]
|
||||
{
|
||||
(621, 8000, 12), // polystyrene ring deformation
|
||||
(795, 3000, 10), // weak
|
||||
(1002, 25000, 8), // ring breathing (strongest)
|
||||
(1031, 10000, 10), // C-H in-plane bend
|
||||
(1155, 5000, 15), // C-C stretch
|
||||
(1450, 6000, 20), // CH2 deformation
|
||||
(1583, 12000, 10), // ring C=C stretch
|
||||
(1602, 14000, 10), // ring C=C stretch
|
||||
};
|
||||
|
||||
var highRangePeaks = new (double, double, double)[]
|
||||
{
|
||||
(1583, 12000, 10),
|
||||
(1602, 14000, 10),
|
||||
(2852, 5000, 25), // C-H symmetric stretch
|
||||
(2904, 7000, 20), // C-H stretch
|
||||
(3054, 9000, 15), // aromatic C-H stretch
|
||||
};
|
||||
|
||||
return
|
||||
[
|
||||
new FakeSpectrometerDevice("USB2000+ (低波数)", "FLMT01234",
|
||||
786, 940, 785, lowRangePeaks),
|
||||
new FakeSpectrometerDevice("USB2000+ (高波数)", "FLMT05678",
|
||||
920, 1080, 785, highRangePeaks),
|
||||
new FakeSpectrometerDevice("UV-Vis (350-1000nm)", "UVVIS01234",
|
||||
350, 1000, _sampleIndex, _noiseLo, _noiseHi),
|
||||
new FakeSpectrometerDevice("NIR (900-1686nm)", "NIR05678",
|
||||
900, 1686, _sampleIndex, _noiseLo, _noiseHi),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,15 @@ public interface ISpectrometerDevice
|
||||
string SerialNumber { get; }
|
||||
int PixelCount { get; }
|
||||
double[] GetWavelengths();
|
||||
|
||||
/// <summary>采集样品光谱(加样品溶液后)。</summary>
|
||||
Task<SpectrumData> AcquireAsync(SpectrometerSettings settings, CancellationToken ct = default);
|
||||
|
||||
/// <summary>采集暗光谱(无光源 / 滤光轮全黑位置)。</summary>
|
||||
Task<SpectrumData> AcquireDarkAsync(SpectrometerSettings settings, CancellationToken ct = default);
|
||||
|
||||
/// <summary>采集明光谱(参比,纯水 / 滤光轮参比位置)。</summary>
|
||||
Task<SpectrumData> AcquireReferenceAsync(SpectrometerSettings settings, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public record SpectrometerSettings(
|
||||
@@ -19,9 +27,7 @@ public record SpectrometerSettings(
|
||||
int AverageCount,
|
||||
int BoxcarWidth,
|
||||
bool DarkNoiseCorrection,
|
||||
bool NonlinearityCorrection,
|
||||
bool DarkAcquisition = false,
|
||||
bool LightAcquisition = false);
|
||||
bool NonlinearityCorrection);
|
||||
|
||||
public interface ISpectrometerDiscovery
|
||||
{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using RamanApp.Models;
|
||||
|
||||
namespace RamanApp.Services;
|
||||
|
||||
public class SampleDataProvider
|
||||
{
|
||||
private static SampleDataProvider? _instance;
|
||||
public static SampleDataProvider Instance => _instance ??= new SampleDataProvider();
|
||||
|
||||
public double[] Wavelengths { get; }
|
||||
public double[] Dark { get; }
|
||||
public double[] Reference { get; }
|
||||
public double[] SampleOD04 { get; }
|
||||
public double[] SampleOD02 { get; }
|
||||
public double[] SampleOD01 { get; }
|
||||
public double[] SampleOD0025 { get; }
|
||||
|
||||
private SampleDataProvider()
|
||||
{
|
||||
var wavelengths = new List<double>();
|
||||
var dark = new List<double>();
|
||||
var reference = new List<double>();
|
||||
var od04 = new List<double>();
|
||||
var od02 = new List<double>();
|
||||
var od01 = new List<double>();
|
||||
var od0025 = new List<double>();
|
||||
|
||||
using var stream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("RamanApp.SampleData.sample_spectra.csv");
|
||||
if (stream == null)
|
||||
throw new InvalidOperationException("Embedded sample data not found");
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
reader.ReadLine(); // skip header
|
||||
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var parts = line.Split(',');
|
||||
if (parts.Length < 7) continue;
|
||||
|
||||
wavelengths.Add(double.Parse(parts[0], CultureInfo.InvariantCulture));
|
||||
dark.Add(double.Parse(parts[1], CultureInfo.InvariantCulture));
|
||||
reference.Add(double.Parse(parts[2], CultureInfo.InvariantCulture));
|
||||
od04.Add(double.Parse(parts[3], CultureInfo.InvariantCulture));
|
||||
od02.Add(double.Parse(parts[4], CultureInfo.InvariantCulture));
|
||||
od01.Add(double.Parse(parts[5], CultureInfo.InvariantCulture));
|
||||
od0025.Add(double.Parse(parts[6], CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
Wavelengths = [.. wavelengths];
|
||||
Dark = [.. dark];
|
||||
Reference = [.. reference];
|
||||
SampleOD04 = [.. od04];
|
||||
SampleOD02 = [.. od02];
|
||||
SampleOD01 = [.. od01];
|
||||
SampleOD0025 = [.. od0025];
|
||||
}
|
||||
|
||||
public SpectrumData GetDark() => new(Copy(Wavelengths), Copy(Dark), "sample-dark");
|
||||
public SpectrumData GetReference() => new(Copy(Wavelengths), Copy(Reference), "sample-reference");
|
||||
public SpectrumData GetSample(int index = 0) => index switch
|
||||
{
|
||||
0 => new(Copy(Wavelengths), Copy(SampleOD04), "sample-OD0.4"),
|
||||
1 => new(Copy(Wavelengths), Copy(SampleOD02), "sample-OD0.2"),
|
||||
2 => new(Copy(Wavelengths), Copy(SampleOD01), "sample-OD0.1"),
|
||||
3 => new(Copy(Wavelengths), Copy(SampleOD0025), "sample-OD0.025"),
|
||||
_ => new(Copy(Wavelengths), Copy(SampleOD04), "sample-OD0.4"),
|
||||
};
|
||||
|
||||
public int SampleCount => 4;
|
||||
|
||||
private static double[] Copy(double[] src)
|
||||
{
|
||||
var dst = new double[src.Length];
|
||||
Array.Copy(src, dst, src.Length);
|
||||
return dst;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,33 @@ namespace RamanApp.Services;
|
||||
|
||||
public static class SpectrumProcessor
|
||||
{
|
||||
public static SpectrumData CalculateAbsorbance(SpectrumData sample, SpectrumData dark, SpectrumData reference)
|
||||
{
|
||||
int len = Math.Min(sample.Intensities.Length,
|
||||
Math.Min(dark.Intensities.Length, reference.Intensities.Length));
|
||||
var absorbance = new double[len];
|
||||
var wavelengths = new double[len];
|
||||
Array.Copy(sample.Wavelengths, wavelengths, len);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double sampleMinusDark = sample.Intensities[i] - dark.Intensities[i];
|
||||
double refMinusDark = reference.Intensities[i] - dark.Intensities[i];
|
||||
|
||||
if (refMinusDark <= 0 || sampleMinusDark <= 0)
|
||||
{
|
||||
absorbance[i] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
double transmittance = sampleMinusDark / refMinusDark;
|
||||
absorbance[i] = transmittance > 0 ? -Math.Log10(transmittance) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new SpectrumData(wavelengths, absorbance, "absorbance");
|
||||
}
|
||||
|
||||
public static SpectrumData Interpolate(SpectrumData input, double startWn, double endWn,
|
||||
double interval, double laserWavelengthNm)
|
||||
{
|
||||
|
||||
@@ -26,6 +26,7 @@ public partial class AcquisitionViewModel : ObservableObject
|
||||
public Func<CancellationToken, Task>? OnContinuousAcquire { get; set; }
|
||||
public Func<CancellationToken, Task>? OnSaveLightSpectrum { get; set; }
|
||||
public Func<CancellationToken, Task>? OnSaveDarkSpectrum { get; set; }
|
||||
public Func<string>? GetCalibrationInfo { get; set; }
|
||||
|
||||
public AcquisitionViewModel()
|
||||
{
|
||||
@@ -118,7 +119,8 @@ public partial class AcquisitionViewModel : ObservableObject
|
||||
if (OnSaveLightSpectrum != null)
|
||||
await OnSaveLightSpectrum(cts.Token);
|
||||
Progress = 100;
|
||||
StatusText = "明光谱已保存";
|
||||
var info = GetCalibrationInfo?.Invoke() ?? "";
|
||||
StatusText = $"明光谱已保存{(info.Length > 0 ? $" ({info})" : "")}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -143,7 +145,8 @@ public partial class AcquisitionViewModel : ObservableObject
|
||||
if (OnSaveDarkSpectrum != null)
|
||||
await OnSaveDarkSpectrum(cts.Token);
|
||||
Progress = 100;
|
||||
StatusText = "暗光谱已保存";
|
||||
var info = GetCalibrationInfo?.Invoke() ?? "";
|
||||
StatusText = $"暗光谱已保存{(info.Length > 0 ? $" ({info})" : "")}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
@@ -42,7 +44,14 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
public partial SpectrumChartViewModel MergedChart { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial SpectrumChartViewModel AbsorbanceChart { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string CalibrationStatus { get; set; }
|
||||
|
||||
private readonly ISpectrometerDiscovery _discovery;
|
||||
private readonly CalibrationStore _calibrationStore;
|
||||
|
||||
private SpectrumData? _lastRaw1;
|
||||
private SpectrumData? _lastRaw2;
|
||||
@@ -52,11 +61,20 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
private SpectrumData? _darkSpectrum2;
|
||||
private SpectrumData? _activeSpectrum;
|
||||
|
||||
public MainWindowViewModel() : this(new FakeSpectrometerDiscovery()) { }
|
||||
private static readonly string[] SampleNames = ["OD0.4", "OD0.2", "OD0.1", "OD0.025"];
|
||||
|
||||
public MainWindowViewModel(ISpectrometerDiscovery discovery)
|
||||
public MainWindowViewModel() : this(-1, null) { }
|
||||
|
||||
public MainWindowViewModel(int simulateIndex, (double Lo, double Hi)? noiseRange = null)
|
||||
: this(new FakeSpectrometerDiscovery(
|
||||
simulateIndex >= 1 ? Math.Clamp(simulateIndex - 1, 0, 3) : 0,
|
||||
noiseRange?.Lo ?? 0, noiseRange?.Hi ?? 0), simulateIndex) { }
|
||||
|
||||
public MainWindowViewModel(ISpectrometerDiscovery discovery, int simulateIndex = -1)
|
||||
{
|
||||
_discovery = discovery;
|
||||
_calibrationStore = new CalibrationStore();
|
||||
CalibrationStatus = "";
|
||||
|
||||
DeviceSettings = new DeviceSettingsViewModel();
|
||||
LaserSettings = new LaserSettingsViewModel();
|
||||
@@ -70,9 +88,17 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
LightDarkChart = new SpectrumChartViewModel { DisplaySettings = DisplaySettings };
|
||||
BackgroundChart = new SpectrumChartViewModel { DisplaySettings = DisplaySettings };
|
||||
MergedChart = new SpectrumChartViewModel { DisplaySettings = DisplaySettings };
|
||||
AbsorbanceChart = new SpectrumChartViewModel { DisplaySettings = DisplaySettings };
|
||||
|
||||
WireUp();
|
||||
DiscoverDevices();
|
||||
UpdateCalibrationStatus();
|
||||
|
||||
if (simulateIndex >= 1)
|
||||
{
|
||||
int idx = Math.Clamp(simulateIndex - 1, 0, 3);
|
||||
Acquisition.StatusText = $"模拟模式 — 样品: {SampleNames[idx]},请依次: 保存暗光谱 → 保存明光谱 → 单次采集";
|
||||
}
|
||||
}
|
||||
|
||||
private void WireUp()
|
||||
@@ -81,6 +107,15 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
Acquisition.OnContinuousAcquire = AcquireOnceAsync;
|
||||
Acquisition.OnSaveLightSpectrum = AcquireAndSaveLightAsync;
|
||||
Acquisition.OnSaveDarkSpectrum = AcquireAndSaveDarkAsync;
|
||||
Acquisition.GetCalibrationInfo = () =>
|
||||
{
|
||||
var parts = new List<string>();
|
||||
if (DeviceSettings.Spectrometer1.AcquireEnabled)
|
||||
parts.Add($"仪1@{DeviceSettings.Spectrometer1.IntegrationTimeMs}ms");
|
||||
if (DeviceSettings.Spectrometer2.AcquireEnabled)
|
||||
parts.Add($"仪2@{DeviceSettings.Spectrometer2.IntegrationTimeMs}ms");
|
||||
return string.Join(", ", parts);
|
||||
};
|
||||
|
||||
DeviceSettings.OnClearAll = ClearAllData;
|
||||
|
||||
@@ -117,6 +152,26 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
};
|
||||
|
||||
DisplaySettings.PropertyChanged += (_, _) => RefreshAllChartVersions();
|
||||
|
||||
DeviceSettings.Spectrometer1.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(SpectrometerGroupViewModel.IntegrationTimeMs))
|
||||
OnIntegrationTimeChanged();
|
||||
};
|
||||
DeviceSettings.Spectrometer2.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(SpectrometerGroupViewModel.IntegrationTimeMs))
|
||||
OnIntegrationTimeChanged();
|
||||
};
|
||||
}
|
||||
|
||||
private void OnIntegrationTimeChanged()
|
||||
{
|
||||
_darkSpectrum1 = null;
|
||||
_darkSpectrum2 = null;
|
||||
_lightSpectrum1 = null;
|
||||
_lightSpectrum2 = null;
|
||||
LoadCalibrationForCurrentSettings();
|
||||
}
|
||||
|
||||
private void RefreshAllChartVersions()
|
||||
@@ -125,6 +180,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
LightDarkChart.DataVersion++;
|
||||
BackgroundChart.DataVersion++;
|
||||
MergedChart.DataVersion++;
|
||||
AbsorbanceChart.DataVersion++;
|
||||
}
|
||||
|
||||
private void OnSelectedPeakChanged()
|
||||
@@ -154,45 +210,53 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
LightDarkChart.LaserWavelengthNm = wl;
|
||||
BackgroundChart.LaserWavelengthNm = wl;
|
||||
MergedChart.LaserWavelengthNm = wl;
|
||||
AbsorbanceChart.LaserWavelengthNm = wl;
|
||||
}
|
||||
|
||||
private void DiscoverDevices()
|
||||
{
|
||||
var devices = _discovery.DiscoverDevices();
|
||||
DeviceSettings.PopulateDevices(devices);
|
||||
LoadCalibrationForCurrentSettings();
|
||||
}
|
||||
|
||||
private Task AcquireOnceAsync(CancellationToken ct) =>
|
||||
AcquireAsync(ct, darkAcquisition: false, lightAcquisition: false);
|
||||
private static Task<SpectrumData> AcquireTargetAsync(ISpectrometerDevice dev,
|
||||
SpectrometerSettings settings, AcquisitionTarget target, CancellationToken ct) => target switch
|
||||
{
|
||||
AcquisitionTarget.Dark => dev.AcquireDarkAsync(settings, ct),
|
||||
AcquisitionTarget.Reference => dev.AcquireReferenceAsync(settings, ct),
|
||||
_ => dev.AcquireAsync(settings, ct),
|
||||
};
|
||||
|
||||
private async Task AcquireAsync(CancellationToken ct, bool darkAcquisition, bool lightAcquisition)
|
||||
/// <summary>串行采集两台已启用的光谱仪,仅返回数据,不修改任何状态或图表。</summary>
|
||||
private async Task<(SpectrumData? S1, SpectrumData? S2)> AcquirePairAsync(
|
||||
AcquisitionTarget target, CancellationToken ct)
|
||||
{
|
||||
var dev1 = DeviceSettings.Spectrometer1.SelectedDevice;
|
||||
var dev2 = DeviceSettings.Spectrometer2.SelectedDevice;
|
||||
var settings1 = DeviceSettings.GetSettings1();
|
||||
var settings2 = DeviceSettings.GetSettings2();
|
||||
|
||||
if (darkAcquisition)
|
||||
{
|
||||
settings1 = settings1 with { DarkAcquisition = true };
|
||||
settings2 = settings2 with { DarkAcquisition = true };
|
||||
}
|
||||
else if (lightAcquisition)
|
||||
{
|
||||
settings1 = settings1 with { LightAcquisition = true };
|
||||
settings2 = settings2 with { LightAcquisition = true };
|
||||
}
|
||||
|
||||
bool acquire1 = dev1 != null && DeviceSettings.Spectrometer1.AcquireEnabled;
|
||||
bool acquire2 = dev2 != null && DeviceSettings.Spectrometer2.AcquireEnabled;
|
||||
|
||||
if (!acquire1 && !acquire2)
|
||||
SpectrumData? s1 = null, s2 = null;
|
||||
if (acquire1)
|
||||
s1 = await AcquireTargetAsync(dev1!, settings1, target, ct);
|
||||
if (acquire2)
|
||||
s2 = await AcquireTargetAsync(dev2!, settings2, target, ct);
|
||||
|
||||
return (s1, s2);
|
||||
}
|
||||
|
||||
private async Task AcquireOnceAsync(CancellationToken ct)
|
||||
{
|
||||
var (s1, s2) = await AcquirePairAsync(AcquisitionTarget.Sample, ct);
|
||||
if (s1 == null && s2 == null)
|
||||
return;
|
||||
|
||||
if (acquire1)
|
||||
_lastRaw1 = await dev1!.AcquireAsync(settings1, ct);
|
||||
if (acquire2)
|
||||
_lastRaw2 = await dev2!.AcquireAsync(settings2, ct);
|
||||
_lastRaw1 = s1;
|
||||
_lastRaw2 = s2;
|
||||
|
||||
UpdateCharts();
|
||||
|
||||
@@ -239,6 +303,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
UpdateLightDarkChart();
|
||||
UpdateBackgroundChart();
|
||||
UpdateAbsorbanceChart();
|
||||
}
|
||||
|
||||
private void ClearAllData()
|
||||
@@ -255,28 +320,58 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
LightDarkChart.Clear();
|
||||
BackgroundChart.Clear();
|
||||
MergedChart.Clear();
|
||||
AbsorbanceChart.Clear();
|
||||
UpdateCalibrationStatus();
|
||||
}
|
||||
|
||||
private async Task AcquireAndSaveLightAsync(CancellationToken ct)
|
||||
{
|
||||
await AcquireAsync(ct, darkAcquisition: false, lightAcquisition: true);
|
||||
if (DeviceSettings.Spectrometer1.AcquireEnabled && _lastRaw1 != null)
|
||||
_lightSpectrum1 = _lastRaw1;
|
||||
if (DeviceSettings.Spectrometer2.AcquireEnabled && _lastRaw2 != null)
|
||||
_lightSpectrum2 = _lastRaw2;
|
||||
var (s1, s2) = await AcquirePairAsync(AcquisitionTarget.Reference, ct);
|
||||
if (s1 != null)
|
||||
{
|
||||
_lightSpectrum1 = s1;
|
||||
var dev = DeviceSettings.Spectrometer1.SelectedDevice;
|
||||
if (dev != null)
|
||||
_calibrationStore.SaveReference(DeviceSettings.Spectrometer1.IntegrationTimeMs,
|
||||
dev.SerialNumber, s1);
|
||||
}
|
||||
if (s2 != null)
|
||||
{
|
||||
_lightSpectrum2 = s2;
|
||||
var dev = DeviceSettings.Spectrometer2.SelectedDevice;
|
||||
if (dev != null)
|
||||
_calibrationStore.SaveReference(DeviceSettings.Spectrometer2.IntegrationTimeMs,
|
||||
dev.SerialNumber, s2);
|
||||
}
|
||||
UpdateCalibrationStatus();
|
||||
UpdateLightDarkChart();
|
||||
UpdateBackgroundChart();
|
||||
UpdateAbsorbanceChart();
|
||||
}
|
||||
|
||||
private async Task AcquireAndSaveDarkAsync(CancellationToken ct)
|
||||
{
|
||||
await AcquireAsync(ct, darkAcquisition: true, lightAcquisition: false);
|
||||
if (DeviceSettings.Spectrometer1.AcquireEnabled && _lastRaw1 != null)
|
||||
_darkSpectrum1 = _lastRaw1;
|
||||
if (DeviceSettings.Spectrometer2.AcquireEnabled && _lastRaw2 != null)
|
||||
_darkSpectrum2 = _lastRaw2;
|
||||
var (s1, s2) = await AcquirePairAsync(AcquisitionTarget.Dark, ct);
|
||||
if (s1 != null)
|
||||
{
|
||||
_darkSpectrum1 = s1;
|
||||
var dev = DeviceSettings.Spectrometer1.SelectedDevice;
|
||||
if (dev != null)
|
||||
_calibrationStore.SaveDark(DeviceSettings.Spectrometer1.IntegrationTimeMs,
|
||||
dev.SerialNumber, s1);
|
||||
}
|
||||
if (s2 != null)
|
||||
{
|
||||
_darkSpectrum2 = s2;
|
||||
var dev = DeviceSettings.Spectrometer2.SelectedDevice;
|
||||
if (dev != null)
|
||||
_calibrationStore.SaveDark(DeviceSettings.Spectrometer2.IntegrationTimeMs,
|
||||
dev.SerialNumber, s2);
|
||||
}
|
||||
UpdateCalibrationStatus();
|
||||
UpdateLightDarkChart();
|
||||
UpdateBackgroundChart();
|
||||
UpdateAbsorbanceChart();
|
||||
}
|
||||
|
||||
private void UpdateLightDarkChart()
|
||||
@@ -317,6 +412,133 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
BackgroundChart.Clear();
|
||||
}
|
||||
|
||||
private void UpdateAbsorbanceChart()
|
||||
{
|
||||
var series = new List<ChartSeries>();
|
||||
|
||||
var abs1 = TryComputeAbsorbance(_lastRaw1, _darkSpectrum1, _lightSpectrum1,
|
||||
DeviceSettings.Spectrometer1);
|
||||
if (abs1 != null)
|
||||
series.Add(new ChartSeries("吸光度-仪1", abs1.Wavelengths, abs1.Intensities, "#E91E63"));
|
||||
|
||||
var abs2 = TryComputeAbsorbance(_lastRaw2, _darkSpectrum2, _lightSpectrum2,
|
||||
DeviceSettings.Spectrometer2);
|
||||
if (abs2 != null)
|
||||
series.Add(new ChartSeries("吸光度-仪2", abs2.Wavelengths, abs2.Intensities, "#9C27B0"));
|
||||
|
||||
if (series.Count > 0)
|
||||
AbsorbanceChart.SetData([.. series]);
|
||||
else
|
||||
AbsorbanceChart.Clear();
|
||||
}
|
||||
|
||||
private SpectrumData? TryComputeAbsorbance(SpectrumData? sample,
|
||||
SpectrumData? dark, SpectrumData? reference,
|
||||
SpectrometerGroupViewModel specGroup)
|
||||
{
|
||||
if (sample == null) return null;
|
||||
|
||||
var darkData = dark;
|
||||
var refData = reference;
|
||||
|
||||
if (darkData == null || refData == null)
|
||||
{
|
||||
var dev = specGroup.SelectedDevice;
|
||||
if (dev == null) return null;
|
||||
int it = specGroup.IntegrationTimeMs;
|
||||
darkData ??= _calibrationStore.LoadDark(it, dev.SerialNumber);
|
||||
refData ??= _calibrationStore.LoadReference(it, dev.SerialNumber);
|
||||
}
|
||||
|
||||
if (darkData == null || refData == null) return null;
|
||||
|
||||
var absorbance = SpectrumProcessor.CalculateAbsorbance(sample, darkData, refData);
|
||||
|
||||
if (DataProcessing.SmoothingEnabled)
|
||||
absorbance = SpectrumProcessor.Smooth(absorbance, (int)DataProcessing.SmoothingLambda);
|
||||
|
||||
return absorbance;
|
||||
}
|
||||
|
||||
private void UpdateCalibrationStatus()
|
||||
{
|
||||
var parts = new List<string>();
|
||||
var dev1 = DeviceSettings.Spectrometer1.SelectedDevice;
|
||||
var dev2 = DeviceSettings.Spectrometer2.SelectedDevice;
|
||||
int it1 = DeviceSettings.Spectrometer1.IntegrationTimeMs;
|
||||
int it2 = DeviceSettings.Spectrometer2.IntegrationTimeMs;
|
||||
|
||||
if (dev1 != null)
|
||||
{
|
||||
bool hasDark = _darkSpectrum1 != null || _calibrationStore.HasDark(it1, dev1.SerialNumber);
|
||||
bool hasRef = _lightSpectrum1 != null || _calibrationStore.HasReference(it1, dev1.SerialNumber);
|
||||
string state = (hasDark, hasRef) switch
|
||||
{
|
||||
(true, true) => "已校准",
|
||||
(true, false) => "缺明光谱",
|
||||
(false, true) => "缺暗光谱",
|
||||
_ => "未校准",
|
||||
};
|
||||
parts.Add($"仪1@{it1}ms: {state}");
|
||||
}
|
||||
|
||||
if (dev2 != null && DeviceSettings.Spectrometer2.AcquireEnabled)
|
||||
{
|
||||
bool hasDark = _darkSpectrum2 != null || _calibrationStore.HasDark(it2, dev2.SerialNumber);
|
||||
bool hasRef = _lightSpectrum2 != null || _calibrationStore.HasReference(it2, dev2.SerialNumber);
|
||||
string state = (hasDark, hasRef) switch
|
||||
{
|
||||
(true, true) => "已校准",
|
||||
(true, false) => "缺明光谱",
|
||||
(false, true) => "缺暗光谱",
|
||||
_ => "未校准",
|
||||
};
|
||||
parts.Add($"仪2@{it2}ms: {state}");
|
||||
}
|
||||
|
||||
var savedTimes = _calibrationStore.GetAvailableIntegrationTimes();
|
||||
if (savedTimes.Count > 0)
|
||||
parts.Add($"已存: {string.Join("/", savedTimes.ConvertAll(t => $"{t}ms"))}");
|
||||
|
||||
CalibrationStatus = parts.Count > 0 ? string.Join(" | ", parts) : "";
|
||||
}
|
||||
|
||||
public void LoadCalibrationForCurrentSettings()
|
||||
{
|
||||
var dev1 = DeviceSettings.Spectrometer1.SelectedDevice;
|
||||
if (dev1 != null && _darkSpectrum1 == null)
|
||||
_darkSpectrum1 = _calibrationStore.LoadDark(DeviceSettings.Spectrometer1.IntegrationTimeMs, dev1.SerialNumber);
|
||||
if (dev1 != null && _lightSpectrum1 == null)
|
||||
_lightSpectrum1 = _calibrationStore.LoadReference(DeviceSettings.Spectrometer1.IntegrationTimeMs, dev1.SerialNumber);
|
||||
|
||||
var dev2 = DeviceSettings.Spectrometer2.SelectedDevice;
|
||||
if (dev2 != null && _darkSpectrum2 == null)
|
||||
_darkSpectrum2 = _calibrationStore.LoadDark(DeviceSettings.Spectrometer2.IntegrationTimeMs, dev2.SerialNumber);
|
||||
if (dev2 != null && _lightSpectrum2 == null)
|
||||
_lightSpectrum2 = _calibrationStore.LoadReference(DeviceSettings.Spectrometer2.IntegrationTimeMs, dev2.SerialNumber);
|
||||
|
||||
UpdateCalibrationStatus();
|
||||
UpdateLightDarkChart();
|
||||
UpdateBackgroundChart();
|
||||
}
|
||||
|
||||
public void QuickSave()
|
||||
{
|
||||
var spectrum = GetActiveSpectrum();
|
||||
if (spectrum == null)
|
||||
{
|
||||
Acquisition.StatusText = "无可保存的光谱数据";
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = Path.Combine(AppContext.BaseDirectory, "spectra");
|
||||
Directory.CreateDirectory(dir);
|
||||
var fileName = $"spectrum_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
|
||||
var path = Path.Combine(dir, fileName);
|
||||
SpectrumFileService.ExportCsv(path, spectrum, LaserSettings.LaserWavelengthNm);
|
||||
Acquisition.StatusText = $"已快速保存: {fileName}";
|
||||
}
|
||||
|
||||
public void ImportSpectrum(SpectrumData spectrum)
|
||||
{
|
||||
_lastRaw1 = spectrum;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Raman.Core.Spectroscopy;
|
||||
using Spec.Sea;
|
||||
|
||||
const int HostSafetyMaxIntegrationTimeMicroseconds = 1_000_000;
|
||||
|
||||
var cancellationToken = CancellationToken.None;
|
||||
var discovery = new SeaSpectrometerDiscovery();
|
||||
|
||||
@@ -67,6 +69,7 @@ static async Task RunDeviceAsync(
|
||||
var maxIntegration = capabilities.MaxIntegrationTimeMicroseconds > 0
|
||||
? capabilities.MaxIntegrationTimeMicroseconds
|
||||
: int.MaxValue;
|
||||
maxIntegration = Math.Min(maxIntegration, HostSafetyMaxIntegrationTimeMicroseconds);
|
||||
var integration = Math.Clamp(requestedIntegration, capabilities.MinIntegrationTimeMicroseconds, maxIntegration);
|
||||
var average = checked((ushort)Math.Clamp(ReadInt("RAMAN_TEST_AVERAGE", originalAcquisition?.Average ?? 1), 1, ushort.MaxValue));
|
||||
var boxcar = checked((ushort)Math.Clamp(ReadInt("RAMAN_TEST_BOXCAR", originalAcquisition?.Boxcar ?? 0), 0, ushort.MaxValue));
|
||||
|
||||
@@ -197,7 +197,7 @@ public sealed class SpectrometerRuntimeWorkspace : IAsyncDisposable
|
||||
return SpectrometerResponse.Ok();
|
||||
}
|
||||
|
||||
private const int MaxIntegrationTimeMicroseconds = 5_000_000;
|
||||
private const int MaxIntegrationTimeMicroseconds = 1_000_000;
|
||||
|
||||
private async Task<SpectrometerResponse> ConfigureAcquisitionAsync(SpectrometerRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -210,7 +210,7 @@ public sealed class SpectrometerRuntimeWorkspace : IAsyncDisposable
|
||||
{
|
||||
return SpectrometerResponse.Fail(
|
||||
SpectrometerErrorCodes.InvalidValue,
|
||||
$"Integration time {request.AcquisitionSettings.IntegrationTimeMicroseconds} us exceeds maximum allowed {MaxIntegrationTimeMicroseconds} us (5s).");
|
||||
$"Integration time {request.AcquisitionSettings.IntegrationTimeMicroseconds} us exceeds maximum allowed {MaxIntegrationTimeMicroseconds} us (1s).");
|
||||
}
|
||||
|
||||
var device = FindConnectedDevice(request.Target);
|
||||
|
||||
@@ -73,6 +73,28 @@ public sealed class SpectrometerRuntimeWorkspaceTests
|
||||
Assert.Equal(validSettings, valid.AcquisitionSettings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigureAcquisitionAsync_rejects_host_integration_time_above_one_second()
|
||||
{
|
||||
var workspace = CreateWorkspaceWithDescriptor();
|
||||
await workspace.HandleAsync(new SpectrometerRequest("sim-spec-001", SpectrometerCommandKind.Connect));
|
||||
|
||||
var atLimit = await workspace.HandleAsync(new SpectrometerRequest(
|
||||
"sim-spec-001",
|
||||
SpectrometerCommandKind.ConfigureAcquisition,
|
||||
new SpectrometerAcquisitionSettings(1_000_000, 1, 0)));
|
||||
var aboveLimit = await workspace.HandleAsync(new SpectrometerRequest(
|
||||
"sim-spec-001",
|
||||
SpectrometerCommandKind.ConfigureAcquisition,
|
||||
new SpectrometerAcquisitionSettings(1_000_001, 1, 0)));
|
||||
|
||||
Assert.True(atLimit.Succeeded);
|
||||
Assert.False(aboveLimit.Succeeded);
|
||||
Assert.Equal(SpectrometerErrorCodes.InvalidValue, aboveLimit.ErrorCode);
|
||||
Assert.Contains("1000000", aboveLimit.ErrorMessage);
|
||||
Assert.Contains("1s", aboveLimit.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AcquireSpectrumAsync_returns_wavelengths_and_intensities()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user