feat: 完成界面相关的代码开发

This commit is contained in:
chuan
2026-06-18 01:17:33 +08:00 Unverified
parent 2bff282125
commit 861ef1add6
37 changed files with 1670 additions and 7 deletions
+69
View File
@@ -0,0 +1,69 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Raman TUI: a Windows terminal application for controlling Raman spectrometers and filter wheels. The TUI accepts text commands (e.g. `spec scan`, `filter connect f1`) and also supports natural-language input via a DeepSeek LLM agent that translates prompts into those commands.
## Build & Test Commands
```bash
# Build everything (solution file is roman.slnx, not .sln)
dotnet build roman.slnx
# Run all tests (xUnit)
dotnet test roman.slnx
# Run a single test project
dotnet test test/Roman.Host.Tests
dotnet test test/Raman.TUI.Tests
# Run a single test by filter
dotnet test test/Roman.Host.Tests --filter "FullyQualifiedName~DiscoverFiltersUseCaseTests"
# Run the TUI in simulation mode (no hardware needed)
dotnet run --project src/Raman.TUI -- --simulate
```
The target framework is **net10.0**. The solution uses the `.slnx` format (XML-based solution file).
## Architecture
### Layer structure (bottom to top)
**Raman.Core** — Hardware abstractions. Defines `ISpectrometerDevice`, `IFilterWheelDevice`, and their associated data types (settings, capabilities, spectra). No implementations here.
**Spec.Sea** — Spectrometer driver for the Sea SDK. Wraps a native C DLL (`SeaSDK.dll`) via `LibraryImport` P/Invoke in `SeaSdkNativeMethods`. `SeaSdkGateway` provides a managed facade; `SeaSpectrometerDevice`/`SeaSpectrometerDiscovery` implement the Core interfaces. Uses `AllowUnsafeBlocks`. The native DLL lives outside this repo at `../main/externals/SeaSDK.dll` and is copied to output.
**Filter.Ello** — Filter wheel driver for Elliptec serial-protocol devices. Communicates over COM ports (`System.IO.Ports`). `SerialProtocol` encodes/decodes the Elliptec hex-based command format. Supports 4-slot and 6-slot wheels.
**Raman.Simulator** — In-memory fakes of both spectrometers and filter wheels. Used for development and testing without hardware. Activated by `--simulate` flag or `RAMAN_TUI_SIMULATE=1` env var.
**Roman.Host** — Business logic orchestration layer. Contains:
- `SpectrometerRuntimeWorkspace` / `FilterRuntimeWorkspace` — stateful command dispatchers that manage device lifecycle (discover, connect, configure, acquire, disconnect).
- Request/response contracts (`SpectrometerRequest`/`SpectrometerResponse`, `FilterRequest`/`FilterResponse`) with command-kind enums.
- Filter discovery subsystem that scans COM ports for Elliptec devices.
- Filter configuration persistence (JSON-based `FilterConfigurationStore`).
**Raman.TUI** — The terminal application. Key components:
- `ICommandSession` — interface for command handlers. `FilterCommandSession` and `SpectrometerCommandSession` parse text commands and delegate to Roman.Host workspaces. `CompositeCommandSession` routes to the right handler.
- `InlineConsoleApp` — custom TUI rendering engine using low-level `Console` APIs and a Windows-specific `ConsoleInputReader` (reads raw input events). Manages a scrollable log of `NotificationEntry` items and a single-line input bar.
- `DeepSeekLlmSession` — LLM integration via `Microsoft.Agents.AI.OpenAI`. Configured from `raman-agent.toml` (TOML format, loaded from app base directory). Environment variables `DEEPSEEK_API_KEY` and `DEEPSEEK_MODEL` override the config file.
**RamanTester** — Standalone x86 console app for direct Sea SDK testing. Targets x86 (`PlatformTarget=x86`) because the native Sea SDK DLL is 32-bit.
### Key patterns
- Commands flow as: user text -> `ICommandSession.ExecuteAsync` -> `*RuntimeWorkspace.HandleAsync` -> `ISpectrometerDevice`/`IFilterWheelDevice` method.
- All command sessions return `IReadOnlyList<NotificationEntry>` which the TUI renders as log lines.
- Background commands use `publishAsync` callback to stream entries to the UI incrementally.
- The LLM agent uses tool calling — `TuiLlmCommandTool.RunTuiCommandAsync` executes TUI commands on behalf of the LLM and returns results as text.
## Code Conventions
- File-scoped namespaces (`csharp_style_namespace_declarations = file_scoped`).
- LF line endings, UTF-8, spaces for indentation.
- 2-space indent for XML/csproj files.
- Commit messages are in Chinese.
- The project language mixes English (code) and Chinese (UI strings, LLM instructions, commit messages).
+5 -3
View File
@@ -2,11 +2,13 @@
<configuration>
<packageSources>
<clear />
<!--<add key="nuget-cdn" value="https://nuget.cdn.azure.cn/v3/index.json" allowInsecureConnections="True" />-->
<add key="nuget-src" value="https://api.nuget.org/v3/index.json" allowInsecureConnections="True" />
<add key="nuget-cdn" value="https://repo.huaweicloud.com/repository/nuget/v3/index.json"
allowInsecureConnections="True" />
<add key="nuget-src" value="https://api.nuget.org/v3/index.json"
allowInsecureConnections="True" />
</packageSources>
<disabledPackageSources>
<clear />
</disabledPackageSources>
</configuration>
</configuration>
+1
View File
@@ -3,6 +3,7 @@
<Project Path="src/Raman.Core/Raman.Core.csproj" Id="b8a8d619-9e32-4df0-ab7d-33398f956c2a" />
<Project Path="src/Raman.Simulator/Raman.Simulator.csproj" Id="42c899c8-76e7-4fa0-82d3-8842430f0512" />
<Project Path="src/Raman.TUI/Raman.TUI.csproj" Id="a5af998c-7236-4d76-94ed-a6d236e43142" />
<Project Path="src/RamanApp/RamanApp.csproj" Id="13b5601c-c4e1-48f2-b2ad-e613589f465a" />
<Project Path="src/RamanTester/RamanTester.csproj" Id="7ab838ce-add8-464b-8a16-e9b8b709852f" />
<Project Path="src/Roman.Host/Roman.Host.csproj" Id="d1e35ba3-866e-41eb-b530-587f06a10e11" />
<Project Path="src/Spec.Sea/Spec.Sea.csproj" Id="06a9c31a-66ff-4fbe-be5a-35533c0ad249" />
+2 -2
View File
@@ -165,7 +165,7 @@ internal sealed class DeepSeekLlmSession : ITuiLlmSession
The tool only supports commands that start with filter or spec.
Never invent device lists, aliases, connection states, positions, spectra, or execution results.
If device state or configuration is needed, call filter show, filter list, spec status, or spec scan first as appropriate.
For spectrometer acquisition configuration, first call spec show <device> and read the integration_us min..max range. Only call spec config <device> with an explicit --integration value inside that range.
For spectrometer acquisition configuration, first call spec show <device> and read the integration_us min..max range. The spec config --integration argument is always microseconds/us. If the user says ms or seconds, convert to microseconds before calling spec config. Only call spec config <device> with an explicit --integration value inside that range.
Never call spec config only to inspect current configuration; use spec show, spec diagnostics, or spec processing <device> status instead.
For control actions such as connect, disconnect, move, acquire, or configuration, state the action briefly, call the tool, then summarize the result based only on tool output.
If required parameters are missing, ask for the missing parameter before executing.
@@ -197,7 +197,7 @@ internal sealed class DeepSeekLlmSession : ITuiLlmSession
spec connect <device>
spec disconnect <device>
spec acquire <device>
spec config <device> ...
spec config <device> --integration <microseconds> [--average <count>] [--boxcar <pixels>]
spec diagnostics <device>
""";
+2 -2
View File
@@ -6,7 +6,7 @@ namespace Raman.TUI;
internal sealed class SpectrometerCommandSession : ICommandSession
{
private const string UsageConfig = "spec config <device> --integration 50000 [--average 1] [--boxcar 0]";
private const string UsageConfig = "spec config <device> --integration <microseconds> [--average 1] [--boxcar 0]";
private const string UsageAcquire = "spec acquire <device>";
private const string UsageHighSpeed = "spec highspeed <device> --frames 10";
private const string UsageProcessing = "spec processing <device> status|set [--boxcar-mode 0] [--stretch-mode 0] [--long true|false] [--saturation-us 0] [--pixels 2048] [--usb-protocol 0] [--serial-protocol 0]";
@@ -602,7 +602,7 @@ internal sealed class SpectrometerCommandSession : ICommandSession
("spec show [device]", "Show all spectrometer statuses or one device status."),
("spec connect <device>", "Connect a discovered spectrometer."),
("spec disconnect <device>", "Disconnect a connected spectrometer."),
("spec config <device>", "Set acquisition parameters. Use --integration, --average, --boxcar."),
("spec config <device>", "Set acquisition parameters. --integration is in microseconds/us. Use --average and --boxcar as needed."),
("spec acquire <device>", "Acquire one formatted spectrum frame."),
("spec highspeed <device> --frames 10", "Acquire a high-speed spectrum batch."),
("spec diagnostics <device>", "Read SDK and device diagnostics."),
+28
View File
@@ -0,0 +1,28 @@
<Application
x:Class="RamanApp.App"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:semi="https://irihi.tech/semi"
RequestedThemeVariant="Light">
<Application.Styles>
<semi:SemiTheme Locale="zh-CN" />
<semi:DataGridSemiTheme />
<Style Selector="Window">
<Setter Property="FontFamily" Value="Microsoft YaHei UI, Microsoft YaHei, SimHei" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="Button">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="CheckBox">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="RadioButton">
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="ToggleSwitch">
<Setter Property="FontSize" Value="12" />
</Style>
</Application.Styles>
</Application>
+24
View File
@@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace RamanApp;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}
+321
View File
@@ -0,0 +1,321 @@
<Window
x:Class="RamanApp.MainWindow"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="using:RamanApp.Views"
xmlns:vm="using:RamanApp.ViewModels"
Title="拉曼光谱仪"
Width="1280"
Height="800"
x:DataType="vm:MainWindowViewModel"
WindowStartupLocation="CenterScreen">
<DockPanel>
<!-- Menu bar -->
<Border DockPanel.Dock="Top"
BorderBrush="{DynamicResource SemiColorBorder}"
BorderThickness="0,0,0,1">
<Menu>
<MenuItem Header="文件">
<MenuItem Header="导入光谱数据" />
<MenuItem Header="导出光谱数据" />
<MenuItem Header="拷贝光谱数据" />
<Separator />
<MenuItem Header="光谱存为图片" />
<MenuItem Header="光谱存为PDF" />
<Separator />
<MenuItem Header="退出" Click="OnExitClick" />
</MenuItem>
<MenuItem Header="显示设置">
<MenuItem Header="显示图例">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.ShowLegend}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="显示峰位X值">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.ShowPeakX}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="显示峰位Y值">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.ShowPeakY}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="显示峰位直线">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.ShowPeakLine}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="X轴坐标翻转显示">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.InvertXAxis}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="填充谱峰">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.FillPeaks}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="所有图谱归一化显示[最大10000]">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding DisplaySettings.NormalizeAll}"
BorderThickness="0" MinWidth="0" Padding="0" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="修改辐射坐标单位" />
</MenuItem>
<MenuItem Header="帮助">
<MenuItem Header="关于" />
</MenuItem>
</Menu>
</Border>
<!-- Toolbar -->
<Border
Padding="4,2"
Background="{DynamicResource SemiColorFillQuaternary}"
BorderBrush="{DynamicResource SemiColorBorder}"
BorderThickness="0,0,0,1"
DockPanel.Dock="Top">
<Border.Styles>
<Style Selector="Button.tool-btn">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="MinWidth" Value="52" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button.tool-btn PathIcon">
<Setter Property="Width" Value="18" />
<Setter Property="Height" Value="18" />
</Style>
<Style Selector="Button.tool-btn TextBlock">
<Setter Property="FontSize" Value="10" />
<Setter Property="HorizontalAlignment" Value="Center" />
</Style>
</Border.Styles>
<DockPanel>
<!-- Right side: progress bar + status text -->
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal"
Spacing="8" VerticalAlignment="Center" Margin="8,0">
<TextBlock Text="进度:" VerticalAlignment="Center" Foreground="Gray" FontSize="10" />
<ProgressBar Value="100" Minimum="0" Maximum="100"
Width="140" Height="14" VerticalAlignment="Center"
Foreground="#4CAF50" />
<TextBlock Text="100%" VerticalAlignment="Center" FontSize="10" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="2">
<!-- 单次 -->
<Button Classes="tool-btn" Command="{Binding Acquisition.SingleAcquireCommand}">
<StackPanel Spacing="2">
<PathIcon Foreground="#4CAF50"
Data="M8,5.14V19.14L19,12.14L8,5.14Z" />
<TextBlock Text="单次" />
</StackPanel>
</Button>
<!-- 连续 -->
<Button Classes="tool-btn" Command="{Binding Acquisition.ContinuousAcquireCommand}">
<StackPanel Spacing="2">
<PathIcon Foreground="#FF9800"
Data="M17.65,6.35A7.958,7.958 0 0,0 12,4C7.58,4 4.01,7.58 4.01,12C4.01,16.42 7.58,20 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18C8.69,18 6,15.31 6,12C6,8.69 8.69,6 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z" />
<TextBlock Text="连续" />
</StackPanel>
</Button>
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
<!-- 保存明光谱 -->
<Button Classes="tool-btn">
<StackPanel Spacing="2">
<PathIcon Foreground="#FFC107"
Data="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
<TextBlock Text="保存明光谱" />
</StackPanel>
</Button>
<!-- 保存暗光谱 -->
<Button Classes="tool-btn">
<StackPanel Spacing="2">
<PathIcon Foreground="#607D8B"
Data="M12,2A7,7 0 0,0 5,9C5,11.38 6.19,13.47 8,14.74V17A1,1 0 0,0 9,18H15A1,1 0 0,0 16,17V14.74C17.81,13.47 19,11.38 19,9A7,7 0 0,0 12,2M9,21A1,1 0 0,0 10,22H14A1,1 0 0,0 15,21V20H9V21Z" />
<TextBlock Text="保存暗光谱" />
</StackPanel>
</Button>
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
<!-- 最大化 -->
<Button Classes="tool-btn">
<StackPanel Spacing="2">
<PathIcon Foreground="#2196F3"
Data="M9.5,13.09L10.91,14.5L6.41,19H10V21H3V14H5V17.59L9.5,13.09M10.91,9.5L9.5,10.91L5,6.41V10H3V3H10V5H6.41L10.91,9.5M14.5,13.09L19,17.59V14H21V21H14V19H17.59L13.09,14.5L14.5,13.09M13.09,9.5L17.59,5H14V3H21V10H19V6.41L14.5,10.91L13.09,9.5Z" />
<TextBlock Text="最大化" />
</StackPanel>
</Button>
<!-- 自适应显示 -->
<Button Classes="tool-btn">
<StackPanel Spacing="2">
<PathIcon Foreground="#00BCD4"
Data="M17,4H7A2,2 0 0,0 5,6V18A2,2 0 0,0 7,20H17A2,2 0 0,0 19,18V6A2,2 0 0,0 17,4M7,18V6H17V18H7M15,8H9V10H15V8M15,12H9V14H15V12Z" />
<TextBlock Text="自适应显示" />
</StackPanel>
</Button>
<!-- 自定义 -->
<Button Classes="tool-btn">
<StackPanel Spacing="2">
<PathIcon Foreground="#4CAF50"
Data="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.04 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.04 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
<TextBlock Text="自定义" />
</StackPanel>
</Button>
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="4,4" />
<!-- 快速保存 -->
<Button Classes="tool-btn">
<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" />
<TextBlock Text="快速保存" />
</StackPanel>
</Button>
<!-- 导出光谱数据 -->
<Button Classes="tool-btn">
<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" />
<TextBlock Text="导出光谱数据" />
</StackPanel>
</Button>
<!-- 导入光谱数据 -->
<Button Classes="tool-btn">
<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" />
<TextBlock Text="导入光谱数据" />
</StackPanel>
</Button>
</StackPanel>
</DockPanel>
</Border>
<!-- Bottom status bar -->
<Border
Padding="8,3"
Background="{DynamicResource SemiColorFillQuaternary}"
BorderBrush="{DynamicResource SemiColorBorder}"
BorderThickness="0,1,0,0"
DockPanel.Dock="Bottom">
<DockPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="6">
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
Text="{Binding DeviceSettings.Spectrometer1.SelectedDevice, StringFormat=设备1: {0}, FallbackValue=设备1: (未连接)}" />
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="2,1" />
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
Text="{Binding DeviceSettings.Spectrometer1.IntegrationTimeMs, StringFormat=积分: {0}ms}" />
<Border Width="1" Background="{DynamicResource SemiColorBorder}" Margin="2,1" />
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
Text="{Binding DeviceSettings.Spectrometer1.AverageCount, StringFormat=平均: {0}次}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
Text="{Binding Acquisition.StatusText}" />
</StackPanel>
</DockPanel>
</Border>
<!-- Main content -->
<Grid ColumnDefinitions="240,Auto,*,Auto,260">
<!-- Left panel: settings + data processing tabs (resizable) -->
<Border
Grid.Column="0"
MinWidth="180"
BorderBrush="{DynamicResource SemiColorBorder}"
BorderThickness="0,0,1,0">
<TabControl TabStripPlacement="Top" Padding="0">
<TabItem Header="光谱仪">
<ScrollViewer Padding="8">
<views:DeviceSettingsView DataContext="{Binding DeviceSettings}" />
</ScrollViewer>
</TabItem>
<TabItem Header="激光器">
<ScrollViewer Padding="8">
<views:LaserSettingsView DataContext="{Binding LaserSettings}" />
</ScrollViewer>
</TabItem>
<TabItem Header="数据处理">
<views:DataProcessingView DataContext="{Binding DataProcessing}" />
</TabItem>
</TabControl>
</Border>
<!-- GridSplitter between left panel and center -->
<GridSplitter Grid.Column="1" Width="4" ResizeDirection="Columns"
Background="Transparent" />
<!-- Center: Spectrum chart tabs -->
<TabControl Grid.Column="2" Padding="0" Margin="4,0">
<TabItem Header="原始图谱">
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
</TabItem>
<TabItem Header="亮/暗光谱">
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
</TabItem>
<TabItem Header="扣除背景">
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
</TabItem>
<TabItem Header="光谱仪 1">
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
</TabItem>
<TabItem Header="光谱仪 2">
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
</TabItem>
<TabItem Header="合并光谱">
<views:SpectrumChartView DataContext="{Binding SpectrumChart}" />
</TabItem>
</TabControl>
<!-- GridSplitter between center and right panel -->
<GridSplitter Grid.Column="3" Width="4" ResizeDirection="Columns"
Background="Transparent" />
<!-- Right panel: peak detection + coordinate + control tabs (resizable) -->
<Border
Grid.Column="4"
MinWidth="180"
BorderBrush="{DynamicResource SemiColorBorder}"
BorderThickness="1,0,0,0">
<TabControl TabStripPlacement="Top" Padding="0">
<TabItem Header="峰位">
<views:PeakDetectionView DataContext="{Binding PeakDetection}" Padding="8" />
</TabItem>
<TabItem Header="坐标">
<views:CoordinateView DataContext="{Binding Coordinate}" Padding="8" />
</TabItem>
</TabControl>
</Border>
</Grid>
</DockPanel>
</Window>
+21
View File
@@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using RamanApp.ViewModels;
namespace RamanApp;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new MainWindowViewModel();
DataContext = vm;
vm.SpectrumChart.LoadDemoData();
}
private void OnExitClick(object? sender, RoutedEventArgs e)
{
Close();
}
}
+21
View File
@@ -0,0 +1,21 @@
using System;
using Avalonia;
namespace RamanApp;
internal class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
+27
View File
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<!--<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>-->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.17" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.17" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.17" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.17" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.17">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<PackageReference Include="Irihi.Ursa" Version="1.15.1" />
<PackageReference Include="Irihi.Ursa.Themes.Semi" Version="1.15.1" />
<PackageReference Include="ScottPlot.Avalonia" Version="5.1.58" />
<PackageReference Include="Semi.Avalonia" Version="11.3.14" />
<PackageReference Include="Semi.Avalonia.DataGrid" Version="11.3.7.2" />
</ItemGroup>
</Project>
@@ -0,0 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace RamanApp.ViewModels;
public partial class AcquisitionViewModel : ObservableObject
{
[ObservableProperty]
public partial string AcquisitionMode { get; set; }
[ObservableProperty]
public partial int ContinuousIntervalMs { get; set; }
[ObservableProperty]
public partial bool IsSingleAcquire { get; set; }
[ObservableProperty]
public partial int ContinuousCount { get; set; }
[ObservableProperty]
public partial bool IsAcquiring { get; set; }
[ObservableProperty]
public partial string StatusText { get; set; }
public AcquisitionViewModel()
{
AcquisitionMode = "普通模式";
IsSingleAcquire = true;
ContinuousCount = 1;
StatusText = "就绪";
}
[RelayCommand]
private void SingleAcquire()
{
StatusText = "单次采集中...";
}
[RelayCommand]
private void ContinuousAcquire()
{
IsAcquiring = !IsAcquiring;
StatusText = IsAcquiring ? "连续采集中..." : "就绪";
}
[RelayCommand]
private void StopAcquire()
{
IsAcquiring = false;
StatusText = "就绪";
}
}
@@ -0,0 +1,64 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace RamanApp.ViewModels;
public partial class CoordinateViewModel : ObservableObject
{
[ObservableProperty]
public partial ObservableCollection<CoordinateEntry> Entries { get; set; }
[ObservableProperty]
public partial string LookupValue { get; set; }
[ObservableProperty]
public partial double PeakWavelength { get; set; }
[ObservableProperty]
public partial double PeakFwhm { get; set; }
[ObservableProperty]
public partial double AreaStart { get; set; }
[ObservableProperty]
public partial double AreaEnd { get; set; }
[ObservableProperty]
public partial double PeakArea { get; set; }
public CoordinateViewModel()
{
Entries = [];
LookupValue = "1";
}
[RelayCommand]
private void Lookup() { }
[RelayCommand]
private void SearchLeft() { }
[RelayCommand]
private void SearchRight() { }
[RelayCommand]
private void CalculateArea() { }
}
public partial class CoordinateEntry : ObservableObject
{
[ObservableProperty]
public partial string Name { get; set; }
[ObservableProperty]
public partial double WavelengthNm { get; set; }
[ObservableProperty]
public partial double Intensity { get; set; }
public CoordinateEntry()
{
Name = string.Empty;
}
}
@@ -0,0 +1,107 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace RamanApp.ViewModels;
public partial class DataProcessingViewModel : ObservableObject
{
[ObservableProperty]
public partial bool AutoProcessAfterAcquire { get; set; }
// 1 - Interpolation & trimming
[ObservableProperty]
public partial bool InterpolationEnabled { get; set; }
[ObservableProperty]
public partial string InterpolationMethod { get; set; }
[ObservableProperty]
public partial int InterpolationInterval { get; set; }
[ObservableProperty]
public partial double InterpolationStart { get; set; }
[ObservableProperty]
public partial double InterpolationEnd { get; set; }
// 2 - Baseline correction
[ObservableProperty]
public partial bool BaselineCorrectionEnabled { get; set; }
[ObservableProperty]
public partial string BaselineMethod { get; set; }
[ObservableProperty]
public partial double BaselineLambda { get; set; }
[ObservableProperty]
public partial double BaselineOrder { get; set; }
// 3 - Smoothing
[ObservableProperty]
public partial bool SmoothingEnabled { get; set; }
[ObservableProperty]
public partial string SmoothingMethod { get; set; }
[ObservableProperty]
public partial double SmoothingLambda { get; set; }
[ObservableProperty]
public partial double SmoothingOrder { get; set; }
// 4 - Spectral derivative
[ObservableProperty]
public partial bool DerivativeEnabled { get; set; }
[ObservableProperty]
public partial string DerivativeMethod { get; set; }
// 5 - Normalization
[ObservableProperty]
public partial bool NormalizationEnabled { get; set; }
[ObservableProperty]
public partial string NormalizationMethod { get; set; }
public DataProcessingViewModel()
{
InterpolationMethod = "线性插值";
InterpolationInterval = 1;
InterpolationStart = 200;
InterpolationEnd = 3100;
BaselineMethod = "airPLS";
BaselineLambda = 100000;
BaselineOrder = 2.00;
SmoothingEnabled = true;
SmoothingMethod = "Whittaker平滑";
SmoothingLambda = 15;
SmoothingOrder = 1.00;
DerivativeMethod = "一阶导数";
NormalizationMethod = "最大值归一化";
}
[RelayCommand]
private void PreviewSelected()
{
}
[RelayCommand]
private void PreviewAll()
{
}
[RelayCommand]
private void ResetProcessing()
{
InterpolationEnabled = false;
BaselineCorrectionEnabled = false;
SmoothingEnabled = true;
DerivativeEnabled = false;
NormalizationEnabled = false;
}
}
@@ -0,0 +1,58 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace RamanApp.ViewModels;
public partial class DeviceSettingsViewModel : ObservableObject
{
[ObservableProperty]
public partial SpectrometerGroupViewModel Spectrometer1 { get; set; }
[ObservableProperty]
public partial SpectrometerGroupViewModel Spectrometer2 { get; set; }
public DeviceSettingsViewModel()
{
Spectrometer1 = new SpectrometerGroupViewModel("光谱仪 1");
Spectrometer2 = new SpectrometerGroupViewModel("光谱仪 2");
}
}
public partial class SpectrometerGroupViewModel : ObservableObject
{
[ObservableProperty]
public partial string GroupName { get; set; }
[ObservableProperty]
public partial ObservableCollection<string> Devices { get; set; }
[ObservableProperty]
public partial string? SelectedDevice { get; set; }
[ObservableProperty]
public partial int IntegrationTimeMs { get; set; }
[ObservableProperty]
public partial int AverageCount { get; set; }
[ObservableProperty]
public partial int BoxcarWidth { get; set; }
[ObservableProperty]
public partial bool DarkNoiseEnabled { get; set; }
[ObservableProperty]
public partial bool NonlinearityCorrectionEnabled { get; set; }
[ObservableProperty]
public partial int TriggerTimeoutMs { get; set; }
public SpectrometerGroupViewModel(string groupName)
{
GroupName = groupName;
Devices = ["(未连接)"];
IntegrationTimeMs = 1;
AverageCount = 1;
TriggerTimeoutMs = 10;
}
}
@@ -0,0 +1,35 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace RamanApp.ViewModels;
public partial class DisplaySettingsViewModel : ObservableObject
{
[ObservableProperty]
public partial bool ShowLegend { get; set; }
[ObservableProperty]
public partial bool ShowPeakX { get; set; }
[ObservableProperty]
public partial bool ShowPeakY { get; set; }
[ObservableProperty]
public partial bool ShowPeakLine { get; set; }
[ObservableProperty]
public partial bool InvertXAxis { get; set; }
[ObservableProperty]
public partial bool FillPeaks { get; set; }
[ObservableProperty]
public partial bool NormalizeAll { get; set; }
public DisplaySettingsViewModel()
{
ShowLegend = true;
ShowPeakX = true;
ShowPeakY = true;
ShowPeakLine = true;
}
}
@@ -0,0 +1,32 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace RamanApp.ViewModels;
public partial class LaserSettingsViewModel : ObservableObject
{
[ObservableProperty]
public partial bool LaserEnabled { get; set; }
[ObservableProperty]
public partial double LaserPowerPercent { get; set; }
[ObservableProperty]
public partial double LaserWavelengthNm { get; set; }
[ObservableProperty]
public partial bool AutoLaserOnAcquire { get; set; }
public LaserSettingsViewModel()
{
LaserPowerPercent = 100;
LaserWavelengthNm = 785;
AutoLaserOnAcquire = true;
}
[RelayCommand]
private void ToggleLaser()
{
LaserEnabled = !LaserEnabled;
}
}
@@ -0,0 +1,42 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace RamanApp.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
public partial DeviceSettingsViewModel DeviceSettings { get; set; }
[ObservableProperty]
public partial LaserSettingsViewModel LaserSettings { get; set; }
[ObservableProperty]
public partial AcquisitionViewModel Acquisition { get; set; }
[ObservableProperty]
public partial SpectrumChartViewModel SpectrumChart { get; set; }
[ObservableProperty]
public partial PeakDetectionViewModel PeakDetection { get; set; }
[ObservableProperty]
public partial CoordinateViewModel Coordinate { get; set; }
[ObservableProperty]
public partial DataProcessingViewModel DataProcessing { get; set; }
[ObservableProperty]
public partial DisplaySettingsViewModel DisplaySettings { get; set; }
public MainWindowViewModel()
{
DeviceSettings = new DeviceSettingsViewModel();
LaserSettings = new LaserSettingsViewModel();
Acquisition = new AcquisitionViewModel();
SpectrumChart = new SpectrumChartViewModel();
PeakDetection = new PeakDetectionViewModel();
Coordinate = new CoordinateViewModel();
DataProcessing = new DataProcessingViewModel();
DisplaySettings = new DisplaySettingsViewModel();
}
}
@@ -0,0 +1,58 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace RamanApp.ViewModels;
public partial class PeakDetectionViewModel : ObservableObject
{
[ObservableProperty]
public partial bool PeakDetectionEnabled { get; set; }
[ObservableProperty]
public partial double Threshold { get; set; }
[ObservableProperty]
public partial int MinPeakDistance { get; set; }
[ObservableProperty]
public partial ObservableCollection<PeakEntry> Peaks { get; set; }
public PeakDetectionViewModel()
{
Threshold = 1000;
MinPeakDistance = 5;
Peaks = [];
}
[RelayCommand]
private void DetectPeaks()
{
}
[RelayCommand]
private void ExportPeaks()
{
}
[RelayCommand]
private void ClearPeaks()
{
Peaks.Clear();
}
}
public partial class PeakEntry : ObservableObject
{
[ObservableProperty]
public partial double WavelengthNm { get; set; }
[ObservableProperty]
public partial double Intensity { get; set; }
[ObservableProperty]
public partial double WavenumberCm { get; set; }
[ObservableProperty]
public partial double CenterWavelengthNm { get; set; }
}
@@ -0,0 +1,56 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
namespace RamanApp.ViewModels;
public partial class SpectrumChartViewModel : ObservableObject
{
[ObservableProperty]
public partial string XAxisMode { get; set; }
[ObservableProperty]
public partial double[] XData { get; set; }
[ObservableProperty]
public partial double[] YData { get; set; }
[ObservableProperty]
public partial string XAxisLabel { get; set; }
[ObservableProperty]
public partial string YAxisLabel { get; set; }
public SpectrumChartViewModel()
{
XAxisMode = "波长(nm)";
XData = [];
YData = [];
XAxisLabel = "Wavelength (nm)";
YAxisLabel = "Intensity";
}
public void UpdateSpectrum(double[] x, double[] y)
{
XData = x;
YData = y;
}
public void LoadDemoData()
{
var rng = new Random(42);
var x = new double[2048];
var y = new double[2048];
for (var i = 0; i < 2048; i++)
{
x[i] = 200 + i * 0.7;
y[i] = rng.NextDouble() * 200;
}
y[700] = 60000;
y[701] = 45000;
y[699] = 35000;
XData = x;
YData = y;
}
}
+48
View File
@@ -0,0 +1,48 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:Class="RamanApp.Views.AcquisitionView"
x:DataType="vm:AcquisitionViewModel">
<StackPanel Spacing="4">
<Grid ColumnDefinitions="90,*">
<TextBlock Text="采集模式" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Column="1" SelectedItem="{Binding AcquisitionMode}"
HorizontalAlignment="Stretch">
<ComboBoxItem Content="普通模式" />
<ComboBoxItem Content="高速模式" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="90,*">
<TextBlock Text="连续间隔(ms)" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Column="1" Value="{Binding ContinuousIntervalMs}"
Minimum="0" Maximum="60000" Increment="100" FormatString="0" />
</Grid>
<Grid ColumnDefinitions="*,*" Margin="0,2">
<RadioButton Content="每次采集" IsChecked="{Binding IsSingleAcquire}"
GroupName="AcquireMode" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4">
<RadioButton Content="固定次数" GroupName="AcquireMode" />
<NumericUpDown Value="{Binding ContinuousCount}"
Minimum="1" Maximum="9999" Increment="1"
FormatString="0" Width="70" />
</StackPanel>
</Grid>
<Separator Margin="0,2" />
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
<Button Content="单次" Command="{Binding SingleAcquireCommand}"
Width="60" HorizontalContentAlignment="Center" />
<Button Content="连续" Command="{Binding ContinuousAcquireCommand}"
Width="60" HorizontalContentAlignment="Center" />
<Button Content="停止" Command="{Binding StopAcquireCommand}"
Width="60" HorizontalContentAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding StatusText}" Foreground="Gray"
HorizontalAlignment="Center" />
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class AcquisitionView : UserControl
{
public AcquisitionView()
{
InitializeComponent();
}
}
+87
View File
@@ -0,0 +1,87 @@
<UserControl
x:Class="RamanApp.Views.CoordinateView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:DataType="vm:CoordinateViewModel">
<DockPanel>
<!-- Bottom section -->
<StackPanel DockPanel.Dock="Bottom" Spacing="8" Margin="0,8,0,0">
<!-- Lookup coordinate -->
<DockPanel>
<Button DockPanel.Dock="Right" Content="查看" Margin="4,0,0,0"
Command="{Binding LookupCommand}" />
<TextBlock DockPanel.Dock="Left" Text="查看指定坐标数值"
VerticalAlignment="Center" Margin="0,0,4,0" />
<TextBox Text="{Binding LookupValue}" />
</DockPanel>
<Separator Margin="0,0" />
<!-- Search buttons -->
<Grid ColumnDefinitions="*,*">
<Button Grid.Column="0" Content="&lt;&lt;— 向左寻峰" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" Margin="0,0,2,0"
Command="{Binding SearchLeftCommand}" />
<Button Grid.Column="1" Content="向右寻峰 —&gt;&gt;" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center" Margin="2,0,0,0"
Command="{Binding SearchRightCommand}" />
</Grid>
<Separator Margin="0,0" />
<!-- Peak position -->
<StackPanel Spacing="4">
<TextBlock Text="峰位 [右键选择]" FontWeight="SemiBold" />
<DockPanel>
<TextBlock Text="波长:" VerticalAlignment="Center" Width="70" />
<TextBox IsReadOnly="True" Text="{Binding PeakWavelength}" />
</DockPanel>
<DockPanel>
<TextBlock Text="半峰宽(nm):" VerticalAlignment="Center" Width="70" />
<TextBox IsReadOnly="True" Text="{Binding PeakFwhm}" />
</DockPanel>
</StackPanel>
<Separator Margin="0,0" />
<!-- Peak area -->
<StackPanel Spacing="4">
<TextBlock Text="计算峰面积" FontWeight="SemiBold" />
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="起始拉曼"
VerticalAlignment="Center" Margin="0,0,4,0" />
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding AreaStart}" Margin="0,0,8,0" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="结束拉曼"
VerticalAlignment="Center" Margin="0,0,4,0" />
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding AreaEnd}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="峰面积"
VerticalAlignment="Center" Margin="0,4,4,0" />
<TextBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
IsReadOnly="True" Text="{Binding PeakArea}" Margin="0,4,8,0" />
<Button Grid.Row="1" Grid.Column="3" Content="计算" Margin="0,4,0,0"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Command="{Binding CalculateAreaCommand}" />
</Grid>
</StackPanel>
</StackPanel>
<!-- Top: data table -->
<DataGrid ItemsSource="{Binding Entries}"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
GridLinesVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="名称" Binding="{Binding Name}" Width="*" />
<DataGridTextColumn Header="波长(nm)" Binding="{Binding WavelengthNm, StringFormat={}{0:F3}}" Width="*" />
<DataGridTextColumn Header="峰强" Binding="{Binding Intensity, StringFormat={}{0:F0}}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class CoordinateView : UserControl
{
public CoordinateView()
{
InitializeComponent();
}
}
+144
View File
@@ -0,0 +1,144 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:Class="RamanApp.Views.DataProcessingView"
x:DataType="vm:DataProcessingViewModel">
<DockPanel>
<!-- Bottom buttons -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
Spacing="6" HorizontalAlignment="Center" Margin="0,8,0,4">
<Button Content="预览选中" Command="{Binding PreviewSelectedCommand}" />
<Button Content="预览全部" Command="{Binding PreviewAllCommand}" />
<Button Content="重置" Command="{Binding ResetProcessingCommand}" />
</StackPanel>
<ScrollViewer Padding="8,4">
<StackPanel Spacing="6">
<CheckBox Content="采集后自动处理" IsChecked="{Binding AutoProcessAfterAcquire}" />
<Separator Margin="0,2" />
<!-- 1 - Interpolation -->
<StackPanel Spacing="4">
<CheckBox Content="1 - 插值和剪切" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}"
IsChecked="{Binding InterpolationEnabled}" />
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto,Auto"
IsEnabled="{Binding InterpolationEnabled}" Margin="20,0,0,0">
<TextBlock Grid.Row="0" Text="插值算法" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding InterpolationMethod}"
HorizontalAlignment="Stretch" Margin="0,0,0,3">
<ComboBoxItem Content="线性插值" />
<ComboBoxItem Content="三次样条" />
</ComboBox>
<TextBlock Grid.Row="1" Text="插值间隔" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding InterpolationInterval}"
Minimum="1" Maximum="100" FormatString="0" Margin="0,0,0,3" />
<TextBlock Grid.Row="2" Text="范围" VerticalAlignment="Center" Margin="0,0,8,0" />
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" Spacing="4">
<NumericUpDown Value="{Binding InterpolationStart}"
Minimum="0" Maximum="10000" FormatString="0" Width="80" />
<TextBlock Text="~" VerticalAlignment="Center" />
<NumericUpDown Value="{Binding InterpolationEnd}"
Minimum="0" Maximum="10000" FormatString="0" Width="80" />
</StackPanel>
</Grid>
</StackPanel>
<Separator Margin="0,2" />
<!-- 2 - Baseline -->
<StackPanel Spacing="4">
<CheckBox Content="2 - 基线处理" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}"
IsChecked="{Binding BaselineCorrectionEnabled}" />
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto,Auto"
IsEnabled="{Binding BaselineCorrectionEnabled}" Margin="20,0,0,0">
<TextBlock Grid.Row="0" Text="处理算法" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding BaselineMethod}"
HorizontalAlignment="Stretch" Margin="0,0,0,3">
<ComboBoxItem Content="airPLS" />
<ComboBoxItem Content="arPLS" />
<ComboBoxItem Content="多项式拟合" />
</ComboBox>
<TextBlock Grid.Row="1" Text="lambda" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding BaselineLambda}"
Minimum="1" Maximum="10000000" FormatString="0" Margin="0,0,0,3" />
<TextBlock Grid.Row="2" Text="阶数" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding BaselineOrder}"
Minimum="0.01" Maximum="10" Increment="0.5" FormatString="0.00" />
</Grid>
</StackPanel>
<Separator Margin="0,2" />
<!-- 3 - Smoothing -->
<StackPanel Spacing="4">
<CheckBox Content="3 - 平滑滤波" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}"
IsChecked="{Binding SmoothingEnabled}" />
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto,Auto"
IsEnabled="{Binding SmoothingEnabled}" Margin="20,0,0,0">
<TextBlock Grid.Row="0" Text="平滑算法" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding SmoothingMethod}"
HorizontalAlignment="Stretch" Margin="0,0,0,3">
<ComboBoxItem Content="Whittaker" />
<ComboBoxItem Content="Savitzky-Golay" />
<ComboBoxItem Content="移动平均" />
</ComboBox>
<TextBlock Grid.Row="1" Text="lambda" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding SmoothingLambda}"
Minimum="1" Maximum="10000" FormatString="0" Margin="0,0,0,3" />
<TextBlock Grid.Row="2" Text="阶数" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding SmoothingOrder}"
Minimum="0.01" Maximum="10" Increment="0.5" FormatString="0.00" />
</Grid>
</StackPanel>
<Separator Margin="0,2" />
<!-- 4 - Derivative -->
<StackPanel Spacing="4">
<CheckBox Content="4 - 光谱导数" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}"
IsChecked="{Binding DerivativeEnabled}" />
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto"
IsEnabled="{Binding DerivativeEnabled}" Margin="20,0,0,0">
<TextBlock Grid.Row="0" Text="处理方法" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding DerivativeMethod}"
HorizontalAlignment="Stretch">
<ComboBoxItem Content="一阶导数" />
<ComboBoxItem Content="二阶导数" />
</ComboBox>
</Grid>
</StackPanel>
<Separator Margin="0,2" />
<!-- 5 - Normalization -->
<StackPanel Spacing="4">
<CheckBox Content="5 - 光谱归一化" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}"
IsChecked="{Binding NormalizationEnabled}" />
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto"
IsEnabled="{Binding NormalizationEnabled}" Margin="20,0,0,0">
<TextBlock Grid.Row="0" Text="处理方法" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" SelectedItem="{Binding NormalizationMethod}"
HorizontalAlignment="Stretch">
<ComboBoxItem Content="最大值归一化" />
<ComboBoxItem Content="面积归一化" />
<ComboBoxItem Content="向量归一化" />
</ComboBox>
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>
</DockPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class DataProcessingView : UserControl
{
public DataProcessingView()
{
InitializeComponent();
}
}
+106
View File
@@ -0,0 +1,106 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:Class="RamanApp.Views.DeviceSettingsView"
x:DataType="vm:DeviceSettingsViewModel">
<StackPanel Spacing="10">
<!-- Spectrometer 1 -->
<HeaderedContentControl>
<HeaderedContentControl.Header>
<TextBlock Text="{Binding Spectrometer1.GroupName}" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}" />
</HeaderedContentControl.Header>
<Grid ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
Margin="0,4,0,0">
<TextBlock Grid.Row="0" Text="光谱仪" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Spectrometer1.Devices}"
SelectedItem="{Binding Spectrometer1.SelectedDevice}"
HorizontalAlignment="Stretch" Margin="0,0,0,4" />
<TextBlock Grid.Row="1" Text="积分时间" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding Spectrometer1.IntegrationTimeMs}"
Minimum="1" Maximum="5000" Increment="1" FormatString="0" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<TextBlock Grid.Row="2" Text="平均次数" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding Spectrometer1.AverageCount}"
Minimum="1" Maximum="1000" Increment="1" FormatString="0" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="次" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<TextBlock Grid.Row="3" Text="Boxcar" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding Spectrometer1.BoxcarWidth}"
Minimum="0" Maximum="100" Increment="1" FormatString="0" Margin="0,0,0,4" />
<TextBlock Grid.Row="4" Text="触发超时" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="4" Grid.Column="1" Value="{Binding Spectrometer1.TriggerTimeoutMs}"
Minimum="0" Maximum="60000" Increment="10" FormatString="0" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<CheckBox Grid.Row="5" Grid.ColumnSpan="2" Content="暗噪声校准"
IsChecked="{Binding Spectrometer1.DarkNoiseEnabled}" Margin="0,0,0,2" />
<CheckBox Grid.Row="6" Grid.ColumnSpan="2" Content="非线性校准"
IsChecked="{Binding Spectrometer1.NonlinearityCorrectionEnabled}" />
</Grid>
</HeaderedContentControl>
<Separator Margin="0,2" />
<!-- Spectrometer 2 -->
<HeaderedContentControl>
<HeaderedContentControl.Header>
<TextBlock Text="{Binding Spectrometer2.GroupName}" FontWeight="SemiBold"
Foreground="{DynamicResource SemiColorPrimary}" />
</HeaderedContentControl.Header>
<Grid ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"
Margin="0,4,0,0">
<TextBlock Grid.Row="0" Text="光谱仪" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding Spectrometer2.Devices}"
SelectedItem="{Binding Spectrometer2.SelectedDevice}"
HorizontalAlignment="Stretch" Margin="0,0,0,4" />
<TextBlock Grid.Row="1" Text="积分时间" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding Spectrometer2.IntegrationTimeMs}"
Minimum="1" Maximum="5000" Increment="1" FormatString="0" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<TextBlock Grid.Row="2" Text="平均次数" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding Spectrometer2.AverageCount}"
Minimum="1" Maximum="1000" Increment="1" FormatString="0" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="次" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<TextBlock Grid.Row="3" Text="Boxcar" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding Spectrometer2.BoxcarWidth}"
Minimum="0" Maximum="100" Increment="1" FormatString="0" Margin="0,0,0,4" />
<TextBlock Grid.Row="4" Text="触发超时" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="4" Grid.Column="1" Value="{Binding Spectrometer2.TriggerTimeoutMs}"
Minimum="0" Maximum="60000" Increment="10" FormatString="0" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="ms" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<CheckBox Grid.Row="5" Grid.ColumnSpan="2" Content="暗噪声校准"
IsChecked="{Binding Spectrometer2.DarkNoiseEnabled}" Margin="0,0,0,2" />
<CheckBox Grid.Row="6" Grid.ColumnSpan="2" Content="非线性校准"
IsChecked="{Binding Spectrometer2.NonlinearityCorrectionEnabled}" />
</Grid>
</HeaderedContentControl>
</StackPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class DeviceSettingsView : UserControl
{
public DeviceSettingsView()
{
InitializeComponent();
}
}
@@ -0,0 +1,34 @@
<UserControl
x:Class="RamanApp.Views.LaserSettingsView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:DataType="vm:LaserSettingsViewModel">
<Grid ColumnDefinitions="90,*" RowDefinitions="Auto,Auto,Auto,Auto"
VerticalAlignment="Top">
<TextBlock Grid.Row="0" Text="激光器开关" VerticalAlignment="Center" Margin="0,0,8,0" />
<ToggleSwitch Grid.Row="0" Grid.Column="1" IsChecked="{Binding LaserEnabled}"
OnContent="开" OffContent="关" Margin="0,0,0,4" />
<TextBlock Grid.Row="1" Text="激光功率" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding LaserPowerPercent}"
Minimum="0" Maximum="100" Increment="5" FormatString="0.#" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="%" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<TextBlock Grid.Row="2" Text="激光波长" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding LaserWavelengthNm}"
Minimum="200" Maximum="1200" Increment="1"
FormatString="0.#" IsReadOnly="True" Margin="0,0,0,4">
<NumericUpDown.InnerRightContent>
<TextBlock Text="nm" Foreground="Gray" VerticalAlignment="Center" Margin="0,0,4,0" />
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<CheckBox Grid.Row="3" Grid.ColumnSpan="2" Content="采集时自动开启激光"
IsChecked="{Binding AutoLaserOnAcquire}" />
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class LaserSettingsView : UserControl
{
public LaserSettingsView()
{
InitializeComponent();
}
}
@@ -0,0 +1,51 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:Class="RamanApp.Views.PeakDetectionView"
x:DataType="vm:PeakDetectionViewModel">
<DockPanel>
<!-- Bottom buttons -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal"
Spacing="8" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button Content="导出" Command="{Binding ExportPeaksCommand}" Width="60" HorizontalContentAlignment="Center" />
<Button Content="删除" Command="{Binding ClearPeaksCommand}" Width="60" HorizontalContentAlignment="Center" />
<Button Content="重置" Command="{Binding DetectPeaksCommand}" Width="60" HorizontalContentAlignment="Center" />
</StackPanel>
<StackPanel Spacing="6">
<CheckBox Content="峰检测" IsChecked="{Binding PeakDetectionEnabled}" />
<Grid ColumnDefinitions="70,*" RowDefinitions="Auto,Auto">
<TextBlock Text="阈值" Grid.Row="0" VerticalAlignment="Center" Margin="0,0,8,4" />
<NumericUpDown Grid.Row="0" Grid.Column="1"
Value="{Binding Threshold}"
Minimum="0" Maximum="100000"
Increment="100" FormatString="0" />
<TextBlock Text="最小峰距" Grid.Row="1" VerticalAlignment="Center" Margin="0,0,8,0" />
<NumericUpDown Grid.Row="1" Grid.Column="1"
Value="{Binding MinPeakDistance}"
Minimum="1" Maximum="500"
Increment="1" FormatString="0" />
</Grid>
<Separator Margin="0,2" />
<!-- Peak table -->
<DataGrid ItemsSource="{Binding Peaks}"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
MinHeight="120">
<DataGrid.Columns>
<DataGridTextColumn Header="波长(nm)" Binding="{Binding WavelengthNm, StringFormat={}{0:0.###}}" Width="*" />
<DataGridTextColumn Header="强度" Binding="{Binding Intensity, StringFormat={}{0:0.#}}" Width="*" />
<DataGridTextColumn Header="波数(cm⁻¹)" Binding="{Binding WavenumberCm, StringFormat={}{0:0.#}}" Width="*" />
<DataGridTextColumn Header="中心波长(nm)" Binding="{Binding CenterWavelengthNm, StringFormat={}{0:0.###}}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</DockPanel>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class PeakDetectionView : UserControl
{
public PeakDetectionView()
{
InitializeComponent();
}
}
@@ -0,0 +1,28 @@
<UserControl
x:Class="RamanApp.Views.SpectrumChartView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sp="clr-namespace:ScottPlot.Avalonia;assembly=ScottPlot.Avalonia"
xmlns:vm="using:RamanApp.ViewModels"
x:DataType="vm:SpectrumChartViewModel">
<DockPanel>
<!-- X axis mode toggle at bottom -->
<StackPanel
Margin="0,6"
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
Orientation="Horizontal"
Spacing="16">
<RadioButton Content="像素(Pixel)" GroupName="XAxis" />
<RadioButton
Content="波长(nm)"
GroupName="XAxis"
IsChecked="True" />
<RadioButton Content="波数(cm⁻¹)" GroupName="XAxis" />
</StackPanel>
<!-- Chart -->
<sp:AvaPlot x:Name="AvaPlot" />
</DockPanel>
</UserControl>
@@ -0,0 +1,52 @@
using System;
using System.ComponentModel;
using Avalonia.Controls;
using RamanApp.ViewModels;
using ScottPlot;
namespace RamanApp.Views;
public partial class SpectrumChartView : UserControl
{
public SpectrumChartView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (DataContext is SpectrumChartViewModel vm)
{
vm.PropertyChanged += OnViewModelPropertyChanged;
RefreshChart(vm);
}
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is SpectrumChartViewModel vm && e.PropertyName is nameof(SpectrumChartViewModel.YData) or nameof(SpectrumChartViewModel.XData))
{
RefreshChart(vm);
}
}
private void RefreshChart(SpectrumChartViewModel vm)
{
var plot = AvaPlot.Plot;
plot.Clear();
if (vm.XData.Length > 0 && vm.YData.Length > 0)
{
var scatter = plot.Add.Scatter(vm.XData, vm.YData);
scatter.LineWidth = 1;
scatter.MarkerSize = 0;
scatter.Color = ScottPlot.Color.FromHex("#1E90FF");
}
plot.Axes.Bottom.Label.Text = vm.XAxisLabel;
plot.Axes.Left.Label.Text = vm.YAxisLabel;
plot.Axes.AutoScale();
AvaPlot.Refresh();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="RamanApp.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>
@@ -197,6 +197,8 @@ public sealed class SpectrometerRuntimeWorkspace : IAsyncDisposable
return SpectrometerResponse.Ok();
}
private const int MaxIntegrationTimeMicroseconds = 5_000_000;
private async Task<SpectrometerResponse> ConfigureAcquisitionAsync(SpectrometerRequest request, CancellationToken cancellationToken)
{
if (request.AcquisitionSettings is null)
@@ -204,6 +206,13 @@ public sealed class SpectrometerRuntimeWorkspace : IAsyncDisposable
return SpectrometerResponse.Fail(SpectrometerErrorCodes.InvalidValue, "ConfigureAcquisition requires acquisition settings.");
}
if (request.AcquisitionSettings.IntegrationTimeMicroseconds > MaxIntegrationTimeMicroseconds)
{
return SpectrometerResponse.Fail(
SpectrometerErrorCodes.InvalidValue,
$"Integration time {request.AcquisitionSettings.IntegrationTimeMicroseconds} us exceeds maximum allowed {MaxIntegrationTimeMicroseconds} us (5s).");
}
var device = FindConnectedDevice(request.Target);
if (device is null)
{
@@ -14,6 +14,7 @@ public sealed class SpectrometerCommandSessionTests
Assert.Contains(entries, entry => entry.Message == "Usage:" && !entry.ShowPrefix);
var acquire = Assert.Single(entries, entry => entry.Message.Contains("spec acquire <device>"));
Assert.Contains(entries, entry => entry.Message.Contains("--integration is in microseconds/us"));
Assert.False(acquire.ShowPrefix);
Assert.NotNull(acquire.Segments);
Assert.Equal(ConsoleColor.Green, acquire.Segments[0].Color);