recipe name filter

This commit is contained in:
meelstorm
2025-07-28 09:52:29 +02:00
parent 71d59df0cd
commit f80b275ad8
58 changed files with 4066 additions and 79 deletions

View 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>

View 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);
}
}
}

View 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";
}
}
}

View 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>

View 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);
}
}

View 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());
}
}
}

View 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);
}
}
}