recipe name filter
This commit is contained in:
15
Plugins/Siemens/B24SiemensEmulator/B24SiemensEmulator.csproj
Normal file
15
Plugins/Siemens/B24SiemensEmulator/B24SiemensEmulator.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
142
Plugins/Siemens/B24SiemensEmulator/ConfigurationManager.cs
Normal file
142
Plugins/Siemens/B24SiemensEmulator/ConfigurationManager.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace B24SiemensEmulator
|
||||
{
|
||||
public class PacketConfiguration
|
||||
{
|
||||
public bool Lifebit { get; set; }
|
||||
public bool KameraStart { get; set; }
|
||||
public int Materialnummer { get; set; }
|
||||
public int ArtikelnummerBall { get; set; }
|
||||
public int MaterialnummerFoil { get; set; }
|
||||
public string Chargennummer { get; set; } = string.Empty;
|
||||
|
||||
public static PacketConfiguration FromPLCPacket(PLCPacket packet)
|
||||
{
|
||||
return new PacketConfiguration
|
||||
{
|
||||
Lifebit = packet.Lifebit,
|
||||
KameraStart = packet.KameraStart,
|
||||
Materialnummer = packet.Materialnummer,
|
||||
ArtikelnummerBall = packet.ArtikelnummerBall,
|
||||
MaterialnummerFoil = packet.MaterialnummerFoil,
|
||||
Chargennummer = packet.Chargennummer
|
||||
};
|
||||
}
|
||||
|
||||
public PLCPacket ToPLCPacket()
|
||||
{
|
||||
return new PLCPacket
|
||||
{
|
||||
Lifebit = Lifebit,
|
||||
KameraStart = KameraStart,
|
||||
Materialnummer = Materialnummer,
|
||||
ArtikelnummerBall = ArtikelnummerBall,
|
||||
MaterialnummerFoil = MaterialnummerFoil,
|
||||
Chargennummer = PLCPacket.ValidateAndTrimChargennummer(Chargennummer)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfigurationManager
|
||||
{
|
||||
private const string DefaultFileName = "PLCPacketConfig.json";
|
||||
private const string FileFilter = "JSON files (*.json)|*.json|All files (*.*)|*.*";
|
||||
|
||||
public static bool SaveConfiguration(PacketConfiguration config, string? filePath = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = filePath ?? GetSaveFilePath();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return false;
|
||||
|
||||
string json = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
File.WriteAllText(path, json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to save configuration: {ex.Message}", "Save Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static PacketConfiguration? LoadConfiguration(string? filePath = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = filePath ?? GetLoadFilePath();
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
||||
return null;
|
||||
|
||||
string json = File.ReadAllText(path);
|
||||
var config = JsonConvert.DeserializeObject<PacketConfiguration>(json);
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
MessageBox.Show("Invalid configuration file format.", "Load Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate loaded configuration
|
||||
if (!PLCPacket.IsValidChargennummer(config.Chargennummer))
|
||||
{
|
||||
MessageBox.Show("Invalid Chargennummer in configuration file. It will be corrected.",
|
||||
"Load Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
config.Chargennummer = PLCPacket.ValidateAndTrimChargennummer(config.Chargennummer);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to parse configuration file: {ex.Message}", "Load Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Failed to load configuration: {ex.Message}", "Load Error",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSaveFilePath()
|
||||
{
|
||||
using var saveDialog = new SaveFileDialog
|
||||
{
|
||||
Filter = FileFilter,
|
||||
FileName = DefaultFileName,
|
||||
Title = "Save PLC Packet Configuration"
|
||||
};
|
||||
|
||||
return saveDialog.ShowDialog() == DialogResult.OK ? saveDialog.FileName : string.Empty;
|
||||
}
|
||||
|
||||
private static string GetLoadFilePath()
|
||||
{
|
||||
using var openDialog = new OpenFileDialog
|
||||
{
|
||||
Filter = FileFilter,
|
||||
Title = "Load PLC Packet Configuration"
|
||||
};
|
||||
|
||||
return openDialog.ShowDialog() == DialogResult.OK ? openDialog.FileName : string.Empty;
|
||||
}
|
||||
|
||||
public static bool ValidateConfiguration(PacketConfiguration config)
|
||||
{
|
||||
if (config == null)
|
||||
return false;
|
||||
|
||||
return PLCPacket.IsValidChargennummer(config.Chargennummer);
|
||||
}
|
||||
}
|
||||
}
|
||||
607
Plugins/Siemens/B24SiemensEmulator/MainForm.cs
Normal file
607
Plugins/Siemens/B24SiemensEmulator/MainForm.cs
Normal file
@@ -0,0 +1,607 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace B24SiemensEmulator
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private GroupBox inputGroupBox;
|
||||
private GroupBox outputGroupBox;
|
||||
private GroupBox controlGroupBox;
|
||||
private GroupBox statusGroupBox;
|
||||
|
||||
// Input controls
|
||||
private CheckBox lifebitCheckBox;
|
||||
private CheckBox kameraStartCheckBox;
|
||||
private NumericUpDown materialnummerNumeric;
|
||||
private NumericUpDown artikelnummerBallNumeric;
|
||||
private NumericUpDown materialnummerFoilNumeric;
|
||||
private TextBox chargennummerTextBox;
|
||||
|
||||
// Output controls (read-only)
|
||||
private Label lifebitResponseLabel;
|
||||
private Label kameraStartResponseLabel;
|
||||
private Label materialnummerResponseLabel;
|
||||
private Label artikelnummerBallResponseLabel;
|
||||
private Label materialnummerFoilResponseLabel;
|
||||
private Label chargennummerResponseLabel;
|
||||
|
||||
// Control buttons
|
||||
private Button startStopButton;
|
||||
private Button saveConfigButton;
|
||||
private Button loadConfigButton;
|
||||
private Button applyButton;
|
||||
|
||||
// Status controls
|
||||
private Label connectionStatusLabel;
|
||||
private Label errorMessageLabel;
|
||||
|
||||
private bool isRunning = false;
|
||||
private SiemensTcpClient? tcpClient;
|
||||
private PLCPacket currentPacket = new PLCPacket();
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializeTcpClient();
|
||||
WireUpEventHandlers();
|
||||
|
||||
// Initialize current packet with form default values
|
||||
currentPacket = GetInputPacket();
|
||||
}
|
||||
|
||||
private void InitializeTcpClient()
|
||||
{
|
||||
tcpClient = new SiemensTcpClient();
|
||||
tcpClient.GetPacketToSend = GetCurrentPacket;
|
||||
tcpClient.ConnectionStatusChanged += OnConnectionStatusChanged;
|
||||
tcpClient.ResponseReceived += OnResponseReceived;
|
||||
tcpClient.ErrorOccurred += OnErrorOccurred;
|
||||
}
|
||||
|
||||
private void WireUpEventHandlers()
|
||||
{
|
||||
startStopButton.Click += OnStartStopButtonClick;
|
||||
saveConfigButton.Click += OnSaveConfigButtonClick;
|
||||
loadConfigButton.Click += OnLoadConfigButtonClick;
|
||||
applyButton.Click += OnApplyButtonClick;
|
||||
|
||||
// Form closing event
|
||||
this.FormClosing += OnFormClosing;
|
||||
}
|
||||
|
||||
private async void OnStartStopButtonClick(object? sender, EventArgs e)
|
||||
{
|
||||
if (tcpClient == null) return;
|
||||
|
||||
if (!isRunning)
|
||||
{
|
||||
// Start
|
||||
var connected = await tcpClient.ConnectAsync();
|
||||
if (connected)
|
||||
{
|
||||
tcpClient.StartSending();
|
||||
SetRunningState(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop
|
||||
tcpClient.StopSending();
|
||||
await tcpClient.DisconnectAsync();
|
||||
SetRunningState(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSaveConfigButtonClick(object? sender, EventArgs e)
|
||||
{
|
||||
var config = PacketConfiguration.FromPLCPacket(currentPacket);
|
||||
|
||||
bool saved = ConfigurationManager.SaveConfiguration(config);
|
||||
if (saved)
|
||||
{
|
||||
MessageBox.Show("Current applied configuration saved successfully.", "Save Complete",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoadConfigButtonClick(object? sender, EventArgs e)
|
||||
{
|
||||
var config = ConfigurationManager.LoadConfiguration();
|
||||
if (config != null)
|
||||
{
|
||||
LoadConfigurationToForm(config);
|
||||
MessageBox.Show("Configuration loaded successfully. Click Apply to use the loaded values.", "Load Complete",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplyButtonClick(object? sender, EventArgs e)
|
||||
{
|
||||
currentPacket = GetInputPacket();
|
||||
|
||||
// Provide visual feedback by briefly changing the button text
|
||||
applyButton.Text = "Applied!";
|
||||
applyButton.BackColor = Color.LightGreen;
|
||||
|
||||
var timer = new System.Windows.Forms.Timer();
|
||||
timer.Interval = 1000; // 1 second
|
||||
timer.Tick += (s, args) =>
|
||||
{
|
||||
applyButton.Text = "Apply";
|
||||
applyButton.BackColor = SystemColors.Control;
|
||||
timer.Stop();
|
||||
timer.Dispose();
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
private void LoadConfigurationToForm(PacketConfiguration config)
|
||||
{
|
||||
lifebitCheckBox.Checked = config.Lifebit;
|
||||
kameraStartCheckBox.Checked = config.KameraStart;
|
||||
materialnummerNumeric.Value = config.Materialnummer;
|
||||
artikelnummerBallNumeric.Value = config.ArtikelnummerBall;
|
||||
materialnummerFoilNumeric.Value = config.MaterialnummerFoil;
|
||||
chargennummerTextBox.Text = config.Chargennummer;
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(bool connected)
|
||||
{
|
||||
UpdateConnectionStatus(connected);
|
||||
if (connected)
|
||||
{
|
||||
HideErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResponseReceived(PLCPacket packet)
|
||||
{
|
||||
UpdateResponseDisplay(packet);
|
||||
}
|
||||
|
||||
private void OnErrorOccurred(string error)
|
||||
{
|
||||
ShowErrorMessage(error);
|
||||
}
|
||||
|
||||
private void OnFormClosing(object? sender, FormClosingEventArgs e)
|
||||
{
|
||||
tcpClient?.Dispose();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
|
||||
// Form properties
|
||||
this.Text = "B24 Siemens PLC Emulator";
|
||||
this.Size = new Size(800, 640);
|
||||
this.FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
this.MaximizeBox = false;
|
||||
this.StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
// Status Group Box (Top)
|
||||
statusGroupBox = new GroupBox
|
||||
{
|
||||
Text = "Status",
|
||||
Location = new Point(12, 12),
|
||||
Size = new Size(760, 80)
|
||||
};
|
||||
|
||||
connectionStatusLabel = new Label
|
||||
{
|
||||
Text = "Disconnected",
|
||||
Location = new Point(15, 25),
|
||||
Size = new Size(200, 20),
|
||||
Font = new Font("Microsoft Sans Serif", 9F, FontStyle.Bold)
|
||||
};
|
||||
|
||||
errorMessageLabel = new Label
|
||||
{
|
||||
Text = "",
|
||||
Location = new Point(15, 50),
|
||||
Size = new Size(720, 20),
|
||||
ForeColor = Color.Red,
|
||||
Font = new Font("Microsoft Sans Serif", 8.25F)
|
||||
};
|
||||
|
||||
statusGroupBox.Controls.Add(connectionStatusLabel);
|
||||
statusGroupBox.Controls.Add(errorMessageLabel);
|
||||
|
||||
// Control Group Box
|
||||
controlGroupBox = new GroupBox
|
||||
{
|
||||
Text = "Controls",
|
||||
Location = new Point(12, 100),
|
||||
Size = new Size(760, 60)
|
||||
};
|
||||
|
||||
startStopButton = new Button
|
||||
{
|
||||
Text = "Start",
|
||||
Location = new Point(15, 25),
|
||||
Size = new Size(100, 30),
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
|
||||
saveConfigButton = new Button
|
||||
{
|
||||
Text = "Save Config",
|
||||
Location = new Point(130, 25),
|
||||
Size = new Size(100, 30),
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
|
||||
loadConfigButton = new Button
|
||||
{
|
||||
Text = "Load Config",
|
||||
Location = new Point(245, 25),
|
||||
Size = new Size(100, 30),
|
||||
UseVisualStyleBackColor = true
|
||||
};
|
||||
|
||||
controlGroupBox.Controls.Add(startStopButton);
|
||||
controlGroupBox.Controls.Add(saveConfigButton);
|
||||
controlGroupBox.Controls.Add(loadConfigButton);
|
||||
|
||||
// Input Group Box (Left Panel)
|
||||
inputGroupBox = new GroupBox
|
||||
{
|
||||
Text = "Input Packet",
|
||||
Location = new Point(12, 180),
|
||||
Size = new Size(370, 390)
|
||||
};
|
||||
|
||||
// Output Group Box (Right Panel)
|
||||
outputGroupBox = new GroupBox
|
||||
{
|
||||
Text = "Response Packet",
|
||||
Location = new Point(400, 180),
|
||||
Size = new Size(370, 390)
|
||||
};
|
||||
|
||||
CreateInputControls();
|
||||
CreateOutputControls();
|
||||
|
||||
// Add all controls to form
|
||||
this.Controls.Add(statusGroupBox);
|
||||
this.Controls.Add(controlGroupBox);
|
||||
this.Controls.Add(inputGroupBox);
|
||||
this.Controls.Add(outputGroupBox);
|
||||
|
||||
this.ResumeLayout();
|
||||
}
|
||||
|
||||
private void CreateInputControls()
|
||||
{
|
||||
int yPos = 30;
|
||||
int spacing = 45;
|
||||
|
||||
// Lifebit
|
||||
var lifebitLabel = new Label
|
||||
{
|
||||
Text = "Lifebit:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(80, 20)
|
||||
};
|
||||
lifebitCheckBox = new CheckBox
|
||||
{
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(20, 20)
|
||||
};
|
||||
inputGroupBox.Controls.Add(lifebitLabel);
|
||||
inputGroupBox.Controls.Add(lifebitCheckBox);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// KameraStart
|
||||
var kameraStartLabel = new Label
|
||||
{
|
||||
Text = "KameraStart:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(80, 20)
|
||||
};
|
||||
kameraStartCheckBox = new CheckBox
|
||||
{
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(20, 20)
|
||||
};
|
||||
inputGroupBox.Controls.Add(kameraStartLabel);
|
||||
inputGroupBox.Controls.Add(kameraStartCheckBox);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// Materialnummer
|
||||
var materialnummerLabel = new Label
|
||||
{
|
||||
Text = "Materialnummer:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
materialnummerNumeric = new NumericUpDown
|
||||
{
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
Minimum = int.MinValue,
|
||||
Maximum = int.MaxValue
|
||||
};
|
||||
inputGroupBox.Controls.Add(materialnummerLabel);
|
||||
inputGroupBox.Controls.Add(materialnummerNumeric);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// ArtikelnummerBall
|
||||
var artikelnummerBallLabel = new Label
|
||||
{
|
||||
Text = "ArtikelnummerBall:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
artikelnummerBallNumeric = new NumericUpDown
|
||||
{
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
Minimum = int.MinValue,
|
||||
Maximum = int.MaxValue
|
||||
};
|
||||
inputGroupBox.Controls.Add(artikelnummerBallLabel);
|
||||
inputGroupBox.Controls.Add(artikelnummerBallNumeric);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// MaterialnummerFoil
|
||||
var materialnummerFoilLabel = new Label
|
||||
{
|
||||
Text = "MaterialnummerFoil:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
materialnummerFoilNumeric = new NumericUpDown
|
||||
{
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
Minimum = int.MinValue,
|
||||
Maximum = int.MaxValue
|
||||
};
|
||||
inputGroupBox.Controls.Add(materialnummerFoilLabel);
|
||||
inputGroupBox.Controls.Add(materialnummerFoilNumeric);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// Chargennummer
|
||||
var chargennummerLabel = new Label
|
||||
{
|
||||
Text = "Chargennummer:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
chargennummerTextBox = new TextBox
|
||||
{
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
MaxLength = 5
|
||||
};
|
||||
inputGroupBox.Controls.Add(chargennummerLabel);
|
||||
inputGroupBox.Controls.Add(chargennummerTextBox);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// Apply button
|
||||
applyButton = new Button
|
||||
{
|
||||
Text = "Apply",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 30),
|
||||
UseVisualStyleBackColor = true,
|
||||
Font = new Font("Microsoft Sans Serif", 8.25F, FontStyle.Bold)
|
||||
};
|
||||
inputGroupBox.Controls.Add(applyButton);
|
||||
}
|
||||
|
||||
private void CreateOutputControls()
|
||||
{
|
||||
int yPos = 30;
|
||||
int spacing = 45;
|
||||
|
||||
// Lifebit Response
|
||||
var lifebitResponseTitle = new Label
|
||||
{
|
||||
Text = "Lifebit:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(80, 20)
|
||||
};
|
||||
lifebitResponseLabel = new Label
|
||||
{
|
||||
Text = "False",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(100, 20),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
BackColor = SystemColors.Control
|
||||
};
|
||||
outputGroupBox.Controls.Add(lifebitResponseTitle);
|
||||
outputGroupBox.Controls.Add(lifebitResponseLabel);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// KameraStart Response
|
||||
var kameraStartResponseTitle = new Label
|
||||
{
|
||||
Text = "KameraStart:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(80, 20)
|
||||
};
|
||||
kameraStartResponseLabel = new Label
|
||||
{
|
||||
Text = "False",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(100, 20),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
BackColor = SystemColors.Control
|
||||
};
|
||||
outputGroupBox.Controls.Add(kameraStartResponseTitle);
|
||||
outputGroupBox.Controls.Add(kameraStartResponseLabel);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// Materialnummer Response
|
||||
var materialnummerResponseTitle = new Label
|
||||
{
|
||||
Text = "Materialnummer:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
materialnummerResponseLabel = new Label
|
||||
{
|
||||
Text = "0",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
BackColor = SystemColors.Control
|
||||
};
|
||||
outputGroupBox.Controls.Add(materialnummerResponseTitle);
|
||||
outputGroupBox.Controls.Add(materialnummerResponseLabel);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// ArtikelnummerBall Response
|
||||
var artikelnummerBallResponseTitle = new Label
|
||||
{
|
||||
Text = "ArtikelnummerBall:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
artikelnummerBallResponseLabel = new Label
|
||||
{
|
||||
Text = "0",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
BackColor = SystemColors.Control
|
||||
};
|
||||
outputGroupBox.Controls.Add(artikelnummerBallResponseTitle);
|
||||
outputGroupBox.Controls.Add(artikelnummerBallResponseLabel);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// MaterialnummerFoil Response
|
||||
var materialnummerFoilResponseTitle = new Label
|
||||
{
|
||||
Text = "MaterialnummerFoil:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
materialnummerFoilResponseLabel = new Label
|
||||
{
|
||||
Text = "0",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
BackColor = SystemColors.Control
|
||||
};
|
||||
outputGroupBox.Controls.Add(materialnummerFoilResponseTitle);
|
||||
outputGroupBox.Controls.Add(materialnummerFoilResponseLabel);
|
||||
|
||||
yPos += spacing;
|
||||
|
||||
// Chargennummer Response
|
||||
var chargennummerResponseTitle = new Label
|
||||
{
|
||||
Text = "Chargennummer:",
|
||||
Location = new Point(15, yPos),
|
||||
Size = new Size(100, 20)
|
||||
};
|
||||
chargennummerResponseLabel = new Label
|
||||
{
|
||||
Text = "",
|
||||
Location = new Point(120, yPos),
|
||||
Size = new Size(120, 20),
|
||||
BorderStyle = BorderStyle.FixedSingle,
|
||||
BackColor = SystemColors.Control
|
||||
};
|
||||
outputGroupBox.Controls.Add(chargennummerResponseTitle);
|
||||
outputGroupBox.Controls.Add(chargennummerResponseLabel);
|
||||
}
|
||||
|
||||
public PLCPacket GetInputPacket()
|
||||
{
|
||||
return new PLCPacket
|
||||
{
|
||||
Lifebit = lifebitCheckBox.Checked,
|
||||
KameraStart = kameraStartCheckBox.Checked,
|
||||
Materialnummer = (int)materialnummerNumeric.Value,
|
||||
ArtikelnummerBall = (int)artikelnummerBallNumeric.Value,
|
||||
MaterialnummerFoil = (int)materialnummerFoilNumeric.Value,
|
||||
Chargennummer = chargennummerTextBox.Text
|
||||
};
|
||||
}
|
||||
|
||||
public PLCPacket GetCurrentPacket()
|
||||
{
|
||||
return currentPacket;
|
||||
}
|
||||
|
||||
public void UpdateResponseDisplay(PLCPacket packet)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action<PLCPacket>(UpdateResponseDisplay), packet);
|
||||
return;
|
||||
}
|
||||
|
||||
lifebitResponseLabel.Text = packet.Lifebit.ToString();
|
||||
kameraStartResponseLabel.Text = packet.KameraStart.ToString();
|
||||
materialnummerResponseLabel.Text = packet.Materialnummer.ToString();
|
||||
artikelnummerBallResponseLabel.Text = packet.ArtikelnummerBall.ToString();
|
||||
materialnummerFoilResponseLabel.Text = packet.MaterialnummerFoil.ToString();
|
||||
chargennummerResponseLabel.Text = packet.Chargennummer;
|
||||
}
|
||||
|
||||
public void UpdateConnectionStatus(bool connected)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action<bool>(UpdateConnectionStatus), connected);
|
||||
return;
|
||||
}
|
||||
|
||||
connectionStatusLabel.Text = connected ? "Connected" : "Disconnected";
|
||||
connectionStatusLabel.ForeColor = connected ? Color.Green : Color.Red;
|
||||
}
|
||||
|
||||
public void ShowErrorMessage(string message)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action<string>(ShowErrorMessage), message);
|
||||
return;
|
||||
}
|
||||
|
||||
errorMessageLabel.Text = message;
|
||||
}
|
||||
|
||||
public void HideErrorMessage()
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(HideErrorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
errorMessageLabel.Text = "";
|
||||
}
|
||||
|
||||
// Event handlers will be wired up later
|
||||
public Button StartStopButton => startStopButton;
|
||||
public Button SaveConfigButton => saveConfigButton;
|
||||
public Button LoadConfigButton => loadConfigButton;
|
||||
public bool IsRunning => isRunning;
|
||||
|
||||
public void SetRunningState(bool running)
|
||||
{
|
||||
isRunning = running;
|
||||
startStopButton.Text = running ? "Stop" : "Start";
|
||||
}
|
||||
}
|
||||
}
|
||||
120
Plugins/Siemens/B24SiemensEmulator/MainForm.resx
Normal file
120
Plugins/Siemens/B24SiemensEmulator/MainForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
135
Plugins/Siemens/B24SiemensEmulator/PLCPacket.cs
Normal file
135
Plugins/Siemens/B24SiemensEmulator/PLCPacket.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace B24SiemensEmulator;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 19)]
|
||||
public struct PLCPacket
|
||||
{
|
||||
[FieldOffset(0)] private byte LifebitAndKamera;
|
||||
[FieldOffset(2)] public int Materialnummer;
|
||||
[FieldOffset(6)] public int ArtikelnummerBall;
|
||||
[FieldOffset(10)] public int MaterialnummerFoil;
|
||||
[FieldOffset(14)] private byte Chargennummer0;
|
||||
[FieldOffset(15)] private byte Chargennummer1;
|
||||
[FieldOffset(16)] private byte Chargennummer2;
|
||||
[FieldOffset(17)] private byte Chargennummer3;
|
||||
[FieldOffset(18)] private byte Chargennummer4;
|
||||
|
||||
public string Chargennummer
|
||||
{
|
||||
get => Encoding.ASCII.GetString(new[]
|
||||
{Chargennummer0, Chargennummer1, Chargennummer2, Chargennummer3, Chargennummer4});
|
||||
set
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(value);
|
||||
var fill = new byte[5];
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
fill[i] = bytes[i];
|
||||
}
|
||||
Chargennummer0 = fill[0];
|
||||
Chargennummer1 = fill[1];
|
||||
Chargennummer2 = fill[2];
|
||||
Chargennummer3 = fill[3];
|
||||
Chargennummer4 = fill[4];
|
||||
}
|
||||
}
|
||||
|
||||
public bool Lifebit
|
||||
{
|
||||
get => (LifebitAndKamera & 1) > 0;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
LifebitAndKamera |= 1;
|
||||
else
|
||||
LifebitAndKamera &= 0xFE;
|
||||
}
|
||||
}
|
||||
|
||||
public bool KameraStart
|
||||
{
|
||||
get => (LifebitAndKamera & 2) > 0;
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
LifebitAndKamera |= 2;
|
||||
else
|
||||
LifebitAndKamera &= 0xFD;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] Serialize()
|
||||
{
|
||||
int size = Marshal.SizeOf(this);
|
||||
byte[] arr = new byte[size];
|
||||
IntPtr ptr = Marshal.AllocHGlobal(size);
|
||||
|
||||
Marshal.StructureToPtr(this, ptr, true);
|
||||
Marshal.Copy(ptr, arr, 0, size);
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(arr, 2, 4); // Materialnummer
|
||||
Array.Reverse(arr, 6, 4); // ArtikelnummerBall
|
||||
Array.Reverse(arr, 10, 4); // MaterialnummerFoil
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static PLCPacket Deserialize(byte[] arr)
|
||||
{
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
Array.Reverse(arr, 2, 4); // Materialnummer
|
||||
Array.Reverse(arr, 6, 4); // ArtikelnummerBall
|
||||
Array.Reverse(arr, 10, 4); // MaterialnummerFoil
|
||||
}
|
||||
|
||||
PLCPacket ds;
|
||||
int size = Marshal.SizeOf(typeof(PLCPacket));
|
||||
IntPtr ptr = Marshal.AllocHGlobal(size);
|
||||
|
||||
Marshal.Copy(arr, 0, ptr, size);
|
||||
|
||||
ds = (PLCPacket)Marshal.PtrToStructure(ptr, typeof(PLCPacket));
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
override public string ToString()
|
||||
{
|
||||
return $"Lifebit: {(Lifebit?'1':'0')}, KameraStart: {KameraStart}, Materialnummer: {Materialnummer}, ArtikelnummerBall: {ArtikelnummerBall}, MaterialnummerFoil: {MaterialnummerFoil}, Chargennummer: {Chargennummer}";
|
||||
}
|
||||
|
||||
public static bool IsValidChargennummer(string chargennummer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chargennummer))
|
||||
return true; // Empty is valid
|
||||
|
||||
if (chargennummer.Length > 5)
|
||||
return false; // Too long
|
||||
|
||||
// Check if all characters are ASCII
|
||||
return chargennummer.All(c => c >= 0 && c <= 127);
|
||||
}
|
||||
|
||||
public static string ValidateAndTrimChargennummer(string chargennummer)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chargennummer))
|
||||
return string.Empty;
|
||||
|
||||
// Trim to max 5 characters and ensure ASCII
|
||||
var trimmed = chargennummer.Substring(0, Math.Min(chargennummer.Length, 5));
|
||||
return new string(trimmed.Where(c => c >= 0 && c <= 127).ToArray());
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return IsValidChargennummer(Chargennummer);
|
||||
}
|
||||
}
|
||||
20
Plugins/Siemens/B24SiemensEmulator/Program.cs
Normal file
20
Plugins/Siemens/B24SiemensEmulator/Program.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace B24SiemensEmulator
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
// To customize application configuration such as set high DPI settings or default font,
|
||||
// see https://aka.ms/applicationconfiguration.
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
}
|
||||
}
|
||||
167
Plugins/Siemens/B24SiemensEmulator/TcpClient.cs
Normal file
167
Plugins/Siemens/B24SiemensEmulator/TcpClient.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Timer = System.Threading.Timer;
|
||||
|
||||
namespace B24SiemensEmulator
|
||||
{
|
||||
public class SiemensTcpClient : IDisposable
|
||||
{
|
||||
private TcpClient? _tcpClient;
|
||||
private NetworkStream? _stream;
|
||||
private Timer? _sendTimer;
|
||||
private bool _isConnected = false;
|
||||
private bool _isRunning = false;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public event Action<bool>? ConnectionStatusChanged;
|
||||
public event Action<PLCPacket>? ResponseReceived;
|
||||
public event Action<string>? ErrorOccurred;
|
||||
|
||||
private readonly string _serverHost = "localhost";
|
||||
private readonly int _serverPort = 42010;
|
||||
private readonly int _sendIntervalMs = 1000;
|
||||
|
||||
public Func<PLCPacket>? GetPacketToSend;
|
||||
|
||||
public bool IsConnected => _isConnected;
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
public async Task<bool> ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isConnected)
|
||||
return true;
|
||||
|
||||
_tcpClient = new TcpClient();
|
||||
await _tcpClient.ConnectAsync(_serverHost, _serverPort);
|
||||
_stream = _tcpClient.GetStream();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_isConnected = true;
|
||||
}
|
||||
|
||||
ConnectionStatusChanged?.Invoke(true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorOccurred?.Invoke($"Connection failed: {ex.Message}");
|
||||
await DisconnectAsync();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_isConnected = false;
|
||||
}
|
||||
|
||||
_stream?.Close();
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
|
||||
_tcpClient?.Close();
|
||||
_tcpClient?.Dispose();
|
||||
_tcpClient = null;
|
||||
|
||||
ConnectionStatusChanged?.Invoke(false);
|
||||
}
|
||||
|
||||
public void StartSending()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
_isRunning = true;
|
||||
_sendTimer = new Timer(SendPacketCallback, null, 0, _sendIntervalMs);
|
||||
}
|
||||
|
||||
public void StopSending()
|
||||
{
|
||||
_isRunning = false;
|
||||
_sendTimer?.Dispose();
|
||||
_sendTimer = null;
|
||||
}
|
||||
|
||||
private async void SendPacketCallback(object? state)
|
||||
{
|
||||
if (!_isConnected || GetPacketToSend == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var packet = GetPacketToSend();
|
||||
await SendPacketAsync(packet);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorOccurred?.Invoke($"Send failed: {ex.Message}");
|
||||
await HandleConnectionLoss();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendPacketAsync(PLCPacket packet)
|
||||
{
|
||||
if (_stream == null || !_isConnected)
|
||||
throw new InvalidOperationException("Not connected");
|
||||
|
||||
var data = packet.Serialize();
|
||||
await _stream.WriteAsync(data, 0, data.Length);
|
||||
|
||||
// Read response
|
||||
var responseBuffer = new byte[19]; // PLCPacket is 19 bytes
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while (totalBytesRead < responseBuffer.Length)
|
||||
{
|
||||
int bytesRead = await _stream.ReadAsync(responseBuffer, totalBytesRead,
|
||||
responseBuffer.Length - totalBytesRead);
|
||||
|
||||
if (bytesRead == 0)
|
||||
throw new Exception("Server disconnected");
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
||||
var responsePacket = PLCPacket.Deserialize(responseBuffer);
|
||||
ResponseReceived?.Invoke(responsePacket);
|
||||
}
|
||||
|
||||
private async Task HandleConnectionLoss()
|
||||
{
|
||||
await DisconnectAsync();
|
||||
|
||||
// Wait 1 second before trying to reconnect
|
||||
await Task.Delay(1000);
|
||||
|
||||
if (_isRunning)
|
||||
{
|
||||
var reconnected = await ConnectAsync();
|
||||
if (!reconnected)
|
||||
{
|
||||
// Schedule another reconnection attempt
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
if (_isRunning && !_isConnected)
|
||||
{
|
||||
await HandleConnectionLoss();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopSending();
|
||||
DisconnectAsync().Wait(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user