feat: 添加ello和ttl

This commit is contained in:
chuan
2026-06-29 15:58:38 +08:00 Unverified
parent cd7a9be9dd
commit 5e860fa66b
20 changed files with 1660 additions and 13 deletions
+47
View File
@@ -0,0 +1,47 @@
{
"IndentSize": 4,
"IndentWithTabs": null,
"AttributesTolerance": 5,
"KeepFirstAttributeOnSameLine": false,
"MaxAttributeCharactersPerLine": 80,
"MaxAttributesPerLine": 0,
"NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransform, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter",
"SeparateByGroups": true,
"AttributeIndentation": 0,
"AttributeIndentationStyle": "Spaces",
"RemoveDesignTimeReferences": false,
"EnableAttributeReordering": true,
"AttributeOrderingRuleGroups": [
"x:Class",
"xmlns, xmlns:x",
"xmlns:*",
"x:Key, Key, x:Name, Name, x:Uid, Uid, Title",
"Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom",
"Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight",
"Classes, Theme, Styles",
"Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex",
"*:*, *",
"PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint",
"mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText",
"Storyboard.*, From, To, Duration"
],
"FirstLineAttributes": "",
"OrderAttributesByName": true,
"IgnoreDesignTimeReferencePrefix": false,
"PutEndingBracketOnNewLine": false,
"RemoveEndingTagOfEmptyElement": true,
"SpaceBeforeClosingSlash": false,
"RootElementLineBreakRule": "Default",
"ReorderVSM": "Last",
"ReorderGridChildren": false,
"ReorderCanvasChildren": false,
"ReorderSetters": "None",
"FormatMarkupExtension": false,
"NoNewLineMarkupExtensions": "x:Bind, Binding",
"ThicknessSeparator": "Comma",
"ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin",
"FormatOnSave": true,
"SaveAndCloseOnFormat": true,
"CommentPadding": 2,
"SuppressProcessing": false
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+9 -1
View File
@@ -27,6 +27,10 @@
<Separator />
<MenuItem Header="退出" Click="OnExitClick" />
</MenuItem>
<MenuItem Header="设置">
<MenuItem Header="EasyTTL 设置" Click="OnEasyTtlSettingsClick" />
<MenuItem Header="ELLO 设置" Click="OnElloSettingsClick" />
</MenuItem>
<MenuItem Header="显示设置">
<MenuItem Header="显示图例">
<MenuItem.Icon>
@@ -280,7 +284,11 @@
</TabItem>
<TabItem Header="激光器">
<ScrollViewer Padding="8">
<views:LaserSettingsView DataContext="{Binding LaserSettings}" />
<StackPanel Spacing="12">
<views:EasyTtlView DataContext="{Binding EasyTtl}" />
<Separator />
<views:ElloFilterView DataContext="{Binding Ello}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="数据处理">
+18 -1
View File
@@ -24,6 +24,24 @@ public partial class MainWindow : Window
private void OnExitClick(object? sender, RoutedEventArgs e) => Close();
private async void OnEasyTtlSettingsClick(object? sender, RoutedEventArgs e)
{
var dialog = new Views.EasyTtlSettingsDialog
{
DataContext = Vm.EasyTtl
};
await dialog.ShowDialog(this);
}
private async void OnElloSettingsClick(object? sender, RoutedEventArgs e)
{
var dialog = new Views.ElloSettingsDialog
{
DataContext = Vm.Ello
};
await dialog.ShowDialog(this);
}
private void OnYAxisUnitClick(object? sender, RoutedEventArgs e)
{
if (sender is MenuItem mi && mi.Header is string unit)
@@ -282,4 +300,3 @@ public partial class MainWindow : Window
.FirstOrDefault();
}
}
+2
View File
@@ -13,6 +13,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EasyTTL.Lib\EasyTTL.Lib.csproj" />
<ProjectReference Include="..\Filter.Ello\Filter.Ello.csproj" />
<ProjectReference Include="..\Spec.Sea\Spec.Sea.csproj" />
</ItemGroup>
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Serilog;
namespace RamanApp.Services;
public sealed class EasyTtlSettingsStore
{
private readonly string _path;
public EasyTtlSettingsStore(string? path = null)
{
_path = path ?? System.IO.Path.Combine(AppContext.BaseDirectory, "settings", "easyttl.json");
}
public string Path => _path;
public EasyTtlSettingsFile Load()
{
if (!File.Exists(_path))
{
Log.Information("EasyTTL settings file not found path={Path}; using defaults", _path);
return new EasyTtlSettingsFile();
}
try
{
var json = File.ReadAllText(_path);
var settings = JsonSerializer.Deserialize(json, EasyTtlJsonContext.Default.EasyTtlSettingsFile);
Log.Information("EasyTTL settings loaded path={Path} channelCount={Count}",
_path, settings?.Channels.Count ?? 0);
return settings ?? new EasyTtlSettingsFile();
}
catch (Exception ex)
{
Log.Warning(ex, "EasyTTL settings load failed path={Path}; using defaults", _path);
return new EasyTtlSettingsFile();
}
}
public void Save(EasyTtlSettingsFile settings)
{
string tmpPath = _path + ".tmp";
try
{
var dir = System.IO.Path.GetDirectoryName(_path);
if (!string.IsNullOrWhiteSpace(dir))
Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(settings, EasyTtlJsonContext.Default.EasyTtlSettingsFile);
File.WriteAllText(tmpPath, json);
File.Move(tmpPath, _path, true);
Log.Information("EasyTTL settings saved path={Path} channelCount={Count}",
_path, settings.Channels.Count);
}
catch (Exception ex)
{
TryDelete(tmpPath);
Log.Warning(ex, "EasyTTL settings save failed path={Path}", _path);
}
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
File.Delete(path);
}
catch
{
// Best effort cleanup only.
}
}
}
public sealed class EasyTtlSettingsFile
{
public int Version { get; set; } = 1;
public List<EasyTtlChannelSettings> Channels { get; set; } = [];
}
public sealed class EasyTtlChannelSettings
{
public int Channel { get; set; }
public string? Alias { get; set; }
public bool? ShowInLaserPanel { get; set; }
}
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(EasyTtlSettingsFile))]
internal sealed partial class EasyTtlJsonContext : JsonSerializerContext;
+117
View File
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using Serilog;
namespace RamanApp.Services;
public sealed class ElloSettingsStore
{
private readonly string _path;
public ElloSettingsStore(string? path = null)
{
_path = path ?? System.IO.Path.Combine(AppContext.BaseDirectory, "settings", "ello.json");
}
public ElloSettingsFile Load()
{
if (!File.Exists(_path))
return new ElloSettingsFile();
try
{
var json = File.ReadAllText(_path);
return JsonSerializer.Deserialize(json, ElloJsonContext.Default.ElloSettingsFile)
?? new ElloSettingsFile();
}
catch (Exception ex)
{
Log.Warning(ex, "ELLO settings load failed path={Path}; using defaults", _path);
return new ElloSettingsFile();
}
}
public void Save(ElloSettingsFile settings)
{
string tmpPath = _path + ".tmp";
try
{
var dir = System.IO.Path.GetDirectoryName(_path);
if (!string.IsNullOrWhiteSpace(dir))
Directory.CreateDirectory(dir);
var json = JsonSerializer.Serialize(settings, ElloJsonContext.Default.ElloSettingsFile);
File.WriteAllText(tmpPath, json);
File.Move(tmpPath, _path, true);
Log.Information("ELLO settings saved path={Path} deviceCount={DeviceCount}",
_path, settings.Devices.Count);
}
catch (Exception ex)
{
TryDelete(tmpPath);
Log.Warning(ex, "ELLO settings save failed path={Path}", _path);
}
}
private static void TryDelete(string path)
{
try
{
if (File.Exists(path))
File.Delete(path);
}
catch
{
// Best effort cleanup only.
}
}
}
public sealed class ElloSettingsFile
{
public int Version { get; set; } = 2;
public List<ElloDeviceSettings> Devices { get; set; } = [];
public string? PortName { get; set; }
public string? Address { get; set; }
public int? SlotCount { get; set; }
public List<ElloSlotSettings>? Slots { get; set; }
}
public sealed class ElloDeviceSettings
{
public string? Name { get; set; }
public string? PortName { get; set; }
public string Address { get; set; } = "0";
public int SlotCount { get; set; } = 6;
public bool ShowInLaserPanel { get; set; } = true;
public List<ElloSlotSettings> Slots { get; set; } = [];
}
public sealed class ElloSlotSettings
{
public int Slot { get; set; }
public string? Alias { get; set; }
public bool? ShowInLaserPanel { get; set; }
}
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(ElloSettingsFile))]
internal sealed partial class ElloJsonContext : JsonSerializerContext;
+369
View File
@@ -0,0 +1,369 @@
using System;
using System.Collections.ObjectModel;
using System.IO.Ports;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using EasyTTL.Lib;
using RamanApp.Services;
using Serilog;
namespace RamanApp.ViewModels;
public partial class EasyTtlViewModel : ObservableObject, IDisposable
{
private const int FixedFrequencyHz = 1;
private const double FixedDutyPercent = 100;
private const string SimulatedPortName = "SIM-EASYTTL";
private EasyTtlDevice? _device;
private readonly bool _simulate;
private readonly EasyTtlSettingsStore _settingsStore;
private bool _suppressHardwareUpdates;
[ObservableProperty]
public partial ObservableCollection<string> PortNames { get; set; } = [];
[ObservableProperty]
public partial string? SelectedPortName { get; set; }
[ObservableProperty]
public partial bool IsConnected { get; set; }
[ObservableProperty]
public partial bool OutputEnabled { get; set; }
[ObservableProperty]
public partial ObservableCollection<EasyTtlChannelViewModel> Channels { get; set; } =
[
new(1, "CH1"),
new(2, "CH2"),
new(3, "CH3"),
new(4, "CH4"),
new(5, "CH5"),
new(6, "CH6"),
];
[ObservableProperty]
public partial string StatusText { get; set; } = "未连接";
public EasyTtlViewModel(bool simulate = false, EasyTtlSettingsStore? settingsStore = null)
{
_simulate = simulate;
_settingsStore = settingsStore ?? new EasyTtlSettingsStore();
LoadSavedSettings();
RefreshPorts();
foreach (var channel in Channels)
channel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(EasyTtlChannelViewModel.IsEnabled))
{
Log.Information("EasyTTL logical channel state changed channel={Channel} enabled={Enabled}",
channel.Channel, channel.IsEnabled);
}
else if (e.PropertyName == nameof(EasyTtlChannelViewModel.HardwareEnabled))
{
ApplyHardwareChannel(channel);
}
else if (e.PropertyName == nameof(EasyTtlChannelViewModel.Alias))
{
SaveSettings();
Log.Information("EasyTTL channel alias changed channel={Channel} alias={Alias}",
channel.Channel, channel.Alias);
}
else if (e.PropertyName == nameof(EasyTtlChannelViewModel.ShowInLaserPanel))
{
SaveSettings();
Log.Information("EasyTTL channel laser panel visibility changed channel={Channel} show={Show}",
channel.Channel, channel.ShowInLaserPanel);
}
};
if (_simulate)
StatusText = "模拟模式";
Log.Information("EasyTTL initialized simulate={Simulate} selectedPort={SelectedPort}", _simulate, SelectedPortName);
}
private void LoadSavedSettings()
{
var settings = _settingsStore.Load();
foreach (var saved in settings.Channels)
{
var channel = Channels.FirstOrDefault(c => c.Channel == saved.Channel);
if (channel == null)
continue;
channel.Alias = string.IsNullOrWhiteSpace(saved.Alias)
? $"CH{channel.Channel}"
: saved.Alias.Trim();
channel.ShowInLaserPanel = saved.ShowInLaserPanel ?? true;
}
}
private void SaveSettings()
{
var settings = new EasyTtlSettingsFile
{
Channels = Channels
.Select(channel => new EasyTtlChannelSettings
{
Channel = channel.Channel,
Alias = channel.Alias,
ShowInLaserPanel = channel.ShowInLaserPanel
})
.ToList()
};
_settingsStore.Save(settings);
}
[RelayCommand]
private void RefreshPorts()
{
var previous = SelectedPortName;
PortNames.Clear();
if (_simulate)
{
PortNames.Add(SimulatedPortName);
}
else
{
foreach (var port in SerialPort.GetPortNames())
PortNames.Add(port);
}
SelectedPortName = previous != null && PortNames.Contains(previous)
? previous
: PortNames.Count > 0 ? PortNames[0] : null;
Log.Information("EasyTTL ports refreshed simulate={Simulate} count={Count} selected={SelectedPort}",
_simulate, PortNames.Count, SelectedPortName ?? "-");
}
[RelayCommand]
private void Connect()
{
if (IsConnected || string.IsNullOrWhiteSpace(SelectedPortName))
return;
try
{
Log.Information("EasyTTL connecting port={PortName} simulate={Simulate}", SelectedPortName, _simulate);
if (!_simulate)
{
_device = new EasyTtlDevice(SelectedPortName);
_device.Open();
Log.Information("EasyTTL serial opened port={PortName}", SelectedPortName);
}
IsConnected = true;
SetHardwareUiOff();
if (!_simulate)
{
Log.Information("EasyTTL safe init start: DISABLE then ALL OFF");
_device!.Disable();
_device.AllOff();
for (int channel = 1; channel <= 6; channel++)
_device.SetChannel(channel, FixedFrequencyHz, (float)FixedDutyPercent);
Log.Information("EasyTTL safe init completed frequency={FrequencyHz} duty={DutyPercent:0.##}",
FixedFrequencyHz, FixedDutyPercent);
}
else
{
Log.Information("EasyTTL simulated safe init completed");
}
StatusText = _simulate ? "模拟已连接" : $"已连接 {SelectedPortName}";
Log.Information("EasyTTL connected port={PortName} simulate={Simulate}; output disabled and all channels off",
SelectedPortName, _simulate);
}
catch (Exception ex)
{
_device?.Dispose();
_device = null;
IsConnected = false;
StatusText = $"连接失败: {ex.Message}";
Log.Warning(ex, "EasyTTL connect failed port={PortName}", SelectedPortName);
}
}
[RelayCommand]
private void Disconnect()
{
try
{
if (!_simulate && _device != null)
{
try
{
Log.Information("EasyTTL disconnect safe shutdown start: DISABLE then ALL OFF");
_device.Disable();
_device.AllOff();
Log.Information("EasyTTL disconnect safe shutdown completed");
}
catch (Exception ex)
{
Log.Warning(ex, "EasyTTL disconnect safe shutdown failed");
}
}
_device?.Dispose();
}
finally
{
_device = null;
IsConnected = false;
SetHardwareUiOff();
StatusText = "未连接";
Log.Information("EasyTTL disconnected");
}
}
private void ApplyHardwareChannel(EasyTtlChannelViewModel channel)
{
if (_suppressHardwareUpdates) return;
if (!_simulate && _device == null)
{
RevertHardwareChannel(channel);
Log.Warning("EasyTTL channel change ignored because device is not connected channel={Channel} requestedOn={On}",
channel.Channel, channel.HardwareEnabled);
return;
}
try
{
Log.Information("EasyTTL channel command start channel={Channel} requestedOn={On} frequency={FrequencyHz} duty={DutyPercent:0.##} simulate={Simulate}",
channel.Channel, channel.HardwareEnabled, FixedFrequencyHz, FixedDutyPercent, _simulate);
if (!_simulate)
{
_device!.SetChannel(channel.Channel, FixedFrequencyHz, (float)FixedDutyPercent);
if (channel.HardwareEnabled)
_device.ChannelOn(channel.Channel);
else
_device.ChannelOff(channel.Channel);
}
var onChannels = Channels.Where(c => c.HardwareEnabled).Select(c => c.Channel).ToArray();
StatusText = onChannels.Length == 0
? "全部通道 OFF"
: $"ON: CH{string.Join(", CH", onChannels)}";
Log.Information("EasyTTL hardware channel changed channel={Channel} on={On} frequency={FrequencyHz} duty={DutyPercent:0.##} simulate={Simulate}",
channel.Channel, channel.HardwareEnabled, FixedFrequencyHz, FixedDutyPercent, _simulate);
}
catch (Exception ex)
{
StatusText = $"设置失败: {ex.Message}";
RevertHardwareChannel(channel);
Log.Warning(ex, "EasyTTL apply channel failed");
}
}
partial void OnOutputEnabledChanged(bool value)
{
if (_suppressHardwareUpdates) return;
if (!_simulate && _device == null)
{
RevertOutputEnabled(value);
Log.Warning("EasyTTL output change ignored because device is not connected requestedEnabled={Enabled}", value);
return;
}
try
{
Log.Information("EasyTTL output command start enabled={Enabled} simulate={Simulate}", value, _simulate);
if (!_simulate)
{
if (value)
_device!.Enable();
else
_device!.Disable();
}
StatusText = _simulate
? value ? "模拟输出已使能" : "模拟输出已禁用"
: value ? "输出已使能" : "输出已禁用";
Log.Information("EasyTTL output enabled={Enabled} simulate={Simulate}", value, _simulate);
}
catch (Exception ex)
{
StatusText = $"输出设置失败: {ex.Message}";
RevertOutputEnabled(value);
Log.Warning(ex, "EasyTTL output toggle failed");
}
}
private void SetHardwareUiOff()
{
_suppressHardwareUpdates = true;
try
{
OutputEnabled = false;
foreach (var channel in Channels)
channel.HardwareEnabled = false;
}
finally
{
_suppressHardwareUpdates = false;
}
}
private void RevertHardwareChannel(EasyTtlChannelViewModel channel)
{
_suppressHardwareUpdates = true;
try
{
channel.HardwareEnabled = !channel.HardwareEnabled;
}
finally
{
_suppressHardwareUpdates = false;
}
}
private void RevertOutputEnabled(bool attemptedValue)
{
_suppressHardwareUpdates = true;
try
{
OutputEnabled = !attemptedValue;
}
finally
{
_suppressHardwareUpdates = false;
}
}
public void Dispose()
{
Disconnect();
GC.SuppressFinalize(this);
}
}
public partial class EasyTtlChannelViewModel(int channel, string alias) : ObservableObject
{
public int Channel { get; } = channel;
[ObservableProperty]
public partial string Alias { get; set; } = alias;
[ObservableProperty]
public partial bool ShowInLaserPanel { get; set; } = true;
[ObservableProperty]
public partial bool IsEnabled { get; set; }
[ObservableProperty]
public partial bool HardwareEnabled { get; set; }
public string DisplayName => string.IsNullOrWhiteSpace(Alias)
? $"CH{Channel}"
: Alias;
partial void OnAliasChanged(string value)
{
OnPropertyChanged(nameof(DisplayName));
}
}
@@ -0,0 +1,659 @@
using System;
using System.Collections.ObjectModel;
using System.IO.Ports;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Filter.Ello;
using Raman.Core.Filters;
using RamanApp.Services;
using Serilog;
namespace RamanApp.ViewModels;
public partial class ElloFilterViewModel : ObservableObject, IAsyncDisposable
{
internal const string SimulatedPortName = "SIM-ELLO";
private readonly bool _simulate;
private readonly ElloSettingsStore _settingsStore;
private bool _suppressSave;
[ObservableProperty]
public partial ObservableCollection<ElloDeviceViewModel> Devices { get; set; } = [];
public ElloFilterViewModel(bool simulate = false, ElloSettingsStore? settingsStore = null)
{
_simulate = simulate;
_settingsStore = settingsStore ?? new ElloSettingsStore();
bool hasSavedConnections = LoadSavedSettings();
if (Devices.Count == 0)
{
AddDevice(new ElloDeviceViewModel(_simulate, SaveSettings, CreateDefaultDevice(1)));
SaveSettings();
}
Log.Information("ELLO initialized simulate={Simulate} deviceCount={DeviceCount}",
_simulate, Devices.Count);
if (hasSavedConnections)
StartAutoConnect();
}
[RelayCommand]
private void AddDevice()
{
AddDevice(new ElloDeviceViewModel(_simulate, SaveSettings, CreateDefaultDevice(Devices.Count + 1)));
SaveSettings();
}
[RelayCommand]
private async Task RemoveDeviceAsync(ElloDeviceViewModel? device, CancellationToken cancellationToken)
{
if (device == null)
return;
await device.DisconnectAsync(cancellationToken);
await device.DisposeAsync();
Devices.Remove(device);
SaveSettings();
Log.Information("ELLO device removed name={Name}", device.Name);
}
private bool LoadSavedSettings()
{
var settings = _settingsStore.Load();
var devices = settings.Devices.Count > 0
? settings.Devices
: LoadLegacySingleDevice(settings);
bool hasSavedConnections = devices.Any(device => !string.IsNullOrWhiteSpace(device.PortName));
_suppressSave = true;
try
{
foreach (var device in devices)
AddDevice(new ElloDeviceViewModel(_simulate, SaveSettings, device));
}
finally
{
_suppressSave = false;
}
return hasSavedConnections;
}
private static System.Collections.Generic.List<ElloDeviceSettings> LoadLegacySingleDevice(ElloSettingsFile settings)
{
if (settings.PortName == null && settings.Address == null && settings.SlotCount == null && settings.Slots == null)
return [];
return
[
new ElloDeviceSettings
{
Name = "ELLO1",
PortName = settings.PortName,
Address = string.IsNullOrWhiteSpace(settings.Address) ? "0" : settings.Address,
SlotCount = settings.SlotCount is 4 or 6 ? settings.SlotCount.Value : 6,
ShowInLaserPanel = true,
Slots = settings.Slots ?? []
}
];
}
private ElloDeviceSettings CreateDefaultDevice(int index)
{
var ports = GetAvailablePorts(_simulate);
return new ElloDeviceSettings
{
Name = $"ELLO{index}",
PortName = ports.FirstOrDefault(),
Address = Math.Clamp(index - 1, 0, 15).ToString("X"),
SlotCount = 6,
ShowInLaserPanel = true,
Slots = Enumerable.Range(1, 6)
.Select(slot => new ElloSlotSettings
{
Slot = slot,
Alias = $"轮{slot}",
ShowInLaserPanel = true
})
.ToList()
};
}
private void AddDevice(ElloDeviceViewModel device)
{
Devices.Add(device);
}
private void StartAutoConnect()
{
foreach (var device in Devices.Where(device => !string.IsNullOrWhiteSpace(device.SelectedPortName)))
_ = AutoConnectAsync(device);
}
private static async Task AutoConnectAsync(ElloDeviceViewModel device)
{
try
{
Log.Information("ELLO auto connect queued name={Name} port={PortName} address={Address}",
device.Name, device.SelectedPortName, device.Address);
await device.ConnectAsync(CancellationToken.None);
}
catch (Exception ex)
{
Log.Warning(ex, "ELLO auto connect failed name={Name} port={PortName} address={Address}",
device.Name, device.SelectedPortName, device.Address);
}
}
internal static string[] GetAvailablePorts(bool simulate)
{
if (simulate)
return [SimulatedPortName];
return SerialPort.GetPortNames();
}
private void SaveSettings()
{
if (_suppressSave)
return;
_settingsStore.Save(new ElloSettingsFile
{
Version = 2,
Devices = Devices.Select(device => device.ToSettings()).ToList()
});
}
public async ValueTask DisposeAsync()
{
foreach (var device in Devices.ToArray())
await device.DisposeAsync();
GC.SuppressFinalize(this);
}
}
public partial class ElloDeviceViewModel : ObservableObject, IAsyncDisposable
{
private readonly bool _simulate;
private readonly Action _save;
private bool _suppressSave;
private IFilterWheelDevice? _device;
[ObservableProperty]
public partial ObservableCollection<string> PortNames { get; set; } = [];
[ObservableProperty]
public partial string Name { get; set; }
[ObservableProperty]
public partial string? SelectedPortName { get; set; }
[ObservableProperty]
public partial string Address { get; set; }
[ObservableProperty]
public partial ObservableCollection<int> SlotCountOptions { get; set; } = [4, 6];
[ObservableProperty]
public partial int SlotCount { get; set; }
[ObservableProperty]
public partial bool ShowInLaserPanel { get; set; }
[ObservableProperty]
public partial ObservableCollection<ElloSlotViewModel> Slots { get; set; } = [];
[ObservableProperty]
public partial ObservableCollection<ElloSlotViewModel> PanelSlots { get; set; } = [];
[ObservableProperty]
public partial bool IsConnected { get; set; }
[ObservableProperty]
public partial bool IsMoving { get; set; }
[ObservableProperty]
public partial int? CurrentSlot { get; set; }
[ObservableProperty]
public partial string CurrentSlotText { get; set; } = "未连接";
[ObservableProperty]
public partial bool ShowCurrentSlotText { get; set; }
[ObservableProperty]
public partial string CurrentSlotCountText { get; set; } = "-";
[ObservableProperty]
public partial string StatusText { get; set; } = "未连接";
[ObservableProperty]
public partial string ConnectionHintText { get; set; } = "未连接";
[ObservableProperty]
public partial bool ShowConnectionHint { get; set; } = true;
public ElloDeviceViewModel(bool simulate, Action save, ElloDeviceSettings settings)
{
_simulate = simulate;
_save = save;
_suppressSave = true;
try
{
Name = string.IsNullOrWhiteSpace(settings.Name) ? "ELLO" : settings.Name.Trim();
SelectedPortName = settings.PortName;
Address = string.IsNullOrWhiteSpace(settings.Address) ? "0" : settings.Address.Trim().ToUpperInvariant();
SlotCount = settings.SlotCount is 4 or 6 ? settings.SlotCount : 6;
ShowInLaserPanel = settings.ShowInLaserPanel;
RebuildSlots(settings.Slots);
RefreshPorts();
}
finally
{
_suppressSave = false;
}
if (_simulate)
StatusText = "模拟模式";
UpdateConnectionHint();
}
[RelayCommand]
private void RefreshPorts()
{
var previous = SelectedPortName;
PortNames.Clear();
foreach (var port in ElloFilterViewModel.GetAvailablePorts(_simulate))
PortNames.Add(port);
SelectedPortName = previous != null && PortNames.Contains(previous)
? previous
: PortNames.Count > 0 ? PortNames[0] : null;
Log.Information("ELLO ports refreshed name={Name} simulate={Simulate} count={Count} selected={SelectedPort}",
Name, _simulate, PortNames.Count, SelectedPortName ?? "-");
}
[RelayCommand]
public async Task ConnectAsync(CancellationToken cancellationToken)
{
if (IsConnected)
return;
if (string.IsNullOrWhiteSpace(SelectedPortName))
{
StatusText = "未选择串口";
return;
}
try
{
var normalizedAddress = NormalizeAddress(Address);
Log.Information("ELLO connecting name={Name} port={PortName} address={Address} slotCount={SlotCount} simulate={Simulate}",
Name, SelectedPortName, normalizedAddress, SlotCount, _simulate);
if (!_simulate)
{
_device = new SerialFilterWheelDevice(
new SerialFilterOptions { PortName = SelectedPortName },
CreateProfile(normalizedAddress));
await _device.ConnectAsync(cancellationToken);
await UpdateStatusFromDeviceAsync(cancellationToken);
}
else
{
CurrentSlot = CurrentSlot ?? 1;
CurrentSlotText = GetCurrentSlotText();
}
Address = normalizedAddress;
IsConnected = true;
StatusText = _simulate ? "模拟已连接" : $"已连接 {SelectedPortName}:{Address}";
UpdateConnectionHint();
Log.Information("ELLO connected name={Name} port={PortName} address={Address} simulate={Simulate}",
Name, SelectedPortName, Address, _simulate);
}
catch (Exception ex)
{
await DisposeDeviceAsync();
IsConnected = false;
CurrentSlot = null;
CurrentSlotText = "未连接";
StatusText = $"连接失败: {ex.Message}";
UpdateConnectionHint();
Log.Warning(ex, "ELLO connect failed name={Name} port={PortName} address={Address}",
Name, SelectedPortName, Address);
}
}
[RelayCommand]
public async Task DisconnectAsync(CancellationToken cancellationToken)
{
try
{
if (_device != null)
await _device.DisconnectAsync(cancellationToken);
}
catch (Exception ex)
{
Log.Warning(ex, "ELLO disconnect command failed name={Name}", Name);
}
finally
{
await DisposeDeviceAsync();
IsConnected = false;
CurrentSlot = null;
CurrentSlotText = "未连接";
StatusText = "未连接";
UpdateConnectionHint();
Log.Information("ELLO disconnected name={Name}", Name);
}
}
public async Task MoveToSlotAsync(int slot, CancellationToken cancellationToken)
{
if (slot < 1 || slot > SlotCount)
return;
if (IsMoving)
return;
if (!IsConnected)
{
StatusText = "滤光轮未连接";
UpdateConnectionHint();
return;
}
try
{
IsMoving = true;
Log.Information("ELLO move start name={Name} slot={Slot} alias={Alias} simulate={Simulate}",
Name, slot, Slots.FirstOrDefault(s => s.Slot == slot)?.DisplayName ?? "-", _simulate);
StatusText = $"切换到轮{slot}";
if (_simulate)
{
await Task.Delay(250, cancellationToken);
CurrentSlot = slot;
CurrentSlotText = GetCurrentSlotText();
}
else
{
if (_device == null)
throw new InvalidOperationException("ELLO device is not connected.");
await _device.MoveToAsync(_device.Profile.CreatePosition(slot), cancellationToken);
await UpdateStatusFromDeviceAsync(cancellationToken);
}
StatusText = $"当前: {CurrentSlotText}";
Log.Information("ELLO move completed name={Name} slot={Slot} current={CurrentSlotText} simulate={Simulate}",
Name, slot, CurrentSlotText, _simulate);
}
catch (Exception ex)
{
StatusText = $"切换失败: {ex.Message}";
UpdateConnectionHint();
Log.Warning(ex, "ELLO move failed name={Name} slot={Slot}", Name, slot);
}
finally
{
IsMoving = false;
}
}
internal ElloDeviceSettings ToSettings()
{
return new ElloDeviceSettings
{
Name = Name,
PortName = SelectedPortName,
Address = string.IsNullOrWhiteSpace(Address) ? "0" : Address.Trim().ToUpperInvariant(),
SlotCount = SlotCount,
ShowInLaserPanel = ShowInLaserPanel,
Slots = Slots.Select(slot => new ElloSlotSettings
{
Slot = slot.Slot,
Alias = slot.Alias,
ShowInLaserPanel = slot.ShowInLaserPanel
}).ToList()
};
}
private void RebuildSlots(System.Collections.Generic.IReadOnlyList<ElloSlotSettings>? savedSlots = null)
{
var previous = Slots.ToDictionary(slot => slot.Slot, slot => slot);
Slots.Clear();
for (int slot = 1; slot <= SlotCount; slot++)
{
var saved = savedSlots?.FirstOrDefault(s => s.Slot == slot);
var alias = saved?.Alias
?? (previous.TryGetValue(slot, out var old) ? old.Alias : null)
?? $"轮{slot}";
var show = saved?.ShowInLaserPanel
?? (previous.TryGetValue(slot, out var oldShow) ? oldShow.ShowInLaserPanel : true);
var vm = new ElloSlotViewModel(this, slot, alias)
{
ShowInLaserPanel = show
};
vm.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(ElloSlotViewModel.Alias)
|| e.PropertyName == nameof(ElloSlotViewModel.ShowInLaserPanel))
{
CurrentSlotText = GetCurrentSlotText();
if (e.PropertyName == nameof(ElloSlotViewModel.ShowInLaserPanel))
RefreshPanelSlots();
Save();
Log.Information("ELLO slot setting changed name={Name} slot={Slot} alias={Alias} show={Show}",
Name, vm.Slot, vm.Alias, vm.ShowInLaserPanel);
}
};
Slots.Add(vm);
}
RefreshPanelSlots();
CurrentSlotText = GetCurrentSlotText();
CurrentSlotCountText = CurrentSlot is int current ? $"{current}/{SlotCount}" : $"-/{SlotCount}";
ShowCurrentSlotText = IsConnected && CurrentSlot is not null;
UpdateSlotSelection();
}
private void RefreshPanelSlots()
{
PanelSlots.Clear();
foreach (var slot in Slots.Where(slot => slot.ShowInLaserPanel))
PanelSlots.Add(slot);
}
partial void OnNameChanged(string value) => Save();
partial void OnSelectedPortNameChanged(string? value) => Save();
partial void OnAddressChanged(string value) => Save();
partial void OnShowInLaserPanelChanged(bool value) => Save();
partial void OnIsConnectedChanged(bool value)
{
foreach (var slot in Slots)
slot.RefreshOwnerState();
ShowCurrentSlotText = IsConnected && CurrentSlot is not null;
UpdateConnectionHint();
}
partial void OnIsMovingChanged(bool value)
{
foreach (var slot in Slots)
slot.RefreshOwnerState();
}
partial void OnStatusTextChanged(string value)
{
UpdateConnectionHint();
}
partial void OnSlotCountChanged(int value)
{
if (value is not (4 or 6))
return;
_suppressSave = true;
try
{
RebuildSlots();
if (CurrentSlot > value)
CurrentSlot = null;
}
finally
{
_suppressSave = false;
}
Save();
CurrentSlotCountText = CurrentSlot is int current ? $"{current}/{SlotCount}" : $"-/{SlotCount}";
}
partial void OnCurrentSlotChanged(int? value)
{
CurrentSlotText = GetCurrentSlotText();
CurrentSlotCountText = value is int slot ? $"{slot}/{SlotCount}" : $"-/{SlotCount}";
ShowCurrentSlotText = IsConnected && value is not null;
UpdateSlotSelection();
}
private void Save()
{
if (!_suppressSave)
_save();
}
private FilterWheelProfile CreateProfile(string address)
{
return new FilterWheelProfile(
$"{SelectedPortName}:{address}",
FilterWheelAddress.Create(address),
SlotCount,
Slots.ToDictionary(slot => slot.Slot, slot => slot.DisplayName));
}
private async Task UpdateStatusFromDeviceAsync(CancellationToken cancellationToken)
{
if (_device == null)
return;
var status = await _device.GetFilterStatusAsync(cancellationToken);
CurrentSlot = status.CurrentPosition?.Value;
CurrentSlotText = GetCurrentSlotText();
CurrentSlotCountText = CurrentSlot is int slot ? $"{slot}/{SlotCount}" : $"-/{SlotCount}";
ShowCurrentSlotText = IsConnected && CurrentSlot is not null;
UpdateSlotSelection();
}
private string GetCurrentSlotText()
{
if (CurrentSlot is not int slot)
return IsConnected ? "未知" : "未连接";
var alias = Slots.FirstOrDefault(s => s.Slot == slot)?.DisplayName;
return string.IsNullOrWhiteSpace(alias) ? $"轮{slot}" : alias;
}
private void UpdateSlotSelection()
{
foreach (var slot in Slots)
slot.IsSelected = slot.Slot == CurrentSlot;
}
private void UpdateConnectionHint()
{
ShowConnectionHint = !IsConnected;
if (IsConnected)
{
ConnectionHintText = "";
return;
}
ConnectionHintText = StatusText.StartsWith("连接失败", StringComparison.Ordinal)
|| StatusText.StartsWith("未选择", StringComparison.Ordinal)
? StatusText
: "未连接";
}
private static string NormalizeAddress(string value)
{
return FilterWheelAddress.Create(value).Value.ToString();
}
private async Task DisposeDeviceAsync()
{
if (_device == null)
return;
try
{
await _device.DisposeAsync();
}
finally
{
_device = null;
}
}
public async ValueTask DisposeAsync()
{
await DisposeDeviceAsync();
GC.SuppressFinalize(this);
}
}
public partial class ElloSlotViewModel(ElloDeviceViewModel owner, int slot, string alias) : ObservableObject
{
private readonly ElloDeviceViewModel _owner = owner;
public int Slot { get; } = slot;
[ObservableProperty]
public partial string Alias { get; set; } = alias;
[ObservableProperty]
public partial bool ShowInLaserPanel { get; set; } = true;
[ObservableProperty]
public partial bool IsSelected { get; set; }
public string DisplayName => string.IsNullOrWhiteSpace(Alias)
? $"轮{Slot}"
: Alias;
public bool CanMove => _owner.IsConnected && !_owner.IsMoving;
[RelayCommand(CanExecute = nameof(CanMove))]
private Task MoveAsync(CancellationToken cancellationToken)
{
return _owner.MoveToSlotAsync(Slot, cancellationToken);
}
partial void OnAliasChanged(string value)
{
OnPropertyChanged(nameof(DisplayName));
}
internal void RefreshOwnerState()
{
OnPropertyChanged(nameof(CanMove));
MoveCommand.NotifyCanExecuteChanged();
}
}
@@ -17,6 +17,12 @@ public partial class MainWindowViewModel : ObservableObject
[ObservableProperty]
public partial LaserSettingsViewModel LaserSettings { get; set; }
[ObservableProperty]
public partial EasyTtlViewModel EasyTtl { get; set; }
[ObservableProperty]
public partial ElloFilterViewModel Ello { get; set; }
[ObservableProperty]
public partial AcquisitionViewModel Acquisition { get; set; }
@@ -82,6 +88,8 @@ public partial class MainWindowViewModel : ObservableObject
DeviceSettings = new DeviceSettingsViewModel();
LaserSettings = new LaserSettingsViewModel();
EasyTtl = new EasyTtlViewModel(simulateIndex >= 1);
Ello = new ElloFilterViewModel(simulateIndex >= 1);
Acquisition = new AcquisitionViewModel();
PeakDetection = new PeakDetectionViewModel();
Coordinate = new CoordinateViewModel();
@@ -0,0 +1,57 @@
<Window
x:Class="RamanApp.Views.EasyTtlSettingsDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
Title="EasyTTL 设置"
Width="540" Height="500"
x:DataType="vm:EasyTtlViewModel" CanResize="False"
WindowStartupLocation="CenterOwner">
<ScrollViewer Margin="12">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
<ComboBox
Grid.Row="0" Grid.Column="0"
Margin="0,0,0,4" HorizontalAlignment="Stretch"
ItemsSource="{Binding PortNames}" SelectedItem="{Binding SelectedPortName}"/>
<Button Grid.Row="0" Grid.Column="1" Margin="6,0,0,4" Command="{Binding RefreshPortsCommand}" Content="刷新"/>
<UniformGrid Grid.Row="1" Grid.ColumnSpan="2" Columns="2">
<Button Margin="0,0,3,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Command="{Binding ConnectCommand}" Content="连接"/>
<Button Margin="3,0,0,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Command="{Binding DisconnectCommand}" Content="断开"/>
</UniformGrid>
</Grid>
<Grid Margin="0,4,0,0" ColumnDefinitions="*,Auto">
<TextBlock VerticalAlignment="Center" Text="使能输出"/>
<ToggleSwitch Grid.Column="1" IsChecked="{Binding OutputEnabled}" IsEnabled="{Binding IsConnected}" OffContent="OFF" OnContent="ON"/>
</Grid>
<Separator/>
<TextBlock FontWeight="SemiBold" Text="通道配置"/>
<ItemsControl ItemsSource="{Binding Channels}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:EasyTtlChannelViewModel">
<Border Padding="0,5" BorderBrush="{DynamicResource SemiColorBorder}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="36,*,Auto,Auto,Auto,Auto">
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontWeight="SemiBold" Text="{Binding Channel, StringFormat=CH{0}}"/>
<TextBox Grid.Column="1" Margin="0,0,8,0" Text="{Binding Alias}" Watermark="通道别名"/>
<TextBlock Grid.Column="2" Margin="0,0,6,0" VerticalAlignment="Center" Foreground="Gray" Text="面板"/>
<ToggleSwitch Grid.Column="3" Margin="0,0,10,0" IsChecked="{Binding ShowInLaserPanel}" OffContent="OFF" OnContent="ON"/>
<TextBlock Grid.Column="4" Margin="0,0,6,0" VerticalAlignment="Center" Foreground="Gray" Text="硬件"/>
<ToggleSwitch Grid.Column="5" IsChecked="{Binding HardwareEnabled}" IsEnabled="{Binding $parent[Window].DataContext.IsConnected}" OffContent="OFF" OnContent="ON"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock FontSize="11" Foreground="Gray" Text="{Binding StatusText}" TextWrapping="Wrap"/>
</StackPanel>
</ScrollViewer>
</Window>
@@ -0,0 +1,10 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class EasyTtlSettingsDialog : Window
{
public EasyTtlSettingsDialog()
{
InitializeComponent();
}
}
+26
View File
@@ -0,0 +1,26 @@
<UserControl
x:Class="RamanApp.Views.EasyTtlView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:DataType="vm:EasyTtlViewModel">
<StackPanel Spacing="8">
<TextBlock Text="激光通道" FontWeight="SemiBold" />
<ItemsControl ItemsSource="{Binding Channels}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:EasyTtlChannelViewModel">
<Grid ColumnDefinitions="*,Auto" Margin="0,2"
IsVisible="{Binding ShowInLaserPanel}">
<TextBlock Text="{Binding DisplayName}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1" IsChecked="{Binding IsEnabled}"
OnContent="ON" OffContent="OFF" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>
+11
View File
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class EasyTtlView : UserControl
{
public EasyTtlView()
{
InitializeComponent();
}
}
+89
View File
@@ -0,0 +1,89 @@
<UserControl
x:Class="RamanApp.Views.ElloFilterView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
x:DataType="vm:ElloFilterViewModel">
<UserControl.Styles>
<Style Selector="Border.ello-slot">
<Setter Property="Background" Value="{DynamicResource SemiColorFillTertiary}" />
<Setter Property="BorderBrush" Value="{DynamicResource SemiColorBorder}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="8,10" />
<Setter Property="MinHeight" Value="54" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="ItemsControl.ello-slot-list ContentPresenter">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="Grid.ello-slot-hit">
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="Grid.ello-slot-hit:pointerover Border.ello-slot">
<Setter Property="Background" Value="{DynamicResource SemiColorFillSecondary}" />
</Style>
<Style Selector="Border.ello-slot.selected">
<Setter Property="Background" Value="#2F6F62" />
<Setter Property="BorderBrush" Value="#2F6F62" />
</Style>
<Style Selector="Border.ello-slot.selected TextBlock">
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Grid.ello-slot-hit:disabled Border.ello-slot">
<Setter Property="Opacity" Value="0.48" />
</Style>
</UserControl.Styles>
<StackPanel Spacing="8">
<ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ElloDeviceViewModel">
<StackPanel Spacing="8" Margin="0,0,0,14"
IsVisible="{Binding ShowInLaserPanel}">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
Text="{Binding ConnectionHintText}"
IsVisible="{Binding ShowConnectionHint}"
FontSize="10"
Foreground="#C62828"
VerticalAlignment="Center" />
</Grid>
<ItemsControl Classes="ello-slot-list" ItemsSource="{Binding PanelSlots}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" RowSpacing="6" ColumnSpacing="6" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ElloSlotViewModel">
<Grid Classes="ello-slot-hit"
IsEnabled="{Binding CanMove}"
Tapped="OnSlotTapped">
<Border Classes="ello-slot"
Classes.selected="{Binding IsSelected}"
IsHitTestVisible="False">
<TextBlock Text="{Binding DisplayName}"
FontWeight="SemiBold"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
TextWrapping="Wrap" />
</Border>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>
@@ -0,0 +1,24 @@
using Avalonia.Controls;
using Avalonia.Input;
using RamanApp.ViewModels;
namespace RamanApp.Views;
public partial class ElloFilterView : UserControl
{
public ElloFilterView()
{
InitializeComponent();
}
private void OnSlotTapped(object? sender, TappedEventArgs e)
{
if (sender is not Control { DataContext: ElloSlotViewModel slot })
return;
if (!slot.CanMove || !slot.MoveCommand.CanExecute(null))
return;
slot.MoveCommand.Execute(null);
e.Handled = true;
}
}
@@ -0,0 +1,88 @@
<Window
x:Class="RamanApp.Views.ElloSettingsDialog"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:RamanApp.ViewModels"
Title="ELLO 设置"
Width="620" Height="560"
x:DataType="vm:ElloFilterViewModel" CanResize="False"
WindowStartupLocation="CenterOwner">
<ScrollViewer Margin="12">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="滤光轮设备" FontWeight="SemiBold" VerticalAlignment="Center" />
<Button Grid.Column="1" Command="{Binding AddDeviceCommand}" Content="添加 ELLO" />
</Grid>
<ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ElloDeviceViewModel">
<Border Padding="0,0,0,10" Margin="0,0,0,10"
BorderBrush="{DynamicResource SemiColorBorder}"
BorderThickness="0,0,0,1">
<StackPanel Spacing="8">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Grid.Column="0" Text="名称" VerticalAlignment="Center" Margin="0,0,6,0" />
<TextBox Grid.Column="1" Text="{Binding Name}" />
<ToggleSwitch Grid.Column="2" Margin="8,0,8,0"
IsChecked="{Binding ShowInLaserPanel}"
OffContent="面板OFF" OnContent="面板ON" />
<Button Grid.Column="3"
Command="{Binding $parent[Window].DataContext.RemoveDeviceCommand}"
CommandParameter="{Binding}"
Content="移除" />
</Grid>
<Grid ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto">
<ComboBox Grid.Column="0"
ItemsSource="{Binding PortNames}"
SelectedItem="{Binding SelectedPortName}" />
<Button Grid.Column="1" Margin="6,0,8,0"
Command="{Binding RefreshPortsCommand}" Content="刷新" />
<TextBlock Grid.Column="2" Text="地址" VerticalAlignment="Center" Margin="0,0,6,0" />
<TextBox Grid.Column="3" Width="48" Text="{Binding Address}" IsEnabled="{Binding !IsConnected}" />
<TextBlock Grid.Column="4" Text="槽数" VerticalAlignment="Center" Margin="8,0,6,0" />
<ComboBox Grid.Column="5" Width="70"
ItemsSource="{Binding SlotCountOptions}"
SelectedItem="{Binding SlotCount}"
IsEnabled="{Binding !IsConnected}" />
</Grid>
<UniformGrid Columns="2">
<Button Margin="0,0,3,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Command="{Binding ConnectCommand}" Content="连接" />
<Button Margin="3,0,0,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Command="{Binding DisconnectCommand}" Content="断开" />
</UniformGrid>
<ItemsControl ItemsSource="{Binding Slots}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ElloSlotViewModel">
<Grid ColumnDefinitions="36,*,Auto,Auto,Auto" Margin="0,2">
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontWeight="SemiBold"
Text="{Binding Slot, StringFormat=轮{0}}" />
<TextBox Grid.Column="1" Margin="0,0,8,0"
Text="{Binding Alias}" Watermark="轮位别名" />
<TextBlock Grid.Column="2" Margin="0,0,6,0" VerticalAlignment="Center"
Foreground="Gray" Text="面板" />
<ToggleSwitch Grid.Column="3" Margin="0,0,10,0"
IsChecked="{Binding ShowInLaserPanel}"
OffContent="OFF" OnContent="ON" />
<Button Grid.Column="4" Padding="10,3"
Command="{Binding MoveCommand}"
Content="切换" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock FontSize="11" Foreground="Gray" Text="{Binding StatusText}" TextWrapping="Wrap" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Window>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace RamanApp.Views;
public partial class ElloSettingsDialog : Window
{
public ElloSettingsDialog()
{
InitializeComponent();
}
}
@@ -1,34 +1,37 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Roman.Host.Filters.Contracts;
namespace Roman.Host.Filters.Pipe;
public static class PipeJsonSerializer
{
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
{
WriteIndented = false
};
public static string Serialize(FilterRequest request)
{
return JsonSerializer.Serialize(request, Options);
return JsonSerializer.Serialize(request, PipeJsonContext.Default.FilterRequest);
}
public static string Serialize(FilterResponse response)
{
return JsonSerializer.Serialize(response, Options);
return JsonSerializer.Serialize(response, PipeJsonContext.Default.FilterResponse);
}
public static FilterRequest DeserializeRequest(string json)
{
return JsonSerializer.Deserialize<FilterRequest>(json, Options)
return JsonSerializer.Deserialize(json, PipeJsonContext.Default.FilterRequest)
?? throw new InvalidOperationException("Pipe request JSON was empty.");
}
public static FilterResponse DeserializeResponse(string json)
{
return JsonSerializer.Deserialize<FilterResponse>(json, Options)
return JsonSerializer.Deserialize(json, PipeJsonContext.Default.FilterResponse)
?? throw new InvalidOperationException("Pipe response JSON was empty.");
}
}
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = false)]
[JsonSerializable(typeof(FilterRequest))]
[JsonSerializable(typeof(FilterResponse))]
internal sealed partial class PipeJsonContext : JsonSerializerContext;
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>