io commander

This commit is contained in:
meelstorm
2025-07-19 15:57:59 +02:00
parent 40d74d6da6
commit cd70088d9a
46 changed files with 2198 additions and 28 deletions

View File

@@ -1,5 +1,6 @@
using OpenCvSharp;
using System.Drawing;
using System.Threading.Channels;
using Ninject;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.RecipeProcessing;
@@ -23,7 +24,31 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Emulation
return _name;
}
private Channel<Mat> _channel = Channel.CreateBounded<Mat>(new BoundedChannelOptions(1)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
SingleWriter = true,
AllowSynchronousContinuations = true
});
public async Task<Mat> GetImage(CancellationToken token)
{
//clear the channel if it is full
if (_channel.Reader.Count > 0)
{
while (await _channel.Reader.WaitToReadAsync(token))
{
if (_channel.Reader.TryRead(out _)) continue;
break;
}
}
//get the next image
await Task.Run(new Action(() => _ = GetImageInternal(CancellationToken.None)), token);
return await _channel.Reader.ReadAsync(token);
}
public async Task<Mat> GetImageInternal(CancellationToken token)
{
if (_selectedFiles.Length == 0) return null;
var id = (_index) % _selectedFiles.Length;
@@ -50,8 +75,9 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Emulation
Console.WriteLine($"Getting image {path}");
return Cv2.ImRead(path);
var res= Cv2.ImRead(path);
await _channel.Writer.WriteAsync(res, token);
return res;
}
public void SetFolder(string imagesFolder)
{

View File

@@ -5,9 +5,14 @@ namespace Hawkeye.VisionBuilder.Workflow;
public class Context
{
public Context()
{
}
public HawkeyeImage? ActiveImage { get; set; }
public HawkeyeImage? LastCameraImage { get; set; }
public List<IGraphicsElement> GraphicsElements { get; set; }=new List<IGraphicsElement>();
public Dictionary<string, HawkeyeImage> Memory { get; set; } = new Dictionary<string, HawkeyeImage>();
public CancellationToken CancellationToken { get; set; }
}

View File

@@ -26,7 +26,7 @@ public class GetImageOperation:BaseOperation,IHaveImage
protected override void InterpretInternal(Context context)
{
var image = _workflowList.ImageSource.GetImage(CancellationToken.None).Result;
var image = _workflowList.ImageSource.GetImage(context.CancellationToken).Result;

View File

@@ -32,6 +32,7 @@ public class WorkflowList
long total = 0;
foreach (BaseOperation operation in Operations)
{
if (Context.CancellationToken.IsCancellationRequested) break;
operation.Interpret(Context);
}
_executionCounter++;

View File

@@ -10,7 +10,12 @@ namespace VisionBuilder.UI.Common.ViewModel;
public partial class MainWindowVM:ObservableObject
{
private readonly ISettingsService _settingsService;
public event Action ProgramStarted = delegate { };
public event Action ProgramClosed = delegate { };
public event Action<bool> TestModeChanged = delegate { };
[ObservableProperty]
private bool _testMode;
public MainWindowVM(ISettingsService settingsService)
{
@@ -27,4 +32,21 @@ public partial class MainWindowVM:ObservableObject
_settingsService.ShowSettingsDialog();
}
[RelayCommand]
public void ToggleTestMode()
{
TestMode = !TestMode;
TestModeChanged(TestMode);
}
public void OnProgramStarted()
{
ProgramStarted?.Invoke();
}
public void OnProgramClosed()
{
ProgramClosed?.Invoke();
}
}

View File

@@ -1,6 +1,7 @@
using Inspectron.Settings;
using Ninject;
using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
namespace VisionBuilder.UI.Common;
@@ -17,6 +18,7 @@ public static class VisionBuilder
mainKernel.Bind<ILearningTool>().To<NoLearning>().InSingletonScope();
// --- //
mainKernel.Bind<MainWindowVM>().ToSelf().InSingletonScope();
return mainKernel;
}

View File

@@ -1,4 +1,5 @@
using Ninject.Extensions.ChildKernel;
using Ninject;
using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Console;
@@ -6,7 +7,7 @@ namespace VisionBuilder.UI.Statistics;
public static class ModuleExtensions
{
public static IChildKernel UseConsole(this IChildKernel self)
public static IKernel UseConsole(this IKernel self)
{
self.Bind<VisionBuilderConsoleSettings, ISettings>().ToConstant(new VisionBuilderConsoleSettings());
self.RegisterModule<VisionBuilderConsole>();

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,8 @@
{
"permissions": {
"allow": [
"Bash(dotnet build)"
],
"deny": []
}
}

View File

@@ -0,0 +1,21 @@
using Ninject.Extensions.ChildKernel;
using System.ComponentModel;
using Ninject;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.IOCommander.Interfaces;
using VisionBuilder.UI.IOCommander.Settings;
using VisionBuilder.UI.IOCommander.Windows;
namespace VisionBuilder.UI.IOCommander;
public static class ModuleExtensions
{
public static IKernel UseIOCommanderWindowsDebug(this IKernel self)
{
TypeDescriptor.AddAttributes(typeof(List<PinLabel>), new TypeConverterAttribute(typeof(PinLabelListConverter)));
self.Rebind<IIOCommanderDebugViewService>().To<WindowsIOCommanderDebugViewService>().InSingletonScope();
self.Bind<WindowsIOCommanderDebugViewSettings,ISettings>().ToConstant(new WindowsIOCommanderDebugViewSettings());
return self;
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,50 @@
namespace VisionBuilder.UI.IOCommander.Windows
{
partial class WindowsIOCommanderDebugViewService
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
SuspendLayout();
//
// WindowsIOCommanderDebugViewService
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(280, 542);
ControlBox = false;
MaximizeBox = false;
MinimizeBox = false;
Name = "WindowsIOCommanderDebugViewService";
ShowIcon = false;
Text = "IOCommander Debug View";
TopMost = true;
ResumeLayout(false);
}
#endregion
}
}

View File

@@ -0,0 +1,218 @@
using System.Drawing;
using System.Windows.Forms;
using VisionBuilder.UI.IOCommander.Interfaces;
namespace VisionBuilder.UI.IOCommander.Windows
{
public partial class WindowsIOCommanderDebugViewService : Form, IIOCommanderDebugViewService
{
private readonly WindowsIOCommanderDebugViewSettings _settings;
private readonly Dictionary<int, PinIndicator> _inputPins = new();
private readonly Dictionary<int, PinIndicator> _outputPins = new();
private readonly object _lockObject = new object();
private bool _isVisible = false;
public WindowsIOCommanderDebugViewService(WindowsIOCommanderDebugViewSettings settings)
{
_settings = settings;
InitializeComponent();
InitializePinDisplay();
}
protected override void OnShown(EventArgs e)
{
this.TopMost = true;
this.Focus();
this.TopMost = true;
}
private void InitializePinDisplay()
{
// Configure form properties
this.Text = "IOCommander Debug View";
this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
this.TopMost = true;
this.TopLevel = true;
this.ShowInTaskbar = false;
this.StartPosition = FormStartPosition.Manual;
this.Location = new Point(50, 50);
this.Size = new Size(280, 450); // Made wider to accommodate labels
this.BackColor = Color.FromArgb(64, 64, 64);
this.MinimizeBox = false;
this.MaximizeBox = false;
// Create pin indicators for input pins 1-20 and output pins 1-20 (20 rows, 2 columns)
int pinSize = 18;
int spacing = 2;
int centerX = 140; // Center the graphics in the wider form
int startY = 10;
// Get label dictionaries for quick lookup
var inputLabelDict = _settings.InputPinLabels?.ToDictionary(pl => pl.Pin, pl => pl.Label) ?? new Dictionary<int, string>();
var outputLabelDict = _settings.OutputPinLabels?.ToDictionary(pl => pl.Pin, pl => pl.Label) ?? new Dictionary<int, string>();
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
// Left column (input pins)
var leftIndicator = new PinIndicator(inputPin, true)
{
Location = new Point(centerX - pinSize - spacing * 2, startY + (row * (pinSize + spacing))),
Size = new Size(pinSize, pinSize)
};
_inputPins[inputPin] = leftIndicator;
this.Controls.Add(leftIndicator);
// Right column (output pins)
var rightIndicator = new PinIndicator(outputPin, false)
{
Location = new Point(centerX + spacing * 2, startY + (row * (pinSize + spacing))),
Size = new Size(pinSize, pinSize)
};
_outputPins[outputPin] = rightIndicator;
this.Controls.Add(rightIndicator);
// Input pin label (left side - label before pin number)
string inputLabelText = inputLabelDict.ContainsKey(inputPin) ? $"{inputLabelDict[inputPin]} {inputPin}" : inputPin.ToString();
var leftLabel = new Label
{
Text = inputLabelText,
Location = new Point(5, startY + (row * (pinSize + spacing)) + 2),
Size = new Size(centerX - pinSize - spacing * 3 - 5, 14),
Font = new Font("Arial", 6, FontStyle.Bold),
ForeColor = Color.White,
BackColor = Color.Transparent,
TextAlign = ContentAlignment.MiddleRight
};
this.Controls.Add(leftLabel);
// Output pin label (right side - pin number followed by label)
string outputLabelText = outputLabelDict.ContainsKey(outputPin) ? $"{outputPin} {outputLabelDict[outputPin]}" : outputPin.ToString();
var rightLabel = new Label
{
Text = outputLabelText,
Location = new Point(centerX + pinSize + spacing * 3, startY + (row * (pinSize + spacing)) + 2),
Size = new Size(this.Width - (centerX + pinSize + spacing * 3) - 10, 14),
Font = new Font("Arial", 6, FontStyle.Bold),
ForeColor = Color.White,
BackColor = Color.Transparent,
TextAlign = ContentAlignment.MiddleLeft
};
this.Controls.Add(rightLabel);
}
}
public void SetInputPinStatus(int pin, bool state)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => SetInputPinStatus(pin, state)));
return;
}
lock (_lockObject)
{
if (_inputPins.ContainsKey(pin))
{
_inputPins[pin].SetState(state);
}
}
}
public void SetOutputPinStatus(int pin, bool state)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => SetOutputPinStatus(pin, state)));
return;
}
lock (_lockObject)
{
if (_outputPins.ContainsKey(pin))
{
_outputPins[pin].SetState(state);
}
}
}
public void ShowDebugView()
{
if (this.InvokeRequired)
{
this.Invoke(new Action(ShowDebugView));
return;
}
lock (_lockObject)
{
if (!_isVisible)
{
this.Show();
_isVisible = true;
}
}
}
private class PinIndicator : Control
{
private readonly int _pinNumber;
private readonly bool _isInput;
private bool _state = false;
private readonly SolidBrush _activeBrush;
private readonly SolidBrush _inactiveBrush;
public PinIndicator(int pinNumber, bool isInput)
{
_pinNumber = pinNumber;
_isInput = isInput;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
// Input pins: Green when active, dark gray when inactive
// Output pins: Red when active, dark gray when inactive
if (_isInput)
{
_activeBrush = new SolidBrush(Color.Lime);
_inactiveBrush = new SolidBrush(Color.DarkGray);
}
else
{
_activeBrush = new SolidBrush(Color.Red);
_inactiveBrush = new SolidBrush(Color.DarkGray);
}
}
public void SetState(bool state)
{
if (_state != state)
{
_state = state;
this.Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e)
{
var brush = _state ? _activeBrush : _inactiveBrush;
e.Graphics.FillEllipse(brush, 0, 0, Width - 1, Height - 1);
e.Graphics.DrawEllipse(Pens.Black, 0, 0, Width - 1, Height - 1);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_activeBrush?.Dispose();
_inactiveBrush?.Dispose();
}
base.Dispose(disposing);
}
}
}
}

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,127 @@
using Inspectron.Settings;
using System.ComponentModel;
using System.Globalization;
using VisionBuilder.UI.Common;
namespace VisionBuilder.UI.IOCommander.Windows;
public class WindowsIOCommanderDebugViewSettings:ISettings
{
public List<PinLabel> InputPinLabels { get; set; }
public List<PinLabel> OutputPinLabels { get; set; }
public void RegisterSettings(InspectronSettings settings)
{
settings.RegisterSimple(this, () => InputPinLabels, "IOCommander/DebugView", nameof(InputPinLabels));
settings.RegisterSimple(this, () => OutputPinLabels, "IOCommander/DebugView", nameof(OutputPinLabels));
}
}
public class PinLabel
{
public int Pin { get; set; }
public string Label { get; set; }
public override string ToString()
{
return $"{Pin}:{Label}";
}
}
public class PinLabelListConverter : TypeConverter
{
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 PinLabel
if (context?.PropertyDescriptor?.PropertyType == typeof(PinLabel))
{
return ConvertFromString(context, culture, stringValue);
}
// Otherwise, convert to a list of PinLabel
var pinLabels = new List<PinLabel>();
if (!string.IsNullOrEmpty(stringValue))
{
var lines = stringValue.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var pinLabel = ConvertFromString(context, culture, line.Trim());
if (pinLabel != null)
pinLabels.Add(pinLabel);
}
}
return pinLabels;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
// Handle single PinLabel
if (value is PinLabel pinLabel)
{
return ConvertToString(context, culture, pinLabel);
}
// Handle list of PinLabel
if (value is List<PinLabel> pinLabels)
{
var lines = pinLabels.Select(pl =>
ConvertToString(context, culture, pl)
).Where(line => !string.IsNullOrEmpty(line));
return string.Join("\n", lines);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Convert a single string to PinLabel
/// </summary>
private PinLabel ConvertFromString(ITypeDescriptorContext context, CultureInfo culture, string stringValue)
{
if (string.IsNullOrEmpty(stringValue))
return null;
var parts = stringValue.Split(',');
if (parts.Length == 2)
{
if (int.TryParse(parts[0], out int pin))
{
return new PinLabel
{
Pin = pin,
Label = parts[1].Trim()
};
}
}
return null;
}
/// <summary>
/// Convert a single PinLabel to string
/// </summary>
private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, PinLabel pinLabel)
{
if (pinLabel == null)
return null;
return $"{pinLabel.Pin},{pinLabel.Label}";
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -0,0 +1,7 @@
namespace VisionBuilder.UI.IOCommander;
public enum EGPIOCommand
{
Start,
Stop,
}

View File

@@ -0,0 +1,12 @@
namespace VisionBuilder.UI.IOCommander;
public enum EProcessingEvent
{
ProgramStarted,
ProgramEnded,
SessionStarted,
SessionEnded,
ErrorOccurred,
GoodOccured,
TestMode
}

View File

@@ -0,0 +1,119 @@
using System.Collections;
using System.IO.Ports;
namespace VisionBuilder.UI.IOCommander;
public class IOCommander
{
SerialPort _port;
public IOCommander(string COMPort)
{
_port = new SerialPort(COMPort);
_port.BaudRate = 57600;
_port.ReadTimeout = 100;
_port.WriteTimeout = 100;
_port.Open();
Task.Factory.StartNew(CheckPins, TaskCreationOptions.LongRunning);
}
private int _currentPinsState = 0;
public BitArray PinState { get; private set; }
private void CheckPins()
{
while (true)
{
lock (this)
{
if (!_port.IsOpen) return;
try
{
var req = new byte[] { 1, 0 };
_port.Write(req, 0, req.Length);
var readState = ~(_port.ReadByte());
BitArray arrreadState = new BitArray(new byte[] { (byte)readState });
PinState = new BitArray(new byte[] { (byte)readState }); ;
string buf = "";
foreach (bool b in arrreadState)
{
buf += b ? 1 : 0;
}
//Console.WriteLine(buf);
if (_currentPinsState != readState)
{
BitArray arrcurrentPinsState = new BitArray(new byte[] { (byte)_currentPinsState });
for (int i = 0; i < 8; i++)
{
if (arrcurrentPinsState[i] != arrreadState[i])
OnPinChanged(i, arrreadState[i]);
}
}
_currentPinsState = readState;
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
Thread.Sleep(100);
}
}
public void Dispose()
{
if (_port.IsOpen) _port.Close();
}
public event Action<int, bool> OnPinChanged = delegate { };
List<int> _readPins = new List<int>();
private int _pinsState = 0;
public void SetPins(int pin, bool state)
{
lock (this)
{
//Log.Logger.ForContext<IOCommander>().Verbose("Setting pin {pin} to {state}", pin, state);
if (state)
_pinsState |= (1 << pin);
else
_pinsState &= ~(1 << pin);
var data = new byte[] { 0x00, (byte)_pinsState };
try
{
_port.Write(data, 0, data.Length);
}
catch (Exception ex)
{
}
}
}
public void SetPins(int pin)
{
lock (this)
{
if (!_readPins.Contains(pin))
{
_readPins.Add(pin);
_port.WriteLine("$pinmo:" + pin + ";");
}
_port.WriteLine("$pinh:" + pin + ";");
}
}
}

View File

@@ -0,0 +1,20 @@
using Inspectron.Settings.Attributes;
namespace VisionBuilder.UI.IOCommander;
public class IOCommanderCommand
{
public int Pin { get; set; }
[SettingDescription("Should be triggered when crossing from low to high or opposite?")]
public bool OnHigh { get; set; }
public EGPIOCommand Command { get; set; }
override public string ToString()
{
return $"Pin: {Pin}, OnHigh: {OnHigh}, Command: {Command}";
}
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel;
using System.Globalization;
namespace VisionBuilder.UI.IOCommander;
public class IOCommanderPinAction
{
public int Pin { get; set; }
public bool Low { get; set; }
public bool Pulse { get; set; }
public override string ToString()
{
return $"Pin: {Pin}, {(Low?"Low":"High")}, Pulse: {Pulse}";
}
}

View File

@@ -0,0 +1,116 @@
using System.ComponentModel;
using System.Globalization;
namespace VisionBuilder.UI.IOCommander;
public class IOCommanderProcessingEvent
{
public IOCommanderProcessingEvent(EProcessingEvent @event)
{
Event = @event;
}
public EProcessingEvent Event { get; set; }
public List<IOCommanderPinAction> Actions { get; set; } = new List<IOCommanderPinAction>();
}
public class IOCommanderPinActionListConverter : TypeConverter
{
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 IOCommanderPinAction
if (context?.PropertyDescriptor?.PropertyType == typeof(IOCommanderPinAction))
{
return ConvertFromString(context, culture, stringValue);
}
// Otherwise, convert to a list of IOCommanderPinAction
var actions = new List<IOCommanderPinAction>();
if (!string.IsNullOrEmpty(stringValue))
{
var lines = stringValue.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var action = ConvertFromString(context, culture, line.Trim());
if (action != null)
actions.Add(action);
}
}
return actions;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
// Handle single IOCommanderPinAction
if (value is IOCommanderPinAction action)
{
return ConvertToString(context, culture, action);
}
// Handle list of IOCommanderPinAction
if (value is List<IOCommanderPinAction> actions)
{
var lines = actions.Select(a =>
ConvertToString(context, culture, a)
).Where(line => !string.IsNullOrEmpty(line));
return string.Join("\n", lines);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Convert a single string to IOCommanderPinAction
/// </summary>
private IOCommanderPinAction ConvertFromString(ITypeDescriptorContext context, CultureInfo culture, string stringValue)
{
if (string.IsNullOrEmpty(stringValue))
return null;
var parts = stringValue.Split(',');
if (parts.Length == 3)
{
if (int.TryParse(parts[0], out int pin) &&
bool.TryParse(parts[1], out bool inverted) &&
bool.TryParse(parts[2], out bool pulse))
{
return new IOCommanderPinAction
{
Pin = pin,
Low = inverted,
Pulse = pulse
};
}
}
return null;
}
/// <summary>
/// Convert a single IOCommanderPinAction to string
/// </summary>
private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, IOCommanderPinAction action)
{
if (action == null)
return null;
return $"{action.Pin},{action.Low},{action.Pulse}";
}
}

View File

@@ -0,0 +1,8 @@
namespace VisionBuilder.UI.IOCommander.Interfaces;
public interface IIOCommanderDebugViewService
{
public void SetInputPinStatus(int pin, bool state);
public void SetOutputPinStatus(int pin, bool state);
public void ShowDebugView();
}

View File

@@ -0,0 +1,36 @@
using Ninject.Extensions.ChildKernel;
using System.ComponentModel;
using Ninject;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.IOCommander.Interfaces;
using VisionBuilder.UI.IOCommander.Modules;
using VisionBuilder.UI.IOCommander.Settings;
namespace VisionBuilder.UI.IOCommander;
public static class ModuleExtensions
{
public static IKernel UseIOCommander(this IKernel self, string[] cameraNames)
{
// 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();
self.Bind<IOCommanderSettings, ISettings>().ToConstant(new IOCommanderSettings(cameras.ToArray()));
self.Bind<IOCommanderModule,IVisionBuilderModule>().To<IOCommanderModule>().InSingletonScope();
self.Bind<IOCommanderProgramModule, IVisionBuilderModule>().To<IOCommanderProgramModule>().InSingletonScope();
self.Bind<IIOCommanderDebugViewService>().To<NoDebugView>().InSingletonScope();
return self;
}
public static IChildKernel UseCameraIOCommander(this IChildKernel self, string cameraName)
{
var ioCommanderSettings = self.Get<IOCommanderSettings>();
var cameraSettings=ioCommanderSettings.Cameras.First(x => x.CameraName == cameraName);
self.Bind<IOCommanderCameraSettings, ISettings>().ToConstant(cameraSettings);
self.Bind<IOCommanderCameraModule, IVisionBuilderModule>().To<IOCommanderCameraModule>().InSingletonScope();
return self;
}
}

View File

@@ -0,0 +1,91 @@
using System.Runtime;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.RecipeProcessing;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.IOCommander.Settings;
namespace VisionBuilder.UI.IOCommander.Modules;
public class IOCommanderCameraModule: IVisionBuilderModule
{
private readonly IOCommanderModule _mainModule;
private readonly IOCommanderSettings _settings;
private readonly IOCommanderCameraSettings _cameraSettings;
private readonly IRecognitionControl _recognitionControl;
private readonly MainWindowVM _mainWindowVm;
public IOCommanderCameraModule(IOCommanderModule mainModule, IOCommanderSettings settings, IOCommanderCameraSettings cameraSettings, IRecognitionControl recognitionControl, MainWindowVM mainWindowVm)
{
_mainModule = mainModule;
_settings = settings;
_cameraSettings = cameraSettings;
_recognitionControl = recognitionControl;
_mainWindowVm = mainWindowVm;
_recognitionControl.ImageProcessed += _recognitionControl_ImageProcessed;
_recognitionControl.SessionStarted += _recognitionControl_SessionStarted;
_recognitionControl.SessionEnded += _recognitionControl_SessionEnded;
}
private void EmitEvent(EProcessingEvent @event)
{
var @eventActions = _cameraSettings.ProcessingEvents.First(x => x.Event == @event);
var actions = @eventActions.Actions;
if (_mainWindowVm.TestMode)
{
var testModeActions = _cameraSettings.TestModeOverride;
var testModePins = testModeActions.Select(x => x.Pin).ToList();
actions = actions.Where(x => !testModePins.Contains(x.Pin)).ToList();
actions.AddRange(testModeActions);
}
foreach (var action in actions)
{
_mainModule.SetOutput(action.Pin, !action.Low);
if (action.Pulse)
{
Task.Run(async () =>
{
await Task.Delay(_settings.PulseLength);
_mainModule.SetOutput(action.Pin, action.Low);
});
}
}
}
public void InitializeModule()
{
}
private void _recognitionControl_SessionEnded(SessionEndedEvent obj)
{
EmitEvent(EProcessingEvent.SessionEnded);
}
private void _recognitionControl_SessionStarted(SessionStartedEvent obj)
{
EmitEvent(EProcessingEvent.SessionStarted);
}
private void _recognitionControl_ImageProcessed(Common.Commands.ImageProcessedEvent obj)
{
if (obj.HasError)
{
EmitEvent(EProcessingEvent.ErrorOccurred);
}
else
{
EmitEvent(EProcessingEvent.GoodOccured);
}
}
}

View File

@@ -0,0 +1,62 @@
using System.Collections.Concurrent;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.RecipeProcessing;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.IOCommander.Interfaces;
using VisionBuilder.UI.IOCommander.Settings;
namespace VisionBuilder.UI.IOCommander.Modules;
public class IOCommanderModule:IVisionBuilderModule
{
private readonly IOCommanderSettings _settings;
private readonly IIOCommanderDebugViewService _debugViewService;
private IOCommander? _ioCommander;
public IOCommanderModule(IOCommanderSettings settings, IIOCommanderDebugViewService debugViewService)
{
_settings = settings;
_debugViewService = debugViewService;
}
public void InitializeModule()
{
if (!_settings.EmulationMode)
{
_ioCommander = new IOCommander(_settings.Port);
_ioCommander.OnPinChanged += OnPinChanged;
}
if(_settings.ShowDebugView)_debugViewService.ShowDebugView();
}
ConcurrentDictionary<int, bool> _pinStates = new ConcurrentDictionary<int, bool>();
private void OnPinChanged(int arg1, bool arg2)
{
_pinStates.TryAdd(arg1, arg2);
_debugViewService.SetInputPinStatus(arg1, arg2);
}
public void SetOutput(int pin, bool value)
{
if (_ioCommander != null)
{
_ioCommander.SetPins(pin, value);
}
_debugViewService.SetOutputPinStatus(pin,value);
}
public bool GetInput(int pin)
{
if (_ioCommander == null) return false;
var found=_pinStates.TryGetValue(pin, out var value);
if (found)
{
return value;
}
return false;
}
}

View File

@@ -0,0 +1,55 @@
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.IOCommander.Settings;
namespace VisionBuilder.UI.IOCommander.Modules;
public class IOCommanderProgramModule: IVisionBuilderModule
{
private readonly IOCommanderSettings _settings;
private readonly IOCommanderModule _mainModule;
private readonly MainWindowVM _mainWindowVm;
public IOCommanderProgramModule(IOCommanderSettings settings, IOCommanderModule mainModule, MainWindowVM mainWindowVm)
{
_settings = settings;
_mainModule = mainModule;
_mainWindowVm = mainWindowVm;
_mainWindowVm.ProgramStarted += _mainWindowVm_ProgramStarted;
_mainWindowVm.ProgramClosed += _mainWindowVm_ProgramClosed;
}
private void EmitEvent(EProcessingEvent @event)
{
var actions = _settings.GlobalEvents.First(x => x.Event == @event);
foreach (var action in actions.Actions)
{
_mainModule.SetOutput(action.Pin,!action.Low);
if (action.Pulse)
{
Task.Run(async () =>
{
await Task.Delay(_settings.PulseLength);
_mainModule.SetOutput(action.Pin, action.Low);
});
}
}
}
private void _mainWindowVm_ProgramClosed()
{
EmitEvent(EProcessingEvent.ProgramEnded);
}
private void _mainWindowVm_ProgramStarted()
{
EmitEvent(EProcessingEvent.ProgramStarted);
}
public void InitializeModule()
{
}
}

View File

@@ -0,0 +1,22 @@
using Serilog;
using VisionBuilder.UI.IOCommander.Interfaces;
namespace VisionBuilder.UI.IOCommander;
public class NoDebugView: IIOCommanderDebugViewService
{
public void SetInputPinStatus(int pin, bool state)
{
throw new NotImplementedException();
}
public void SetOutputPinStatus(int pin, bool state)
{
throw new NotImplementedException();
}
public void ShowDebugView()
{
Log.Warning("Debug view is not available.");
}
}

View File

@@ -0,0 +1,150 @@
using Inspectron.Settings;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using Inspectron.Settings.Attributes;
using VisionBuilder.UI.Common;
namespace VisionBuilder.UI.IOCommander.Settings
{
public class IOCommanderCameraSettings:ISettings
{
public string CameraName { get; }
public List<IOCommanderProcessingEvent> ProcessingEvents { get; set; } = [
new (EProcessingEvent.SessionStarted),
new (EProcessingEvent.SessionEnded),
new (EProcessingEvent.ErrorOccurred),
new (EProcessingEvent.GoodOccured),
];
[SettingDescription(@"
Overrides pin values during test mode.
")]
public List<IOCommanderPinAction> TestModeOverride { get; set; } = new List<IOCommanderPinAction>();
public List<IOCommanderCommand> GPIOCommands { get; set; } = new List<IOCommanderCommand>();
public IOCommanderCameraSettings(string cameraName)
{
CameraName = cameraName;
}
public void RegisterSettings(InspectronSettings settings)
{
foreach (var processingEvent in ProcessingEvents)
{
settings.RegisterSimple(processingEvent, () => processingEvent.Actions, CameraName + "/IOCommander/Outputs/"+processingEvent.Event.ToString(), nameof(processingEvent.Actions));
}
settings.RegisterSimple(this, () => TestModeOverride, CameraName + "/IOCommander/Outputs/" + nameof(TestModeOverride), nameof(TestModeOverride));
settings.RegisterSimple(this, () => GPIOCommands, CameraName + "/IOCommander/Inputs", nameof(GPIOCommands));
}
}
public class IOCommanderCommandListConverter : TypeConverter
{
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 IOCommanderCommand
if (context?.PropertyDescriptor?.PropertyType == typeof(IOCommanderCommand))
{
return ConvertFromString(context, culture, stringValue);
}
// Otherwise, convert to a list of IOCommanderCommand
var commands = new List<IOCommanderCommand>();
if (!string.IsNullOrEmpty(stringValue))
{
var lines = stringValue.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
var command = ConvertFromString(context, culture, line.Trim());
if (command != null)
commands.Add(command);
}
}
return commands;
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
// Handle single IOCommanderCommand
if (value is IOCommanderCommand command)
{
return ConvertToString(context, culture, command);
}
// Handle list of IOCommanderCommand
if (value is List<IOCommanderCommand> commands)
{
var lines = commands.Select(c =>
ConvertToString(context, culture, c)
).Where(line => !string.IsNullOrEmpty(line));
return string.Join("\n", lines);
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
/// <summary>
/// Convert a single string to IOCommanderCommand
/// </summary>
private IOCommanderCommand ConvertFromString(ITypeDescriptorContext context, CultureInfo culture, string stringValue)
{
if (string.IsNullOrEmpty(stringValue))
return null;
var parts = stringValue.Split(',');
if (parts.Length == 3)
{
if (int.TryParse(parts[0], out int pin) &&
bool.TryParse(parts[1], out bool onHigh) &&
Enum.TryParse<EGPIOCommand>(parts[2], out EGPIOCommand command))
{
return new IOCommanderCommand
{
Pin = pin,
OnHigh = onHigh,
Command = command
};
}
}
return null;
}
/// <summary>
/// Convert a single IOCommanderCommand to string
/// </summary>
private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, IOCommanderCommand command)
{
if (command == null)
return null;
return $"{command.Pin},{command.OnHigh},{command.Command}";
}
}
}

View File

@@ -0,0 +1,38 @@
using Inspectron.Settings;
using Inspectron.Settings.Attributes;
using VisionBuilder.UI.Common;
namespace VisionBuilder.UI.IOCommander.Settings;
public class IOCommanderSettings: ISettings
{
public IOCommanderCameraSettings[] Cameras { get; }
public int PulseLength { get; set; } = 100;
public bool EmulationMode { get; set; } = true;
public bool ShowDebugView { get; set; } = false;
public string Port { get; set; }="COM3";
public List<IOCommanderProcessingEvent> GlobalEvents { get; set; } =
[
new(EProcessingEvent.ProgramStarted),
new(EProcessingEvent.ProgramEnded),
];
public IOCommanderSettings(IOCommanderCameraSettings[] cameras)
{
Cameras = cameras;
}
public void RegisterSettings(InspectronSettings settings)
{
settings.RegisterSimple(this, () => Port, "IOCommander", nameof(Port));
settings.RegisterSimple(this, () => PulseLength, "IOCommander", nameof(PulseLength));
settings.RegisterSimple(this, () => EmulationMode, "IOCommander", nameof(EmulationMode));
settings.RegisterSimple(this, () => ShowDebugView, "IOCommander", nameof(ShowDebugView));
foreach (IOCommanderProcessingEvent @event in GlobalEvents)
{
settings.RegisterSimple(@event, () => @event.Actions, "IOCommander/Global outputs/" + @event.Event.ToString(), nameof(@event.Actions));
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -141,8 +141,18 @@ namespace VisionBuilder.UI.Recipes.HawkeyeRecipe
var sw = Stopwatch.StartNew();
_workflow.Context.CancellationToken= token;
_workflow.ImageSource= _imageSource;
_workflow.Execute();
try
{
_workflow.Execute();
}
catch (Exception e) when(token.IsCancellationRequested)
{
sw.Stop();
return;
}
sw.Stop();
if (sw.ElapsedMilliseconds < _recognitionConfiguration.MinimumProcessingTime)

View File

@@ -33,6 +33,7 @@
btnExit = new MaterialSkin.Controls.MaterialRaisedButton();
materialDivider1 = new MaterialSkin.Controls.MaterialDivider();
singleCameraControl1 = new VisionBuilder.UI.Windows.Components.SingleCameraControl();
btnTestMode = new MaterialSkin.Controls.MaterialRaisedButton();
SuspendLayout();
//
// btnMinimize
@@ -43,7 +44,7 @@
btnMinimize.DrawBorder = true;
btnMinimize.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnMinimize.Icon = null;
btnMinimize.Location = new Point(784, 984);
btnMinimize.Location = new Point(960, 984);
btnMinimize.MouseState = MaterialSkin.MouseState.HOVER;
btnMinimize.Name = "btnMinimize";
btnMinimize.Primary = false;
@@ -61,7 +62,7 @@
btnSettings.DrawBorder = true;
btnSettings.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnSettings.Icon = null;
btnSettings.Location = new Point(552, 984);
btnSettings.Location = new Point(728, 984);
btnSettings.MouseState = MaterialSkin.MouseState.HOVER;
btnSettings.Name = "btnSettings";
btnSettings.Primary = false;
@@ -79,7 +80,7 @@
btnExit.DrawBorder = true;
btnExit.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnExit.Icon = null;
btnExit.Location = new Point(1016, 984);
btnExit.Location = new Point(1192, 984);
btnExit.MouseState = MaterialSkin.MouseState.HOVER;
btnExit.Name = "btnExit";
btnExit.Primary = false;
@@ -108,11 +109,30 @@
singleCameraControl1.Size = new Size(1904, 912);
singleCameraControl1.TabIndex = 10;
//
// btnTestMode
//
btnTestMode.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
btnTestMode.AutoSizeMode = AutoSizeMode.GrowAndShrink;
btnTestMode.Depth = 0;
btnTestMode.DrawBorder = true;
btnTestMode.Font = new Font("Segoe UI Semibold", 12F, FontStyle.Bold);
btnTestMode.Icon = null;
btnTestMode.Location = new Point(496, 984);
btnTestMode.MouseState = MaterialSkin.MouseState.HOVER;
btnTestMode.Name = "btnTestMode";
btnTestMode.Primary = false;
btnTestMode.Size = new Size(224, 64);
btnTestMode.TabIndex = 11;
btnTestMode.Text = "Test mode";
btnTestMode.UseVisualStyleBackColor = true;
btnTestMode.Click += btnTestMode_Click;
//
// Form1
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1920, 1080);
Controls.Add(btnTestMode);
Controls.Add(singleCameraControl1);
Controls.Add(materialDivider1);
Controls.Add(btnMinimize);
@@ -131,5 +151,6 @@
private MaterialSkin.Controls.MaterialRaisedButton btnExit;
private MaterialSkin.Controls.MaterialDivider materialDivider1;
private Components.SingleCameraControl singleCameraControl1;
private MaterialSkin.Controls.MaterialRaisedButton btnTestMode;
}
}

View File

@@ -1,3 +1,4 @@
using MaterialSkin;
using MaterialSkin.Controls;
using VisionBuilder.UI.Common.ViewModel;
@@ -13,10 +14,35 @@ namespace VisionBuilder.UI.Windows.Test
MaterialSkin.MaterialSkinManager.ConfigureForInspectron();
InitializeComponent();
singleCameraControl1.SetViewModel(mainWindowVm.SingleCameraVms[0]);
Load += Form1_Load;
_mainWindowVm.PropertyChanged += _mainWindowVm_PropertyChanged;
}
private void _mainWindowVm_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(_mainWindowVm.TestMode))
{
if (_mainWindowVm.TestMode)
{
MaterialSkinManager.Instance.ColorScheme = new ColorScheme(Primary.Green800, Primary.Green800,
Primary.Green400, Accent.Green200, TextShade.WHITE);
}
else
{
MaterialSkinManager.ConfigureForInspectron();
}
Refresh();
}
}
private void Form1_Load(object sender, EventArgs e)
{
_mainWindowVm.OnProgramStarted();
}
private void btnExit_Click(object sender, EventArgs e)
{
_mainWindowVm.OnProgramClosed();
Environment.Exit(0);
}
@@ -29,5 +55,10 @@ namespace VisionBuilder.UI.Windows.Test
{
_mainWindowVm.SettingsCommand.Execute(null);
}
private void btnTestMode_Click(object sender, EventArgs e)
{
_mainWindowVm.ToggleTestModeCommand.Execute(null);
}
}
}

View File

@@ -1,11 +1,3 @@
using System.Collections.ObjectModel;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
using VisionBuilder.UI.Windows.Settings;
using System.Threading;
using Hawkeye.VisionBuilder.UI.Sources.Emulation;
using Inspectron.Settings;
using Inspectron.Settings.Windows.Configuration;
@@ -13,11 +5,21 @@ using Lindt.Colorballs.Duo.Utils;
using Ninject;
using Ninject.Extensions.ChildKernel;
using OpenCvSharp;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.RecipeProcessing;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
using VisionBuilder.UI.Console;
using VisionBuilder.UI.IOCommander;
using VisionBuilder.UI.Recipes.HawkeyeRecipe;
using VisionBuilder.UI.Ringbuffer;
using VisionBuilder.UI.Statistics;
using VisionBuilder.UI.Windows.Settings;
namespace VisionBuilder.UI.Windows.Test
{
@@ -33,16 +35,20 @@ namespace VisionBuilder.UI.Windows.Test
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
ApplicationConfiguration.Initialize();
InspectronSettings settings = new InspectronSettings("..\\Config\\AppSettings.xml");
// In your application startup code
InspectronSettings settings = new InspectronSettings("..\\Config");
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
mainKernel
.UseIOCommander([CAMERA1])
.UseIOCommanderWindowsDebug()
.UseConsole()
.UseWindowsServices();
// CAMERA 1 //
ChildKernel kernelCamera1 = new ChildKernel(mainKernel);
mainKernel.UseWindowsServices();
// MODULES //
kernelCamera1
@@ -50,10 +56,14 @@ namespace VisionBuilder.UI.Windows.Test
.UseStatistics(CAMERA1)
.UseRingbuffer(CAMERA1)
.UseHawkeyeRecipes(CAMERA1)
.UseConsole();
.UseCameraIOCommander(CAMERA1)
;
// LOAD SETTINGS //
mainKernel.RegisterSettings();
kernelCamera1.RegisterSettings();
settings.LoadSettings();
@@ -62,7 +72,7 @@ namespace VisionBuilder.UI.Windows.Test
kernelCamera1.UseCamera();
mainKernel.InitializeModules();
kernelCamera1.InitializeModules();
var mainWindowVm = mainKernel.Get<MainWindowVM>();

View File

@@ -21,6 +21,8 @@
<ProjectReference Include="..\VisionBuilder.UI.Camera\VisionBuilder.UI.Camera.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Console\VisionBuilder.UI.Console.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.IOCommander.Windows\VisionBuilder.UI.IOCommander.Windows.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Recipes.HawkeyeRecipe\VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Ringbuffer\VisionBuilder.UI.Ringbuffer.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Statistics\VisionBuilder.UI.Statistics.csproj" />

View File

@@ -57,6 +57,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Camera", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.IOCommander", "VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj", "{E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utility", "Utility", "{250F2B27-FA2B-4CE6-BFDE-54D0B7FC9FAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.IOCommander.Windows", "VisionBuilder.UI.IOCommander.Windows\VisionBuilder.UI.IOCommander.Windows.csproj", "{254841AD-0706-4C92-A364-7E5DC17AF4AD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -155,6 +159,10 @@ Global
{E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9}.Release|Any CPU.Build.0 = Release|Any CPU
{254841AD-0706-4C92-A364-7E5DC17AF4AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{254841AD-0706-4C92-A364-7E5DC17AF4AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{254841AD-0706-4C92-A364-7E5DC17AF4AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{254841AD-0706-4C92-A364-7E5DC17AF4AD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -176,6 +184,7 @@ Global
{4B1817EF-A3D1-48CC-BE22-4138740E450F} = {C828783C-1CE1-4245-8731-4AE18A28F590}
{FBDCC120-B6B3-4A28-8845-DF1690F619C1} = {C828783C-1CE1-4245-8731-4AE18A28F590}
{E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9} = {C828783C-1CE1-4245-8731-4AE18A28F590}
{254841AD-0706-4C92-A364-7E5DC17AF4AD} = {250F2B27-FA2B-4CE6-BFDE-54D0B7FC9FAF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677}

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;
using Inspectron.Settings.Attributes;
@@ -36,7 +38,12 @@ public class DefaultControlFactory : IControlFactory
};
}
if (type == typeof(string) && name.EndsWith("Path", StringComparison.OrdinalIgnoreCase))
// Check for List<T> types
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
return CreateListControl(name, description, type, initialValue, valueChangedCallback, optionsWindow, settingDescription, previewLabel, getPreview);
}
else if (type == typeof(string) && name.EndsWith("Path", StringComparison.OrdinalIgnoreCase))
{
return CreatePathControl(name, description, initialValue, valueChangedCallback, optionsWindow, settingDescription, previewLabel, getPreview);
}
@@ -62,6 +69,63 @@ public class DefaultControlFactory : IControlFactory
}
}
private Control CreateListControl(string name, string description, Type listType, object initialValue,
Action<object> valueChangedCallback, OptionsWindow optionsWindow, string settingDescription,
Label previewLabel, Func<object, string> getPreview)
{
var elementType = listType.GetGenericArguments()[0];
var list = (IList)initialValue ?? (IList)Activator.CreateInstance(listType);
var label = new Label { Text = description, AutoSize = true, Padding = new Padding(0, 5, 0, 0) };
var textBox = new TextBox
{
Text = GetListDisplayText(list),
Width = 500,
Height = 60,
Multiline = true,
ReadOnly = true,
ScrollBars = ScrollBars.Vertical
};
var editButton = new Button { Text = "Edit", AutoSize = true };
editButton.Click += (s, e) =>
{
using (var listEditor = new ListEditorDialog(elementType, list, this, optionsWindow, name))
{
if (listEditor.ShowDialog(optionsWindow) == DialogResult.OK)
{
var newList = listEditor.GetEditedList();
list.Clear();
foreach (object o in newList)
{
list.Add(o);
}
textBox.Text = GetListDisplayText(list);
valueChangedCallback(list);
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(list);
}
}
};
return CreateOuterPanel(settingDescription, previewLabel, label, textBox, editButton);
}
private string GetListDisplayText(IList list)
{
if (list == null || list.Count == 0)
return "(empty list)";
var items = new List<string>();
foreach (var item in list)
{
items.Add(item?.ToString() ?? "(null)");
}
return string.Join(Environment.NewLine, items);
}
private Control CreatePathControl(string name, string description, object initialValue, Action<object> valueChangedCallback,
OptionsWindow optionsWindow, string settingDescription, Label previewLabel, Func<object, string> getPreview)
{
@@ -186,3 +250,9 @@ public class DefaultControlFactory : IControlFactory
return outerPanel;
}
}
// List Editor Dialog
// Item Editor Dialog
// Helper class for simple property info

View File

@@ -0,0 +1,163 @@
using System;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
namespace Inspectron.Settings.Windows.Configuration;
public partial class ItemEditorDialog : Form
{
private readonly Type _itemType;
private object _workingValue;
private readonly DefaultControlFactory _controlFactory;
private readonly OptionsWindow _parentOptionsWindow;
private FlowLayoutPanel _propertyPanel;
private Button _okButton;
private Button _cancelButton;
public ItemEditorDialog(Type itemType, object initialValue, DefaultControlFactory controlFactory, OptionsWindow parentOptionsWindow)
{
_itemType = itemType;
_workingValue = CloneObject(initialValue);
_controlFactory = controlFactory;
_parentOptionsWindow = parentOptionsWindow;
InitializeComponent();
CreatePropertyControls();
}
private void InitializeComponent()
{
this.Text = $"Edit {_itemType.Name}";
this.Size = new System.Drawing.Size(650, 420);
this.StartPosition = FormStartPosition.CenterParent;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
_propertyPanel = new FlowLayoutPanel
{
Location = new System.Drawing.Point(12, 12),
Size = new System.Drawing.Size(620, 320),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
FlowDirection = FlowDirection.TopDown,
AutoScroll = true
};
_okButton = new Button
{
Text = "OK",
Location = new System.Drawing.Point(470, 340),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
DialogResult = DialogResult.OK
};
_cancelButton = new Button
{
Text = "Cancel",
Location = new System.Drawing.Point(550, 340),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
DialogResult = DialogResult.Cancel
};
this.Controls.AddRange(new Control[] { _propertyPanel, _okButton, _cancelButton });
}
private void CreatePropertyControls()
{
if (_itemType.IsValueType || _itemType == typeof(string))
{
// For simple types, create a single control
CreateSimpleValueControl();
}
else
{
// For complex types, create controls for each property
CreateComplexObjectControls();
}
}
private void CreateSimpleValueControl()
{
var dummyProperty = new SimplePropertyInfo(_itemType, "Value");
var control = _controlFactory.CreateControl("Value", "Value", dummyProperty, _workingValue,
newValue => _workingValue = newValue, _parentOptionsWindow);
_propertyPanel.Controls.Add(control);
}
private void CreateComplexObjectControls()
{
var properties = _itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite)
.ToArray();
foreach (var property in properties)
{
var currentValue = property.GetValue(_workingValue);
var control = _controlFactory.CreateControl(property.Name, property.Name, property, currentValue,
newValue => property.SetValue(_workingValue, newValue), _parentOptionsWindow);
_propertyPanel.Controls.Add(control);
}
}
public object GetEditedValue()
{
return _workingValue;
}
private object CloneObject(object obj)
{
if (obj == null) return GetDefaultValue(_itemType);
var type = obj.GetType();
if (type.IsValueType || type == typeof(string))
{
return obj;
}
try
{
var clone = Activator.CreateInstance(type);
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (prop.CanRead && prop.CanWrite)
{
prop.SetValue(clone, prop.GetValue(obj));
}
}
return clone;
}
catch
{
return GetDefaultValue(type);
}
}
private object GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
else if (type == typeof(string))
{
return "";
}
else
{
try
{
return Activator.CreateInstance(type);
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,235 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;
namespace Inspectron.Settings.Windows.Configuration;
public partial class ListEditorDialog : Form
{
private readonly Type _elementType;
private readonly IList _originalList;
private readonly IList _workingList;
private readonly DefaultControlFactory _controlFactory;
private readonly OptionsWindow _parentOptionsWindow;
private readonly string _name;
private ListBox _listBox;
private Button _addButton;
private Button _editButton;
private Button _removeButton;
private Button _okButton;
private Button _cancelButton;
public ListEditorDialog(Type elementType, IList originalList, DefaultControlFactory controlFactory, OptionsWindow parentOptionsWindow, string name)
{
_elementType = elementType;
_originalList = originalList;
_controlFactory = controlFactory;
_parentOptionsWindow = parentOptionsWindow;
_name = name;
// Create a working copy of the list
var listType = typeof(List<>).MakeGenericType(elementType);
_workingList = (IList)Activator.CreateInstance(listType);
foreach (var item in originalList)
{
_workingList.Add(CloneObject(item));
}
InitializeComponent();
LoadListItems();
}
private void InitializeComponent()
{
this.Text = $"Edit {_name}";
this.Size = new System.Drawing.Size(600, 400);
this.StartPosition = FormStartPosition.CenterParent;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
// Create controls
_listBox = new ListBox
{
Location = new System.Drawing.Point(12, 12),
Size = new System.Drawing.Size(460, 300),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom
};
_addButton = new Button
{
Text = "Add",
Location = new System.Drawing.Point(490, 12),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Top | AnchorStyles.Right
};
_editButton = new Button
{
Text = "Edit",
Location = new System.Drawing.Point(490, 45),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Top | AnchorStyles.Right,
Enabled = false
};
_removeButton = new Button
{
Text = "Remove",
Location = new System.Drawing.Point(490, 78),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Top | AnchorStyles.Right,
Enabled = false
};
_okButton = new Button
{
Text = "OK",
Location = new System.Drawing.Point(410, 330),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
DialogResult = DialogResult.OK
};
_cancelButton = new Button
{
Text = "Cancel",
Location = new System.Drawing.Point(490, 330),
Size = new System.Drawing.Size(75, 23),
Anchor = AnchorStyles.Bottom | AnchorStyles.Right,
DialogResult = DialogResult.Cancel
};
// Add event handlers
_listBox.SelectedIndexChanged += ListBox_SelectedIndexChanged;
_addButton.Click += AddButton_Click;
_editButton.Click += EditButton_Click;
_removeButton.Click += RemoveButton_Click;
// Add controls to form
this.Controls.AddRange(new Control[] { _listBox, _addButton, _editButton, _removeButton, _okButton, _cancelButton });
}
private void LoadListItems()
{
_listBox.Items.Clear();
foreach (var item in _workingList)
{
_listBox.Items.Add(item?.ToString() ?? "(null)");
}
}
private void ListBox_SelectedIndexChanged(object sender, EventArgs e)
{
bool hasSelection = _listBox.SelectedIndex >= 0;
_editButton.Enabled = hasSelection;
_removeButton.Enabled = hasSelection;
}
private void AddButton_Click(object sender, EventArgs e)
{
var defaultValue = GetDefaultValue(_elementType);
using (var itemEditor = new ItemEditorDialog(_elementType, defaultValue, _controlFactory, _parentOptionsWindow))
{
if (itemEditor.ShowDialog(this) == DialogResult.OK)
{
_workingList.Add(itemEditor.GetEditedValue());
LoadListItems();
_listBox.SelectedIndex = _listBox.Items.Count - 1;
}
}
}
private void EditButton_Click(object sender, EventArgs e)
{
if (_listBox.SelectedIndex >= 0)
{
var selectedItem = _workingList[_listBox.SelectedIndex];
using (var itemEditor = new ItemEditorDialog(_elementType, selectedItem, _controlFactory, _parentOptionsWindow))
{
if (itemEditor.ShowDialog(this) == DialogResult.OK)
{
_workingList[_listBox.SelectedIndex] = itemEditor.GetEditedValue();
LoadListItems();
}
}
}
}
private void RemoveButton_Click(object sender, EventArgs e)
{
if (_listBox.SelectedIndex >= 0)
{
int selectedIndex = _listBox.SelectedIndex;
_workingList.RemoveAt(selectedIndex);
LoadListItems();
// Maintain selection if possible
if (_listBox.Items.Count > 0)
{
_listBox.SelectedIndex = Math.Min(selectedIndex, _listBox.Items.Count - 1);
}
}
}
public IList GetEditedList()
{
return _workingList;
}
private object GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
else if (type == typeof(string))
{
return "";
}
else
{
try
{
return Activator.CreateInstance(type);
}
catch
{
return null;
}
}
}
private object CloneObject(object obj)
{
if (obj == null) return null;
var type = obj.GetType();
if (type.IsValueType || type == typeof(string))
{
return obj;
}
// For complex objects, try to create a new instance and copy properties
try
{
var clone = Activator.CreateInstance(type);
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (prop.CanRead && prop.CanWrite)
{
prop.SetValue(clone, prop.GetValue(obj));
}
}
return clone;
}
catch
{
return obj; // Fallback to original object if cloning fails
}
}
}

View File

@@ -21,6 +21,7 @@ public partial class OptionsWindow
// treeViewCategories
//
treeViewCategories.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left;
treeViewCategories.HideSelection = false;
treeViewCategories.Location = new Point(12, 12);
treeViewCategories.Name = "treeViewCategories";
treeViewCategories.Size = new Size(200, 582);

View File

@@ -145,7 +145,10 @@ public partial class OptionsWindow : Form
setting.Description,
setting.Property,
setting.Value,
(newValue) => setting.Value = newValue,
(newValue) =>
{
setting.Value = newValue;
},
this);
flowLayoutPanelSettings.Controls.Add(control);

View File

@@ -0,0 +1,34 @@
using System;
using System.Reflection;
namespace Inspectron.Settings.Windows.Configuration;
internal class SimplePropertyInfo : PropertyInfo
{
private readonly Type _propertyType;
private readonly string _name;
public SimplePropertyInfo(Type propertyType, string name)
{
_propertyType = propertyType;
_name = name;
}
public override PropertyAttributes Attributes => PropertyAttributes.None;
public override bool CanRead => true;
public override bool CanWrite => true;
public override Type PropertyType => _propertyType;
public override string Name => _name;
public override Type DeclaringType => typeof(object);
public override Type ReflectedType => typeof(object);
public override MethodInfo[] GetAccessors(bool nonPublic) => new MethodInfo[0];
public override MethodInfo GetGetMethod(bool nonPublic) => null;
public override ParameterInfo[] GetIndexParameters() => new ParameterInfo[0];
public override MethodInfo GetSetMethod(bool nonPublic) => null;
public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo culture) => obj;
public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo culture) { }
public override object[] GetCustomAttributes(Type attributeType, bool inherit) => new object[0];
public override object[] GetCustomAttributes(bool inherit) => new object[0];
public override bool IsDefined(Type attributeType, bool inherit) => false;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
@@ -755,6 +756,20 @@ namespace Inspectron.Settings
{
v = Enum.Parse(setting.PropertyDescriptor.PropertyType, setting.ValueString);
}
// if is list
else if (setting.PropertyDescriptor.PropertyType.IsGenericType &&
setting.PropertyDescriptor.PropertyType.GetGenericTypeDefinition() == typeof(List<>))
{
Type itemType = setting.PropertyDescriptor.PropertyType.GetGenericArguments()[0];
TypeConverter converter = TypeDescriptor.GetConverter(itemType);
string[] items = setting.ValueString.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
IList list = (IList)Activator.CreateInstance(setting.PropertyDescriptor.PropertyType);
foreach (string item in items)
{
list.Add(converter.ConvertFromInvariantString(item.Trim()));
}
v = list;
}
else
{
v = Convert.ChangeType(setting.ValueString, setting.PropertyDescriptor.PropertyType);
@@ -815,7 +830,7 @@ namespace Inspectron.Settings
}
}
ApplyStoredSettings();
//ApplyStoredSettings();
Reloaded.Raise(this, EventArgs.Empty);
}
catch (Exception ex)