添加项目文件。

This commit is contained in:
Creshare
2025-12-17 09:57:43 +08:00
parent 037868c473
commit 985b286917
17 changed files with 5543 additions and 0 deletions

3
OneClick.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="OneClick/OneClick.csproj" />
</Solution>

8
OneClick/App.xaml Normal file
View File

@@ -0,0 +1,8 @@
<Application
x:Class="OneClick.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:OneClick"
StartupUri="MainWindow.xaml">
<Application.Resources />
</Application>

16
OneClick/App.xaml.cs Normal file
View File

@@ -0,0 +1,16 @@
using Prism.Ioc;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Windows;
namespace OneClick
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

10
OneClick/AssemblyInfo.cs Normal file
View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

344
OneClick/AxisService.cs Normal file
View File

@@ -0,0 +1,344 @@
using GTN;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace OneClick
{
public class AxisService
{
public void InitMotionCard()
{
// 初始化运动控制卡
int sRtn = -1;
short eCATStatus = -1;
short core = 1; // 使用第一个EtherCAT主站
try
{
sRtn = GTN.mc.GTN_Open(5, 2);
//MessageBox.Show($"运动控制卡初始化成功,返回代码{sRtn}", "信息", MessageBoxButton.OK, MessageBoxImage.Information);
sRtn = GTN.mc.GTN_InitEcatComm(core);
int waitcount = 0;
do
{
sRtn = GTN.mc.GTN_IsEcatReady(core, out eCATStatus);
//waitcount++;
if (waitcount > 50000)
{
waitcount = 0;
MessageBox.Show("运动控制卡以太网通信初始化失败,请检查连接!", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
} while (eCATStatus != 1 || sRtn != 0);
sRtn = GTN.mc.GTN_StartEcatComm(core);
}
catch (Exception ex)
{
MessageBox.Show($"运动控制卡初始化失败:{ex.Message},错误代码{sRtn}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 上使能
/// </summary>
/// <param name="selectedindex">轴号</param>
/// <param name="core">核号</param>
/// returns>状态码</returns>
public int AxisOn(short selectedindex, short core)
{
int sRtn = -1;
sRtn = mc.GTN_ClrSts(core, (short)(selectedindex), 1);
sRtn = GTN.mc.GTN_AxisOn(core, selectedindex);
return sRtn;
}
/// <summary>
/// 下使能
/// </summary>
/// <param name="selectedindex">轴号</param>
/// <param name="core">核号</param>
/// <returns>状态码</returns>
public int AxisOff(short selectedindex, short core)
{
int sRtn = -1;
sRtn = GTN.mc.GTN_AxisOff(core, selectedindex);
return sRtn;
}
/// <summary>
/// 重置报警
/// </summary>
/// <param name="selectedindex">轴号</param>
/// <param name="core">核号</param>
/// <returns></returns>
public int ResetAlarm(short selectedindex, short core)
{
int sRtn = -1;
sRtn = mc.GTN_ClrSts(core, (short)(selectedindex), 1);
return sRtn;
}
/// <summary>
/// 重置位置
/// </summary>
/// <param name="selectedindex">轴号</param>
/// <param name="core">核号</param>
/// <returns></returns>
public int ResetPosition(short selectedindex, short core)
{
int sRtn = -1;
sRtn = mc.GTN_ZeroPos(core, selectedindex, 1);
return sRtn;
}
/// <summary>
/// 读取轴的状态
/// </summary>
/// <param name="core">核号</param>
/// <param name="axis">轴号</param>
/// <param name="AxisSts">轴状态</param>
/// <returns></returns>
public int ReadAxisStatus(short core, short axis, out string AxisSts)
{
int sRtn = -1;
Int32 psts;
UInt32 clock;
AxisSts = "";
// 读取轴状态
sRtn = mc.GTN_GetSts(core, axis, out psts, 1, out clock);
if ((psts & 0x2) != 0)
{
AxisSts += "报警\r\n";
}
if ((psts & 0x200) != 0)
{
AxisSts += "使能\r\n";
}
if ((psts & 0x40) != 0)
{
AxisSts += "负限位触发\r\n";
}
if ((psts & 0x20) != 0)
{
AxisSts += "正限位触发\r\n";
}
if ((psts & 0x400) != 0)
{
AxisSts += "正在移动\r\n";
}
if ((psts & 0x800) != 0)
{
AxisSts += "到位\r\n";
}
if ((psts & 0x10) != 0)
{
AxisSts += "跟随误差超限\r\n";
}
if ((psts & 0x80) != 0)
{
AxisSts += "平滑停止IO触发\r\n";
}
if ((psts & 0x100) != 0)
{
AxisSts += "急停触发\r\n";
}
return sRtn;
}
/// <summary>
/// Jog开始
/// </summary>
/// <param name="core">核号</param>
/// <param name="axis">轴号</param>
/// <returns></returns>
public int JogMove(short core, short axis,int rpm)
{
int sRtn = -1;
double speed;
Int32 mask;
// 设置轴为点动模式
sRtn = mc.GTN_PrfJog(core, axis);
if (sRtn != mc.CMD_SUCCESS) return sRtn;
// 将参数写死
mc.TJogPrm jog = new mc.TJogPrm
{
acc = 10, // 加速度
dec = 10, // 减速度
smooth = 0.0 // 平滑时间
};
speed = rpm*0.0667;
// 传入结构体引用
sRtn = mc.GTN_SetJogPrm(core, axis, ref jog);
if (sRtn != mc.CMD_SUCCESS) return sRtn;
// 设置点动速度
sRtn = mc.GTN_SetVel(1, axis, speed);
// 开始点动
mask= (int)AxisAndCoreToMask(core, axis);
sRtn =mc.GTN_Update(core,mask);
return sRtn;
}
/// <summary>
/// 停止运动
/// </summary>
/// <param name="core">核号</param>
/// <param name="axis">轴号</param>
/// <returns></returns>
public int AxisStop(short core, short axis)
{
int sRtn = -1;
Int32 mask;
Int32 option=0;
// 停止点动
mask = (int)AxisAndCoreToMask(core, axis);
sRtn = mc.GTN_Stop(core, mask,option);
return sRtn;
}
/// <summary>
/// 定时旋转
/// </summary>
/// <param name="core">核号</param>
/// <param name="axis">轴号</param>
/// <param name="rpm">速度</param>
/// <param name="time">时间</param>
/// <returns></returns>
public int TimelyRotate(short core,short axis,int rpm,int time)
{
int sRtn = -1;
sRtn = mc.GTN_PrfTrap(core, axis);
mc.TTrapPrm trap = new mc.TTrapPrm
{
acc = 10, // 加速度
dec = 10, // 减速度
velStart = 0.0, // 平滑时间
smoothTime = 10
};
sRtn = mc.GTN_SetTrapPrm(core, axis, ref trap);
sRtn=mc.GTN_SetPos(core, axis, (int)((double)rpm * 0.0667*time*1000));
sRtn=mc.GTN_SetVel(core,axis, (double)rpm*0.0667);
sRtn=mc.GTN_Update(core, (int)AxisAndCoreToMask(core, axis));
return sRtn;
}
/// <summary>
/// 关闭控制卡
/// </summary>
/// <param name="core">核号</param>
public void CloseMotionCard(short core)
{
// 关闭运动控制卡
int sRtn = -1;
try
{
sRtn = mc.GTN_TerminateEcatComm(core);
sRtn = GTN.mc.GTN_Close();
//MessageBox.Show($"运动控制卡关闭成功,返回代码{sRtn}", "信息", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"运动控制卡关闭失败:{ex.Message},错误代码{sRtn}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 将给定的 core(核号) 与 axis(轴号) 转换为 32 位掩码 (mask)。
/// 规则:
/// - core=1 映射轴 1..32 到 mask 的位 0..31轴1 -> bit0轴32 -> bit31
/// - core=2 映射轴 33..64 到 mask 的位 0..31轴33 -> bit0轴64 -> bit31
/// 返回值为 UInt32 掩码;当输入不合法时抛出 <see cref="ArgumentOutOfRangeException"/>。
/// </summary>
/// <param name="core">核号(目前支持 1 或 2</param>
/// <param name="axis">轴号1..64</param>
/// <returns>对应的 32 位掩码</returns>
public static uint AxisAndCoreToMask(short core, short axis)
{
if (core < 1 || core > 2)
{
throw new ArgumentOutOfRangeException(nameof(core), "目前只支持 core=1 或 core=2。");
}
int baseAxis = (core - 1) * 32; // core=1 -> 0, core=2 -> 32
if (axis <= baseAxis || axis > baseAxis + 32)
{
throw new ArgumentOutOfRangeException(nameof(axis), $"轴号不在 core {core} 的有效范围 ({baseAxis + 1}..{baseAxis + 32})。");
}
int bitIndex = axis - baseAxis - 1; // 0..31
return 1u << bitIndex;
}
/// <summary>
/// 将 32 位掩码转换为该 core 下的轴列表。
/// 规则同 <see cref="AxisAndCoreToMask"/>。
/// 返回的轴号为全局轴号1..64)。
/// </summary>
/// <param name="core">核号1 或 2</param>
/// <param name="mask">32 位掩码bit0 对应该 core 的最低号轴</param>
/// <returns>被选中轴的列表按升序掩码为0时返回空列表。</returns>
public static List<short> MaskToAxisList(short core, uint mask)
{
var axes = new List<short>();
if (core < 1 || core > 2)
{
throw new ArgumentOutOfRangeException(nameof(core), "目前只支持 core=1 或 core=2。");
}
int baseAxis = (core - 1) * 32; // core=1 -> 0, core=2 -> 32
for (int bit = 0; bit < 32; bit++)
{
if ((mask & (1u << bit)) != 0)
{
short axis = (short)(baseAxis + bit + 1);
axes.Add(axis);
}
}
return axes;
}
/// <summary>
/// 尝试将 axis 和 core 转换为 mask转换成功返回 true 并输出 mask失败返回 false 并输出 0。
/// 该方法便于在不抛出异常的情况下做验证。
/// </summary>
/// <param name="core">核号</param>
/// <param name="axis">轴号</param>
/// <param name="mask">输出掩码</param>
/// <returns>是否成功</returns>
public static bool TryAxisAndCoreToMask(short core, short axis, out uint mask)
{
mask = 0;
if (core < 1 || core > 2) return false;
int baseAxis = (core - 1) * 32;
if (axis <= baseAxis || axis > baseAxis + 32) return false;
int bitIndex = axis - baseAxis - 1;
mask = 1u << bitIndex;
return true;
}
}
}

171
OneClick/CameraService.cs Normal file
View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using Basler.Pylon;
namespace OneClick
{
public interface ICameraService
{
IList<string> EnumerateDevices();
void Open(string deviceId);
void Close();
bool StartGrabbing(bool continuous = true);
void StopGrabbing();
IGrabResult? GrabOnePic(int timeoutMs = 2000);
string? CurrentDeviceId { get; }
event EventHandler? GrabStarted;
event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
event EventHandler<GrabStopEventArgs>? GrabStopped;
event EventHandler? ConnectionLost;
}
public class BaslerCameraService : ICameraService
{
private Camera? _camera;
private readonly PixelDataConverter _converter = new PixelDataConverter();
public string? CurrentDeviceId { get; private set; }
public event EventHandler? GrabStarted;
public event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
public event EventHandler<GrabStopEventArgs>? GrabStopped;
public event EventHandler? ConnectionLost;
public IList<string> EnumerateDevices()
{
var devices = CameraFinder.Enumerate();
var ids = new List<string>();
foreach (var info in devices)
{
if (info.ContainsKey(CameraInfoKey.SerialNumber))
{
ids.Add(info[CameraInfoKey.SerialNumber]);
}
}
return ids;
}
public void Open(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
throw new ArgumentException("<22>豸IDΪ<44>ա<EFBFBD>", nameof(deviceId));
Close();
_camera = new Camera(deviceId);
// ע<><D7A2><EFBFBD>¼<EFBFBD>
_camera.ConnectionLost += Camera_ConnectionLost;
_camera.StreamGrabber.GrabStarted += StreamGrabber_GrabStarted;
_camera.StreamGrabber.ImageGrabbed += StreamGrabber_ImageGrabbed;
_camera.StreamGrabber.GrabStopped += StreamGrabber_GrabStopped;
_camera.Open();
CurrentDeviceId = deviceId;
}
public void Close()
{
try
{
if (_camera != null)
{
if (_camera.StreamGrabber.IsGrabbing)
_camera.StreamGrabber.Stop();
}
}
catch
{
// ֹͣʧ<D6B9>ܲ<EFBFBD>Ӱ<EFBFBD><D3B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD>
}
try
{
if (_camera != null)
{
// ע<><D7A2><EFBFBD>¼<EFBFBD>
_camera.ConnectionLost -= Camera_ConnectionLost;
_camera.StreamGrabber.GrabStarted -= StreamGrabber_GrabStarted;
_camera.StreamGrabber.ImageGrabbed -= StreamGrabber_ImageGrabbed;
_camera.StreamGrabber.GrabStopped -= StreamGrabber_GrabStopped;
_camera.Close();
_camera.Dispose();
_camera = null;
}
}
finally
{
CurrentDeviceId = null;
}
}
public bool StartGrabbing(bool continuous = true)
{
if (_camera == null)
throw new InvalidOperationException("<22><><EFBFBD><EFBFBD>δ<EFBFBD>򿪡<EFBFBD>");
try
{
if (continuous)
{
Configuration.AcquireContinuous(_camera, null);
_camera.StreamGrabber.Start(GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber);
}
else
{
Configuration.AcquireSingleFrame(_camera, null);
_camera.StreamGrabber.Start(1, GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber);
}
return true;
}
catch
{
return false;
}
}
public void StopGrabbing()
{
if (_camera == null) return;
try
{
_camera.StreamGrabber.Stop();
}
catch
{
// <20><><EFBFBD><EFBFBD>ֹͣ<CDA3>
}
}
public IGrabResult? GrabOnePic(int timeoutMs = 2000)
{
if (_camera == null)
return null;
return _camera.StreamGrabber.GrabOne(timeoutMs);
}
private void Camera_ConnectionLost(object? sender, EventArgs e)
{
ConnectionLost?.Invoke(this, EventArgs.Empty);
}
private void StreamGrabber_GrabStarted(object? sender, EventArgs e)
{
GrabStarted?.Invoke(this, EventArgs.Empty);
}
private void StreamGrabber_ImageGrabbed(object? sender, ImageGrabbedEventArgs e)
{
// ֱ<><D6B1>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> UI <20><><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
ImageGrabbed?.Invoke(this, e);
}
private void StreamGrabber_GrabStopped(object? sender, GrabStopEventArgs e)
{
GrabStopped?.Invoke(this, e);
}
}
}

420
OneClick/ChangeSetting.xaml Normal file
View File

@@ -0,0 +1,420 @@
<Window
x:Class="OneClick.ChangeSetting"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:OneClick"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="更改设置"
Width="1000"
Height="600"
MinWidth="600"
MinHeight="400"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid Margin="12">
<Grid.RowDefinitions>
<!-- 顶部:配方选择 -->
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<GroupBox
Grid.Row="0"
Margin="0,0,0,12"
Header="配方选择">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label
Grid.Column="0"
VerticalAlignment="Center"
Content="选择配方:" />
<ComboBox
x:Name="RecipeComboBox"
Grid.Column="2"
MinWidth="220"
IsEditable="False" />
<Button
x:Name="AddRecipeButton"
Grid.Column="4"
Padding="12,4"
Content="添加配方" />
<Button
x:Name="DeleteRecipeButton"
Grid.Column="6"
Padding="12,4"
Content="删除配方" />
</Grid>
</GroupBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 左侧:相机/拍照设置 + 光源控制(光源移动到左侧) -->
<StackPanel
Grid.Column="0"
Margin="0,0,8,0"
VerticalAlignment="Stretch"
Orientation="Vertical">
<GroupBox Margin="0,0,0,8" Header="相机与拍照设置">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 拍照间隔(毫秒) -->
<Label
Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"
Content="拍照间隔:" />
<TextBox
x:Name="CaptureIntervalTextBox"
Grid.Row="2"
Grid.Column="2"
MinWidth="120"
HorizontalAlignment="Left"
VerticalAlignment="Center"
ToolTip="输入拍照间隔(毫秒)" />
<Label
Grid.Row="2"
Grid.Column="4"
VerticalAlignment="Center"
Content="毫秒" />
<!-- 照片保存位置 + 选择文件夹 -->
<Label
Grid.Row="4"
Grid.Column="0"
VerticalAlignment="Center"
Content="保存位置:" />
<TextBox
x:Name="SaveFolderTextBox"
Grid.Row="4"
Grid.Column="2"
MinWidth="100"
Margin="0,0,0,0"
VerticalAlignment="Center" />
<Button
x:Name="BrowseFolderButton"
Grid.Row="4"
Grid.Column="4"
Padding="12,4"
Content="选择..." />
</Grid>
</GroupBox>
<!-- 光源控制:已移到左侧 -->
<GroupBox Margin="0,0,0,8" Header="光源控制">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<CheckBox
x:Name="LightEnableCheckBox"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
VerticalAlignment="Center"
Content="启用光源(暂时无效)" />
<Label
Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"
Content="亮度:" />
<Slider
x:Name="LightIntensitySlider"
Grid.Row="2"
Grid.Column="2"
IsSnapToTickEnabled="False"
Maximum="100"
Minimum="0"
TickFrequency="10"
ToolTip="调整光源亮度(百分比)"
Value="50" />
<StackPanel
Grid.Row="4"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Left"
Orientation="Horizontal">
<Label VerticalAlignment="Center" Content="模式:" />
<ComboBox x:Name="LightModeComboBox" MinWidth="140">
<ComboBoxItem Content="持续" />
<ComboBoxItem Content="脉冲" />
<ComboBoxItem Content="同步触发" />
</ComboBox>
<!-- 测试光源链接按钮 -->
<Button
x:Name="TestLightButton"
Width="110"
Margin="8,0,0,0"
Padding="8,4"
VerticalAlignment="Center"
Click="TestLightButton_Click"
Content="测试光源链接" />
</StackPanel>
<!-- 光源指令输入行 -->
<StackPanel
Grid.Row="6"
Grid.Column="0"
Grid.ColumnSpan="3"
HorizontalAlignment="Left"
Orientation="Horizontal">
<Label VerticalAlignment="Center" Content="指令:" />
<TextBox
x:Name="LightCommandTextBox"
MinWidth="150"
VerticalAlignment="Center"
ToolTip="在此输入要发送给光源控制器的原始指令(例如 SA0100#" />
<Button
x:Name="SendLightCommandButton"
Width="90"
Margin="8,0,0,0"
Padding="8,4"
VerticalAlignment="Center"
Click="SendLightCommandButton_Click"
Content="发送指令" />
</StackPanel>
</Grid>
</GroupBox>
<!-- 保持左侧与右侧高度一致的占位(可留空) -->
<Border Height="8" Background="Transparent" />
</StackPanel>
<!-- 右侧:仅电机参数和控制(移除位置/应用/回原点新增使能、清警、Jog按钮 -->
<GroupBox
Grid.Column="1"
Margin="8,0,0,0"
Header="电机参数和控制">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- 转速行 (0) -->
<RowDefinition Height="8" /> <!-- 1 -->
<RowDefinition Height="Auto" />
<!-- 转动时间行 (2) -->
<RowDefinition Height="8" /> <!-- 3 -->
<RowDefinition Height="Auto" />
<!-- 轴选择行 (4) -->
<RowDefinition Height="8" /> <!-- 5 -->
<RowDefinition Height="Auto" />
<!-- 滑块行 (6) -->
<RowDefinition Height="12" /> <!-- 7 -->
<RowDefinition Height="Auto" />
<!-- 使能/去使能/清警 (8) -->
<RowDefinition Height="8" /> <!-- 9 -->
<RowDefinition Height="Auto" />
<!-- Jog 按钮 (10) -->
<RowDefinition Height="8" /> <!-- 11 -->
<RowDefinition Height="Auto" />
<!-- 开始停止旋转(保留) (12) -->
<RowDefinition Height="*" /> <!-- 13 -->
</Grid.RowDefinitions>
<Label
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Content="转速:" />
<TextBox
x:Name="MotorSpeedTextBox"
Grid.Row="0"
Grid.Column="2"
MinWidth="120"
VerticalAlignment="Center"
ToolTip="输入电机转速RPM" />
<Label
Grid.Row="0"
Grid.Column="4"
VerticalAlignment="Center"
Content="RPM" />
<!-- 新增:转动时间输入(毫秒) -->
<Label
Grid.Row="2"
Grid.Column="0"
VerticalAlignment="Center"
Content="转动时间:" />
<TextBox
x:Name="RotationTimeTextBox"
Grid.Row="2"
Grid.Column="2"
MinWidth="120"
VerticalAlignment="Center"
ToolTip="输入转动时间(秒)" />
<Label
Grid.Row="2"
Grid.Column="4"
VerticalAlignment="Center"
Content="秒" />
<Label
Grid.Row="4"
Grid.Column="0"
VerticalAlignment="Center"
Content="选择轴:" />
<ComboBox
x:Name="AxisComboBox"
Grid.Row="4"
Grid.Column="2"
MinWidth="120"
IsEditable="False"
ToolTip="在此输入或选择轴编号(例如 A1 / 轴1 / 1后台自行处理" />
<Slider
x:Name="MotorSpeedSlider"
Grid.Row="6"
Grid.Column="0"
Grid.ColumnSpan="5"
IsSnapToTickEnabled="False"
Maximum="7000"
Minimum="0"
TickFrequency="500"
ToolTip="滑动以快速调整转速RPM" />
<!-- 新增:选定轴上使能 / 去使能 / 清除警报和位置 -->
<StackPanel
Grid.Row="8"
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="0,6,0,0"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="EnableAxisButton"
Width="130"
Height="28"
Margin="5"
Content="选定轴上使能" Click="EnableAxisButton_Click" />
<Button
x:Name="DisableAxisButton"
Width="130"
Height="28"
Margin="5"
Content="选定轴去使能" Click="DisableAxisButton_Click" />
<Button
x:Name="ClearAlarmPositionButton"
Width="150"
Height="28"
Margin="5"
Content="清除警报和位置" Click="ClearAlarmPositionButton_Click" />
</StackPanel>
<!-- 新增Jog 正/反转(保持按住触发逻辑在后端实现) -->
<StackPanel
Grid.Row="10"
Grid.Column="0"
Grid.ColumnSpan="5"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="JogForwardButton"
Width="140"
Height="36"
Margin="10"
Content="Jog运动正转" MouseDown="JogForwardButton_MouseDown" MouseUp="JogForwardButton_MouseUp" MouseLeftButtonDown="JogForwardButton_MouseLeftButtonDown"/>
<Button
x:Name="JogReverseButton"
Width="140"
Height="36"
Margin="10"
Content="Jog运动反转" />
</StackPanel>
<!-- 保留开始/停止旋转按钮 -->
<StackPanel
Grid.Row="12"
Grid.Column="0"
Grid.ColumnSpan="5"
Margin="0,8,0,0"
HorizontalAlignment="Center"
Orientation="Horizontal">
<Button
x:Name="StartRotationButton"
Width="110"
Height="28"
Margin="0,0,10,0"
Click="StartRotationButton_Click"
Content="开始旋转" />
<Button
x:Name="StopRotationButton"
Width="110"
Height="28"
Click="StopRotationButton_Click"
Content="停止旋转" />
</StackPanel>
</Grid>
</GroupBox>
</Grid>
<!-- 底部操作按钮 -->
<StackPanel
Grid.Row="2"
Margin="0,12,0,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
x:Name="SaveButton"
Margin="0,0,8,0"
Padding="16,6"
Content="保存"
IsDefault="True" />
<Button
x:Name="CancelButton"
Padding="16,6"
Content="取消"
IsCancel="True" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,636 @@
using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace OneClick
{
/// <summary>
/// ChangeSetting.xaml 的交互逻辑
/// </summary>
public partial class ChangeSetting : Window
{
private readonly RecipeManager _recipeManager;
private Recipe? _workingRecipe;
// 新的构造函数:注入外部 RecipeManager 实例
public ChangeSetting(RecipeManager recipeManager)
{
_recipeManager = recipeManager ?? throw new ArgumentNullException(nameof(recipeManager));
InitializeComponent();
// 事件绑定
AddRecipeButton.Click += AddRecipeButton_Click;
DeleteRecipeButton.Click += DeleteRecipeButton_Click;
BrowseFolderButton.Click += BrowseFolderButton_Click;
SaveButton.Click += SaveButton_Click;
RecipeComboBox.SelectionChanged += RecipeComboBox_SelectionChanged;
MotorSpeedSlider.ValueChanged += MotorSpeedSlider_ValueChanged;
LightIntensitySlider.ValueChanged += LightIntensitySlider_ValueChanged;
AxisComboBox.SelectionChanged += AxisComboBox_SelectionChanged;
LoadRecipeNamesToCombo();
InitializeAxisComboItems();
InitializeWorkingRecipe();
LoadWorkingRecipeToUI();
}
private void LoadRecipeNamesToCombo()
{
RecipeComboBox.ItemsSource = _recipeManager.GetNames().OrderBy(x => x).ToList();
if (_recipeManager.CurrentRecipe != null)
{
RecipeComboBox.SelectedItem = _recipeManager.CurrentRecipe.Name;
}
}
private void InitializeAxisComboItems()
{
// 依据实际项目轴数填充,这里示例填充 1-8
AxisComboBox.ItemsSource = Enumerable.Range(1, 8).ToList();
}
private void InitializeWorkingRecipe()
{
if (_recipeManager.CurrentRecipe != null)
{
_workingRecipe = CloneRecipe(_recipeManager.CurrentRecipe);
return;
}
// 若无当前配方,创建一个默认配方
_workingRecipe = new Recipe
{
Name = "Default",
CaptureIntervalMs = 1000,
SaveFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "OneClick"),
MotorSpeed = 1000,
SelectedAxis = 1,
LightIntensity = 50,
MotorRunTimeSec = 10
};
if (!_recipeManager.GetNames().Contains("Default"))
{
_recipeManager.Add(CloneRecipe(_workingRecipe));
_recipeManager.SetCurrent("Default");
LoadRecipeNamesToCombo();
RecipeComboBox.SelectedItem = "Default";
}
}
private void LoadWorkingRecipeToUI()
{
CaptureIntervalTextBox.Text = _workingRecipe?.CaptureIntervalMs?.ToString() ?? string.Empty;
SaveFolderTextBox.Text = _workingRecipe?.SaveFolder ?? string.Empty;
MotorSpeedTextBox.Text = _workingRecipe?.MotorSpeed?.ToString() ?? string.Empty;
MotorSpeedSlider.Value = _workingRecipe?.MotorSpeed.HasValue == true ? _workingRecipe!.MotorSpeed!.Value : 0;
// 光源亮度
LightIntensitySlider.Value = _workingRecipe?.LightIntensity.HasValue == true ? _workingRecipe!.LightIntensity!.Value : 0;
// 选择轴
if (_workingRecipe?.SelectedAxis.HasValue == true)
{
AxisComboBox.SelectedItem = _workingRecipe!.SelectedAxis!.Value;
}
else
{
AxisComboBox.SelectedItem = null;
}
// 转动时间(毫秒)
RotationTimeTextBox.Text = _workingRecipe?.MotorRunTimeSec?.ToString() ?? string.Empty;
}
private static Recipe CloneRecipe(Recipe r)
{
return new Recipe
{
Name = r.Name,
CaptureIntervalMs = r.CaptureIntervalMs,
SaveFolder = r.SaveFolder,
MotorSpeed = r.MotorSpeed,
// 包含新参数
SelectedAxis = r.SelectedAxis,
LightIntensity = r.LightIntensity,
MotorRunTimeSec = r.MotorRunTimeSec
};
}
private void RecipeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var name = RecipeComboBox.SelectedItem as string;
if (string.IsNullOrWhiteSpace(name)) return;
var recipe = _recipeManager.Get(name);
if (recipe == null) return;
_workingRecipe = CloneRecipe(recipe);
LoadWorkingRecipeToUI();
}
private void AddRecipeButton_Click(object? sender, RoutedEventArgs e)
{
// 生成一个建议名称(保持与之前逻辑一致)
var baseName = "NewRecipe";
var idx = 1;
string suggested;
do
{
suggested = $"{baseName}{idx}";
idx++;
} while (_recipeManager.GetNames().Contains(suggested));
// 循环弹出输入框,直到用户取消或输入合法且不重复的名称
while (true)
{
var input = PromptForRecipeName(suggested);
if (input == null)
return; // 用户取消
input = input.Trim();
if (string.IsNullOrWhiteSpace(input))
{
MessageBox.Show(this, "配方名称不能为空,请重新输入。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
suggested = input == string.Empty ? suggested : input;
continue;
}
if (ContainsInvalidFileNameChars(input))
{
MessageBox.Show(this, "配方名称包含非法字符(例如 \\ / : * ? \" < > | 等),请使用其他名称。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
suggested = input;
continue;
}
if (_recipeManager.GetNames().Contains(input))
{
MessageBox.Show(this, "该配方名称已存在,请使用不同的名称。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
suggested = input;
continue;
}
var newRecipe = new Recipe
{
Name = input,
CaptureIntervalMs = 1000,
SaveFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "OneClick"),
MotorSpeed = 1000,
SelectedAxis = 1,
LightIntensity = 50,
MotorRunTimeSec = 1000
};
if (_recipeManager.Add(newRecipe))
{
_recipeManager.SetCurrent(input);
_workingRecipe = CloneRecipe(newRecipe);
LoadRecipeNamesToCombo();
RecipeComboBox.SelectedItem = input;
LoadWorkingRecipeToUI();
}
return;
}
}
// 在内存中弹出一个简单的输入对话框,返回 null 表示取消
private string? PromptForRecipeName(string defaultName)
{
var dlg = new Window
{
Title = "输入配方名称",
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Width = 420,
Height = 150,
ResizeMode = ResizeMode.NoResize,
WindowStyle = WindowStyle.SingleBorderWindow,
ShowInTaskbar = false
};
var panel = new Grid
{
Margin = new Thickness(10)
};
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label = new TextBlock
{
Text = "请输入配方名称:",
Margin = new Thickness(0, 0, 0, 6)
};
Grid.SetRow(label, 0);
panel.Children.Add(label);
var textBox = new TextBox
{
Text = defaultName ?? string.Empty,
MinWidth = 360
};
Grid.SetRow(textBox, 1);
panel.Children.Add(textBox);
var btnPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 8, 0, 0)
};
var okBtn = new Button
{
Content = "确定",
Width = 80,
IsDefault = true,
Margin = new Thickness(0, 0, 8, 0)
};
var cancelBtn = new Button
{
Content = "取消",
Width = 80,
IsCancel = true
};
btnPanel.Children.Add(okBtn);
btnPanel.Children.Add(cancelBtn);
Grid.SetRow(btnPanel, 2);
panel.Children.Add(btnPanel);
dlg.Content = panel;
okBtn.Click += (_, _) =>
{
dlg.DialogResult = true;
dlg.Close();
};
cancelBtn.Click += (_, _) =>
{
dlg.DialogResult = false;
dlg.Close();
};
// 聚焦并选择全部文本,便于用户直接输入
dlg.Loaded += (_, _) =>
{
textBox.Focus();
textBox.SelectAll();
};
var result = dlg.ShowDialog();
if (result == true)
return textBox.Text;
return null;
}
private static bool ContainsInvalidFileNameChars(string name)
{
var invalid = Path.GetInvalidFileNameChars();
return name.IndexOfAny(invalid) >= 0;
}
private void DeleteRecipeButton_Click(object? sender, RoutedEventArgs e)
{
var name = RecipeComboBox.SelectedItem as string;
if (string.IsNullOrWhiteSpace(name)) return;
if (_recipeManager.Remove(name))
{
LoadRecipeNamesToCombo();
// 切到任意一个剩余配方
var next = _recipeManager.GetNames().OrderBy(x => x).FirstOrDefault();
if (next != null)
{
_recipeManager.SetCurrent(next);
_workingRecipe = CloneRecipe(_recipeManager.Get(next)!);
RecipeComboBox.SelectedItem = next;
}
else
{
// 没有配方,创建默认
InitializeWorkingRecipe();
LoadRecipeNamesToCombo();
}
LoadWorkingRecipeToUI();
}
}
private void BrowseFolderButton_Click(object? sender, RoutedEventArgs e)
{
var dlg = new OpenFolderDialog
{
Title = "选择图片保存位置"
};
// 根据当前文本框内容设定初始目录
var current = SaveFolderTextBox.Text;
if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current))
{
dlg.InitialDirectory = current;
}
var result = dlg.ShowDialog(this);
if (result == true)
{
SaveFolderTextBox.Text = dlg.FolderName;
}
}
private void SaveButton_Click(object? sender, RoutedEventArgs e)
{
// 将 UI 的值写回工作配方
if (string.IsNullOrWhiteSpace(_workingRecipe?.Name))
{
MessageBox.Show("配方名称不能为空。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (int.TryParse(CaptureIntervalTextBox.Text, out var interval))
_workingRecipe!.CaptureIntervalMs = interval;
else
_workingRecipe!.CaptureIntervalMs = null;
var folder = SaveFolderTextBox.Text;
_workingRecipe!.SaveFolder = string.IsNullOrWhiteSpace(folder) ? null : folder;
if (double.TryParse(MotorSpeedTextBox.Text, out var rpm))
_workingRecipe!.MotorSpeed = rpm;
else
_workingRecipe!.MotorSpeed = null;
// 新增:保存光源亮度和选择轴号
_workingRecipe!.LightIntensity = (int)Math.Round(LightIntensitySlider.Value);
var selectedAxisObj = AxisComboBox.SelectedItem;
if (selectedAxisObj is int axisNum)
{
_workingRecipe!.SelectedAxis = axisNum;
}
else
{
_workingRecipe!.SelectedAxis = null;
}
// 新增:保存转动时间(秒)
if (int.TryParse(RotationTimeTextBox.Text, out var rotMs))
_workingRecipe!.MotorRunTimeSec = rotMs;
else
_workingRecipe!.MotorRunTimeSec = null;
// 写入 RecipeManager 并设为当前
var exists = _recipeManager.Get(_workingRecipe!.Name!) != null;
if (exists)
_recipeManager.Update(CloneRecipe(_workingRecipe!));
else
_recipeManager.Add(CloneRecipe(_workingRecipe!));
_recipeManager.SetCurrent(_workingRecipe!.Name!);
_recipeManager.Save();
// 关闭并返回主界面
DialogResult = true;
Close();
}
private void MotorSpeedSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
MotorSpeedTextBox.Text = ((int)e.NewValue).ToString();
}
private void LightIntensitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
// 若需要在界面显示当前亮度值,可在此将值显示到 ToolTip 或状态文本
// 保留为轻量处理:无需文本框,值直接在保存时取用
}
private void AxisComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// 轻量:选择变更时暂不立即写回,统一在保存时处理
}
private void TestLightButton_Click(object sender, RoutedEventArgs e)
{
var lightService = new LightSourceService();
lightService.TestSourceService();
}
private void SendLightCommandButton_Click(object sender, RoutedEventArgs e)
{
var cmd = LightCommandTextBox.Text?.Trim();
if (string.IsNullOrEmpty(cmd))
{
MessageBox.Show(this, "请输入要发送的光源指令。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
LightCommandTextBox.Focus();
return;
}
var lightService = new LightSourceService();
try
{
var response = lightService.SendCommand(cmd);
if (!string.IsNullOrEmpty(response))
{
MessageBox.Show(this, "收到响应: " + response, "光源响应", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show(this, "未收到响应或响应为空。", "光源响应", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "发送命令时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// 新增的占位事件处理器:开始旋转(后端由你实现)
private void StartRotationButton_Click(object sender, RoutedEventArgs e)
{
var axisService = new AxisService();
int axisNum = 1;
int speed = (int)MotorSpeedSlider.Value;
int time = 0;
var sel = AxisComboBox.SelectedItem;
if (sel is int i)
{
axisNum = i;
}
else if (sel is string s && int.TryParse(s, out var parsed))
{
axisNum = parsed;
}
// 解析 RotationTimeTextBox.Text 为 int
if (!int.TryParse(RotationTimeTextBox.Text, out time))
{
MessageBox.Show(this, "转动时间输入无效,已默认为 0。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
time = 0;
}
axisService.TimelyRotate(1, (short)axisNum, speed, time);
//清除位置
//axisService.ResetPosition((short)axisNum, 1);
}
// 停止旋转
private void StopRotationButton_Click(object sender, RoutedEventArgs e)
{
var sel = AxisComboBox.SelectedItem;
var axisService = new AxisService();
if (sel is int i)
{
axisService.AxisStop(1, (short)i);
//axisService.ResetPosition((short)i, 1);
}
else if (sel is short s)
{
axisService.AxisStop(1, s);
//axisService.ResetPosition(s, 1);
}
else if (sel is string str && short.TryParse(str, out var p))
{
axisService.AxisStop(1, p);
//axisService.ResetPosition(p, 1);
}
else
{
// 处理未选择或不可解析的情况
}
}
private void EnableAxisButton_Click(object sender, RoutedEventArgs e)
{
var axisService = new AxisService();
int axisToEnable;
var sel = AxisComboBox.SelectedItem;
try
{
if (sel is int i)
{
axisToEnable = i;
}
else if (sel is string s && int.TryParse(s, out var parsed))
{
axisToEnable = parsed;
}
else
{
axisToEnable = 1;
MessageBox.Show(this, "未选择轴号,默认启用轴 1。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
axisService.AxisOn((short)axisToEnable, 1);
}
catch (Exception ex)
{
MessageBox.Show(this, "启用轴时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void DisableAxisButton_Click(object sender, RoutedEventArgs e)
{
var axisService = new AxisService();
int axisToDisable;
var sel = AxisComboBox.SelectedItem;
try
{
if (sel is int i)
{
axisToDisable = i;
}
else if (sel is string s && int.TryParse(s, out var parsed))
{
axisToDisable = parsed;
}
else
{
axisToDisable = 1;
MessageBox.Show(this, "未选择轴号,默认禁用轴 1。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
axisService.AxisOff((short)axisToDisable, 1);
}
catch (Exception ex)
{
MessageBox.Show(this, "禁用轴时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ClearAlarmPositionButton_Click(object sender, RoutedEventArgs e)
{
var axisService = new AxisService();
int axisToClear;
var sel = AxisComboBox.SelectedItem;
try
{
if (sel is int i)
{
axisToClear = i;
}
else if (sel is string s && int.TryParse(s, out var parsed))
{
axisToClear = parsed;
}
else
{
axisToClear = 1;
MessageBox.Show(this, "未选择轴号,默认清除轴 1 的报警位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
axisService.ResetAlarm((short)axisToClear, 1);
axisService.ResetPosition((short)axisToClear, 1);
}
catch (Exception ex)
{
MessageBox.Show(this, "清除报警位置时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void JogForwardButton_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var sel = AxisComboBox.SelectedItem;
var speed = (int)MotorSpeedSlider.Value;
var axisService = new AxisService();
axisService.JogMove(1, (short)sel,speed);
}
private void JogForwardButton_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
var sel = AxisComboBox.SelectedItem;
var axisService = new AxisService();
if (sel is int i)
{
axisService.AxisStop(1, (short)i);
}
else if (sel is short s)
{
axisService.AxisStop(1, s);
}
else if (sel is string str && short.TryParse(str, out var p))
{
axisService.AxisStop(1, p);
}
else
{
// 处理未选择或不可解析的情况
}
}
private void JogForwardButton_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Net.Sockets;
using System.Text;
using System.Windows;
namespace OneClick
{
class LightSourceService
{
private static TcpClient? OpenSocket()
{
try
{
TcpClient tcpclient = new TcpClient();
tcpclient.Connect("192.168.0.7", 1234);
return tcpclient;
}
catch (Exception ex)
{
MessageBox.Show("光源连接失败: " + ex.Message, "连接错误", MessageBoxButton.OK, MessageBoxImage.Error);
return null;
}
}
public int TestSourceService()
{
// 使用通用发送方法测试固定命令
var response = SendCommand("SA0000#");
if (!string.IsNullOrEmpty(response) && response.StartsWith("A", StringComparison.Ordinal))
{
//MessageBox.Show("光源通信成功!收到响应: " + response, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return 0;
}
else if (response != null)
{
MessageBox.Show("光源响应异常,收到: " + response, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return 1;
}
return 1;
}
/// <summary>
/// 发送指定的原始指令到光源控制器,并尝试读取响应。
/// 调用者根据返回值判断是否通信成功;方法内部会在连接失败时弹窗提示错误。
/// </summary>
/// <param name="command">要发送的命令(请包含必要的结束符,例如 #</param>
/// <param name="timeoutMs">发送/接收超时(毫秒)</param>
/// <returns>收到的响应字符串(可能为空),连接失败或异常时返回 null</returns>
public string? SendCommand(string command, int timeoutMs = 2000)
{
if (string.IsNullOrEmpty(command))
throw new ArgumentException("command 不能为空", nameof(command));
var tcpclient = OpenSocket();
if (tcpclient == null)
return null;
NetworkStream? networkstream = null;
try
{
tcpclient.ReceiveTimeout = timeoutMs;
tcpclient.SendTimeout = timeoutMs;
networkstream = tcpclient.GetStream();
byte[] sendbytes = Encoding.ASCII.GetBytes(command);
networkstream.Write(sendbytes, 0, sendbytes.Length);
networkstream.Flush();
// 读取响应(阻塞,受 ReceiveTimeout 控制)
byte[] buffer = new byte[1024];
int bytesRead = networkstream.Read(buffer, 0, buffer.Length); // 受 tcpclient.ReceiveTimeout 控制
if (bytesRead > 0)
{
string response = Encoding.ASCII.GetString(buffer, 0, bytesRead).Trim();
return response;
}
else
{
return string.Empty;
}
}
catch (SocketException se)
{
MessageBox.Show("光源通信超时或套接字错误: " + se.Message, "通信错误", MessageBoxButton.OK, MessageBoxImage.Error);
return null;
}
catch (Exception ex)
{
MessageBox.Show("光源通信错误: " + ex.Message, "通信错误", MessageBoxButton.OK, MessageBoxImage.Error);
return null;
}
finally
{
try { networkstream?.Close(); } catch { }
try { tcpclient.Close(); } catch { }
}
}
}
}

View File

@@ -0,0 +1,37 @@
<Window
x:Class="OneClick.LoadingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:OneClick"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="LoadingDialog"
Width="400"
Height="200"
AllowsTransparency="False"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
mc:Ignorable="d">
<Grid>
<Border
Padding="20"
Background="White"
BorderBrush="Gray"
BorderThickness="1"
CornerRadius="10">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock
x:Name="LoadStatusMessage"
Margin="0,0,0,20"
HorizontalAlignment="Center"
FontSize="24"
FontWeight="Bold"
Text="系统启动中,请等待" />
<ProgressBar
Width="300"
Height="30"
IsIndeterminate="True" />
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace OneClick
{
/// <summary>
/// LoadingDialog.xaml 的交互逻辑
/// </summary>
public partial class LoadingDialog : Window
{
public LoadingDialog()
{
InitializeComponent();
}
}
}

87
OneClick/MainWindow.xaml Normal file
View File

@@ -0,0 +1,87 @@
<Window
x:Class="OneClick.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ha="http://schemas.mvtec.com/halcondotnet"
xmlns:local="clr-namespace:OneClick"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="800"
Height="450"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<ha:HSmartWindowControlWPF x:Name="ImageWindow" />
</Grid>
<StackPanel Grid.Column="1" Margin="10">
<TextBlock
Margin="0,20,0,20"
HorizontalAlignment="Center"
FontSize="20"
FontWeight="Bold"
Text="更改设置" />
<StackPanel
Margin="0,0,0,10"
HorizontalAlignment="Center"
Orientation="Horizontal">
<TextBlock
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="选择配方:" />
<ComboBox x:Name="SettingsListBox" Width="120" />
</StackPanel>
<!-- 新增:选择相机与重新扫描按钮 -->
<StackPanel
Margin="0,0,0,10"
HorizontalAlignment="Center"
Orientation="Horizontal">
<TextBlock
Margin="0,0,10,0"
VerticalAlignment="Center"
Text="选择相机:" />
<ComboBox x:Name="CameraComboBox" Width="160" />
<Button
x:Name="RescanButton"
Width="80"
Height="25"
Margin="10,0,0,0"
Content="重新扫描" />
</StackPanel>
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button
Width="100"
Height="25"
Margin="0,10,0,0"
Click="ChangeSettingButton_Click"
Content="更改设置" />
<Button
Width="100"
Height="25"
Margin="10,10,0,0"
Click="ResetSettingButton_Click"
Content="重置设置" />
<Button
Width="100"
Height="25"
Margin="10,10,0,0"
Click="StartButton_Click"
Content="开始" />
</StackPanel>
<Button
x:Name="CameraTest"
Height="25"
Margin="30"
Click="CameraTest_Click"
Content="停止" />
</StackPanel>
</Grid>
</Window>

593
OneClick/MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,593 @@
using Basler.Pylon;
using HalconDotNet;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using GTN; // 示例运动控制卡命名空间
namespace OneClick
{
public partial class MainWindow : Window
{
private readonly RecipeManager _recipeManager;
private readonly ICameraService _cameraService;
private HImage? image;
// 启动或刷新列表时抑制相机选择事件
private bool _suppressCameraSelectionChanged;
private readonly Stopwatch _displayStopwatch = new Stopwatch();
private readonly Dispatcher _uiDispatcher;
// 间隔保存相关
private DispatcherTimer? _captureTimer;
private int _captureIndex;
private string? _currentSaveFolder;
public MainWindow()
{
InitializeComponent();
var dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OneClick");
var storePath = Path.Combine(dataDir, "recipes.json");
_recipeManager = new RecipeManager(storePath);
_cameraService = new BaslerCameraService();
Loaded += MainWindow_Loaded;
RescanButton.Click += RescanButton_Click;
CameraComboBox.SelectionChanged += CameraComboBox_SelectionChanged;
Closing += MainWindow_Closing;
_uiDispatcher = Dispatcher.CurrentDispatcher;
// 订阅相机服务事件
_cameraService.GrabStarted += CameraService_GrabStarted;
_cameraService.ImageGrabbed += CameraService_ImageGrabbed;
_cameraService.GrabStopped += CameraService_GrabStopped;
_cameraService.ConnectionLost += CameraService_ConnectionLost;
}
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// 载入配方列表到主界面下拉框
SettingsListBox.ItemsSource = _recipeManager.GetNames().OrderBy(x => x).ToList();
if (_recipeManager.CurrentRecipe != null)
{
SettingsListBox.SelectedItem = _recipeManager.CurrentRecipe.Name;
}
// 显示启动对话框
var loadingDialog = new LoadingDialog
{
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
loadingDialog.Show();
// 异步枚举相机并在 UI 线程更新列表(避免 UI 卡顿)
try
{
loadingDialog.LoadStatusMessage.Text = "正在初始化运动控制卡...";
await Task.Run(() =>
{
var axisService = new AxisService();
axisService.InitMotionCard();
Thread.Sleep(500); // 模拟初始化延迟
});
loadingDialog.LoadStatusMessage.Text = "正在启动光源...";
await Task.Run(() =>
{
var lightservice = new LightSourceService();
lightservice.TestSourceService();
Thread.Sleep(500); // 模拟初始化延迟
});
loadingDialog.LoadStatusMessage.Text = "正在启动相机...";
var devices = await Task.Run(() =>
{
try
{
return _cameraService.EnumerateDevices();
}
catch
{
return Enumerable.Empty<string>();
}
});
_uiDispatcher.Invoke(() =>
{
_suppressCameraSelectionChanged = true;
try
{
var list = devices.ToList();
CameraComboBox.ItemsSource = list;
CameraComboBox.SelectedIndex = list.Count > 0 ? 0 : -1;
}
finally
{
_suppressCameraSelectionChanged = false;
}
});
}
catch (Exception ex)
{
MessageBox.Show($"枚举相机失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
// 隐藏启动对话框
_uiDispatcher.BeginInvoke(new Action(() => loadingDialog.Close()));
}
try
{
var firstId = _cameraService.EnumerateDevices().FirstOrDefault();
if (!string.IsNullOrWhiteSpace(firstId))
{
_cameraService.Open(firstId);
_cameraService.Close();
}
}
catch (Exception ex)
{
MessageBox.Show($"相机初始化失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void RefreshCameraList()
{
// 枚举相机设备
var devices = _cameraService.EnumerateDevices();
// 在更新列表和默认选择期间抑制 SelectionChanged 事件
_suppressCameraSelectionChanged = true;
try
{
CameraComboBox.ItemsSource = devices;
CameraComboBox.SelectedIndex = devices.Count > 0 ? 0 : -1;
}
finally
{
_suppressCameraSelectionChanged = false;
}
}
private void RescanButton_Click(object sender, RoutedEventArgs e)
{
RefreshCameraList();
}
private void CameraComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (_suppressCameraSelectionChanged) return;
var id = CameraComboBox.SelectedItem as string;
if (string.IsNullOrWhiteSpace(id)) return;
try
{
_cameraService.StopGrabbing();
_cameraService.Close();
_cameraService.Open(id);
}
catch (Exception ex)
{
MessageBox.Show($"打开相机失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void ChangeSettingButton_Click(object sender, RoutedEventArgs e)
{
// 通过构造函数注入同一个 RecipeManager 实例
var changeSettingWindow = new ChangeSetting(_recipeManager)
{
Owner = this,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
bool? dialogResult = changeSettingWindow.ShowDialog();
if (dialogResult == true)
{
// 直接从相同的 _recipeManager 刷新 UI内存状态已被更新
SettingsListBox.ItemsSource = _recipeManager.GetNames().OrderBy(x => x).ToList();
if (_recipeManager.CurrentRecipe != null)
{
SettingsListBox.SelectedItem = _recipeManager.CurrentRecipe.Name;
}
}
}
private void ResetSettingButton_Click(object sender, RoutedEventArgs e)
{
// 简单重置当前配方(示例)
var currentName = SettingsListBox.SelectedItem as string;
if (string.IsNullOrWhiteSpace(currentName))
{
MessageBox.Show("请先选择一个配方。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var recipe = _recipeManager.Get(currentName);
if (recipe == null) return;
recipe.CaptureIntervalMs = 1000;
recipe.SaveFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "OneClick");
recipe.MotorSpeed = 1000;
_recipeManager.Update(recipe);
_recipeManager.Save();
MessageBox.Show("已重置当前配方为默认参数。", "完成", MessageBoxButton.OK, MessageBoxImage.Information);
}
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
var currentName = SettingsListBox.SelectedItem as string;
if (string.IsNullOrWhiteSpace(currentName))
{
MessageBox.Show("请先选择一个配方。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var recipe = _recipeManager.Get(currentName);
if (recipe == null)
{
MessageBox.Show("未找到所选配方。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (string.IsNullOrWhiteSpace(recipe.SaveFolder))
{
MessageBox.Show("保存位置未配置。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
Directory.CreateDirectory(recipe.SaveFolder);
var id = CameraComboBox.SelectedItem as string;
if (string.IsNullOrWhiteSpace(id))
{
MessageBox.Show("未选择相机。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
try
{
_cameraService.StopGrabbing();
_cameraService.Close();
_cameraService.Open(id);
// 启动持续抓取(参考示例)
if (!_cameraService.StartGrabbing(continuous: true))
{
MessageBox.Show("启动抓取失败。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
// 启动按配方的间隔保存
StartIntervalCapture(recipe);
}
catch (Exception ex)
{
MessageBox.Show($"启动抓取失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
}
private void CameraService_GrabStarted(object? sender, EventArgs e)
{
_uiDispatcher.Invoke(() =>
{
_displayStopwatch.Reset();
});
}
private void CameraService_ImageGrabbed(object? sender, ImageGrabbedEventArgs e)
{
// 与示例一致只显示最新帧节流到约30FPS
if (!_displayStopwatch.IsRunning || _displayStopwatch.ElapsedMilliseconds > 33)
{
_displayStopwatch.Restart();
// 由于该事件可能在非 UI 线程,切回 UI 线程
_uiDispatcher.Invoke(() =>
{
try
{
using var grabResult = e.GrabResult;
if (grabResult == null || !grabResult.IsValid || !grabResult.GrabSucceeded) return;
var hImage = ConvertGrabResultToHalconImage(grabResult);
if (hImage == null) return;
image?.Dispose();
image = hImage;
// 显示到 HSmartWindowControl
HOperatorSet.DispImage(image, ImageWindow.HalconWindow);
}
catch (Exception ex)
{
MessageBox.Show($"显示图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
}
// 释放克隆的抓取结果(仿照 WinForms 示例中 e.DisposeGrabResultIfClone
e.DisposeGrabResultIfClone();
}
private void CameraService_GrabStopped(object? sender, GrabStopEventArgs e)
{
_uiDispatcher.Invoke(() =>
{
_displayStopwatch.Reset();
StopIntervalCapture();
if (e.Reason != GrabStopReason.UserRequest)
{
MessageBox.Show($"抓取错误:{e.ErrorMessage}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
});
}
private void CameraService_ConnectionLost(object? sender, EventArgs e)
{
_uiDispatcher.Invoke(() =>
{
_cameraService.StopGrabbing();
_cameraService.Close();
StopIntervalCapture();
RefreshCameraList();
MessageBox.Show("相机连接丢失。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
});
}
public HImage? ReadBuffer()
{
IGrabResult? grabResult = null;
try
{
grabResult = _cameraService.GrabOnePic();
if (grabResult == null || !grabResult.GrabSucceeded)
return null;
return ConvertGrabResultToHalconImage(grabResult);
}
finally
{
grabResult?.Dispose();
}
}
private HImage? ConvertGrabResultToHalconImage(IGrabResult grabResult)
{
var hImg = new HImage();
if (IsMonoData(grabResult))
{
// 对高位深单通道,统一转换到 Mono16 并以 16 位生成 Halcon 图像,避免 8 位截断
switch (grabResult.PixelTypeValue)
{
case PixelType.Mono10:
case PixelType.Mono10p:
case PixelType.Mono10packed:
case PixelType.Mono12:
case PixelType.Mono12p:
case PixelType.Mono12packed:
case PixelType.Mono16:
{
var converter = new PixelDataConverter { OutputPixelFormat = PixelType.Mono16 };
var bytes = new byte[grabResult.Width * grabResult.Height * 2]; // 16位
converter.Convert(bytes, grabResult);
var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
var ptr = handle.AddrOfPinnedObject();
// Halcon 中 16 位无符号类型为 "uint2"
hImg.GenImage1("uint2", grabResult.Width, grabResult.Height, ptr);
}
finally
{
handle.Free();
}
break;
}
default:
{
// 纯 8 位单通道,保持 8 位
var buffer = grabResult.PixelData as byte[];
if (buffer == null || buffer.Length < grabResult.Width * grabResult.Height)
return null;
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
var ptr = handle.AddrOfPinnedObject();
hImg.GenImage1("byte", grabResult.Width, grabResult.Height, ptr);
}
finally
{
handle.Free();
}
break;
}
}
}
else
{
// 彩色:仍使用 RGB8packed无损容器保存若需原始 Bayer 可改为保存原始缓冲区而非去马赛克
var rgb = new byte[grabResult.Width * grabResult.Height * 3];
var converter = new PixelDataConverter { OutputPixelFormat = PixelType.RGB8packed };
converter.Convert(rgb, grabResult);
var handle = GCHandle.Alloc(rgb, GCHandleType.Pinned);
try
{
var ptr = handle.AddrOfPinnedObject();
hImg.GenImageInterleaved(ptr, "rgb", grabResult.Width, grabResult.Height, 0, "byte",
grabResult.Width, grabResult.Height, 0, 0, -1, 0);
}
finally
{
handle.Free();
}
}
return hImg;
}
// 窗口关闭处理:确保停止抓取并释放相机资源
private void MainWindow_Closing(object? sender, CancelEventArgs e)
{
try
{
// 停止抓取(若正在抓取)
_cameraService.StopGrabbing();
}
catch
{
// 忽略停止时的异常,仍尝试关闭资源
}
try
{
// 关闭并释放相机资源
_cameraService.Close();
}
catch
{
// 忽略关闭时的异常
}
StopIntervalCapture();
// 如果需要,终结 Pylon 库资源(只有在使用 Pylon.Initialize 时才需要)
}
private bool IsMonoData(IGrabResult iGrabResult)
{
switch (iGrabResult.PixelTypeValue)
{
case PixelType.Mono1packed:
case PixelType.Mono2packed:
case PixelType.Mono4packed:
case PixelType.Mono8:
case PixelType.Mono8signed:
case PixelType.Mono10:
case PixelType.Mono10p:
case PixelType.Mono10packed:
case PixelType.Mono12:
case PixelType.Mono12p:
case PixelType.Mono12packed:
case PixelType.Mono16:
return true;
default:
return false;
}
}
private void CameraTest_Click(object sender, RoutedEventArgs e)
{
try
{
_cameraService.StopGrabbing();
StopIntervalCapture();
MessageBox.Show("已停止抓取。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"停止抓取失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// ===== 新增:间隔保存实现 =====
private void StartIntervalCapture(Recipe recipe)
{
StopIntervalCapture();
_currentSaveFolder = recipe.SaveFolder;
if (string.IsNullOrWhiteSpace(_currentSaveFolder))
return;
Directory.CreateDirectory(_currentSaveFolder);
_captureIndex = 0;
_captureTimer = new DispatcherTimer(DispatcherPriority.Background, _uiDispatcher)
{
Interval = TimeSpan.FromMilliseconds(Math.Max(50, recipe.CaptureIntervalMs ?? 1000))
};
_captureTimer.Tick += CaptureTimer_Tick;
_captureTimer.Start();
}
private void StopIntervalCapture()
{
if (_captureTimer != null)
{
_captureTimer.Tick -= CaptureTimer_Tick;
_captureTimer.Stop();
_captureTimer = null;
}
}
private void CaptureTimer_Tick(object? sender, EventArgs e)
{
if (image == null || string.IsNullOrWhiteSpace(_currentSaveFolder))
return;
HImage? copy = null;
try
{
copy = image.Clone();
}
catch
{
copy?.Dispose();
return;
}
// 改为保存 tiff无损且支持 16 位灰度
var fileName = $"{DateTime.Now:yyyyMMdd_HHmmss_fff}_{_captureIndex:D5}.tiff";
var fullPath = Path.Combine(_currentSaveFolder!, fileName);
_captureIndex++;
_ = Task.Run(() =>
{
try
{
if (copy != null)
{
// 第三个参数对 tiff 无效,仅用于 jpeg此处传 0
HOperatorSet.WriteImage(copy, "tiff", 0, fullPath);
}
}
catch (Exception ex)
{
_uiDispatcher.BeginInvoke(() =>
{
MessageBox.Show($"保存图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
});
}
finally
{
copy?.Dispose();
}
});
}
}
}

36
OneClick/OneClick.csproj Normal file
View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Prism.Core" Version="9.0.537" />
<PackageReference Include="Prism.Unity" Version="9.0.537" />
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="View\" />
<Folder Include="ViewModel\" />
<Folder Include="Service\" />
</ItemGroup>
<ItemGroup>
<Reference Include="Basler.Pylon">
<HintPath>D:\Pylon\Development\Assemblies\Basler.Pylon\net8.0\x64\Basler.Pylon.dll</HintPath>
</Reference>
<Reference Include="halcondotnetxl">
<HintPath>D:\MVTec\Halcon\HALCON-25.11-Progress\bin\dotnet35\halcondotnetxl.dll</HintPath>
</Reference>
<Reference Include="hdevenginedotnetxl">
<HintPath>D:\MVTec\Halcon\HALCON-25.11-Progress\bin\dotnet35\hdevenginedotnetxl.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

137
OneClick/RecipeManager.cs Normal file
View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
namespace OneClick
{
public class RecipeManager
{
private readonly Dictionary<string, Recipe> _recipes = new();
// 当前配方(可选,但非常实用)
public Recipe? CurrentRecipe { get; private set; }
private readonly string _storePath;
public RecipeManager(string storePath)
{
_storePath = storePath;
Directory.CreateDirectory(Path.GetDirectoryName(_storePath)!);
Load();
}
/// <summary>
/// 新增配方
/// </summary>
public bool Add(Recipe recipe)
{
if (recipe == null || string.IsNullOrWhiteSpace(recipe.Name))
return false;
if (_recipes.ContainsKey(recipe.Name))
return false;
_recipes.Add(recipe.Name, recipe);
Save();
return true;
}
/// <summary>
/// 删除配方
/// </summary>
public bool Remove(string name)
{
if (!_recipes.Remove(name))
return false;
if (CurrentRecipe?.Name == name)
CurrentRecipe = null;
Save();
return true;
}
/// <summary>
/// 查询配方
/// </summary>
public Recipe? Get(string name)
{
_recipes.TryGetValue(name, out var recipe);
return recipe;
}
/// <summary>
/// 切换当前配方
/// </summary>
public bool SetCurrent(string name)
{
var recipe = Get(name);
if (recipe == null)
return false;
CurrentRecipe = recipe;
return true;
}
/// <summary>
/// 修改配方(按名字整体替换)
/// </summary>
public bool Update(Recipe recipe)
{
if (recipe == null || string.IsNullOrWhiteSpace(recipe.Name))
return false;
if (!_recipes.ContainsKey(recipe.Name))
return false;
_recipes[recipe.Name] = recipe;
if (CurrentRecipe?.Name == recipe.Name)
CurrentRecipe = recipe;
Save();
return true;
}
/// <summary>
/// 获取所有配方名(给 UI / 下拉框用)
/// </summary>
public IReadOnlyCollection<string> GetNames()
{
return _recipes.Keys;
}
public void Save()
{
var list = new List<Recipe>(_recipes.Values);
var json = JsonSerializer.Serialize(list, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_storePath, json);
}
public void Load()
{
if (!File.Exists(_storePath))
return;
try
{
var json = File.ReadAllText(_storePath);
var list = JsonSerializer.Deserialize<List<Recipe>>(json) ?? new List<Recipe>();
_recipes.Clear();
foreach (var r in list)
{
if (!string.IsNullOrWhiteSpace(r.Name))
_recipes[r.Name] = r;
}
}
catch
{
// 读失败保持空,不抛异常以避免影响启动
}
}
}
}

26
OneClick/RecipePara.cs Normal file
View File

@@ -0,0 +1,26 @@
using System;
namespace OneClick
{
public class Recipe
{
// 配方名称
public string? Name { get; set; }
// 相机拍照间隔时间,单位毫秒
public int? CaptureIntervalMs { get; set; }
//保存文件夹路径
public string? SaveFolder { get; set; }
// 电机速度单位RPM
public double? MotorSpeed { get; set; }
//电机转动时间,单位秒
public double? MotorRunTimeSec { get; set; }
// 光源强度范围0-255
public int? LightIntensity { get; set; }
// 选择的轴号
public int? SelectedAxis { get; set; }
//
// 可根据需要扩展更多参数
}
}

2891
OneClick/gts.cs Normal file

File diff suppressed because it is too large Load Diff