recipe name filter
This commit is contained in:
63
Plugins/Siemens/.claude/commands/create-prd.md
Normal file
63
Plugins/Siemens/.claude/commands/create-prd.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Rule: Generating a Product Requirements Document (PRD)
|
||||
|
||||
## Goal
|
||||
|
||||
To guide an AI assistant in creating a detailed Product Requirements Document (PRD) in Markdown format, based on an initial user prompt. The PRD should be clear, actionable, and suitable for a junior developer to understand and implement the feature.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Receive Initial Prompt:** The user provides a brief description or request for a new feature or functionality.
|
||||
2. **Ask Clarifying Questions:** Before writing the PRD, the AI *must* ask clarifying questions to gather sufficient detail. The goal is to understand the "what" and "why" of the feature, not necessarily the "how" (which the developer will figure out). Make sure to provide options in letter/number lists so I can respond easily with my selections.
|
||||
3. **Generate PRD:** Based on the initial prompt and the user's answers to the clarifying questions, generate a PRD using the structure outlined below.
|
||||
4. **Save PRD:** Save the generated document as `prd-[feature-name].md` inside the `/tasks` directory.
|
||||
|
||||
## Clarifying Questions (Examples)
|
||||
|
||||
The AI should adapt its questions based on the prompt, but here are some common areas to explore:
|
||||
|
||||
* **Problem/Goal:** "What problem does this feature solve for the user?" or "What is the main goal we want to achieve with this feature?"
|
||||
* **Target User:** "Who is the primary user of this feature?"
|
||||
* **Core Functionality:** "Can you describe the key actions a user should be able to perform with this feature?"
|
||||
* **User Stories:** "Could you provide a few user stories? (e.g., As a [type of user], I want to [perform an action] so that [benefit].)"
|
||||
* **Acceptance Criteria:** "How will we know when this feature is successfully implemented? What are the key success criteria?"
|
||||
* **Scope/Boundaries:** "Are there any specific things this feature *should not* do (non-goals)?"
|
||||
* **Data Requirements:** "What kind of data does this feature need to display or manipulate?"
|
||||
* **Design/UI:** "Are there any existing design mockups or UI guidelines to follow?" or "Can you describe the desired look and feel?"
|
||||
* **Edge Cases:** "Are there any potential edge cases or error conditions we should consider?"
|
||||
|
||||
## PRD Structure
|
||||
|
||||
The generated PRD should include the following sections:
|
||||
|
||||
1. **Introduction/Overview:** Briefly describe the feature and the problem it solves. State the goal.
|
||||
2. **Goals:** List the specific, measurable objectives for this feature.
|
||||
3. **User Stories:** Detail the user narratives describing feature usage and benefits.
|
||||
4. **Functional Requirements:** List the specific functionalities the feature must have. Use clear, concise language (e.g., "The system must allow users to upload a profile picture."). Number these requirements.
|
||||
5. **Non-Goals (Out of Scope):** Clearly state what this feature will *not* include to manage scope.
|
||||
6. **Design Considerations (Optional):** Link to mockups, describe UI/UX requirements, or mention relevant components/styles if applicable.
|
||||
7. **Technical Considerations (Optional):** Mention any known technical constraints, dependencies, or suggestions (e.g., "Should integrate with the existing Auth module").
|
||||
8. **Success Metrics:** How will the success of this feature be measured? (e.g., "Increase user engagement by 10%", "Reduce support tickets related to X").
|
||||
9. **Open Questions:** List any remaining questions or areas needing further clarification.
|
||||
|
||||
## Target Audience
|
||||
|
||||
Assume the primary reader of the PRD is a **junior developer**. Therefore, requirements should be explicit, unambiguous, and avoid jargon where possible. Provide enough detail for them to understand the feature's purpose and core logic.
|
||||
|
||||
## Output
|
||||
|
||||
* **Format:** Markdown (`.md`)
|
||||
* **Location:** `/tasks/`
|
||||
* **Filename:** `prd-[feature-name].md`
|
||||
|
||||
## Final instructions
|
||||
|
||||
1. Do NOT start implementing the PRD
|
||||
2. Make sure to ask the user clarifying questions
|
||||
3. Take the user's answers to the clarifying questions and improve the PRD
|
||||
|
||||
$ARGUMENTS
|
||||
64
Plugins/Siemens/.claude/commands/generate-tasks.md
Normal file
64
Plugins/Siemens/.claude/commands/generate-tasks.md
Normal file
@@ -0,0 +1,64 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Rule: Generating a Task List from a PRD
|
||||
|
||||
## Goal
|
||||
|
||||
To guide an AI assistant in creating a detailed, step-by-step task list in Markdown format based on an existing Product Requirements Document (PRD). The task list should guide a developer through implementation.
|
||||
|
||||
## Output
|
||||
|
||||
- **Format:** Markdown (`.md`)
|
||||
- **Location:** `/tasks/`
|
||||
- **Filename:** `tasks-[prd-file-name].md` (e.g., `tasks-prd-user-profile-editing.md`)
|
||||
|
||||
## Process
|
||||
|
||||
1. **Receive PRD Reference:** The user points the AI to a specific PRD file
|
||||
2. **Analyze PRD:** The AI reads and analyzes the functional requirements, user stories, and other sections of the specified PRD.
|
||||
3. **Phase 1: Generate Parent Tasks:** Based on the PRD analysis, create the file and generate the main, high-level tasks required to implement the feature. Use your judgement on how many high-level tasks to use. It's likely to be about 5. Present these tasks to the user in the specified format (without sub-tasks yet). Inform the user: "I have generated the high-level tasks based on the PRD. Ready to generate the sub-tasks? Respond with 'Go' to proceed."
|
||||
4. **Wait for Confirmation:** Pause and wait for the user to respond with "Go".
|
||||
5. **Phase 2: Generate Sub-Tasks:** Once the user confirms, break down each parent task into smaller, actionable sub-tasks necessary to complete the parent task. Ensure sub-tasks logically follow from the parent task and cover the implementation details implied by the PRD.
|
||||
6. **Identify Relevant Files:** Based on the tasks and PRD, identify potential files that will need to be created or modified. List these under the `Relevant Files` section, including corresponding test files if applicable.
|
||||
7. **Generate Final Output:** Combine the parent tasks, sub-tasks, relevant files, and notes into the final Markdown structure.
|
||||
8. **Save Task List:** Save the generated document in the `/tasks/` directory with the filename `tasks-[prd-file-name].md`, where `[prd-file-name]` matches the base name of the input PRD file (e.g., if the input was `prd-user-profile-editing.md`, the output is `tasks-prd-user-profile-editing.md`).
|
||||
|
||||
## Output Format
|
||||
|
||||
The generated task list _must_ follow this structure:
|
||||
|
||||
```markdown
|
||||
## Relevant Files
|
||||
|
||||
- `path/to/potential/file1.ts` - Brief description of why this file is relevant (e.g., Contains the main component for this feature).
|
||||
- `path/to/file1.test.ts` - Unit tests for `file1.ts`.
|
||||
- `path/to/another/file.tsx` - Brief description (e.g., API route handler for data submission).
|
||||
- `path/to/another/file.test.tsx` - Unit tests for `another/file.tsx`.
|
||||
- `lib/utils/helpers.ts` - Brief description (e.g., Utility functions needed for calculations).
|
||||
- `lib/utils/helpers.test.ts` - Unit tests for `helpers.ts`.
|
||||
|
||||
### Notes
|
||||
|
||||
- Unit tests should typically be placed alongside the code files they are testing (e.g., `MyComponent.tsx` and `MyComponent.test.tsx` in the same directory).
|
||||
- Use `npx jest [optional/path/to/test/file]` to run tests. Running without a path executes all tests found by the Jest configuration.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] 1.0 Parent Task Title
|
||||
- [ ] 1.1 [Sub-task description 1.1]
|
||||
- [ ] 1.2 [Sub-task description 1.2]
|
||||
- [ ] 2.0 Parent Task Title
|
||||
- [ ] 2.1 [Sub-task description 2.1]
|
||||
- [ ] 3.0 Parent Task Title (may not require sub-tasks if purely structural or configuration)
|
||||
```
|
||||
|
||||
## Interaction Model
|
||||
|
||||
The process explicitly requires a pause after generating parent tasks to get user confirmation ("Go") before proceeding to generate the detailed sub-tasks. This ensures the high-level plan aligns with user expectations before diving into details.
|
||||
|
||||
## Target Audience
|
||||
|
||||
Assume the primary reader of the task list is a **junior developer** who will implement the feature.
|
||||
52
Plugins/Siemens/.claude/commands/process-task-list.md
Normal file
52
Plugins/Siemens/.claude/commands/process-task-list.md
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Task List Management
|
||||
|
||||
Guidelines for managing task lists in markdown files to track progress on completing a PRD
|
||||
|
||||
## Task Implementation
|
||||
- **One sub-task at a time:** Do **NOT** start the next sub‑task until you ask the user for permission and they say "yes" or "y"
|
||||
- **Completion protocol:**
|
||||
1. When you finish a **sub-task**, immediately mark it as completed by changing `[ ]` to `[x]`.
|
||||
2. If **all** subtasks underneath a parent task are now `[x]`, follow this sequence:
|
||||
- **First**: Run the full test suite
|
||||
- **Only if all tests pass**: Stage changes (`git add .`)
|
||||
- **Clean up**: Remove any temporary files and temporary code before committing
|
||||
- **Commit**: Use a descriptive commit message that:
|
||||
- Uses conventional commit format (`feat:`, `fix:`, `refactor:`, etc.)
|
||||
- Summarizes what was accomplished in the parent task
|
||||
- Lists key changes and additions
|
||||
- References the task number and PRD context
|
||||
- **Formats the message as a single-line command using `-m` flags**, e.g.:
|
||||
|
||||
```
|
||||
git commit -m "feat: add payment validation logic" -m "- Validates card type and expiry" -m "- Adds unit tests for edge cases" -m "Related to T123 in PRD"
|
||||
```
|
||||
3. Once all the subtasks are marked completed and changes have been committed, mark the **parent task** as completed.
|
||||
- Stop after each sub-task and wait for the user's go-ahead.
|
||||
|
||||
## Task List Maintenance
|
||||
|
||||
1. **Update the task list as you work:**
|
||||
- Mark tasks and subtasks as completed (`[x]`) per the protocol above.
|
||||
- Add new tasks as they emerge.
|
||||
|
||||
2. **Maintain the "Relevant Files" section:**
|
||||
- List every file created or modified.
|
||||
- Give each file a one‑line description of its purpose.
|
||||
|
||||
## AI Instructions
|
||||
|
||||
When working with task lists, the AI must:
|
||||
|
||||
1. Regularly update the task list file after finishing any significant work.
|
||||
2. Follow the completion protocol:
|
||||
- Mark each finished **sub-task** `[x]`.
|
||||
- Mark the **parent task** `[x]` once **all** its subtasks are `[x]`.
|
||||
3. Add newly discovered tasks.
|
||||
4. Keep "Relevant Files" accurate and up to date.
|
||||
5. Before starting work, check which sub‑task is next.
|
||||
6. After implementing a sub‑task, update the file and then pause for user approval.
|
||||
9
Plugins/Siemens/.claude/settings.local.json
Normal file
9
Plugins/Siemens/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(dotnet add:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Plugins/Siemens/B24SiemensPlugin/B24SiemensPlugin.csproj
Normal file
20
Plugins/Siemens/B24SiemensPlugin/B24SiemensPlugin.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<OutDir>D:\Inspectron\Hawkeye\code\VisionBuilder5\VisionBuilder.UI\VisionBuilder.UI.Windows.Test\bin\Debug\Data\Plugins\B24SiemensPlugin</OutDir>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\framework\Inspectron.Settings\Inspectron.Settings.csproj">
|
||||
<Private>False</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
85
Plugins/Siemens/B24SiemensPlugin/B24SiemensSettings.cs
Normal file
85
Plugins/Siemens/B24SiemensPlugin/B24SiemensSettings.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Inspectron.Settings;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Utils;
|
||||
|
||||
namespace B24SiemensPlugin;
|
||||
|
||||
public class B24SiemensSettings:ISettings
|
||||
{
|
||||
public string CameraName { get; }
|
||||
|
||||
public B24SiemensSettings(string cameraName)
|
||||
{
|
||||
CameraName = cameraName;
|
||||
}
|
||||
|
||||
public List<SiemensRecipeMapping> RecipeMappings { get; set; } = new List<SiemensRecipeMapping>();
|
||||
|
||||
public void RegisterSettings(InspectronSettings settings)
|
||||
{
|
||||
settings.RegisterSimple(this, ()=>this.RecipeMappings,$"{CameraName}/Siemens(B24)", nameof(RecipeMappings));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class SiemensRecipeMapping
|
||||
{
|
||||
public int MaterialNumber { get; set; }
|
||||
public string RecipeName { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{MaterialNumber} -> {RecipeName}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ListRecipeMappingConverter : ITypeConverter
|
||||
{
|
||||
public object ConvertFrom(object value)
|
||||
{
|
||||
if (value is string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<SiemensRecipeMapping>>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
});
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize SiemensRecipeMapping list from JSON.");
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Value must be a JSON string.");
|
||||
|
||||
}
|
||||
|
||||
public object ConvertTo(object value, Type destinationType)
|
||||
{
|
||||
if (value is List<SiemensRecipeMapping> mappings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(mappings, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
});
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to serialize SiemensRecipeMapping list to JSON.");
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Value must be a List<SiemensRecipeMapping>.");
|
||||
|
||||
}
|
||||
}
|
||||
112
Plugins/Siemens/B24SiemensPlugin/PLCPacket.cs
Normal file
112
Plugins/Siemens/B24SiemensPlugin/PLCPacket.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace B24SiemensPlugin;
|
||||
|
||||
[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}";
|
||||
}
|
||||
}
|
||||
24
Plugins/Siemens/B24SiemensPlugin/Plugin.cs
Normal file
24
Plugins/Siemens/B24SiemensPlugin/Plugin.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Ninject;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Inspectron.Settings;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Plugins;
|
||||
|
||||
namespace B24SiemensPlugin
|
||||
{
|
||||
public class Plugin:IPlugin
|
||||
{
|
||||
public void RegisterGlobalModules(IKernel kernel)
|
||||
{
|
||||
TypeConverterRegistry.Register<List<SiemensRecipeMapping>>(new ListRecipeMappingConverter());
|
||||
}
|
||||
|
||||
public void RegisterCameraModules(IKernel kernel, string cameraName)
|
||||
{
|
||||
kernel.RegisterModule<SiemensCameraModule>();
|
||||
// Register settings
|
||||
kernel.Bind<B24SiemensSettings, ISettings>().ToConstant(new B24SiemensSettings(cameraName));
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Plugins/Siemens/B24SiemensPlugin/SiemensCameraModule.cs
Normal file
56
Plugins/Siemens/B24SiemensPlugin/SiemensCameraModule.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.RecipeProcessing;
|
||||
|
||||
namespace B24SiemensPlugin;
|
||||
|
||||
public class SiemensCameraModule: IVisionBuilderModule
|
||||
{
|
||||
private readonly SingleCameraVM _singleCameraVm;
|
||||
private readonly B24SiemensSettings _settings;
|
||||
|
||||
private SiemensServer _server;
|
||||
|
||||
public SiemensCameraModule(SingleCameraVM singleCameraVm,B24SiemensSettings settings)
|
||||
{
|
||||
_singleCameraVm = singleCameraVm;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public void InitializeModule()
|
||||
{
|
||||
_server = new SiemensServer();
|
||||
_server.OnPLCPacket += _server_OnPLCPacket;
|
||||
_server.Start();
|
||||
}
|
||||
string? GetMapping(int material)
|
||||
{
|
||||
var mapping = _settings.RecipeMappings.FirstOrDefault(x => x.MaterialNumber == material);
|
||||
if (mapping == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return mapping.RecipeName;
|
||||
}
|
||||
private PLCPacket _server_OnPLCPacket(PLCPacket packet)
|
||||
{
|
||||
packet.KameraStart= _singleCameraVm.IsRunning;
|
||||
var recipe = GetMapping(packet.Materialnummer);
|
||||
var selectedRecipe = _singleCameraVm.SelectedRecipe;
|
||||
if (recipe != null && recipe != selectedRecipe?.RecipeName)
|
||||
{
|
||||
var vm = _singleCameraVm.GetRecipeSelectionVm();
|
||||
vm.SelectedRecipe = vm.Recipes.FirstOrDefault(r => r.RecipeName == recipe);
|
||||
if (vm.SelectedRecipe != null)
|
||||
{
|
||||
_singleCameraVm.SynchronizationContext!.Post((_) =>
|
||||
{
|
||||
_singleCameraVm.ProcessRecipeSelectionVm(vm);
|
||||
}, null);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return packet;
|
||||
|
||||
}
|
||||
}
|
||||
25
Plugins/Siemens/B24SiemensPlugin/SiemensServer.cs
Normal file
25
Plugins/Siemens/B24SiemensPlugin/SiemensServer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Net;
|
||||
using B24SiemensPlugin.TCP;
|
||||
using Serilog;
|
||||
|
||||
namespace B24SiemensPlugin;
|
||||
|
||||
class SiemensServer : TcpServer
|
||||
{
|
||||
public SiemensServer(int port=42010) : base(IPAddress.Any, port)
|
||||
{
|
||||
}
|
||||
public event Func<PLCPacket, PLCPacket> OnPLCPacket;
|
||||
protected override void OnConnected(TcpSession session)
|
||||
{
|
||||
Log.Logger.ForContext<SiemensServer>().Information("PLC client connected");
|
||||
}
|
||||
|
||||
protected override TcpSession CreateSession()
|
||||
{
|
||||
var session = new SiemensTCPSession(this);
|
||||
session.OnPLCPacketReceived += OnPLCPacket;
|
||||
session.ReceiveAsync();
|
||||
return session;
|
||||
}
|
||||
}
|
||||
29
Plugins/Siemens/B24SiemensPlugin/SiemensTCPSession.cs
Normal file
29
Plugins/Siemens/B24SiemensPlugin/SiemensTCPSession.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using B24SiemensPlugin.TCP;
|
||||
using Buffer = System.Buffer;
|
||||
|
||||
namespace B24SiemensPlugin;
|
||||
|
||||
public class SiemensTCPSession : TcpSession
|
||||
{
|
||||
public event Func<PLCPacket, PLCPacket>? OnPLCPacketReceived;
|
||||
public SiemensTCPSession(TcpServer server) : base(server)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected override void OnReceived(byte[] buffer, long offset, long size)
|
||||
{
|
||||
|
||||
var data = new byte[size];
|
||||
Buffer.BlockCopy(buffer, (int)offset, data, 0, (int)size);
|
||||
PLCPacket packet = PLCPacket.Deserialize(data);
|
||||
if(OnPLCPacketReceived!=null)
|
||||
packet=OnPLCPacketReceived(packet);
|
||||
SendAsync(packet.Serialize());
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
182
Plugins/Siemens/B24SiemensPlugin/TCP/Buffer.cs
Normal file
182
Plugins/Siemens/B24SiemensPlugin/TCP/Buffer.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace B24SiemensPlugin.TCP
|
||||
{
|
||||
/// <summary>
|
||||
/// Dynamic byte buffer
|
||||
/// </summary>
|
||||
public class Buffer
|
||||
{
|
||||
private byte[] _data;
|
||||
private long _size;
|
||||
private long _offset;
|
||||
|
||||
/// <summary>
|
||||
/// Is the buffer empty?
|
||||
/// </summary>
|
||||
public bool IsEmpty => (_data == null) || (_size == 0);
|
||||
/// <summary>
|
||||
/// Bytes memory buffer
|
||||
/// </summary>
|
||||
public byte[] Data => _data;
|
||||
/// <summary>
|
||||
/// Bytes memory buffer capacity
|
||||
/// </summary>
|
||||
public long Capacity => _data.Length;
|
||||
/// <summary>
|
||||
/// Bytes memory buffer size
|
||||
/// </summary>
|
||||
public long Size => _size;
|
||||
/// <summary>
|
||||
/// Bytes memory buffer offset
|
||||
/// </summary>
|
||||
public long Offset => _offset;
|
||||
|
||||
/// <summary>
|
||||
/// Buffer indexer operator
|
||||
/// </summary>
|
||||
public byte this[int index] => _data[index];
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new expandable buffer with zero capacity
|
||||
/// </summary>
|
||||
public Buffer() { _data = new byte[0]; _size = 0; _offset = 0; }
|
||||
/// <summary>
|
||||
/// Initialize a new expandable buffer with the given capacity
|
||||
/// </summary>
|
||||
public Buffer(long capacity) { _data = new byte[capacity]; _size = 0; _offset = 0; }
|
||||
/// <summary>
|
||||
/// Initialize a new expandable buffer with the given data
|
||||
/// </summary>
|
||||
public Buffer(byte[] data) { _data = data; _size = data.Length; _offset = 0; }
|
||||
|
||||
#region Memory buffer methods
|
||||
|
||||
/// <summary>
|
||||
/// Get string from the current buffer
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return ExtractString(0, _size);
|
||||
}
|
||||
|
||||
// Clear the current buffer and its offset
|
||||
public void Clear()
|
||||
{
|
||||
_size = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the string from buffer of the given offset and size
|
||||
/// </summary>
|
||||
public string ExtractString(long offset, long size)
|
||||
{
|
||||
Debug.Assert(((offset + size) <= Size), "Invalid offset & size!");
|
||||
if ((offset + size) > Size)
|
||||
throw new ArgumentException("Invalid offset & size!", nameof(offset));
|
||||
|
||||
return Encoding.UTF8.GetString(_data, (int)offset, (int)size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove the buffer of the given offset and size
|
||||
/// </summary>
|
||||
public void Remove(long offset, long size)
|
||||
{
|
||||
Debug.Assert(((offset + size) <= Size), "Invalid offset & size!");
|
||||
if ((offset + size) > Size)
|
||||
throw new ArgumentException("Invalid offset & size!", nameof(offset));
|
||||
|
||||
Array.Copy(_data, offset + size, _data, offset, _size - size - offset);
|
||||
_size -= size;
|
||||
if (_offset >= (offset + size))
|
||||
_offset -= size;
|
||||
else if (_offset >= offset)
|
||||
{
|
||||
_offset -= _offset - offset;
|
||||
if (_offset > Size)
|
||||
_offset = Size;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reserve the buffer of the given capacity
|
||||
/// </summary>
|
||||
public void Reserve(long capacity)
|
||||
{
|
||||
Debug.Assert((capacity >= 0), "Invalid reserve capacity!");
|
||||
if (capacity < 0)
|
||||
throw new ArgumentException("Invalid reserve capacity!", nameof(capacity));
|
||||
|
||||
if (capacity > Capacity)
|
||||
{
|
||||
byte[] data = new byte[Math.Max(capacity, 2 * Capacity)];
|
||||
Array.Copy(_data, 0, data, 0, _size);
|
||||
_data = data;
|
||||
}
|
||||
}
|
||||
|
||||
// Resize the current buffer
|
||||
public void Resize(long size)
|
||||
{
|
||||
Reserve(size);
|
||||
_size = size;
|
||||
if (_offset > _size)
|
||||
_offset = _size;
|
||||
}
|
||||
|
||||
// Shift the current buffer offset
|
||||
public void Shift(long offset) { _offset += offset; }
|
||||
// Unshift the current buffer offset
|
||||
public void Unshift(long offset) { _offset -= offset; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Buffer I/O methods
|
||||
|
||||
/// <summary>
|
||||
/// Append the given buffer
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to append</param>
|
||||
/// <returns>Count of append bytes</returns>
|
||||
public long Append(byte[] buffer)
|
||||
{
|
||||
Reserve(_size + buffer.Length);
|
||||
Array.Copy(buffer, 0, _data, _size, buffer.Length);
|
||||
_size += buffer.Length;
|
||||
return buffer.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append the given buffer fragment
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to append</param>
|
||||
/// <param name="offset">Buffer offset</param>
|
||||
/// <param name="size">Buffer size</param>
|
||||
/// <returns>Count of append bytes</returns>
|
||||
public long Append(byte[] buffer, long offset, long size)
|
||||
{
|
||||
Reserve(_size + size);
|
||||
Array.Copy(buffer, offset, _data, _size, size);
|
||||
_size += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append the given text in UTF-8 encoding
|
||||
/// </summary>
|
||||
/// <param name="text">Text to append</param>
|
||||
/// <returns>Count of append bytes</returns>
|
||||
public long Append(string text)
|
||||
{
|
||||
Reserve(_size + Encoding.UTF8.GetMaxByteCount(text.Length));
|
||||
long result = Encoding.UTF8.GetBytes(text, 0, text.Length, _data, (int)_size);
|
||||
_size += result;
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
525
Plugins/Siemens/B24SiemensPlugin/TCP/TcpServer.cs
Normal file
525
Plugins/Siemens/B24SiemensPlugin/TCP/TcpServer.cs
Normal file
@@ -0,0 +1,525 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace B24SiemensPlugin.TCP
|
||||
{
|
||||
/// <summary>
|
||||
/// TCP server is used to connect, disconnect and manage TCP sessions
|
||||
/// </summary>
|
||||
/// <remarks>Thread-safe</remarks>
|
||||
public class TcpServer : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize TCP server with a given IP address and port number
|
||||
/// </summary>
|
||||
/// <param name="address">IP address</param>
|
||||
/// <param name="port">Port number</param>
|
||||
public TcpServer(IPAddress address, int port) : this(new IPEndPoint(address, port)) {}
|
||||
/// <summary>
|
||||
/// Initialize TCP server with a given IP address and port number
|
||||
/// </summary>
|
||||
/// <param name="address">IP address</param>
|
||||
/// <param name="port">Port number</param>
|
||||
public TcpServer(string address, int port) : this(new IPEndPoint(IPAddress.Parse(address), port)) {}
|
||||
/// <summary>
|
||||
/// Initialize TCP server with a given IP endpoint
|
||||
/// </summary>
|
||||
/// <param name="endpoint">IP endpoint</param>
|
||||
public TcpServer(IPEndPoint endpoint)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Endpoint = endpoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server Id
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// IP endpoint
|
||||
/// </summary>
|
||||
public IPEndPoint Endpoint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of sessions connected to the server
|
||||
/// </summary>
|
||||
public long ConnectedSessions { get { return Sessions.Count; } }
|
||||
/// <summary>
|
||||
/// Number of bytes pending sent by the server
|
||||
/// </summary>
|
||||
public long BytesPending { get { return _bytesPending; } }
|
||||
/// <summary>
|
||||
/// Number of bytes sent by the server
|
||||
/// </summary>
|
||||
public long BytesSent { get { return _bytesSent; } }
|
||||
/// <summary>
|
||||
/// Number of bytes received by the server
|
||||
/// </summary>
|
||||
public long BytesReceived { get { return _bytesReceived; } }
|
||||
|
||||
/// <summary>
|
||||
/// Option: acceptor backlog size
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option will set the listening socket's backlog size
|
||||
/// </remarks>
|
||||
public int OptionAcceptorBacklog { get; set; } = 1024;
|
||||
/// <summary>
|
||||
/// Option: dual mode socket
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Specifies whether the Socket is a dual-mode socket used for both IPv4 and IPv6.
|
||||
/// Will work only if socket is bound on IPv6 address.
|
||||
/// </remarks>
|
||||
public bool OptionDualMode { get; set; }
|
||||
/// <summary>
|
||||
/// Option: keep alive
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option will setup SO_KEEPALIVE if the OS support this feature
|
||||
/// </remarks>
|
||||
public bool OptionKeepAlive { get; set; }
|
||||
/// <summary>
|
||||
/// Option: no delay
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option will enable/disable Nagle's algorithm for TCP protocol
|
||||
/// </remarks>
|
||||
public bool OptionNoDelay { get; set; }
|
||||
/// <summary>
|
||||
/// Option: reuse address
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option will enable/disable SO_REUSEADDR if the OS support this feature
|
||||
/// </remarks>
|
||||
public bool OptionReuseAddress { get; set; }
|
||||
/// <summary>
|
||||
/// Option: enables a socket to be bound for exclusive access
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option will enable/disable SO_EXCLUSIVEADDRUSE if the OS support this feature
|
||||
/// </remarks>
|
||||
public bool OptionExclusiveAddressUse { get; set; }
|
||||
/// <summary>
|
||||
/// Option: receive buffer size
|
||||
/// </summary>
|
||||
public int OptionReceiveBufferSize { get; set; } = 8192;
|
||||
/// <summary>
|
||||
/// Option: send buffer size
|
||||
/// </summary>
|
||||
public int OptionSendBufferSize { get; set; } = 8192;
|
||||
|
||||
#region Start/Stop server
|
||||
|
||||
// Server acceptor
|
||||
private Socket _acceptorSocket;
|
||||
private SocketAsyncEventArgs _acceptorEventArg;
|
||||
|
||||
// Server statistic
|
||||
internal long _bytesPending;
|
||||
internal long _bytesSent;
|
||||
internal long _bytesReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Is the server started?
|
||||
/// </summary>
|
||||
public bool IsStarted { get; private set; }
|
||||
/// <summary>
|
||||
/// Is the server accepting new clients?
|
||||
/// </summary>
|
||||
public bool IsAccepting { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new socket object
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Method may be override if you need to prepare some specific socket object in your implementation.
|
||||
/// </remarks>
|
||||
/// <returns>Socket object</returns>
|
||||
protected virtual Socket CreateSocket()
|
||||
{
|
||||
return new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start the server
|
||||
/// </summary>
|
||||
/// <returns>'true' if the server was successfully started, 'false' if the server failed to start</returns>
|
||||
public virtual bool Start()
|
||||
{
|
||||
Debug.Assert(!IsStarted, "TCP server is already started!");
|
||||
if (IsStarted)
|
||||
return false;
|
||||
|
||||
// Setup acceptor event arg
|
||||
_acceptorEventArg = new SocketAsyncEventArgs();
|
||||
_acceptorEventArg.Completed += OnAsyncCompleted;
|
||||
|
||||
// Create a new acceptor socket
|
||||
_acceptorSocket = CreateSocket();
|
||||
|
||||
// Update the acceptor socket disposed flag
|
||||
IsSocketDisposed = false;
|
||||
|
||||
// Apply the option: reuse address
|
||||
_acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, OptionReuseAddress);
|
||||
// Apply the option: exclusive address use
|
||||
_acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, OptionExclusiveAddressUse);
|
||||
// Apply the option: dual mode (this option must be applied before listening)
|
||||
if (_acceptorSocket.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
_acceptorSocket.DualMode = OptionDualMode;
|
||||
|
||||
// Bind the acceptor socket to the IP endpoint
|
||||
_acceptorSocket.Bind(Endpoint);
|
||||
// Refresh the endpoint property based on the actual endpoint created
|
||||
Endpoint = (IPEndPoint)_acceptorSocket.LocalEndPoint;
|
||||
// Start listen to the acceptor socket with the given accepting backlog size
|
||||
_acceptorSocket.Listen(OptionAcceptorBacklog);
|
||||
|
||||
// Reset statistic
|
||||
_bytesPending = 0;
|
||||
_bytesSent = 0;
|
||||
_bytesReceived = 0;
|
||||
|
||||
// Update the started flag
|
||||
IsStarted = true;
|
||||
|
||||
// Call the server started handler
|
||||
OnStarted();
|
||||
|
||||
// Perform the first server accept
|
||||
IsAccepting = true;
|
||||
StartAccept(_acceptorEventArg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop the server
|
||||
/// </summary>
|
||||
/// <returns>'true' if the server was successfully stopped, 'false' if the server is already stopped</returns>
|
||||
public virtual bool Stop()
|
||||
{
|
||||
Debug.Assert(IsStarted, "TCP server is not started!");
|
||||
if (!IsStarted)
|
||||
return false;
|
||||
|
||||
// Stop accepting new clients
|
||||
IsAccepting = false;
|
||||
|
||||
// Reset acceptor event arg
|
||||
_acceptorEventArg.Completed -= OnAsyncCompleted;
|
||||
|
||||
// Close the acceptor socket
|
||||
_acceptorSocket.Close();
|
||||
|
||||
// Dispose the acceptor socket
|
||||
_acceptorSocket.Dispose();
|
||||
|
||||
// Dispose event arguments
|
||||
_acceptorEventArg.Dispose();
|
||||
|
||||
// Update the acceptor socket disposed flag
|
||||
IsSocketDisposed = true;
|
||||
|
||||
// Disconnect all sessions
|
||||
DisconnectAll();
|
||||
|
||||
// Update the started flag
|
||||
IsStarted = false;
|
||||
|
||||
// Call the server stopped handler
|
||||
OnStopped();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restart the server
|
||||
/// </summary>
|
||||
/// <returns>'true' if the server was successfully restarted, 'false' if the server failed to restart</returns>
|
||||
public virtual bool Restart()
|
||||
{
|
||||
if (!Stop())
|
||||
return false;
|
||||
|
||||
while (IsStarted)
|
||||
Thread.Yield();
|
||||
|
||||
return Start();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Accepting clients
|
||||
|
||||
/// <summary>
|
||||
/// Start accept a new client connection
|
||||
/// </summary>
|
||||
private void StartAccept(SocketAsyncEventArgs e)
|
||||
{
|
||||
// Socket must be cleared since the context object is being reused
|
||||
e.AcceptSocket = null;
|
||||
|
||||
// Async accept a new client connection
|
||||
if (!_acceptorSocket.AcceptAsync(e))
|
||||
ProcessAccept(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process accepted client connection
|
||||
/// </summary>
|
||||
private void ProcessAccept(SocketAsyncEventArgs e)
|
||||
{
|
||||
if (e.SocketError == SocketError.Success)
|
||||
{
|
||||
// Create a new session to register
|
||||
var session = CreateSession();
|
||||
|
||||
// Register the session
|
||||
RegisterSession(session);
|
||||
|
||||
// Connect new session
|
||||
session.Connect(e.AcceptSocket);
|
||||
}
|
||||
else
|
||||
SendError(e.SocketError);
|
||||
|
||||
// Accept the next client connection
|
||||
if (IsAccepting)
|
||||
StartAccept(e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is the callback method associated with Socket.AcceptAsync()
|
||||
/// operations and is invoked when an accept operation is complete
|
||||
/// </summary>
|
||||
private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
ProcessAccept(e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Session factory
|
||||
|
||||
/// <summary>
|
||||
/// Create TCP session factory method
|
||||
/// </summary>
|
||||
/// <returns>TCP session</returns>
|
||||
protected virtual TcpSession CreateSession() { return new TcpSession(this); }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Session management
|
||||
|
||||
// Server sessions
|
||||
protected readonly ConcurrentDictionary<Guid, TcpSession> Sessions = new ConcurrentDictionary<Guid, TcpSession>();
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect all connected sessions
|
||||
/// </summary>
|
||||
/// <returns>'true' if all sessions were successfully disconnected, 'false' if the server is not started</returns>
|
||||
public virtual bool DisconnectAll()
|
||||
{
|
||||
if (!IsStarted)
|
||||
return false;
|
||||
|
||||
// Disconnect all sessions
|
||||
foreach (var session in Sessions.Values)
|
||||
session.Disconnect();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find a session with a given Id
|
||||
/// </summary>
|
||||
/// <param name="id">Session Id</param>
|
||||
/// <returns>Session with a given Id or null if the session it not connected</returns>
|
||||
public TcpSession FindSession(Guid id)
|
||||
{
|
||||
// Try to find the required session
|
||||
return Sessions.TryGetValue(id, out TcpSession result) ? result : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new session
|
||||
/// </summary>
|
||||
/// <param name="session">Session to register</param>
|
||||
internal void RegisterSession(TcpSession session)
|
||||
{
|
||||
// Register a new session
|
||||
Sessions.TryAdd(session.Id, session);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregister session by Id
|
||||
/// </summary>
|
||||
/// <param name="id">Session Id</param>
|
||||
internal void UnregisterSession(Guid id)
|
||||
{
|
||||
// Unregister session by Id
|
||||
Sessions.TryRemove(id, out TcpSession temp);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multicasting
|
||||
|
||||
/// <summary>
|
||||
/// Multicast data to all connected sessions
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to multicast</param>
|
||||
/// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
|
||||
public virtual bool Multicast(byte[] buffer) { return Multicast(buffer, 0, buffer.Length); }
|
||||
|
||||
/// <summary>
|
||||
/// Multicast data to all connected clients
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to multicast</param>
|
||||
/// <param name="offset">Buffer offset</param>
|
||||
/// <param name="size">Buffer size</param>
|
||||
/// <returns>'true' if the data was successfully multicasted, 'false' if the data was not multicasted</returns>
|
||||
public virtual bool Multicast(byte[] buffer, long offset, long size)
|
||||
{
|
||||
if (!IsStarted)
|
||||
return false;
|
||||
|
||||
if (size == 0)
|
||||
return true;
|
||||
|
||||
// Multicast data to all sessions
|
||||
foreach (var session in Sessions.Values)
|
||||
session.SendAsync(buffer, offset, size);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multicast text to all connected clients
|
||||
/// </summary>
|
||||
/// <param name="text">Text string to multicast</param>
|
||||
/// <returns>'true' if the text was successfully multicasted, 'false' if the text was not multicasted</returns>
|
||||
public virtual bool Multicast(string text) { return Multicast(Encoding.UTF8.GetBytes(text)); }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Server handlers
|
||||
|
||||
/// <summary>
|
||||
/// Handle server started notification
|
||||
/// </summary>
|
||||
protected virtual void OnStarted() {}
|
||||
/// <summary>
|
||||
/// Handle server stopped notification
|
||||
/// </summary>
|
||||
protected virtual void OnStopped() {}
|
||||
|
||||
/// <summary>
|
||||
/// Handle session connected notification
|
||||
/// </summary>
|
||||
/// <param name="session">Connected session</param>
|
||||
protected virtual void OnConnected(TcpSession session) {}
|
||||
/// <summary>
|
||||
/// Handle session disconnected notification
|
||||
/// </summary>
|
||||
/// <param name="session">Disconnected session</param>
|
||||
protected virtual void OnDisconnected(TcpSession session) {}
|
||||
|
||||
/// <summary>
|
||||
/// Handle error notification
|
||||
/// </summary>
|
||||
/// <param name="error">Socket error code</param>
|
||||
protected virtual void OnError(SocketError error) {}
|
||||
|
||||
internal void OnConnectedInternal(TcpSession session) { OnConnected(session); }
|
||||
internal void OnDisconnectedInternal(TcpSession session) { OnDisconnected(session); }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error handling
|
||||
|
||||
/// <summary>
|
||||
/// Send error notification
|
||||
/// </summary>
|
||||
/// <param name="error">Socket error code</param>
|
||||
private void SendError(SocketError error)
|
||||
{
|
||||
// Skip disconnect errors
|
||||
if ((error == SocketError.ConnectionAborted) ||
|
||||
(error == SocketError.ConnectionRefused) ||
|
||||
(error == SocketError.ConnectionReset) ||
|
||||
(error == SocketError.OperationAborted) ||
|
||||
(error == SocketError.Shutdown))
|
||||
return;
|
||||
|
||||
OnError(error);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable implementation
|
||||
|
||||
/// <summary>
|
||||
/// Disposed flag
|
||||
/// </summary>
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Acceptor socket disposed flag
|
||||
/// </summary>
|
||||
public bool IsSocketDisposed { get; private set; } = true;
|
||||
|
||||
// Implement IDisposable.
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposingManagedResources)
|
||||
{
|
||||
// The idea here is that Dispose(Boolean) knows whether it is
|
||||
// being called to do explicit cleanup (the Boolean is true)
|
||||
// versus being called due to a garbage collection (the Boolean
|
||||
// is false). This distinction is useful because, when being
|
||||
// disposed explicitly, the Dispose(Boolean) method can safely
|
||||
// execute code using reference type fields that refer to other
|
||||
// objects knowing for sure that these other objects have not been
|
||||
// finalized or disposed of yet. When the Boolean is false,
|
||||
// the Dispose(Boolean) method should not execute code that
|
||||
// refer to reference type fields because those objects may
|
||||
// have already been finalized."
|
||||
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposingManagedResources)
|
||||
{
|
||||
// Dispose managed resources here...
|
||||
Stop();
|
||||
}
|
||||
|
||||
// Dispose unmanaged resources here...
|
||||
|
||||
// Set large fields to null here...
|
||||
|
||||
// Mark as disposed.
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use C# destructor syntax for finalization code.
|
||||
~TcpServer()
|
||||
{
|
||||
// Simply call Dispose(false).
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
722
Plugins/Siemens/B24SiemensPlugin/TCP/TcpSession.cs
Normal file
722
Plugins/Siemens/B24SiemensPlugin/TCP/TcpSession.cs
Normal file
@@ -0,0 +1,722 @@
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
|
||||
namespace B24SiemensPlugin.TCP
|
||||
{
|
||||
/// <summary>
|
||||
/// TCP session is used to read and write data from the connected TCP client
|
||||
/// </summary>
|
||||
/// <remarks>Thread-safe</remarks>
|
||||
public class TcpSession : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Initialize the session with a given server
|
||||
/// </summary>
|
||||
/// <param name="server">TCP server</param>
|
||||
public TcpSession(TcpServer server)
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
Server = server;
|
||||
OptionReceiveBufferSize = server.OptionReceiveBufferSize;
|
||||
OptionSendBufferSize = server.OptionSendBufferSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Session Id
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Server
|
||||
/// </summary>
|
||||
public TcpServer Server { get; }
|
||||
/// <summary>
|
||||
/// Socket
|
||||
/// </summary>
|
||||
public Socket Socket { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes pending sent by the session
|
||||
/// </summary>
|
||||
public long BytesPending { get; private set; }
|
||||
/// <summary>
|
||||
/// Number of bytes sending by the session
|
||||
/// </summary>
|
||||
public long BytesSending { get; private set; }
|
||||
/// <summary>
|
||||
/// Number of bytes sent by the session
|
||||
/// </summary>
|
||||
public long BytesSent { get; private set; }
|
||||
/// <summary>
|
||||
/// Number of bytes received by the session
|
||||
/// </summary>
|
||||
public long BytesReceived { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Option: receive buffer size
|
||||
/// </summary>
|
||||
public int OptionReceiveBufferSize { get; set; } = 8192;
|
||||
/// <summary>
|
||||
/// Option: send buffer size
|
||||
/// </summary>
|
||||
public int OptionSendBufferSize { get; set; } = 8192;
|
||||
|
||||
#region Connect/Disconnect session
|
||||
|
||||
/// <summary>
|
||||
/// Is the session connected?
|
||||
/// </summary>
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connect the session
|
||||
/// </summary>
|
||||
/// <param name="socket">Session socket</param>
|
||||
internal void Connect(Socket socket)
|
||||
{
|
||||
Socket = socket;
|
||||
|
||||
// Update the session socket disposed flag
|
||||
IsSocketDisposed = false;
|
||||
|
||||
// Setup buffers
|
||||
_receiveBuffer = new Buffer();
|
||||
_sendBufferMain = new Buffer();
|
||||
_sendBufferFlush = new Buffer();
|
||||
|
||||
// Setup event args
|
||||
_receiveEventArg = new SocketAsyncEventArgs();
|
||||
_receiveEventArg.Completed += OnAsyncCompleted;
|
||||
_sendEventArg = new SocketAsyncEventArgs();
|
||||
_sendEventArg.Completed += OnAsyncCompleted;
|
||||
|
||||
// Apply the option: keep alive
|
||||
if (Server.OptionKeepAlive)
|
||||
Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||
// Apply the option: no delay
|
||||
if (Server.OptionNoDelay)
|
||||
Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
|
||||
|
||||
// Prepare receive & send buffers
|
||||
_receiveBuffer.Reserve(OptionReceiveBufferSize);
|
||||
_sendBufferMain.Reserve(OptionSendBufferSize);
|
||||
_sendBufferFlush.Reserve(OptionSendBufferSize);
|
||||
|
||||
// Reset statistic
|
||||
BytesPending = 0;
|
||||
BytesSending = 0;
|
||||
BytesSent = 0;
|
||||
BytesReceived = 0;
|
||||
|
||||
// Update the connected flag
|
||||
IsConnected = true;
|
||||
|
||||
// Try to receive something from the client
|
||||
TryReceive();
|
||||
|
||||
// Call the session connected handler
|
||||
OnConnected();
|
||||
|
||||
// Call the session connected handler in the server
|
||||
Server.OnConnectedInternal(this);
|
||||
|
||||
// Call the empty send buffer handler
|
||||
if (_sendBufferMain.IsEmpty)
|
||||
OnEmpty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect the session
|
||||
/// </summary>
|
||||
/// <returns>'true' if the section was successfully disconnected, 'false' if the section is already disconnected</returns>
|
||||
public virtual bool Disconnect()
|
||||
{
|
||||
if (!IsConnected)
|
||||
return false;
|
||||
|
||||
// Reset event args
|
||||
_receiveEventArg.Completed -= OnAsyncCompleted;
|
||||
_sendEventArg.Completed -= OnAsyncCompleted;
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
// Shutdown the socket associated with the client
|
||||
Socket.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch (SocketException) {}
|
||||
|
||||
// Close the session socket
|
||||
Socket.Close();
|
||||
|
||||
// Dispose the session socket
|
||||
Socket.Dispose();
|
||||
|
||||
// Dispose event arguments
|
||||
_receiveEventArg.Dispose();
|
||||
_sendEventArg.Dispose();
|
||||
|
||||
// Update the session socket disposed flag
|
||||
IsSocketDisposed = true;
|
||||
}
|
||||
catch (ObjectDisposedException) {}
|
||||
|
||||
// Update the connected flag
|
||||
IsConnected = false;
|
||||
|
||||
// Update sending/receiving flags
|
||||
_receiving = false;
|
||||
_sending = false;
|
||||
|
||||
// Clear send/receive buffers
|
||||
ClearBuffers();
|
||||
|
||||
// Call the session disconnected handler
|
||||
OnDisconnected();
|
||||
|
||||
// Call the session disconnected handler in the server
|
||||
Server.OnDisconnectedInternal(this);
|
||||
|
||||
// Unregister session
|
||||
Server.UnregisterSession(Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Send/Recieve data
|
||||
|
||||
// Receive buffer
|
||||
private bool _receiving;
|
||||
private Buffer _receiveBuffer;
|
||||
private SocketAsyncEventArgs _receiveEventArg;
|
||||
// Send buffer
|
||||
private readonly object _sendLock = new object();
|
||||
private bool _sending;
|
||||
private Buffer _sendBufferMain;
|
||||
private Buffer _sendBufferFlush;
|
||||
private SocketAsyncEventArgs _sendEventArg;
|
||||
private long _sendBufferFlushOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Send data to the client (synchronous)
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to send</param>
|
||||
/// <returns>Size of sent data</returns>
|
||||
public virtual long Send(byte[] buffer) { return Send(buffer, 0, buffer.Length); }
|
||||
|
||||
/// <summary>
|
||||
/// Send data to the client (synchronous)
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to send</param>
|
||||
/// <param name="offset">Buffer offset</param>
|
||||
/// <param name="size">Buffer size</param>
|
||||
/// <returns>Size of sent data</returns>
|
||||
public virtual long Send(byte[] buffer, long offset, long size)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return 0;
|
||||
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
// Sent data to the client
|
||||
long sent = Socket.Send(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec);
|
||||
if (sent > 0)
|
||||
{
|
||||
// Update statistic
|
||||
BytesSent += sent;
|
||||
Interlocked.Add(ref Server._bytesSent, size);
|
||||
|
||||
// Call the buffer sent handler
|
||||
OnSent(sent, BytesPending + BytesSending);
|
||||
}
|
||||
|
||||
// Check for socket error
|
||||
if (ec != SocketError.Success)
|
||||
{
|
||||
SendError(ec);
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send text to the client (synchronous)
|
||||
/// </summary>
|
||||
/// <param name="text">Text string to send</param>
|
||||
/// <returns>Size of sent data</returns>
|
||||
public virtual long Send(string text) { return Send(Encoding.UTF8.GetBytes(text)); }
|
||||
|
||||
/// <summary>
|
||||
/// Send data to the client (asynchronous)
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to send</param>
|
||||
/// <returns>'true' if the data was successfully sent, 'false' if the session is not connected</returns>
|
||||
public virtual bool SendAsync(byte[] buffer) { return SendAsync(buffer, 0, buffer.Length); }
|
||||
|
||||
/// <summary>
|
||||
/// Send data to the client (asynchronous)
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to send</param>
|
||||
/// <param name="offset">Buffer offset</param>
|
||||
/// <param name="size">Buffer size</param>
|
||||
/// <returns>'true' if the data was successfully sent, 'false' if the session is not connected</returns>
|
||||
public virtual bool SendAsync(byte[] buffer, long offset, long size)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return false;
|
||||
|
||||
if (size == 0)
|
||||
return true;
|
||||
|
||||
lock (_sendLock)
|
||||
{
|
||||
// Fill the main send buffer
|
||||
_sendBufferMain.Append(buffer, offset, size);
|
||||
|
||||
// Update statistic
|
||||
BytesPending = _sendBufferMain.Size;
|
||||
|
||||
// Avoid multiple send handlers
|
||||
if (_sending)
|
||||
return true;
|
||||
else
|
||||
_sending = true;
|
||||
|
||||
// Try to send the main buffer
|
||||
Task.Factory.StartNew(TrySend);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send text to the client (asynchronous)
|
||||
/// </summary>
|
||||
/// <param name="text">Text string to send</param>
|
||||
/// <returns>'true' if the text was successfully sent, 'false' if the session is not connected</returns>
|
||||
public virtual bool SendAsync(string text) { return SendAsync(Encoding.UTF8.GetBytes(text)); }
|
||||
|
||||
/// <summary>
|
||||
/// Receive data from the client (synchronous)
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to receive</param>
|
||||
/// <returns>Size of received data</returns>
|
||||
public virtual long Receive(byte[] buffer) { return Receive(buffer, 0, buffer.Length); }
|
||||
|
||||
/// <summary>
|
||||
/// Receive data from the client (synchronous)
|
||||
/// </summary>
|
||||
/// <param name="buffer">Buffer to receive</param>
|
||||
/// <param name="offset">Buffer offset</param>
|
||||
/// <param name="size">Buffer size</param>
|
||||
/// <returns>Size of received data</returns>
|
||||
public virtual long Receive(byte[] buffer, long offset, long size)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return 0;
|
||||
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
// Receive data from the client
|
||||
long received = Socket.Receive(buffer, (int)offset, (int)size, SocketFlags.None, out SocketError ec);
|
||||
if (received > 0)
|
||||
{
|
||||
// Update statistic
|
||||
BytesReceived += received;
|
||||
Interlocked.Add(ref Server._bytesReceived, received);
|
||||
|
||||
// Call the buffer received handler
|
||||
OnReceived(buffer, 0, received);
|
||||
}
|
||||
|
||||
// Check for socket error
|
||||
if (ec != SocketError.Success)
|
||||
{
|
||||
SendError(ec);
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
return received;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receive text from the client (synchronous)
|
||||
/// </summary>
|
||||
/// <param name="size">Text size to receive</param>
|
||||
/// <returns>Received text</returns>
|
||||
public virtual string Receive(long size)
|
||||
{
|
||||
var buffer = new byte[size];
|
||||
var length = Receive(buffer);
|
||||
return Encoding.UTF8.GetString(buffer, 0, (int)length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receive data from the client (asynchronous)
|
||||
/// </summary>
|
||||
public virtual void ReceiveAsync()
|
||||
{
|
||||
// Try to receive data from the client
|
||||
TryReceive();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to receive new data
|
||||
/// </summary>
|
||||
private void TryReceive()
|
||||
{
|
||||
if (_receiving)
|
||||
return;
|
||||
|
||||
if (!IsConnected)
|
||||
return;
|
||||
|
||||
bool process = true;
|
||||
|
||||
while (process)
|
||||
{
|
||||
process = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Async receive with the receive handler
|
||||
_receiving = true;
|
||||
_receiveEventArg.SetBuffer(_receiveBuffer.Data, 0, (int)_receiveBuffer.Capacity);
|
||||
if (!Socket.ReceiveAsync(_receiveEventArg))
|
||||
process = ProcessReceive(_receiveEventArg);
|
||||
}
|
||||
catch (ObjectDisposedException) {}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to send pending data
|
||||
/// </summary>
|
||||
private void TrySend()
|
||||
{
|
||||
if (!IsConnected)
|
||||
return;
|
||||
|
||||
bool empty = false;
|
||||
bool process = true;
|
||||
|
||||
while (process)
|
||||
{
|
||||
process = false;
|
||||
|
||||
lock (_sendLock)
|
||||
{
|
||||
// Is previous socket send in progress?
|
||||
if (_sendBufferFlush.IsEmpty)
|
||||
{
|
||||
// Swap flush and main buffers
|
||||
_sendBufferFlush = Interlocked.Exchange(ref _sendBufferMain, _sendBufferFlush);
|
||||
_sendBufferFlushOffset = 0;
|
||||
|
||||
// Update statistic
|
||||
BytesPending = 0;
|
||||
BytesSending += _sendBufferFlush.Size;
|
||||
|
||||
// Check if the flush buffer is empty
|
||||
if (_sendBufferFlush.IsEmpty)
|
||||
{
|
||||
// Need to call empty send buffer handler
|
||||
empty = true;
|
||||
|
||||
// End sending process
|
||||
_sending = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the empty send buffer handler
|
||||
if (empty)
|
||||
{
|
||||
OnEmpty();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Async write with the write handler
|
||||
_sendEventArg.SetBuffer(_sendBufferFlush.Data, (int)_sendBufferFlushOffset, (int)(_sendBufferFlush.Size - _sendBufferFlushOffset));
|
||||
if (!Socket.SendAsync(_sendEventArg))
|
||||
process = ProcessSend(_sendEventArg);
|
||||
}
|
||||
catch (ObjectDisposedException) {}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear send/receive buffers
|
||||
/// </summary>
|
||||
private void ClearBuffers()
|
||||
{
|
||||
lock (_sendLock)
|
||||
{
|
||||
// Clear send buffers
|
||||
_sendBufferMain.Clear();
|
||||
_sendBufferFlush.Clear();
|
||||
_sendBufferFlushOffset= 0;
|
||||
|
||||
// Update statistic
|
||||
BytesPending = 0;
|
||||
BytesSending = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IO processing
|
||||
|
||||
/// <summary>
|
||||
/// This method is called whenever a receive or send operation is completed on a socket
|
||||
/// </summary>
|
||||
private void OnAsyncCompleted(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
// Determine which type of operation just completed and call the associated handler
|
||||
switch (e.LastOperation)
|
||||
{
|
||||
case SocketAsyncOperation.Receive:
|
||||
if (ProcessReceive(e))
|
||||
TryReceive();
|
||||
break;
|
||||
case SocketAsyncOperation.Send:
|
||||
if (ProcessSend(e))
|
||||
TrySend();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("The last operation completed on the socket was not a receive or send");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is invoked when an asynchronous receive operation completes
|
||||
/// </summary>
|
||||
private bool ProcessReceive(SocketAsyncEventArgs e)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return false;
|
||||
|
||||
long size = e.BytesTransferred;
|
||||
|
||||
// Received some data from the client
|
||||
if (size > 0)
|
||||
{
|
||||
// Update statistic
|
||||
BytesReceived += size;
|
||||
Interlocked.Add(ref Server._bytesReceived, size);
|
||||
|
||||
// Call the buffer received handler
|
||||
OnReceived(_receiveBuffer.Data, 0, size);
|
||||
|
||||
// If the receive buffer is full increase its size
|
||||
if (_receiveBuffer.Capacity == size)
|
||||
_receiveBuffer.Reserve(2 * size);
|
||||
}
|
||||
|
||||
_receiving = false;
|
||||
|
||||
// Try to receive again if the session is valid
|
||||
if (e.SocketError == SocketError.Success)
|
||||
{
|
||||
// If zero is returned from a read operation, the remote end has closed the connection
|
||||
if (size > 0)
|
||||
return true;
|
||||
else
|
||||
Disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
SendError(e.SocketError);
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is invoked when an asynchronous send operation completes
|
||||
/// </summary>
|
||||
private bool ProcessSend(SocketAsyncEventArgs e)
|
||||
{
|
||||
if (!IsConnected)
|
||||
return false;
|
||||
|
||||
long size = e.BytesTransferred;
|
||||
|
||||
// Send some data to the client
|
||||
if (size > 0)
|
||||
{
|
||||
// Update statistic
|
||||
BytesSending -= size;
|
||||
BytesSent += size;
|
||||
Interlocked.Add(ref Server._bytesSent, size);
|
||||
|
||||
// Increase the flush buffer offset
|
||||
_sendBufferFlushOffset += size;
|
||||
|
||||
// Successfully send the whole flush buffer
|
||||
if (_sendBufferFlushOffset == _sendBufferFlush.Size)
|
||||
{
|
||||
// Clear the flush buffer
|
||||
_sendBufferFlush.Clear();
|
||||
_sendBufferFlushOffset = 0;
|
||||
}
|
||||
|
||||
// Call the buffer sent handler
|
||||
OnSent(size, BytesPending + BytesSending);
|
||||
}
|
||||
|
||||
// Try to send again if the session is valid
|
||||
if (e.SocketError == SocketError.Success)
|
||||
return true;
|
||||
else
|
||||
{
|
||||
SendError(e.SocketError);
|
||||
Disconnect();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Session handlers
|
||||
|
||||
/// <summary>
|
||||
/// Handle client connected notification
|
||||
/// </summary>
|
||||
protected virtual void OnConnected() {}
|
||||
/// <summary>
|
||||
/// Handle client disconnected notification
|
||||
/// </summary>
|
||||
protected virtual void OnDisconnected() {}
|
||||
|
||||
/// <summary>
|
||||
/// Handle buffer received notification
|
||||
/// </summary>
|
||||
/// <param name="buffer">Received buffer</param>
|
||||
/// <param name="offset">Received buffer offset</param>
|
||||
/// <param name="size">Received buffer size</param>
|
||||
/// <remarks>
|
||||
/// Notification is called when another chunk of buffer was received from the client
|
||||
/// </remarks>
|
||||
protected virtual void OnReceived(byte[] buffer, long offset, long size) {}
|
||||
/// <summary>
|
||||
/// Handle buffer sent notification
|
||||
/// </summary>
|
||||
/// <param name="sent">Size of sent buffer</param>
|
||||
/// <param name="pending">Size of pending buffer</param>
|
||||
/// <remarks>
|
||||
/// Notification is called when another chunk of buffer was sent to the client.
|
||||
/// This handler could be used to send another buffer to the client for instance when the pending size is zero.
|
||||
/// </remarks>
|
||||
protected virtual void OnSent(long sent, long pending) {}
|
||||
|
||||
/// <summary>
|
||||
/// Handle empty send buffer notification
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Notification is called when the send buffer is empty and ready for a new data to send.
|
||||
/// This handler could be used to send another buffer to the client.
|
||||
/// </remarks>
|
||||
protected virtual void OnEmpty() {}
|
||||
|
||||
/// <summary>
|
||||
/// Handle error notification
|
||||
/// </summary>
|
||||
/// <param name="error">Socket error code</param>
|
||||
protected virtual void OnError(SocketError error) {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Error handling
|
||||
|
||||
/// <summary>
|
||||
/// Send error notification
|
||||
/// </summary>
|
||||
/// <param name="error">Socket error code</param>
|
||||
private void SendError(SocketError error)
|
||||
{
|
||||
// Skip disconnect errors
|
||||
if ((error == SocketError.ConnectionAborted) ||
|
||||
(error == SocketError.ConnectionRefused) ||
|
||||
(error == SocketError.ConnectionReset) ||
|
||||
(error == SocketError.OperationAborted) ||
|
||||
(error == SocketError.Shutdown))
|
||||
return;
|
||||
|
||||
OnError(error);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable implementation
|
||||
|
||||
/// <summary>
|
||||
/// Disposed flag
|
||||
/// </summary>
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Session socket disposed flag
|
||||
/// </summary>
|
||||
public bool IsSocketDisposed { get; private set; } = true;
|
||||
|
||||
// Implement IDisposable.
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposingManagedResources)
|
||||
{
|
||||
// The idea here is that Dispose(Boolean) knows whether it is
|
||||
// being called to do explicit cleanup (the Boolean is true)
|
||||
// versus being called due to a garbage collection (the Boolean
|
||||
// is false). This distinction is useful because, when being
|
||||
// disposed explicitly, the Dispose(Boolean) method can safely
|
||||
// execute code using reference type fields that refer to other
|
||||
// objects knowing for sure that these other objects have not been
|
||||
// finalized or disposed of yet. When the Boolean is false,
|
||||
// the Dispose(Boolean) method should not execute code that
|
||||
// refer to reference type fields because those objects may
|
||||
// have already been finalized."
|
||||
|
||||
if (!IsDisposed)
|
||||
{
|
||||
if (disposingManagedResources)
|
||||
{
|
||||
// Dispose managed resources here...
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
// Dispose unmanaged resources here...
|
||||
|
||||
// Set large fields to null here...
|
||||
|
||||
// Mark as disposed.
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use C# destructor syntax for finalization code.
|
||||
~TcpSession()
|
||||
{
|
||||
// Simply call Dispose(false).
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
132
Plugins/Siemens/tasks/prd-b24-siemens-emulator.md
Normal file
132
Plugins/Siemens/tasks/prd-b24-siemens-emulator.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Product Requirements Document: B24SiemensEmulator
|
||||
|
||||
## Introduction/Overview
|
||||
|
||||
The B24SiemensEmulator is a Windows Forms application that serves as a PLC simulator for testing the B24SiemensPlugin server. The emulator connects to the existing B24SiemensPlugin (which listens on TCP port 42010) and sends PLCPacket data every second, while displaying the response data received from the plugin. This tool enables developers and testers to simulate PLC behavior without requiring actual hardware.
|
||||
|
||||
**Goal:** Create a user-friendly PLC emulator that allows manual configuration of packet data and real-time monitoring of server responses for testing B24SiemensPlugin functionality.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Enable testing of B24SiemensPlugin without physical PLC hardware
|
||||
2. Provide intuitive UI for configuring PLCPacket field values
|
||||
3. Display real-time responses from the B24SiemensPlugin server
|
||||
4. Support saving and loading of packet configurations
|
||||
5. Maintain continuous communication with automatic reconnection handling
|
||||
|
||||
## User Stories
|
||||
|
||||
1. **As a developer**, I want to input custom PLCPacket field values through a Windows Forms interface so that I can test different scenarios with the B24SiemensPlugin.
|
||||
|
||||
2. **As a tester**, I want to see the latest response from the server displayed in real-time so that I can verify the plugin is processing packets correctly.
|
||||
|
||||
3. **As a developer**, I want to save my current packet configuration to a JSON file so that I can reuse test scenarios without re-entering data.
|
||||
|
||||
4. **As a tester**, I want the emulator to automatically reconnect when connection is lost so that testing can continue without manual intervention.
|
||||
|
||||
5. **As a developer**, I want to start and stop packet transmission manually so that I can control when testing occurs.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
### Core Communication
|
||||
1. The system must establish TCP connection to B24SiemensPlugin on port 42010
|
||||
2. The system must send PLCPacket data every 1 second when transmission is active
|
||||
3. The system must receive and display the echo response from the server
|
||||
4. The system must handle connection failures gracefully with automatic retry every 1 second
|
||||
|
||||
### User Interface - Input Fields (Editable)
|
||||
5. The system must provide input controls for all PLCPacket fields:
|
||||
- **Lifebit** (checkbox) - Connection status bit
|
||||
- **KameraStart** (checkbox) - Camera start command bit
|
||||
- **Materialnummer** (numeric input) - Material number (integer)
|
||||
- **ArtikelnummerBall** (numeric input) - Ball article number (integer)
|
||||
- **MaterialnummerFoil** (numeric input) - Foil material number (integer)
|
||||
- **Chargennummer** (text input, 5 characters max) - Charge number string
|
||||
|
||||
### User Interface - Response Display (Read-only)
|
||||
6. The system must display received response packet fields in read-only format:
|
||||
- All PLCPacket fields showing the latest response values
|
||||
- Fields must be clearly labeled and visually distinct from input fields
|
||||
|
||||
### Control Functions
|
||||
7. The system must provide a Start/Stop button to control packet transmission
|
||||
8. The system must provide Save Configuration button to export current field values to JSON
|
||||
9. The system must provide Load Configuration button to import field values from JSON file
|
||||
|
||||
### Error Handling & Status
|
||||
10. The system must display connection errors in red text on the form when connection fails
|
||||
11. The system must hide error messages when connection is successfully established
|
||||
12. The system must show connection status (Connected/Disconnected) clearly on the form
|
||||
|
||||
### Configuration Management
|
||||
13. The system must save packet field values to JSON file format
|
||||
14. The system must load packet field values from JSON file format
|
||||
15. The system must validate loaded JSON data and show error if invalid
|
||||
|
||||
## Non-Goals (Out of Scope)
|
||||
|
||||
1. **No modifications to B24SiemensPlugin** - The existing plugin must remain unchanged
|
||||
2. **No support for multiple simultaneous connections** - Single connection only
|
||||
3. **No logging or history of responses** - Only latest response is displayed
|
||||
4. **No advanced network configuration** - Uses hardcoded IP (localhost) and port (42010)
|
||||
5. **No custom packet intervals** - Fixed 1-second transmission rate
|
||||
6. **No protocol validation** - Assumes B24SiemensPlugin handles malformed packets
|
||||
7. **No visual connection status indicators** - Simple text-based status only
|
||||
|
||||
## Design Considerations
|
||||
|
||||
### Windows Forms Layout
|
||||
- **Left Panel:** Input controls for PLCPacket fields with clear labels
|
||||
- **Right Panel:** Read-only display of response packet fields
|
||||
- **Top Section:** Connection status and Start/Stop controls
|
||||
- **Bottom Section:** Save/Load configuration buttons and error message area
|
||||
|
||||
### Data Validation
|
||||
- Numeric fields should accept valid integer ranges
|
||||
- Chargennummer field should limit input to 5 ASCII characters
|
||||
- JSON file operations should include basic error handling
|
||||
|
||||
### Visual Design
|
||||
- Group related fields using GroupBox controls
|
||||
- Use consistent spacing and alignment
|
||||
- Error messages displayed in red text
|
||||
- Clear visual separation between input and response sections
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
### Framework & Dependencies
|
||||
- Target same .NET Framework version as B24SiemensPlugin project
|
||||
- Use Windows Forms for UI (System.Windows.Forms)
|
||||
- JSON serialization using Newtonsoft.Json or System.Text.Json
|
||||
- Async TCP client implementation for network communication
|
||||
|
||||
### PLCPacket Integration
|
||||
- Reference or copy PLCPacket class structure from B24SiemensPlugin
|
||||
- Implement same serialization/deserialization logic (19-byte binary format)
|
||||
- Handle big-endian byte order for integer fields
|
||||
|
||||
### Network Implementation
|
||||
- TCP client connecting to localhost:42010
|
||||
- Asynchronous send/receive operations
|
||||
- Timer-based packet transmission (1-second interval)
|
||||
- Proper connection cleanup and resource disposal
|
||||
|
||||
## Success Metrics
|
||||
|
||||
1. **Functional Success:** Emulator successfully connects to B24SiemensPlugin and exchanges packets
|
||||
2. **Usability Success:** Developer can configure and save test scenarios in under 2 minutes
|
||||
3. **Reliability Success:** Automatic reconnection works within 5 seconds of connection loss
|
||||
4. **Data Integrity Success:** All PLCPacket fields are correctly transmitted and received
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should the application remember the last used configuration file path?
|
||||
2. Do we need input validation ranges for numeric fields (min/max values)?
|
||||
3. Should there be a manual "Send Now" button in addition to automatic transmission?
|
||||
4. Is there a preferred location for saving configuration files (default directory)?
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Created:** 2025-07-25
|
||||
**Target Implementation:** B24SiemensEmulator Windows Forms Application
|
||||
52
Plugins/Siemens/tasks/tasks-prd-b24-siemens-emulator.md
Normal file
52
Plugins/Siemens/tasks/tasks-prd-b24-siemens-emulator.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Task List: B24SiemensEmulator Implementation
|
||||
|
||||
Based on the PRD for B24SiemensEmulator, here are the detailed tasks required to implement the feature:
|
||||
|
||||
## Relevant Files
|
||||
|
||||
- `B24SiemensEmulator/MainForm.cs` - Main Windows Forms interface with input/output panels and event handling
|
||||
- `B24SiemensEmulator/PLCPacket.cs` - PLCPacket data structure with serialization and validation methods
|
||||
- `B24SiemensEmulator/TcpClient.cs` - TCP client for connecting to B24SiemensPlugin with auto-reconnect
|
||||
- `B24SiemensEmulator/ConfigurationManager.cs` - JSON save/load functionality for packet configurations
|
||||
- `B24SiemensEmulator/Program.cs` - Application entry point that launches MainForm
|
||||
- `B24SiemensEmulator/B24SiemensEmulator.csproj` - Project file with Newtonsoft.Json dependency
|
||||
|
||||
### Notes
|
||||
|
||||
- The existing B24SiemensEmulator project structure will be used as the foundation
|
||||
- PLCPacket.cs should match the structure from B24SiemensPlugin exactly
|
||||
- TCP communication must use localhost:42010 to connect to B24SiemensPlugin
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1.0 Setup Project Structure and Dependencies
|
||||
- [x] 1.1 Verify existing B24SiemensEmulator project configuration
|
||||
- [x] 1.2 Add required NuGet packages (Newtonsoft.Json for JSON serialization)
|
||||
- [x] 1.3 Update project references and using statements
|
||||
- [x] 2.0 Implement PLCPacket Data Structure and Communication
|
||||
- [x] 2.1 Copy PLCPacket class from B24SiemensPlugin project
|
||||
- [x] 2.2 Implement PLCPacket serialization/deserialization methods
|
||||
- [x] 2.3 Add validation methods for PLCPacket fields
|
||||
- [x] 3.0 Create Windows Forms User Interface
|
||||
- [x] 3.1 Design main form layout with input and output panels
|
||||
- [x] 3.2 Add input controls for all PLCPacket fields (checkboxes, numeric inputs, text input)
|
||||
- [x] 3.3 Add read-only output controls to display response packet
|
||||
- [x] 3.4 Add control buttons (Start/Stop, Save/Load Config)
|
||||
- [x] 3.5 Add status display and error message areas
|
||||
- [x] 4.0 Implement TCP Client Network Communication
|
||||
- [x] 4.1 Create TCP client class for connecting to localhost:42010
|
||||
- [x] 4.2 Implement asynchronous packet sending every 1 second
|
||||
- [x] 4.3 Implement response packet receiving and parsing
|
||||
- [x] 4.4 Add connection error handling with automatic retry
|
||||
- [x] 4.5 Integrate network communication with UI controls
|
||||
- [x] 5.0 Add Configuration Management (JSON Save/Load)
|
||||
- [x] 5.1 Create configuration data model for PLCPacket fields
|
||||
- [x] 5.2 Implement JSON serialization for configuration saving
|
||||
- [x] 5.3 Implement JSON deserialization for configuration loading
|
||||
- [x] 5.4 Add file dialog integration for save/load operations
|
||||
- [x] 5.5 Add error handling and validation for JSON operations
|
||||
- [x] 6.0 Add Apply Button for Packet Configuration
|
||||
- [x] 6.1 Add Apply button to input section UI
|
||||
- [x] 6.2 Implement currentPacket storage separate from UI values
|
||||
- [x] 6.3 Update TCP client to use applied packet instead of live UI values
|
||||
- [x] 6.4 Add visual feedback for Apply button action
|
||||
Reference in New Issue
Block a user