recipe name filter

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

View File

@@ -0,0 +1,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

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

View 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 subtask 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 oneline 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 subtask is next.
6. After implementing a subtask, update the file and then pause for user approval.

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(dotnet add:*)"
],
"deny": []
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,142 @@
using System;
using System.IO;
using System.Windows.Forms;
using Newtonsoft.Json;
namespace B24SiemensEmulator
{
public class PacketConfiguration
{
public bool Lifebit { get; set; }
public bool KameraStart { get; set; }
public int Materialnummer { get; set; }
public int ArtikelnummerBall { get; set; }
public int MaterialnummerFoil { get; set; }
public string Chargennummer { get; set; } = string.Empty;
public static PacketConfiguration FromPLCPacket(PLCPacket packet)
{
return new PacketConfiguration
{
Lifebit = packet.Lifebit,
KameraStart = packet.KameraStart,
Materialnummer = packet.Materialnummer,
ArtikelnummerBall = packet.ArtikelnummerBall,
MaterialnummerFoil = packet.MaterialnummerFoil,
Chargennummer = packet.Chargennummer
};
}
public PLCPacket ToPLCPacket()
{
return new PLCPacket
{
Lifebit = Lifebit,
KameraStart = KameraStart,
Materialnummer = Materialnummer,
ArtikelnummerBall = ArtikelnummerBall,
MaterialnummerFoil = MaterialnummerFoil,
Chargennummer = PLCPacket.ValidateAndTrimChargennummer(Chargennummer)
};
}
}
public class ConfigurationManager
{
private const string DefaultFileName = "PLCPacketConfig.json";
private const string FileFilter = "JSON files (*.json)|*.json|All files (*.*)|*.*";
public static bool SaveConfiguration(PacketConfiguration config, string? filePath = null)
{
try
{
string path = filePath ?? GetSaveFilePath();
if (string.IsNullOrEmpty(path))
return false;
string json = JsonConvert.SerializeObject(config, Formatting.Indented);
File.WriteAllText(path, json);
return true;
}
catch (Exception ex)
{
MessageBox.Show($"Failed to save configuration: {ex.Message}", "Save Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
}
public static PacketConfiguration? LoadConfiguration(string? filePath = null)
{
try
{
string path = filePath ?? GetLoadFilePath();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
return null;
string json = File.ReadAllText(path);
var config = JsonConvert.DeserializeObject<PacketConfiguration>(json);
if (config == null)
{
MessageBox.Show("Invalid configuration file format.", "Load Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
// Validate loaded configuration
if (!PLCPacket.IsValidChargennummer(config.Chargennummer))
{
MessageBox.Show("Invalid Chargennummer in configuration file. It will be corrected.",
"Load Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
config.Chargennummer = PLCPacket.ValidateAndTrimChargennummer(config.Chargennummer);
}
return config;
}
catch (JsonException ex)
{
MessageBox.Show($"Failed to parse configuration file: {ex.Message}", "Load Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
catch (Exception ex)
{
MessageBox.Show($"Failed to load configuration: {ex.Message}", "Load Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return null;
}
}
private static string GetSaveFilePath()
{
using var saveDialog = new SaveFileDialog
{
Filter = FileFilter,
FileName = DefaultFileName,
Title = "Save PLC Packet Configuration"
};
return saveDialog.ShowDialog() == DialogResult.OK ? saveDialog.FileName : string.Empty;
}
private static string GetLoadFilePath()
{
using var openDialog = new OpenFileDialog
{
Filter = FileFilter,
Title = "Load PLC Packet Configuration"
};
return openDialog.ShowDialog() == DialogResult.OK ? openDialog.FileName : string.Empty;
}
public static bool ValidateConfiguration(PacketConfiguration config)
{
if (config == null)
return false;
return PLCPacket.IsValidChargennummer(config.Chargennummer);
}
}
}

View File

@@ -0,0 +1,607 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace B24SiemensEmulator
{
public partial class MainForm : Form
{
private GroupBox inputGroupBox;
private GroupBox outputGroupBox;
private GroupBox controlGroupBox;
private GroupBox statusGroupBox;
// Input controls
private CheckBox lifebitCheckBox;
private CheckBox kameraStartCheckBox;
private NumericUpDown materialnummerNumeric;
private NumericUpDown artikelnummerBallNumeric;
private NumericUpDown materialnummerFoilNumeric;
private TextBox chargennummerTextBox;
// Output controls (read-only)
private Label lifebitResponseLabel;
private Label kameraStartResponseLabel;
private Label materialnummerResponseLabel;
private Label artikelnummerBallResponseLabel;
private Label materialnummerFoilResponseLabel;
private Label chargennummerResponseLabel;
// Control buttons
private Button startStopButton;
private Button saveConfigButton;
private Button loadConfigButton;
private Button applyButton;
// Status controls
private Label connectionStatusLabel;
private Label errorMessageLabel;
private bool isRunning = false;
private SiemensTcpClient? tcpClient;
private PLCPacket currentPacket = new PLCPacket();
public MainForm()
{
InitializeComponent();
InitializeTcpClient();
WireUpEventHandlers();
// Initialize current packet with form default values
currentPacket = GetInputPacket();
}
private void InitializeTcpClient()
{
tcpClient = new SiemensTcpClient();
tcpClient.GetPacketToSend = GetCurrentPacket;
tcpClient.ConnectionStatusChanged += OnConnectionStatusChanged;
tcpClient.ResponseReceived += OnResponseReceived;
tcpClient.ErrorOccurred += OnErrorOccurred;
}
private void WireUpEventHandlers()
{
startStopButton.Click += OnStartStopButtonClick;
saveConfigButton.Click += OnSaveConfigButtonClick;
loadConfigButton.Click += OnLoadConfigButtonClick;
applyButton.Click += OnApplyButtonClick;
// Form closing event
this.FormClosing += OnFormClosing;
}
private async void OnStartStopButtonClick(object? sender, EventArgs e)
{
if (tcpClient == null) return;
if (!isRunning)
{
// Start
var connected = await tcpClient.ConnectAsync();
if (connected)
{
tcpClient.StartSending();
SetRunningState(true);
}
}
else
{
// Stop
tcpClient.StopSending();
await tcpClient.DisconnectAsync();
SetRunningState(false);
}
}
private void OnSaveConfigButtonClick(object? sender, EventArgs e)
{
var config = PacketConfiguration.FromPLCPacket(currentPacket);
bool saved = ConfigurationManager.SaveConfiguration(config);
if (saved)
{
MessageBox.Show("Current applied configuration saved successfully.", "Save Complete",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private void OnLoadConfigButtonClick(object? sender, EventArgs e)
{
var config = ConfigurationManager.LoadConfiguration();
if (config != null)
{
LoadConfigurationToForm(config);
MessageBox.Show("Configuration loaded successfully. Click Apply to use the loaded values.", "Load Complete",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private void OnApplyButtonClick(object? sender, EventArgs e)
{
currentPacket = GetInputPacket();
// Provide visual feedback by briefly changing the button text
applyButton.Text = "Applied!";
applyButton.BackColor = Color.LightGreen;
var timer = new System.Windows.Forms.Timer();
timer.Interval = 1000; // 1 second
timer.Tick += (s, args) =>
{
applyButton.Text = "Apply";
applyButton.BackColor = SystemColors.Control;
timer.Stop();
timer.Dispose();
};
timer.Start();
}
private void LoadConfigurationToForm(PacketConfiguration config)
{
lifebitCheckBox.Checked = config.Lifebit;
kameraStartCheckBox.Checked = config.KameraStart;
materialnummerNumeric.Value = config.Materialnummer;
artikelnummerBallNumeric.Value = config.ArtikelnummerBall;
materialnummerFoilNumeric.Value = config.MaterialnummerFoil;
chargennummerTextBox.Text = config.Chargennummer;
}
private void OnConnectionStatusChanged(bool connected)
{
UpdateConnectionStatus(connected);
if (connected)
{
HideErrorMessage();
}
}
private void OnResponseReceived(PLCPacket packet)
{
UpdateResponseDisplay(packet);
}
private void OnErrorOccurred(string error)
{
ShowErrorMessage(error);
}
private void OnFormClosing(object? sender, FormClosingEventArgs e)
{
tcpClient?.Dispose();
}
private void InitializeComponent()
{
this.SuspendLayout();
// Form properties
this.Text = "B24 Siemens PLC Emulator";
this.Size = new Size(800, 640);
this.FormBorderStyle = FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.StartPosition = FormStartPosition.CenterScreen;
// Status Group Box (Top)
statusGroupBox = new GroupBox
{
Text = "Status",
Location = new Point(12, 12),
Size = new Size(760, 80)
};
connectionStatusLabel = new Label
{
Text = "Disconnected",
Location = new Point(15, 25),
Size = new Size(200, 20),
Font = new Font("Microsoft Sans Serif", 9F, FontStyle.Bold)
};
errorMessageLabel = new Label
{
Text = "",
Location = new Point(15, 50),
Size = new Size(720, 20),
ForeColor = Color.Red,
Font = new Font("Microsoft Sans Serif", 8.25F)
};
statusGroupBox.Controls.Add(connectionStatusLabel);
statusGroupBox.Controls.Add(errorMessageLabel);
// Control Group Box
controlGroupBox = new GroupBox
{
Text = "Controls",
Location = new Point(12, 100),
Size = new Size(760, 60)
};
startStopButton = new Button
{
Text = "Start",
Location = new Point(15, 25),
Size = new Size(100, 30),
UseVisualStyleBackColor = true
};
saveConfigButton = new Button
{
Text = "Save Config",
Location = new Point(130, 25),
Size = new Size(100, 30),
UseVisualStyleBackColor = true
};
loadConfigButton = new Button
{
Text = "Load Config",
Location = new Point(245, 25),
Size = new Size(100, 30),
UseVisualStyleBackColor = true
};
controlGroupBox.Controls.Add(startStopButton);
controlGroupBox.Controls.Add(saveConfigButton);
controlGroupBox.Controls.Add(loadConfigButton);
// Input Group Box (Left Panel)
inputGroupBox = new GroupBox
{
Text = "Input Packet",
Location = new Point(12, 180),
Size = new Size(370, 390)
};
// Output Group Box (Right Panel)
outputGroupBox = new GroupBox
{
Text = "Response Packet",
Location = new Point(400, 180),
Size = new Size(370, 390)
};
CreateInputControls();
CreateOutputControls();
// Add all controls to form
this.Controls.Add(statusGroupBox);
this.Controls.Add(controlGroupBox);
this.Controls.Add(inputGroupBox);
this.Controls.Add(outputGroupBox);
this.ResumeLayout();
}
private void CreateInputControls()
{
int yPos = 30;
int spacing = 45;
// Lifebit
var lifebitLabel = new Label
{
Text = "Lifebit:",
Location = new Point(15, yPos),
Size = new Size(80, 20)
};
lifebitCheckBox = new CheckBox
{
Location = new Point(120, yPos),
Size = new Size(20, 20)
};
inputGroupBox.Controls.Add(lifebitLabel);
inputGroupBox.Controls.Add(lifebitCheckBox);
yPos += spacing;
// KameraStart
var kameraStartLabel = new Label
{
Text = "KameraStart:",
Location = new Point(15, yPos),
Size = new Size(80, 20)
};
kameraStartCheckBox = new CheckBox
{
Location = new Point(120, yPos),
Size = new Size(20, 20)
};
inputGroupBox.Controls.Add(kameraStartLabel);
inputGroupBox.Controls.Add(kameraStartCheckBox);
yPos += spacing;
// Materialnummer
var materialnummerLabel = new Label
{
Text = "Materialnummer:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
materialnummerNumeric = new NumericUpDown
{
Location = new Point(120, yPos),
Size = new Size(120, 20),
Minimum = int.MinValue,
Maximum = int.MaxValue
};
inputGroupBox.Controls.Add(materialnummerLabel);
inputGroupBox.Controls.Add(materialnummerNumeric);
yPos += spacing;
// ArtikelnummerBall
var artikelnummerBallLabel = new Label
{
Text = "ArtikelnummerBall:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
artikelnummerBallNumeric = new NumericUpDown
{
Location = new Point(120, yPos),
Size = new Size(120, 20),
Minimum = int.MinValue,
Maximum = int.MaxValue
};
inputGroupBox.Controls.Add(artikelnummerBallLabel);
inputGroupBox.Controls.Add(artikelnummerBallNumeric);
yPos += spacing;
// MaterialnummerFoil
var materialnummerFoilLabel = new Label
{
Text = "MaterialnummerFoil:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
materialnummerFoilNumeric = new NumericUpDown
{
Location = new Point(120, yPos),
Size = new Size(120, 20),
Minimum = int.MinValue,
Maximum = int.MaxValue
};
inputGroupBox.Controls.Add(materialnummerFoilLabel);
inputGroupBox.Controls.Add(materialnummerFoilNumeric);
yPos += spacing;
// Chargennummer
var chargennummerLabel = new Label
{
Text = "Chargennummer:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
chargennummerTextBox = new TextBox
{
Location = new Point(120, yPos),
Size = new Size(120, 20),
MaxLength = 5
};
inputGroupBox.Controls.Add(chargennummerLabel);
inputGroupBox.Controls.Add(chargennummerTextBox);
yPos += spacing;
// Apply button
applyButton = new Button
{
Text = "Apply",
Location = new Point(120, yPos),
Size = new Size(120, 30),
UseVisualStyleBackColor = true,
Font = new Font("Microsoft Sans Serif", 8.25F, FontStyle.Bold)
};
inputGroupBox.Controls.Add(applyButton);
}
private void CreateOutputControls()
{
int yPos = 30;
int spacing = 45;
// Lifebit Response
var lifebitResponseTitle = new Label
{
Text = "Lifebit:",
Location = new Point(15, yPos),
Size = new Size(80, 20)
};
lifebitResponseLabel = new Label
{
Text = "False",
Location = new Point(120, yPos),
Size = new Size(100, 20),
BorderStyle = BorderStyle.FixedSingle,
BackColor = SystemColors.Control
};
outputGroupBox.Controls.Add(lifebitResponseTitle);
outputGroupBox.Controls.Add(lifebitResponseLabel);
yPos += spacing;
// KameraStart Response
var kameraStartResponseTitle = new Label
{
Text = "KameraStart:",
Location = new Point(15, yPos),
Size = new Size(80, 20)
};
kameraStartResponseLabel = new Label
{
Text = "False",
Location = new Point(120, yPos),
Size = new Size(100, 20),
BorderStyle = BorderStyle.FixedSingle,
BackColor = SystemColors.Control
};
outputGroupBox.Controls.Add(kameraStartResponseTitle);
outputGroupBox.Controls.Add(kameraStartResponseLabel);
yPos += spacing;
// Materialnummer Response
var materialnummerResponseTitle = new Label
{
Text = "Materialnummer:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
materialnummerResponseLabel = new Label
{
Text = "0",
Location = new Point(120, yPos),
Size = new Size(120, 20),
BorderStyle = BorderStyle.FixedSingle,
BackColor = SystemColors.Control
};
outputGroupBox.Controls.Add(materialnummerResponseTitle);
outputGroupBox.Controls.Add(materialnummerResponseLabel);
yPos += spacing;
// ArtikelnummerBall Response
var artikelnummerBallResponseTitle = new Label
{
Text = "ArtikelnummerBall:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
artikelnummerBallResponseLabel = new Label
{
Text = "0",
Location = new Point(120, yPos),
Size = new Size(120, 20),
BorderStyle = BorderStyle.FixedSingle,
BackColor = SystemColors.Control
};
outputGroupBox.Controls.Add(artikelnummerBallResponseTitle);
outputGroupBox.Controls.Add(artikelnummerBallResponseLabel);
yPos += spacing;
// MaterialnummerFoil Response
var materialnummerFoilResponseTitle = new Label
{
Text = "MaterialnummerFoil:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
materialnummerFoilResponseLabel = new Label
{
Text = "0",
Location = new Point(120, yPos),
Size = new Size(120, 20),
BorderStyle = BorderStyle.FixedSingle,
BackColor = SystemColors.Control
};
outputGroupBox.Controls.Add(materialnummerFoilResponseTitle);
outputGroupBox.Controls.Add(materialnummerFoilResponseLabel);
yPos += spacing;
// Chargennummer Response
var chargennummerResponseTitle = new Label
{
Text = "Chargennummer:",
Location = new Point(15, yPos),
Size = new Size(100, 20)
};
chargennummerResponseLabel = new Label
{
Text = "",
Location = new Point(120, yPos),
Size = new Size(120, 20),
BorderStyle = BorderStyle.FixedSingle,
BackColor = SystemColors.Control
};
outputGroupBox.Controls.Add(chargennummerResponseTitle);
outputGroupBox.Controls.Add(chargennummerResponseLabel);
}
public PLCPacket GetInputPacket()
{
return new PLCPacket
{
Lifebit = lifebitCheckBox.Checked,
KameraStart = kameraStartCheckBox.Checked,
Materialnummer = (int)materialnummerNumeric.Value,
ArtikelnummerBall = (int)artikelnummerBallNumeric.Value,
MaterialnummerFoil = (int)materialnummerFoilNumeric.Value,
Chargennummer = chargennummerTextBox.Text
};
}
public PLCPacket GetCurrentPacket()
{
return currentPacket;
}
public void UpdateResponseDisplay(PLCPacket packet)
{
if (InvokeRequired)
{
Invoke(new Action<PLCPacket>(UpdateResponseDisplay), packet);
return;
}
lifebitResponseLabel.Text = packet.Lifebit.ToString();
kameraStartResponseLabel.Text = packet.KameraStart.ToString();
materialnummerResponseLabel.Text = packet.Materialnummer.ToString();
artikelnummerBallResponseLabel.Text = packet.ArtikelnummerBall.ToString();
materialnummerFoilResponseLabel.Text = packet.MaterialnummerFoil.ToString();
chargennummerResponseLabel.Text = packet.Chargennummer;
}
public void UpdateConnectionStatus(bool connected)
{
if (InvokeRequired)
{
Invoke(new Action<bool>(UpdateConnectionStatus), connected);
return;
}
connectionStatusLabel.Text = connected ? "Connected" : "Disconnected";
connectionStatusLabel.ForeColor = connected ? Color.Green : Color.Red;
}
public void ShowErrorMessage(string message)
{
if (InvokeRequired)
{
Invoke(new Action<string>(ShowErrorMessage), message);
return;
}
errorMessageLabel.Text = message;
}
public void HideErrorMessage()
{
if (InvokeRequired)
{
Invoke(new Action(HideErrorMessage));
return;
}
errorMessageLabel.Text = "";
}
// Event handlers will be wired up later
public Button StartStopButton => startStopButton;
public Button SaveConfigButton => saveConfigButton;
public Button LoadConfigButton => loadConfigButton;
public bool IsRunning => isRunning;
public void SetRunningState(bool running)
{
isRunning = running;
startStopButton.Text = running ? "Stop" : "Start";
}
}
}

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,135 @@
using System.Runtime.InteropServices;
using System.Text;
namespace B24SiemensEmulator;
[StructLayout(LayoutKind.Explicit, Size = 19)]
public struct PLCPacket
{
[FieldOffset(0)] private byte LifebitAndKamera;
[FieldOffset(2)] public int Materialnummer;
[FieldOffset(6)] public int ArtikelnummerBall;
[FieldOffset(10)] public int MaterialnummerFoil;
[FieldOffset(14)] private byte Chargennummer0;
[FieldOffset(15)] private byte Chargennummer1;
[FieldOffset(16)] private byte Chargennummer2;
[FieldOffset(17)] private byte Chargennummer3;
[FieldOffset(18)] private byte Chargennummer4;
public string Chargennummer
{
get => Encoding.ASCII.GetString(new[]
{Chargennummer0, Chargennummer1, Chargennummer2, Chargennummer3, Chargennummer4});
set
{
var bytes = Encoding.ASCII.GetBytes(value);
var fill = new byte[5];
for (int i = 0; i < bytes.Length; i++)
{
fill[i] = bytes[i];
}
Chargennummer0 = fill[0];
Chargennummer1 = fill[1];
Chargennummer2 = fill[2];
Chargennummer3 = fill[3];
Chargennummer4 = fill[4];
}
}
public bool Lifebit
{
get => (LifebitAndKamera & 1) > 0;
set
{
if (value)
LifebitAndKamera |= 1;
else
LifebitAndKamera &= 0xFE;
}
}
public bool KameraStart
{
get => (LifebitAndKamera & 2) > 0;
set
{
if (value)
LifebitAndKamera |= 2;
else
LifebitAndKamera &= 0xFD;
}
}
public byte[] Serialize()
{
int size = Marshal.SizeOf(this);
byte[] arr = new byte[size];
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(this, ptr, true);
Marshal.Copy(ptr, arr, 0, size);
Marshal.FreeHGlobal(ptr);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(arr, 2, 4); // Materialnummer
Array.Reverse(arr, 6, 4); // ArtikelnummerBall
Array.Reverse(arr, 10, 4); // MaterialnummerFoil
}
return arr;
}
public static PLCPacket Deserialize(byte[] arr)
{
if (BitConverter.IsLittleEndian)
{
Array.Reverse(arr, 2, 4); // Materialnummer
Array.Reverse(arr, 6, 4); // ArtikelnummerBall
Array.Reverse(arr, 10, 4); // MaterialnummerFoil
}
PLCPacket ds;
int size = Marshal.SizeOf(typeof(PLCPacket));
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(arr, 0, ptr, size);
ds = (PLCPacket)Marshal.PtrToStructure(ptr, typeof(PLCPacket));
Marshal.FreeHGlobal(ptr);
return ds;
}
override public string ToString()
{
return $"Lifebit: {(Lifebit?'1':'0')}, KameraStart: {KameraStart}, Materialnummer: {Materialnummer}, ArtikelnummerBall: {ArtikelnummerBall}, MaterialnummerFoil: {MaterialnummerFoil}, Chargennummer: {Chargennummer}";
}
public static bool IsValidChargennummer(string chargennummer)
{
if (string.IsNullOrEmpty(chargennummer))
return true; // Empty is valid
if (chargennummer.Length > 5)
return false; // Too long
// Check if all characters are ASCII
return chargennummer.All(c => c >= 0 && c <= 127);
}
public static string ValidateAndTrimChargennummer(string chargennummer)
{
if (string.IsNullOrEmpty(chargennummer))
return string.Empty;
// Trim to max 5 characters and ensure ASCII
var trimmed = chargennummer.Substring(0, Math.Min(chargennummer.Length, 5));
return new string(trimmed.Where(c => c >= 0 && c <= 127).ToArray());
}
public bool IsValid()
{
return IsValidChargennummer(Chargennummer);
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Windows.Forms;
namespace B24SiemensEmulator
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
}

View File

@@ -0,0 +1,167 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Timer = System.Threading.Timer;
namespace B24SiemensEmulator
{
public class SiemensTcpClient : IDisposable
{
private TcpClient? _tcpClient;
private NetworkStream? _stream;
private Timer? _sendTimer;
private bool _isConnected = false;
private bool _isRunning = false;
private readonly object _lock = new object();
public event Action<bool>? ConnectionStatusChanged;
public event Action<PLCPacket>? ResponseReceived;
public event Action<string>? ErrorOccurred;
private readonly string _serverHost = "localhost";
private readonly int _serverPort = 42010;
private readonly int _sendIntervalMs = 1000;
public Func<PLCPacket>? GetPacketToSend;
public bool IsConnected => _isConnected;
public bool IsRunning => _isRunning;
public async Task<bool> ConnectAsync()
{
try
{
if (_isConnected)
return true;
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_serverHost, _serverPort);
_stream = _tcpClient.GetStream();
lock (_lock)
{
_isConnected = true;
}
ConnectionStatusChanged?.Invoke(true);
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"Connection failed: {ex.Message}");
await DisconnectAsync();
return false;
}
}
public async Task DisconnectAsync()
{
lock (_lock)
{
_isConnected = false;
}
_stream?.Close();
_stream?.Dispose();
_stream = null;
_tcpClient?.Close();
_tcpClient?.Dispose();
_tcpClient = null;
ConnectionStatusChanged?.Invoke(false);
}
public void StartSending()
{
if (_isRunning)
return;
_isRunning = true;
_sendTimer = new Timer(SendPacketCallback, null, 0, _sendIntervalMs);
}
public void StopSending()
{
_isRunning = false;
_sendTimer?.Dispose();
_sendTimer = null;
}
private async void SendPacketCallback(object? state)
{
if (!_isConnected || GetPacketToSend == null)
return;
try
{
var packet = GetPacketToSend();
await SendPacketAsync(packet);
}
catch (Exception ex)
{
ErrorOccurred?.Invoke($"Send failed: {ex.Message}");
await HandleConnectionLoss();
}
}
private async Task SendPacketAsync(PLCPacket packet)
{
if (_stream == null || !_isConnected)
throw new InvalidOperationException("Not connected");
var data = packet.Serialize();
await _stream.WriteAsync(data, 0, data.Length);
// Read response
var responseBuffer = new byte[19]; // PLCPacket is 19 bytes
int totalBytesRead = 0;
while (totalBytesRead < responseBuffer.Length)
{
int bytesRead = await _stream.ReadAsync(responseBuffer, totalBytesRead,
responseBuffer.Length - totalBytesRead);
if (bytesRead == 0)
throw new Exception("Server disconnected");
totalBytesRead += bytesRead;
}
var responsePacket = PLCPacket.Deserialize(responseBuffer);
ResponseReceived?.Invoke(responsePacket);
}
private async Task HandleConnectionLoss()
{
await DisconnectAsync();
// Wait 1 second before trying to reconnect
await Task.Delay(1000);
if (_isRunning)
{
var reconnected = await ConnectAsync();
if (!reconnected)
{
// Schedule another reconnection attempt
_ = Task.Run(async () =>
{
await Task.Delay(1000);
if (_isRunning && !_isConnected)
{
await HandleConnectionLoss();
}
});
}
}
}
public void Dispose()
{
StopSending();
DisconnectAsync().Wait(1000);
}
}
}

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

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

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

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

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

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

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

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

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

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

View 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

View 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

View File

@@ -0,0 +1,36 @@
using Ninject;
using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Plugins;
namespace TestPlugin
{
public class TestGlobalPlugin:IVisionBuilderModule
{
public void InitializeModule()
{
Console.WriteLine("Global plugin initialized");
}
}
public class TestCameraPlugin:IVisionBuilderModule
{
public void InitializeModule()
{
Console.WriteLine("Camera plugin initialized");
}
}
public class TestPlugin: IPlugin
{
public void RegisterGlobalModules(IKernel kernel)
{
kernel.RegisterModule<TestGlobalPlugin>();
}
public void RegisterCameraModules(IKernel kernel, string cameraName)
{
kernel.RegisterModule<TestCameraPlugin>();
}
}
}

View File

@@ -0,0 +1,23 @@
<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\TestPlugin</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>

View File

@@ -2,6 +2,7 @@
using Ninject;
using Ninject.Extensions.ChildKernel;
using Serilog;
using VisionBuilder.UI.Common.Plugins;
namespace VisionBuilder.UI.Common;
@@ -23,6 +24,8 @@ public static class Extensions
self.Bind<T, IVisionBuilderModule>().To<T>().InSingletonScope();
}
public static void InitializeModules(this IKernel self)
{
@@ -45,6 +48,42 @@ public static class Extensions
}
}
public static IKernel LoadGlobalPlugins(this IKernel self)
{
var plugins = PluginLoader.Instance.Plugins;
foreach (var plugin in plugins)
{
try
{
Log.Information($"Initializing global plugin {plugin.GetType().Name}");
plugin.RegisterGlobalModules(self);
}
catch (Exception e)
{
Log.Error($"Error initializing global plugin {plugin.GetType().Name}: {e}", e);
}
}
return self;
}
public static IKernel LoadCameraPlugins(this IChildKernel self, string cameraName)
{
var plugins = PluginLoader.Instance.Plugins;
foreach (var plugin in plugins)
{
try
{
Log.Information($"Initializing camera plugin {plugin.GetType().Name} for camera {cameraName}");
plugin.RegisterCameraModules(self,cameraName);
}
catch (Exception e)
{
Log.Error($"Error initializing camera plugin {plugin.GetType().Name} for camera {cameraName}: {e}", e);
}
}
return self;
}

View File

@@ -0,0 +1,10 @@
using Ninject;
using Ninject.Extensions.ChildKernel;
namespace VisionBuilder.UI.Common.Plugins;
public interface IPlugin
{
public void RegisterGlobalModules(IKernel kernel);
public void RegisterCameraModules(IKernel kernel, string cameraName);
}

View File

@@ -0,0 +1,37 @@
using System.Reflection;
using System.Runtime.Loader;
namespace VisionBuilder.UI.Common.Plugins;
class PluginLoadContext : AssemblyLoadContext
{
private AssemblyDependencyResolver _resolver;
public PluginLoadContext(string pluginPath)
{
_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
if (assemblyPath != null)
{
return LoadFromAssemblyPath(assemblyPath);
}
return null;
}
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
if (libraryPath != null)
{
return LoadUnmanagedDllFromPath(libraryPath);
}
return IntPtr.Zero;
}
}

View File

@@ -0,0 +1,67 @@
using System.Reflection;
using Inspectron.Settings;
using Ninject;
using Serilog;
namespace VisionBuilder.UI.Common.Plugins;
public class PluginLoader
{
private static PluginLoader _instance;
public static PluginLoader Instance => _instance ??= new PluginLoader();
const string PLUGINS_DIRECTORY = "Plugins";
private PluginLoader()
{
Initialize();
}
public List<IPlugin> Plugins { get; } = new List<IPlugin>();
public void Initialize()
{
Log.Information("Loading plugins...");
var pluginsPath = Path.Combine("..", "Data", PLUGINS_DIRECTORY);
Directory.CreateDirectory(pluginsPath);
var pluginFolders = Directory.GetDirectories(pluginsPath);
foreach (string folder in pluginFolders)
{
var assemblyPath = Path.Combine(folder, Path.GetFileNameWithoutExtension(folder) + ".dll");
if (!File.Exists(assemblyPath))
{
Log.Warning("Plugin assembly not found: {AssemblyPath}", assemblyPath);
continue;
}
try
{
LoadPluginAssembly(assemblyPath);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to load plugin from {AssemblyPath}", assemblyPath);
}
}
Log.Information("Plugin loading completed.");
}
private void LoadPluginAssembly(string assemblyPath)
{
var absPath = Path.GetFullPath(assemblyPath);
var loadContext = new PluginLoadContext(absPath);
var assembly = loadContext.LoadFromAssemblyPath(absPath);
Log.Information("Loaded plugin assembly: {AssemblyName}", assembly.GetName().Name);
foreach (var type in assembly.GetTypes())
{
if (type.IsAssignableTo(typeof(IPlugin)) && !type.IsAbstract)
{
Plugins.Add((IPlugin)Activator.CreateInstance(type));
Log.Information("Discovered plugin: {PluginName}", type.Name);
}
}
}
}

View File

@@ -10,6 +10,8 @@ public interface IRecognitionControl
void SetRecipe(RecipeData recipe);
void Start();
void Stop();
void Pause();
void Resume();
event Action<ImageProcessedEvent> ImageProcessed;
event Action<SessionStartedEvent> SessionStarted;
event Action<SessionEndedEvent> SessionEnded;

View File

@@ -1,4 +1,7 @@
using Inspectron.Settings;
using System.ComponentModel;
using System.Globalization;
using VisionBuilder.UI.Common.Utils;
namespace VisionBuilder.UI.Common;
@@ -11,11 +14,55 @@ public class UIConfiguration: ISettings
public string AdminPassword { get; set; } = "";
public List<ErrorShortName> ErrorShortNames { get; set; } = new List<ErrorShortName>();
public void RegisterSettings(InspectronSettings settings)
{
settings.RegisterSimple(this, () => this.MaxErrorCount, "Errors", "Max errors count", "UI");
settings.RegisterSimple(this, () => this.AdminPassword, "System", "Password", "UI");
settings.RegisterSimple(this, () => this.ErrorShortNames, "Errors", "Error short names", "UI");
}
}
public class ErrorShortName
{
public string FullName { get; set; }
public string ShortName { get; set; }
public override string ToString()
{
return $"{FullName} -> {ShortName}";
}
}
public class ErrorShortNameConverter : GeneralListTypeConverter<ErrorShortName>
{
protected override ErrorShortName ConvertFromString(ITypeDescriptorContext context, CultureInfo culture, string stringValue)
{
if (string.IsNullOrEmpty(stringValue))
return null;
var parts = stringValue.Split(',');
if (parts.Length == 2)
{
return new ErrorShortName
{
FullName = parts[0].Trim(),
ShortName = parts[1].Trim()
};
}
return null;
}
protected override string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, ErrorShortName errorShortName)
{
if (errorShortName == null)
return null;
return $"{errorShortName.FullName},{errorShortName.ShortName}";
}
}

View File

@@ -0,0 +1,85 @@
using System.ComponentModel;
using System.Globalization;
namespace VisionBuilder.UI.Common.Utils;
public abstract class GeneralListTypeConverter<T>:TypeConverter
where T: class
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value is string stringValue)
{
// Check if we're converting to a single ErrorShortName
if (context?.PropertyDescriptor?.PropertyType == typeof(T))
{
return ConvertFromString(context, culture, stringValue);
}
// Otherwise, convert to a list of ErrorShortName
var errorShortNames = new List<T>();
if (!string.IsNullOrEmpty(stringValue))
{
var lines = stringValue.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var errorShortName = ConvertFromString(context, culture, line.Trim());
if (errorShortName != null)
errorShortNames.Add(errorShortName);
}
}
return errorShortNames;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value,
Type destinationType)
{
if (destinationType == typeof(string))
{
// Handle single ErrorShortName
if (value is T errorShortName)
{
return this.ConvertToString(context, culture, errorShortName);
}
// Handle list of ErrorShortName
if (value is List<T> errorShortNames)
{
var lines = errorShortNames.Select(e =>
ConvertToString(context, culture, e)
).Where(line => !string.IsNullOrEmpty(line));
return string.Join("\n", lines);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Convert a single string to ErrorShortName
/// </summary>
protected abstract T ConvertFromString(ITypeDescriptorContext context, CultureInfo culture,
string stringValue);
/// <summary>
/// Convert a single ErrorShortName to string
/// </summary>
protected abstract string ConvertToString(ITypeDescriptorContext context, CultureInfo culture,
T errorShortName);
}

View File

@@ -37,12 +37,21 @@ public class ErrorsVM:IEventHandler<ImageProcessedEvent>, IEventHandler<SessionS
{
if(!@event.HasError)return;
string additionalInfo = string.Empty;
var errorShortName = _uiConfiguration.ErrorShortNames
.FirstOrDefault(x => x.FullName == @event.ErrorNames[0]);
if (errorShortName != null)
{
additionalInfo += " ,"+errorShortName.ShortName;
}
ErrorData errorData = new ErrorData
{
RecipeName = @event.RecipeName,
ImageOriginal = @event.ImageOriginal,
ImageAnalysis = @event.ImageAnalysis,
Title = @event.ErrorNames +" "+ DateTime.Now.ToString("T")
Title =DateTime.Now.ToString("G")+ additionalInfo
};
if (Errors.Count >= _uiConfiguration.MaxErrorCount)

View File

@@ -6,5 +6,5 @@ namespace VisionBuilder.UI.Common.ViewModel;
public class RecipeSelectionVM
{
public ObservableCollection<RecipeData> Recipes { get; set; }
public RecipeData SelectedRecipe { get; set; }
public RecipeData? SelectedRecipe { get; set; }
}

View File

@@ -1,6 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.ComponentModel;
using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.Commands.Interfaces;
using VisionBuilder.UI.Common.RecipeProcessing;
@@ -12,6 +13,8 @@ namespace VisionBuilder.UI.Common
{
public partial class SingleCameraVM : ObservableObject, IEventHandler<ImageProcessedEvent>
{
public SynchronizationContext? SynchronizationContext { get; set; }
private readonly IRecipeSelectionDialogService _recipeSelectionDialogService;
private readonly IRecognitionControl _recognitionControl;
@@ -29,21 +32,38 @@ namespace VisionBuilder.UI.Common
[NotifyCanExecuteChangedFor(nameof(StartCommand))]
[NotifyCanExecuteChangedFor(nameof(StopCommand))]
private bool _isRunning;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CameraLabelAndStatus))]
private string _cameraLabel;
public string CameraLabelAndStatus=>
$"{CameraLabel}{(IsPaused ? "(PAUSED)" : "")}";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CameraLabelAndStatus))]
private bool _isPaused;
public bool IsResumed => !IsPaused;
public bool CanStop => IsRunning;
public bool CanStart => !IsRunning && SelectedRecipe!=null;
public bool IsNotRunning => !IsRunning;
private bool _handlingEnabled = true;
public bool RecipeSelected => SelectedRecipe != null;
public bool RecipeNotSelected => SelectedRecipe == null;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanStop))]
[NotifyPropertyChangedFor(nameof(CanStart))]
[NotifyCanExecuteChangedFor(nameof(SelectRecipeCommand))]
[NotifyCanExecuteChangedFor(nameof(StartCommand))]
[NotifyCanExecuteChangedFor(nameof(StopCommand))]
[NotifyPropertyChangedFor(nameof(RecipeSelected))]
[NotifyPropertyChangedFor(nameof(RecipeNotSelected))]
private RecipeData? _selectedRecipe;
@@ -69,11 +89,24 @@ namespace VisionBuilder.UI.Common
[RelayCommand(CanExecute = nameof(IsNotRunning))]
public void SelectRecipe()
{
var vm = new RecipeSelectionVM();
vm.Recipes = new ObservableCollection<RecipeData>(_recognitionControl.GetRecipesData());
var vm = GetRecipeSelectionVm();
if (!_recipeSelectionDialogService.SelectRecipe(vm))return;
ProcessRecipeSelectionVm(vm);
}
public RecipeSelectionVM GetRecipeSelectionVm()
{
var vm= new RecipeSelectionVM();
vm.Recipes = new ObservableCollection<RecipeData>(_recognitionControl.GetRecipesData());
vm.SelectedRecipe = SelectedRecipe;
return vm;
}
public void ProcessRecipeSelectionVm(RecipeSelectionVM vm)
{
if(!SelectRecipeCommand.CanExecute(null))return;
SelectedRecipe = vm.SelectedRecipe;
_recognitionControl.SetRecipe(SelectedRecipe);
_recognitionControl.SetRecipe(SelectedRecipe!);
CurrentRecipeName = SelectedRecipe?.RecipeName ?? "Select recipe";
}
@@ -93,6 +126,22 @@ namespace VisionBuilder.UI.Common
IsRunning = false;
}
[RelayCommand(CanExecute = nameof(IsResumed))]
public void Pause()
{
_recognitionControl.Pause();
IsPaused = true;
}
[RelayCommand(CanExecute = nameof(IsPaused))]
public void Resume()
{
_recognitionControl.Resume();
IsPaused = false;
}
public void Handle(ImageProcessedEvent @event)
{

View File

@@ -17,10 +17,14 @@ public partial class StatisticsVM: ObservableObject,IEventHandler<ImageProcessed
[ObservableProperty] private int _good;
[ObservableProperty] private int _bad;
[ObservableProperty] private int _total;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ErrorRate))]
private int _total;
[ObservableProperty] private string _statisticsDetails="";
public string ErrorRate => _bad/(float)_total * 100 + "%";
private readonly Dictionary<string, int> _errorTypes = new();
public void Handle(ImageProcessedEvent @event)

View File

@@ -1,6 +1,8 @@
using Inspectron.Settings;
using Ninject;
using Ninject.Extensions.ChildKernel;
using System.ComponentModel;
using VisionBuilder.UI.Common.Plugins;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
@@ -10,6 +12,8 @@ public static class VisionBuilder
{
public static IKernel CreateMainKernel(InspectronSettings settings)
{
TypeDescriptor.AddAttributes(typeof(List<ErrorShortName>), new TypeConverterAttribute(typeof(ErrorShortNameConverter)));
StandardKernel mainKernel = new StandardKernel();
// GLOBAL SETTINGS //
@@ -22,4 +26,6 @@ public static class VisionBuilder
return mainKernel;
}
}

View File

@@ -54,7 +54,7 @@ namespace VisionBuilder.UI.IOCommander.Windows
for (int i = 0; i < 20; i++)
{
int pinNumber = i + 1; // Pins are 1-indexed
int pinNumber = i; // Pins are 1-indexed
_checkBoxes[i] = new CheckBox
{
Text = pinNumber.ToString(),
@@ -78,7 +78,7 @@ namespace VisionBuilder.UI.IOCommander.Windows
bool state = checkBox.Checked;
// Update the BitArray
PinState.Set(pin - 1, state); // Adjust for 0-based indexing in BitArray
PinState.Set(pin, state); // Adjust for 0-based indexing in BitArray
// Trigger the event
OnPinChanged?.Invoke(pin, state);

View File

@@ -43,7 +43,7 @@ namespace VisionBuilder.UI.IOCommander.Windows
this.MinimizeBox = false;
this.MaximizeBox = false;
// Create pin indicators for input pins 1-20 and output pins 1-20 (20 rows, 2 columns)
// Create pin indicators for input pins 0-19 and output pins 0-19 (20 rows, 2 columns)
int pinSize = 18;
int spacing = 2;
int centerX = 140; // Center the graphics in the wider form
@@ -55,8 +55,8 @@ namespace VisionBuilder.UI.IOCommander.Windows
for (int row = 0; row < 20; row++)
{
int inputPin = row + 1; // Input pins numbered 1-20
int outputPin = row + 1; // Output pins numbered 1-20
int inputPin = row; // Input pins numbered 0-19
int outputPin = row; // Output pins numbered 0-19
// Left column (input pins)
var leftIndicator = new PinIndicator(inputPin, true)

View File

@@ -4,4 +4,6 @@ public enum EGPIOCommand
{
Start,
Stop,
Pause,
Resume,
}

View File

@@ -1,6 +1,7 @@
using Ninject.Extensions.ChildKernel;
using Ninject;
using Ninject.Extensions.ChildKernel;
using System.ComponentModel;
using Ninject;
using System.Reflection;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.IOCommander.Interfaces;
using VisionBuilder.UI.IOCommander.Modules;
@@ -14,7 +15,6 @@ public static class ModuleExtensions
{
// for settings to work properly
TypeDescriptor.AddAttributes(typeof(List<IOCommanderPinAction>), new TypeConverterAttribute(typeof(IOCommanderPinActionListConverter)));
TypeDescriptor.AddAttributes(typeof(List<IOCommanderCommand>), new TypeConverterAttribute(typeof(IOCommanderCommandListConverter)));
List<IOCommanderCameraSettings> cameras = cameraNames.Select(name => new IOCommanderCameraSettings(name)).ToList();

View File

@@ -49,6 +49,19 @@ public class IOCommanderCameraModule: IVisionBuilderModule
_cameraVm.StopCommand.Execute(null);
}
break;
case EGPIOCommand.Pause:
if(_cameraVm.PauseCommand.CanExecute(null))
{
_cameraVm.PauseCommand.Execute(null);
}
break;
case EGPIOCommand.Resume:
if(_cameraVm.ResumeCommand.CanExecute(null))
{
_cameraVm.ResumeCommand.Execute(null);
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(command), command, null);
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using Serilog;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.RecipeProcessing;
@@ -25,8 +26,17 @@ public class IOCommanderModule:IVisionBuilderModule
public void InitializeModule()
{
_ioCommander = _factory.Create();
_ioCommander.OnPinChanged += OnPinChanged;
try
{
_ioCommander = _factory.Create();
_ioCommander.OnPinChanged += OnPinChanged;
}
catch (Exception e)
{
Log.Warning("IOCommander did non load: {e}",e);
_ioCommander= null;
}
if (_settings.ShowDebugView)_debugViewService.ShowDebugView();

View File

@@ -30,7 +30,7 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe
_loadingService.StartLoading($"Loading recipes for {_recognitionConfiguration.CameraName}...");
try
{
var files = WorkflowRecipeHelper.ListRecipeFiles();
var files = WorkflowRecipeHelper.ListRecipeFiles(_recognitionConfiguration.RecipeNameFilter);
List<RecipeData> recipes = new List<RecipeData>();
foreach (var file in files)
{
@@ -65,6 +65,7 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe
}
public bool IsRunning { get; set; }
public bool IsPaused { get; set; }
private DateTime _startTime;
private WorkflowList _workflow;
@@ -127,13 +128,27 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe
}
private void Loop(CancellationToken token)
public void Pause()
{
IsPaused= true;
}
public void Resume()
{
IsPaused = false;
}
private async Task Loop(CancellationToken token)
{
// Start the recognition process using the Hawkeye library
while (!token.IsCancellationRequested)
{
if (IsPaused)
{
await Task.Delay(100, token);
continue;
}
var sw = Stopwatch.StartNew();
_workflow.Context.CancellationToken= token;

View File

@@ -13,8 +13,10 @@ public class HawkeyeRecognitionSettings: ISettings
}
public int MinimumProcessingTime { get; set; }
public string RecipeNameFilter { get; set; } = "*";
public void RegisterSettings(InspectronSettings settings)
{
settings.RegisterSimple(this, () => this.MinimumProcessingTime, CameraName+"/Hawkeye recognition", "Minimum processing time (ms)");
settings.RegisterSimple(this, () => this.RecipeNameFilter, CameraName + "/Hawkeye recognition", nameof(RecipeNameFilter));
}
}

View File

@@ -7,10 +7,10 @@ public static class WorkflowRecipeHelper
private const string RECIPES_DIR = "..\\Data\\Recipes";
public static List<string> ListRecipeFiles()
public static List<string> ListRecipeFiles(string nameFilter)
{
Directory.CreateDirectory(RECIPES_DIR);
var recipes = Directory.GetFiles(RECIPES_DIR, "*.hrcp");
var recipes = Directory.GetFiles(RECIPES_DIR, nameFilter+".hrcp");
return recipes.ToList();
}

View File

@@ -436,6 +436,11 @@ public class VisionBuilderStatisticsTests
// Test implementation of IRecognitionControl using reflection approach
public class TestRecognitionControl : IRecognitionControl
{
public void Resume()
{
}
public event Action<ImageProcessedEvent> ImageProcessed = delegate { };
public event Action<SessionStartedEvent> SessionStarted = delegate { };
public event Action<SessionEndedEvent> SessionEnded = delegate { };
@@ -444,6 +449,10 @@ public class TestRecognitionControl : IRecognitionControl
public void SetRecipe(RecipeData recipe) { }
public void Start() { }
public void Stop() { }
public void Pause()
{
}
public void TriggerImageProcessed(ImageProcessedEvent eventArgs) => ImageProcessed(eventArgs);
public void TriggerSessionStarted(SessionStartedEvent eventArgs) => SessionStarted(eventArgs);

View File

@@ -34,6 +34,8 @@
materialDivider1 = new MaterialSkin.Controls.MaterialDivider();
singleCameraControl1 = new VisionBuilder.UI.Windows.Components.SingleCameraControl();
btnTestMode = new MaterialSkin.Controls.MaterialRaisedButton();
flowLayoutPanel1 = new FlowLayoutPanel();
flowLayoutPanel1.SuspendLayout();
SuspendLayout();
//
// btnMinimize
@@ -44,11 +46,11 @@
btnMinimize.DrawBorder = true;
btnMinimize.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnMinimize.Icon = null;
btnMinimize.Location = new Point(960, 984);
btnMinimize.Location = new Point(367, 3);
btnMinimize.MouseState = MaterialSkin.MouseState.HOVER;
btnMinimize.Name = "btnMinimize";
btnMinimize.Primary = false;
btnMinimize.Size = new Size(224, 64);
btnMinimize.Size = new Size(176, 64);
btnMinimize.TabIndex = 8;
btnMinimize.Text = "Minimize";
btnMinimize.UseVisualStyleBackColor = true;
@@ -62,11 +64,11 @@
btnSettings.DrawBorder = true;
btnSettings.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnSettings.Icon = null;
btnSettings.Location = new Point(728, 984);
btnSettings.Location = new Point(185, 3);
btnSettings.MouseState = MaterialSkin.MouseState.HOVER;
btnSettings.Name = "btnSettings";
btnSettings.Primary = false;
btnSettings.Size = new Size(224, 64);
btnSettings.Size = new Size(176, 64);
btnSettings.TabIndex = 7;
btnSettings.Text = "Settings";
btnSettings.UseVisualStyleBackColor = true;
@@ -80,11 +82,11 @@
btnExit.DrawBorder = true;
btnExit.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnExit.Icon = null;
btnExit.Location = new Point(1192, 984);
btnExit.Location = new Point(549, 3);
btnExit.MouseState = MaterialSkin.MouseState.HOVER;
btnExit.Name = "btnExit";
btnExit.Primary = false;
btnExit.Size = new Size(224, 64);
btnExit.Size = new Size(176, 64);
btnExit.TabIndex = 6;
btnExit.Text = "Exit";
btnExit.UseVisualStyleBackColor = true;
@@ -94,7 +96,7 @@
//
materialDivider1.BackColor = Color.FromArgb(55, 71, 79);
materialDivider1.Depth = 0;
materialDivider1.Location = new Point(8, 960);
materialDivider1.Location = new Point(8, 984);
materialDivider1.MouseState = MaterialSkin.MouseState.HOVER;
materialDivider1.Name = "materialDivider1";
materialDivider1.Size = new Size(1904, 1);
@@ -106,7 +108,7 @@
singleCameraControl1.BackColor = Color.White;
singleCameraControl1.Location = new Point(8, 32);
singleCameraControl1.Name = "singleCameraControl1";
singleCameraControl1.Size = new Size(1904, 912);
singleCameraControl1.Size = new Size(1904, 944);
singleCameraControl1.TabIndex = 10;
//
// btnTestMode
@@ -117,30 +119,40 @@
btnTestMode.DrawBorder = true;
btnTestMode.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnTestMode.Icon = null;
btnTestMode.Location = new Point(496, 984);
btnTestMode.Location = new Point(3, 3);
btnTestMode.MouseState = MaterialSkin.MouseState.HOVER;
btnTestMode.Name = "btnTestMode";
btnTestMode.Primary = false;
btnTestMode.Size = new Size(224, 64);
btnTestMode.Size = new Size(176, 64);
btnTestMode.TabIndex = 11;
btnTestMode.Text = "Test mode";
btnTestMode.UseVisualStyleBackColor = true;
btnTestMode.Click += btnTestMode_Click;
//
// flowLayoutPanel1
//
flowLayoutPanel1.BackColor = Color.Transparent;
flowLayoutPanel1.Controls.Add(btnTestMode);
flowLayoutPanel1.Controls.Add(btnSettings);
flowLayoutPanel1.Controls.Add(btnMinimize);
flowLayoutPanel1.Controls.Add(btnExit);
flowLayoutPanel1.Location = new Point(664, 992);
flowLayoutPanel1.Name = "flowLayoutPanel1";
flowLayoutPanel1.Size = new Size(728, 80);
flowLayoutPanel1.TabIndex = 12;
//
// Form1
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1920, 1080);
Controls.Add(btnTestMode);
Controls.Add(flowLayoutPanel1);
Controls.Add(singleCameraControl1);
Controls.Add(materialDivider1);
Controls.Add(btnMinimize);
Controls.Add(btnSettings);
Controls.Add(btnExit);
Name = "Form1";
StartPosition = FormStartPosition.CenterScreen;
Text = "Form1";
flowLayoutPanel1.ResumeLayout(false);
ResumeLayout(false);
}
@@ -152,5 +164,6 @@
private MaterialSkin.Controls.MaterialDivider materialDivider1;
private Components.SingleCameraControl singleCameraControl1;
private MaterialSkin.Controls.MaterialRaisedButton btnTestMode;
private FlowLayoutPanel flowLayoutPanel1;
}
}

View File

@@ -36,6 +36,7 @@ namespace VisionBuilder.UI.Windows.Test
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
// In your application startup code
MaterialSkin.MaterialSkinManager.ConfigureForInspectron();
InspectronSettings settings = new InspectronSettings("..\\Config");
@@ -45,7 +46,12 @@ namespace VisionBuilder.UI.Windows.Test
.UseIOCommander([CAMERA1])
.UseIOCommanderWindowsDebug()
.UseIOCommanderVirtualInput()
.UseWindowsServices();
.UseWindowsServices()
.LoadGlobalPlugins()
;
mainKernel.Get<ILoadingService>().StartLoading("Camera initialization");
// CAMERA 1 //
ChildKernel kernelCamera1 = new ChildKernel(mainKernel);
@@ -58,6 +64,7 @@ namespace VisionBuilder.UI.Windows.Test
.UseRingbuffer(CAMERA1)
.UseHawkeyeRecipes(CAMERA1)
.UseCameraIOCommander(CAMERA1)
.LoadCameraPlugins(CAMERA1)
;
@@ -76,6 +83,8 @@ namespace VisionBuilder.UI.Windows.Test
mainKernel.InitializeModules();
kernelCamera1.InitializeModules();
mainKernel.Get<ILoadingService>().StopLoading("Camera initialization");
var mainWindowVm = mainKernel.Get<MainWindowVM>();
mainWindowVm.SingleCameraVms = [
kernelCamera1.Get<SingleCameraVM>()

View File

@@ -46,12 +46,14 @@ namespace VisionBuilder.UI.Windows.Components
private void AddErrorPreviewInternal(ErrorData errorData)
{
var pb = new PictureBox();
var width = 293 / 2;
var height = 220 / 2;
pb.Size = new Size(width, height + 20);
var width = 293 / 1.6f;
var height = 220 / 1.6f;
pb.Size = new Size((int)width, (int)height + 20);
pb.SizeMode = PictureBoxSizeMode.Zoom;
pb.Margin = new Padding(5,0,5,5);
var numberedImage = new Bitmap(width, height + 20);
var text = DateTime.Now.ToString("yy-MM-dd") + " " + DateTime.Now.ToString("T");
var numberedImage = new Bitmap((int)width, (int)height + 20);
var text = errorData.Title;
using (Graphics g = Graphics.FromImage(numberedImage))
{

View File

@@ -36,11 +36,13 @@
stats1 = new Stats();
btnStart = new MaterialSkin.Controls.MaterialRaisedButton();
flowLayoutPanel1 = new FlowLayoutPanel();
flowLayoutPanel2 = new FlowLayoutPanel();
btnSelectRecipe = new MaterialSkin.Controls.MaterialRaisedButton();
btnStop = new MaterialSkin.Controls.MaterialRaisedButton();
materialDivider2 = new MaterialSkin.Controls.MaterialDivider();
((System.ComponentModel.ISupportInitialize)previewWindow1).BeginInit();
flowLayoutPanel1.SuspendLayout();
flowLayoutPanel2.SuspendLayout();
SuspendLayout();
//
// lblCameraName
@@ -110,8 +112,9 @@
btnStart.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnStart.Depth = 0;
btnStart.DrawBorder = false;
btnStart.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
btnStart.Icon = null;
btnStart.Location = new Point(85, 97);
btnStart.Location = new Point(21, 73);
btnStart.MouseState = MaterialSkin.MouseState.HOVER;
btnStart.Name = "btnStart";
btnStart.Primary = true;
@@ -122,9 +125,7 @@
//
// flowLayoutPanel1
//
flowLayoutPanel1.Controls.Add(btnSelectRecipe);
flowLayoutPanel1.Controls.Add(btnStart);
flowLayoutPanel1.Controls.Add(btnStop);
flowLayoutPanel1.Controls.Add(flowLayoutPanel2);
flowLayoutPanel1.Dock = DockStyle.Right;
flowLayoutPanel1.FlowDirection = FlowDirection.TopDown;
flowLayoutPanel1.Location = new Point(1408, 0);
@@ -134,14 +135,25 @@
flowLayoutPanel1.Size = new Size(280, 498);
flowLayoutPanel1.TabIndex = 10;
//
// flowLayoutPanel2
//
flowLayoutPanel2.Controls.Add(btnSelectRecipe);
flowLayoutPanel2.Controls.Add(btnStart);
flowLayoutPanel2.Controls.Add(btnStop);
flowLayoutPanel2.Location = new Point(61, 27);
flowLayoutPanel2.Name = "flowLayoutPanel2";
flowLayoutPanel2.Size = new Size(200, 237);
flowLayoutPanel2.TabIndex = 12;
//
// btnSelectRecipe
//
btnSelectRecipe.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnSelectRecipe.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnSelectRecipe.Depth = 0;
btnSelectRecipe.DrawBorder = true;
btnSelectRecipe.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
btnSelectRecipe.Icon = null;
btnSelectRecipe.Location = new Point(85, 27);
btnSelectRecipe.Location = new Point(21, 3);
btnSelectRecipe.MouseState = MaterialSkin.MouseState.HOVER;
btnSelectRecipe.Name = "btnSelectRecipe";
btnSelectRecipe.Primary = false;
@@ -156,8 +168,9 @@
btnStop.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnStop.Depth = 0;
btnStop.DrawBorder = false;
btnStop.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
btnStop.Icon = null;
btnStop.Location = new Point(85, 167);
btnStop.Location = new Point(21, 143);
btnStop.MouseState = MaterialSkin.MouseState.HOVER;
btnStop.Name = "btnStop";
btnStop.Primary = true;
@@ -195,6 +208,7 @@
Size = new Size(1688, 498);
((System.ComponentModel.ISupportInitialize)previewWindow1).EndInit();
flowLayoutPanel1.ResumeLayout(false);
flowLayoutPanel2.ResumeLayout(false);
ResumeLayout(false);
}
@@ -211,5 +225,6 @@
private MaterialSkin.Controls.MaterialRaisedButton btnStop;
private MaterialSkin.Controls.MaterialRaisedButton btnSelectRecipe;
private MaterialSkin.Controls.MaterialDivider materialDivider2;
private FlowLayoutPanel flowLayoutPanel2;
}
}

View File

@@ -18,6 +18,7 @@ namespace VisionBuilder.UI.Windows.Components
public void SetViewModel(SingleCameraVM singleCameraVm)
{
_singleCameraVm = singleCameraVm;
singleCameraVm.SynchronizationContext = SynchronizationContext.Current;
this.previewWindow1.SetViewModel(singleCameraVm.PreviewVm);
this.errorPreview1.SetViewModel(singleCameraVm.ErrorsVm);
this.stats1.SetViewModel(singleCameraVm.StatisticsVm);
@@ -37,8 +38,11 @@ namespace VisionBuilder.UI.Windows.Components
btnStop.Command=_singleCameraVm.StopCommand;
btnSelectRecipe.Command=_singleCameraVm.SelectRecipeCommand;
lblCameraName.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CameraLabel), true, DataSourceUpdateMode.OnPropertyChanged);
lblCameraName.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CameraLabelAndStatus), true, DataSourceUpdateMode.OnPropertyChanged);
btnSelectRecipe.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CurrentRecipeName), true, DataSourceUpdateMode.OnPropertyChanged);
btnStart.DataBindings.Add(nameof(btnStart.Visible), _singleCameraVm, nameof(_singleCameraVm.CanStart), true, DataSourceUpdateMode.OnPropertyChanged);
btnStop.DataBindings.Add(nameof(btnStop.Visible), _singleCameraVm, nameof(_singleCameraVm.CanStop), true, DataSourceUpdateMode.OnPropertyChanged);
btnSelectRecipe.DataBindings.Add(nameof(btnSelectRecipe.Primary), _singleCameraVm, nameof(_singleCameraVm.RecipeNotSelected), true, DataSourceUpdateMode.OnPropertyChanged);
}

View File

@@ -5,11 +5,23 @@ namespace VisionBuilder.UI.Windows.Components
{
public partial class Stats : UserControl
{
private const string StartedAtLabel = "Started at";
private const string RecipeLabel = "Recipe";
private const string ProcessingTimeLabel = "Processing time";
private const string GoodLabel = "Good";
private const string BadLabel = "Bad";
private const string TotalLabel = "Total";
private const string DetailsLabel = "Details";
private const string ErrorRateLabel = "Error rate";
private const string FontFamilyName = "Verdana";
private const string Ellipsis = "...";
public void SetViewModel(StatisticsVM statisticsVm)
{
_statisticsVm = statisticsVm;
_statisticsVm.PropertyChanged += _statisticsVm_PropertyChanged;
Clear();
}
private void _statisticsVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
@@ -17,29 +29,46 @@ namespace VisionBuilder.UI.Windows.Components
switch (e.PropertyName)
{
case nameof(StatisticsVM.SessionStarted):
UpdateMeta("Started at", _statisticsVm.SessionStarted, true);
UpdateMeta(StartedAtLabel, _statisticsVm.SessionStarted, true);
break;
case nameof(StatisticsVM.RecipeName):
UpdateMeta("Recipe", _statisticsVm.RecipeName, true);
// we don't use RecipeName(it's on button)
//UpdateMeta(RecipeLabel, _statisticsVm.RecipeName, true);
break;
case nameof(StatisticsVM.ProcessingTime):
UpdateMeta("Processing time", _statisticsVm.ProcessingTime, true);
UpdateMeta(ProcessingTimeLabel, _statisticsVm.ProcessingTime, true);
break;
case nameof(StatisticsVM.Good):
UpdateMeta("Good", _statisticsVm.Good.ToString(), true);
// don't need to show good count
//UpdateMeta(GoodLabel, _statisticsVm.Good.ToString(), false);
break;
case nameof(StatisticsVM.Bad):
UpdateMeta("Bad", _statisticsVm.Bad.ToString(), true);
UpdateMeta(BadLabel, _statisticsVm.Bad.ToString(), false);
break;
case nameof(StatisticsVM.Total):
UpdateMeta("Total", _statisticsVm.Total.ToString(), true);
UpdateMeta(TotalLabel, _statisticsVm.Total.ToString(), false);
break;
case nameof(StatisticsVM.StatisticsDetails):
UpdateMeta("Details", _statisticsVm.StatisticsDetails, false);
UpdateMeta(DetailsLabel, _statisticsVm.StatisticsDetails, false);
break;
case nameof(StatisticsVM.ErrorRate):
UpdateMeta(ErrorRateLabel, _statisticsVm.ErrorRate, false);
break;
}
}
void InitializeFields()
{
UpdateMeta(StartedAtLabel,"");
UpdateMeta(ProcessingTimeLabel, "");
UpdateMeta(TotalLabel, "0");
UpdateMeta(BadLabel, "0");
UpdateMeta(ErrorRateLabel, "0");
UpdateMeta(DetailsLabel, "");
}
public Stats()
{
@@ -64,15 +93,27 @@ namespace VisionBuilder.UI.Windows.Components
private readonly Bitmap _imageBadIndicator;
private StatisticsVM _statisticsVm;
public void UpdateMeta(string name, string text, bool bold=false)
public static string ShortenString(string input, int maxLength)
{
if (input.Length <= maxLength)
{
return input;
}
else
{
return input.Substring(0, maxLength) + Ellipsis;
}
}
public void UpdateMeta(string name, string text, bool bold = false)
{
Action a = new Action(() =>
{
if (!_meta.ContainsKey(name))
{
metaPanel.Controls.Add(new Label() { AutoSize = true, Margin = new Padding(0, 8, 0, 0), Text = name + ":", Font = new Font(new FontFamily("Verdana"), 8, FontStyle.Bold) });
metaPanel.Controls.Add(new Label() { AutoSize = true, Margin = new Padding(0, 8, 0, 0), Text = name + ":", Font = new Font(new FontFamily(FontFamilyName), 8, FontStyle.Bold) });
_meta[name] = new Label() { AutoSize = true, Margin = new Padding(0) };
_meta[name].Font = new Font(new FontFamily("Verdana"), 8, bold?FontStyle.Bold:FontStyle.Regular);
_meta[name].Font = new Font(new FontFamily(FontFamilyName), 8, bold ? FontStyle.Bold : FontStyle.Regular);
metaPanel.Controls.Add(_meta[name]);
}
@@ -94,22 +135,11 @@ namespace VisionBuilder.UI.Windows.Components
public static string ShortenString(string input, int maxLength)
{
if (input.Length <= maxLength)
{
return input;
}
else
{
return input.Substring(0, maxLength) + "...";
}
}
public void Clear()
{
_meta.Clear();
metaPanel.Controls.Clear();
InitializeFields();
}
}
}

View File

@@ -88,39 +88,39 @@ namespace Inspectron.HawkEye.View
//
// btnNext
//
btnNext.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnNext.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnNext.Cursor = Cursors.Hand;
btnNext.Depth = 0;
btnNext.DrawBorder = true;
btnNext.Font = new Font("Verdana", 9F);
btnNext.Icon = null;
btnNext.Location = new Point(16, 56);
btnNext.Location = new Point(1208, 56);
btnNext.MouseState = MaterialSkin.MouseState.HOVER;
btnNext.Name = "btnNext";
btnNext.Primary = false;
btnNext.Size = new Size(168, 64);
btnNext.TabIndex = 4;
btnNext.Text = "Backwards";
btnNext.Text = "Next image";
btnNext.UseVisualStyleBackColor = true;
btnNext.Visible = false;
btnNext.Click += btnNext_Click;
//
// btnPrev
//
btnPrev.Anchor = AnchorStyles.Top | AnchorStyles.Right;
btnPrev.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnPrev.Cursor = Cursors.Hand;
btnPrev.Depth = 0;
btnPrev.DrawBorder = true;
btnPrev.Font = new Font("Verdana", 9F);
btnPrev.Icon = null;
btnPrev.Location = new Point(1208, 56);
btnPrev.Location = new Point(16, 56);
btnPrev.MouseState = MaterialSkin.MouseState.HOVER;
btnPrev.Name = "btnPrev";
btnPrev.Primary = false;
btnPrev.Size = new Size(168, 64);
btnPrev.TabIndex = 5;
btnPrev.Text = "Forward";
btnPrev.Text = "Previous image";
btnPrev.UseVisualStyleBackColor = true;
btnPrev.Visible = false;
btnPrev.Click += btnPrev_Click;

View File

@@ -40,6 +40,9 @@ namespace Inspectron.HawkEye.View
case nameof(ErrorPreviewVM.IsNextImageEnabled):
btnNext.Visible = _previewVm.IsNextImageEnabled;
break;
case nameof(ErrorPreviewVM.ImageName):
this.lblErrName.Text = _previewVm.ImageName;
break;
}
}

View File

@@ -67,6 +67,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F2406FBB
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IDSTest", "IDSTest\IDSTest.csproj", "{AD1A176C-B19F-4E60-9935-728722BAE3C1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{583A77DF-A293-4F3E-AB96-7310BC495822}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B24SiemensPlugin", "Plugins\Siemens\B24SiemensPlugin\B24SiemensPlugin.csproj", "{DB20082B-60E8-D623-3F3A-04612A929CD6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B24SiemensEmulator", "Plugins\Siemens\B24SiemensEmulator\B24SiemensEmulator.csproj", "{52D6A9AE-D0F1-4C52-B688-E0219169E179}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "Plugins\TestPlugin\TestPlugin.csproj", "{F7D39916-489A-3583-09A0-175AE82D08B7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -177,6 +185,18 @@ Global
{AD1A176C-B19F-4E60-9935-728722BAE3C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD1A176C-B19F-4E60-9935-728722BAE3C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD1A176C-B19F-4E60-9935-728722BAE3C1}.Release|Any CPU.Build.0 = Release|Any CPU
{DB20082B-60E8-D623-3F3A-04612A929CD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB20082B-60E8-D623-3F3A-04612A929CD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB20082B-60E8-D623-3F3A-04612A929CD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB20082B-60E8-D623-3F3A-04612A929CD6}.Release|Any CPU.Build.0 = Release|Any CPU
{52D6A9AE-D0F1-4C52-B688-E0219169E179}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{52D6A9AE-D0F1-4C52-B688-E0219169E179}.Debug|Any CPU.Build.0 = Debug|Any CPU
{52D6A9AE-D0F1-4C52-B688-E0219169E179}.Release|Any CPU.ActiveCfg = Release|Any CPU
{52D6A9AE-D0F1-4C52-B688-E0219169E179}.Release|Any CPU.Build.0 = Release|Any CPU
{F7D39916-489A-3583-09A0-175AE82D08B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F7D39916-489A-3583-09A0-175AE82D08B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7D39916-489A-3583-09A0-175AE82D08B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7D39916-489A-3583-09A0-175AE82D08B7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -201,6 +221,9 @@ Global
{254841AD-0706-4C92-A364-7E5DC17AF4AD} = {250F2B27-FA2B-4CE6-BFDE-54D0B7FC9FAF}
{86511F06-AABC-4F1D-AB4E-9763AE4A0EF2} = {D05689E3-04C6-4E3B-ACA7-3F4507CED4CC}
{AD1A176C-B19F-4E60-9935-728722BAE3C1} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71}
{DB20082B-60E8-D623-3F3A-04612A929CD6} = {583A77DF-A293-4F3E-AB96-7310BC495822}
{52D6A9AE-D0F1-4C52-B688-E0219169E179} = {583A77DF-A293-4F3E-AB96-7310BC495822}
{F7D39916-489A-3583-09A0-175AE82D08B7} = {F2406FBB-DFD3-4CBE-9644-A9FFC2FCBB71}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677}

View File

@@ -276,6 +276,10 @@ namespace Inspectron.Settings
{
value = converter.ConvertFromInvariantString(valueString);
}
else if (TypeConverterRegistry.CanConvert(type))
{
value = TypeConverterRegistry.GetConverter(type).ConvertFrom(valueString);
}
else
{
// deserialize
@@ -295,8 +299,8 @@ namespace Inspectron.Settings
}
private static bool CanConvertToAndFromString(TypeConverter converter)
{
return converter.CanConvertFrom(typeof(string)) &&
converter.CanConvertTo(typeof(string));
return (converter.CanConvertFrom(typeof(string)) &&
converter.CanConvertTo(typeof(string)));
}
/// <summary>
/// Class for group of user settings</summary>
@@ -478,6 +482,10 @@ namespace Inspectron.Settings
{
valueString = converter.ConvertToInvariantString(value);
}
else if (TypeConverterRegistry.TryGetConverter(type, out var typeConverter))
{
valueString = typeConverter.ConvertTo(value, typeof(string)) as string;
}
else if (type.IsSerializable)
{
// serialize

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
namespace Inspectron.Settings
{
public static class TypeConverterRegistry
{
private static readonly Dictionary<Type, ITypeConverter> _converters = new Dictionary<Type, ITypeConverter>();
public static void Register<T>(ITypeConverter converter)
{
_converters[typeof(T)] = converter;
}
public static ITypeConverter GetConverter<T>()
{
return _converters[typeof(T)];
}
public static ITypeConverter GetConverter(Type type)
{
if (_converters.TryGetValue(type, out var converter))
{
return converter;
}
throw new KeyNotFoundException($"No converter registered for type {type.FullName}");
}
public static bool TryGetConverter<T>(out ITypeConverter converter)
{
return _converters.TryGetValue(typeof(T), out converter);
}
public static bool TryGetConverter(Type type, out ITypeConverter converter)
{
return _converters.TryGetValue(type, out converter);
}
public static bool CanConvert<T>()
{
return _converters.ContainsKey(typeof(T));
}
public static bool CanConvert(Type type)
{
return _converters.ContainsKey(type);
}
}
public interface ITypeConverter
{
// Converts an object from one type to another
object ConvertFrom(object value);
// Converts an object to a specified type
object ConvertTo(object value, Type destinationType);
}
}