From cd70088d9ae2c419065a0c9c709079f00fa3a68e Mon Sep 17 00:00:00 2001 From: meelstorm <8780022+TesAnti@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:57:59 +0200 Subject: [PATCH] io commander --- .../EmulationCameraImageSource.cs | 30 ++- Hawkeye.VisionBuilder.Workflow/Context.cs | 5 + .../Operations/GetImageOperation.cs | 2 +- .../WorkflowList.cs | 1 + .../ViewModel/MainWindowVM.cs | 24 +- VisionBuilder.UI.Common/VisionBuilder.cs | 2 + VisionBuilder.UI.Console/ModuleExtensions.cs | 5 +- .../.claude/commands/create-prd.md | 63 +++++ .../.claude/commands/generate-tasks.md | 64 +++++ .../.claude/commands/process-task-list.md | 52 ++++ .../.claude/settings.local.json | 8 + .../ModuleExtensions.cs | 21 ++ ...isionBuilder.UI.IOCommander.Windows.csproj | 15 ++ ...owsIOCommanderDebugViewService.Designer.cs | 50 ++++ .../WindowsIOCommanderDebugViewService.cs | 218 ++++++++++++++++ .../WindowsIOCommanderDebugViewService.resx | 120 +++++++++ .../WindowsIOCommanderDebugViewSettings.cs | 127 ++++++++++ .../sample.png | Bin 0 -> 76750 bytes VisionBuilder.UI.IOCommander/EGPIOCommand.cs | 7 + .../EProcessingEvent.cs | 12 + VisionBuilder.UI.IOCommander/IOCommander.cs | 119 +++++++++ .../IOCommanderCommand.cs | 20 ++ .../IOCommanderPinAction.cs | 16 ++ .../IOCommanderProcessingEvent.cs | 116 +++++++++ .../IIOCommanderDebugViewService.cs | 8 + .../ModuleExtensions.cs | 36 +++ .../Modules/IOCommanderCameraModule.cs | 91 +++++++ .../Modules/IOCommanderModule.cs | 62 +++++ .../Modules/IOCommanderProgramModule.cs | 55 ++++ VisionBuilder.UI.IOCommander/NoDebugView.cs | 22 ++ .../Settings/IOCommanderCameraSettings.cs | 150 +++++++++++ .../Settings/IOCommanderSettings.cs | 38 +++ .../VisionBuilder.UI.IOCommander.csproj | 19 ++ .../HawkeyeRecognitionControl.cs | 12 +- .../Form1.Designer.cs | 27 +- VisionBuilder.UI.Windows.Test/Form1.cs | 31 +++ VisionBuilder.UI.Windows.Test/Program.cs | 40 +-- .../VisionBuilder.UI.Windows.Test.csproj | 2 + VisionBuilder.UI.sln | 9 + .../Configuration/DefaultControlFactory.cs | 72 +++++- .../Configuration/ItemEditorDialog.cs | 163 ++++++++++++ .../Configuration/ListEditorDialog.cs | 235 ++++++++++++++++++ .../Configuration/OptionsWindow.Designer.cs | 1 + .../Configuration/OptionsWindow.cs | 5 +- .../Configuration/SimplePropertyInfo.cs | 34 +++ .../Inspectron.Settings/InspectronSettings.cs | 17 +- 46 files changed, 2198 insertions(+), 28 deletions(-) create mode 100644 VisionBuilder.UI.IOCommander.Windows/.claude/commands/create-prd.md create mode 100644 VisionBuilder.UI.IOCommander.Windows/.claude/commands/generate-tasks.md create mode 100644 VisionBuilder.UI.IOCommander.Windows/.claude/commands/process-task-list.md create mode 100644 VisionBuilder.UI.IOCommander.Windows/.claude/settings.local.json create mode 100644 VisionBuilder.UI.IOCommander.Windows/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.IOCommander.Windows/VisionBuilder.UI.IOCommander.Windows.csproj create mode 100644 VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.Designer.cs create mode 100644 VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.cs create mode 100644 VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.resx create mode 100644 VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewSettings.cs create mode 100644 VisionBuilder.UI.IOCommander.Windows/sample.png create mode 100644 VisionBuilder.UI.IOCommander/EGPIOCommand.cs create mode 100644 VisionBuilder.UI.IOCommander/EProcessingEvent.cs create mode 100644 VisionBuilder.UI.IOCommander/IOCommander.cs create mode 100644 VisionBuilder.UI.IOCommander/IOCommanderCommand.cs create mode 100644 VisionBuilder.UI.IOCommander/IOCommanderPinAction.cs create mode 100644 VisionBuilder.UI.IOCommander/IOCommanderProcessingEvent.cs create mode 100644 VisionBuilder.UI.IOCommander/Interfaces/IIOCommanderDebugViewService.cs create mode 100644 VisionBuilder.UI.IOCommander/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.IOCommander/Modules/IOCommanderCameraModule.cs create mode 100644 VisionBuilder.UI.IOCommander/Modules/IOCommanderModule.cs create mode 100644 VisionBuilder.UI.IOCommander/Modules/IOCommanderProgramModule.cs create mode 100644 VisionBuilder.UI.IOCommander/NoDebugView.cs create mode 100644 VisionBuilder.UI.IOCommander/Settings/IOCommanderCameraSettings.cs create mode 100644 VisionBuilder.UI.IOCommander/Settings/IOCommanderSettings.cs create mode 100644 VisionBuilder.UI.IOCommander/VisionBuilder.UI.IOCommander.csproj create mode 100644 framework/Inspectron.Settings.Windows/Configuration/ItemEditorDialog.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/ListEditorDialog.cs create mode 100644 framework/Inspectron.Settings.Windows/Configuration/SimplePropertyInfo.cs diff --git a/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs index e9d2cf7..c7d5d2c 100644 --- a/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs +++ b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs @@ -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 _channel = Channel.CreateBounded(new BoundedChannelOptions(1) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = true + }); public async Task 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 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) { diff --git a/Hawkeye.VisionBuilder.Workflow/Context.cs b/Hawkeye.VisionBuilder.Workflow/Context.cs index de08597..a13a9bf 100644 --- a/Hawkeye.VisionBuilder.Workflow/Context.cs +++ b/Hawkeye.VisionBuilder.Workflow/Context.cs @@ -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 GraphicsElements { get; set; }=new List(); public Dictionary Memory { get; set; } = new Dictionary(); + public CancellationToken CancellationToken { get; set; } } \ No newline at end of file diff --git a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs index 7e1e760..4203ad2 100644 --- a/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs +++ b/Hawkeye.VisionBuilder.Workflow/Operations/GetImageOperation.cs @@ -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; diff --git a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs index 678796a..de14fa5 100644 --- a/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs +++ b/Hawkeye.VisionBuilder.Workflow/WorkflowList.cs @@ -32,6 +32,7 @@ public class WorkflowList long total = 0; foreach (BaseOperation operation in Operations) { + if (Context.CancellationToken.IsCancellationRequested) break; operation.Interpret(Context); } _executionCounter++; diff --git a/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs b/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs index 01286be..78c8ec6 100644 --- a/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs +++ b/VisionBuilder.UI.Common/ViewModel/MainWindowVM.cs @@ -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 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(); + } + } \ No newline at end of file diff --git a/VisionBuilder.UI.Common/VisionBuilder.cs b/VisionBuilder.UI.Common/VisionBuilder.cs index dc13ea2..7788dc1 100644 --- a/VisionBuilder.UI.Common/VisionBuilder.cs +++ b/VisionBuilder.UI.Common/VisionBuilder.cs @@ -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().To().InSingletonScope(); // --- // + mainKernel.Bind().ToSelf().InSingletonScope(); return mainKernel; } diff --git a/VisionBuilder.UI.Console/ModuleExtensions.cs b/VisionBuilder.UI.Console/ModuleExtensions.cs index 8f21f53..f7e50be 100644 --- a/VisionBuilder.UI.Console/ModuleExtensions.cs +++ b/VisionBuilder.UI.Console/ModuleExtensions.cs @@ -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().ToConstant(new VisionBuilderConsoleSettings()); self.RegisterModule(); diff --git a/VisionBuilder.UI.IOCommander.Windows/.claude/commands/create-prd.md b/VisionBuilder.UI.IOCommander.Windows/.claude/commands/create-prd.md new file mode 100644 index 0000000..4ede6d8 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/.claude/commands/create-prd.md @@ -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 \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander.Windows/.claude/commands/generate-tasks.md b/VisionBuilder.UI.IOCommander.Windows/.claude/commands/generate-tasks.md new file mode 100644 index 0000000..c325b51 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/.claude/commands/generate-tasks.md @@ -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. diff --git a/VisionBuilder.UI.IOCommander.Windows/.claude/commands/process-task-list.md b/VisionBuilder.UI.IOCommander.Windows/.claude/commands/process-task-list.md new file mode 100644 index 0000000..368ffcd --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/.claude/commands/process-task-list.md @@ -0,0 +1,52 @@ +--- +description: +globs: +alwaysApply: false +--- +# Task List Management + +Guidelines for managing task lists in markdown files to track progress on completing a PRD + +## Task Implementation +- **One sub-task at a time:** Do **NOT** start the next sub‑task until you ask the user for permission and they say "yes" or "y" +- **Completion protocol:** + 1. When you finish a **sub-task**, immediately mark it as completed by changing `[ ]` to `[x]`. + 2. If **all** subtasks underneath a parent task are now `[x]`, follow this sequence: + - **First**: Run the full test suite + - **Only if all tests pass**: Stage changes (`git add .`) + - **Clean up**: Remove any temporary files and temporary code before committing + - **Commit**: Use a descriptive commit message that: + - Uses conventional commit format (`feat:`, `fix:`, `refactor:`, etc.) + - Summarizes what was accomplished in the parent task + - Lists key changes and additions + - References the task number and PRD context + - **Formats the message as a single-line command using `-m` flags**, e.g.: + + ``` + git commit -m "feat: add payment validation logic" -m "- Validates card type and expiry" -m "- Adds unit tests for edge cases" -m "Related to T123 in PRD" + ``` + 3. Once all the subtasks are marked completed and changes have been committed, mark the **parent task** as completed. +- Stop after each sub-task and wait for the user's go-ahead. + +## Task List Maintenance + +1. **Update the task list as you work:** + - Mark tasks and subtasks as completed (`[x]`) per the protocol above. + - Add new tasks as they emerge. + +2. **Maintain the "Relevant Files" section:** + - List every file created or modified. + - Give each file a one‑line description of its purpose. + +## AI Instructions + +When working with task lists, the AI must: + +1. Regularly update the task list file after finishing any significant work. +2. Follow the completion protocol: + - Mark each finished **sub-task** `[x]`. + - Mark the **parent task** `[x]` once **all** its subtasks are `[x]`. +3. Add newly discovered tasks. +4. Keep "Relevant Files" accurate and up to date. +5. Before starting work, check which sub‑task is next. +6. After implementing a sub‑task, update the file and then pause for user approval. diff --git a/VisionBuilder.UI.IOCommander.Windows/.claude/settings.local.json b/VisionBuilder.UI.IOCommander.Windows/.claude/settings.local.json new file mode 100644 index 0000000..363ed58 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander.Windows/ModuleExtensions.cs b/VisionBuilder.UI.IOCommander.Windows/ModuleExtensions.cs new file mode 100644 index 0000000..8ec85b6 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/ModuleExtensions.cs @@ -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), new TypeConverterAttribute(typeof(PinLabelListConverter))); + + self.Rebind().To().InSingletonScope(); + self.Bind().ToConstant(new WindowsIOCommanderDebugViewSettings()); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander.Windows/VisionBuilder.UI.IOCommander.Windows.csproj b/VisionBuilder.UI.IOCommander.Windows/VisionBuilder.UI.IOCommander.Windows.csproj new file mode 100644 index 0000000..b3d4eda --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/VisionBuilder.UI.IOCommander.Windows.csproj @@ -0,0 +1,15 @@ + + + + Library + net8.0-windows + enable + true + enable + + + + + + + \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.Designer.cs b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.Designer.cs new file mode 100644 index 0000000..aba60e6 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.Designer.cs @@ -0,0 +1,50 @@ +namespace VisionBuilder.UI.IOCommander.Windows +{ + partial class WindowsIOCommanderDebugViewService + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + 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 + } +} diff --git a/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.cs b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.cs new file mode 100644 index 0000000..02e16c7 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.cs @@ -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 _inputPins = new(); + private readonly Dictionary _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(); + var outputLabelDict = _settings.OutputPinLabels?.ToDictionary(pl => pl.Pin, pl => pl.Label) ?? new Dictionary(); + + 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); + } + } + } +} diff --git a/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.resx b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewService.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewSettings.cs b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewSettings.cs new file mode 100644 index 0000000..fdc5c8c --- /dev/null +++ b/VisionBuilder.UI.IOCommander.Windows/WindowsIOCommanderDebugViewSettings.cs @@ -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 InputPinLabels { get; set; } + public List 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(); + 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 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); + } + + /// + /// Convert a single string to PinLabel + /// + 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; + } + + /// + /// Convert a single PinLabel to string + /// + private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, PinLabel pinLabel) + { + if (pinLabel == null) + return null; + + return $"{pinLabel.Pin},{pinLabel.Label}"; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander.Windows/sample.png b/VisionBuilder.UI.IOCommander.Windows/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..2cb08deacfa65e72fd6a8b08b69e7ddcec767f1d GIT binary patch literal 76750 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®E6a(LSrC~w!gofP|Ag*8bJ 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 OnPinChanged = delegate { }; + List _readPins = new List(); + private int _pinsState = 0; + + + public void SetPins(int pin, bool state) + { + lock (this) + { + //Log.Logger.ForContext().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 + ";"); + } + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/IOCommanderCommand.cs b/VisionBuilder.UI.IOCommander/IOCommanderCommand.cs new file mode 100644 index 0000000..ce4d4e2 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/IOCommanderCommand.cs @@ -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}"; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/IOCommanderPinAction.cs b/VisionBuilder.UI.IOCommander/IOCommanderPinAction.cs new file mode 100644 index 0000000..43d6419 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/IOCommanderPinAction.cs @@ -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}"; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/IOCommanderProcessingEvent.cs b/VisionBuilder.UI.IOCommander/IOCommanderProcessingEvent.cs new file mode 100644 index 0000000..b6bd9cf --- /dev/null +++ b/VisionBuilder.UI.IOCommander/IOCommanderProcessingEvent.cs @@ -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 Actions { get; set; } = new List(); +} + +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(); + 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 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); + } + + /// + /// Convert a single string to IOCommanderPinAction + /// + 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; + } + + /// + /// Convert a single IOCommanderPinAction to string + /// + private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, IOCommanderPinAction action) + { + if (action == null) + return null; + + return $"{action.Pin},{action.Low},{action.Pulse}"; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/Interfaces/IIOCommanderDebugViewService.cs b/VisionBuilder.UI.IOCommander/Interfaces/IIOCommanderDebugViewService.cs new file mode 100644 index 0000000..527e9ce --- /dev/null +++ b/VisionBuilder.UI.IOCommander/Interfaces/IIOCommanderDebugViewService.cs @@ -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(); +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/ModuleExtensions.cs b/VisionBuilder.UI.IOCommander/ModuleExtensions.cs new file mode 100644 index 0000000..6a22782 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/ModuleExtensions.cs @@ -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), new TypeConverterAttribute(typeof(IOCommanderPinActionListConverter))); + + TypeDescriptor.AddAttributes(typeof(List), new TypeConverterAttribute(typeof(IOCommanderCommandListConverter))); + + List cameras = cameraNames.Select(name => new IOCommanderCameraSettings(name)).ToList(); + self.Bind().ToConstant(new IOCommanderSettings(cameras.ToArray())); + self.Bind().To().InSingletonScope(); + self.Bind().To().InSingletonScope(); + self.Bind().To().InSingletonScope(); + return self; + } + + public static IChildKernel UseCameraIOCommander(this IChildKernel self, string cameraName) + { + var ioCommanderSettings = self.Get(); + var cameraSettings=ioCommanderSettings.Cameras.First(x => x.CameraName == cameraName); + self.Bind().ToConstant(cameraSettings); + self.Bind().To().InSingletonScope(); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/Modules/IOCommanderCameraModule.cs b/VisionBuilder.UI.IOCommander/Modules/IOCommanderCameraModule.cs new file mode 100644 index 0000000..ea0ea84 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/Modules/IOCommanderCameraModule.cs @@ -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); + } + + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/Modules/IOCommanderModule.cs b/VisionBuilder.UI.IOCommander/Modules/IOCommanderModule.cs new file mode 100644 index 0000000..9dedc8f --- /dev/null +++ b/VisionBuilder.UI.IOCommander/Modules/IOCommanderModule.cs @@ -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 _pinStates = new ConcurrentDictionary(); + 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; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/Modules/IOCommanderProgramModule.cs b/VisionBuilder.UI.IOCommander/Modules/IOCommanderProgramModule.cs new file mode 100644 index 0000000..8c428e4 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/Modules/IOCommanderProgramModule.cs @@ -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() + { + + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/NoDebugView.cs b/VisionBuilder.UI.IOCommander/NoDebugView.cs new file mode 100644 index 0000000..8721cf7 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/NoDebugView.cs @@ -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."); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/Settings/IOCommanderCameraSettings.cs b/VisionBuilder.UI.IOCommander/Settings/IOCommanderCameraSettings.cs new file mode 100644 index 0000000..7bf7c2f --- /dev/null +++ b/VisionBuilder.UI.IOCommander/Settings/IOCommanderCameraSettings.cs @@ -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 ProcessingEvents { get; set; } = [ + new (EProcessingEvent.SessionStarted), + new (EProcessingEvent.SessionEnded), + new (EProcessingEvent.ErrorOccurred), + new (EProcessingEvent.GoodOccured), + ]; + + [SettingDescription(@" +Overrides pin values during test mode. +")] + public List TestModeOverride { get; set; } = new List(); + + public List GPIOCommands { get; set; } = new List(); + + 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(); + 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 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); + } + + /// + /// Convert a single string to IOCommanderCommand + /// + 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(parts[2], out EGPIOCommand command)) + { + return new IOCommanderCommand + { + Pin = pin, + OnHigh = onHigh, + Command = command + }; + } + } + return null; + } + + /// + /// Convert a single IOCommanderCommand to string + /// + private string ConvertToString(ITypeDescriptorContext context, CultureInfo culture, IOCommanderCommand command) + { + if (command == null) + return null; + + return $"{command.Pin},{command.OnHigh},{command.Command}"; + } + } +} + + diff --git a/VisionBuilder.UI.IOCommander/Settings/IOCommanderSettings.cs b/VisionBuilder.UI.IOCommander/Settings/IOCommanderSettings.cs new file mode 100644 index 0000000..a8b807d --- /dev/null +++ b/VisionBuilder.UI.IOCommander/Settings/IOCommanderSettings.cs @@ -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 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)); + } + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/VisionBuilder.UI.IOCommander.csproj b/VisionBuilder.UI.IOCommander/VisionBuilder.UI.IOCommander.csproj new file mode 100644 index 0000000..5543c17 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/VisionBuilder.UI.IOCommander.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs index 0c5d809..f6f090c 100644 --- a/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/HawkeyeRecognitionControl.cs @@ -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) diff --git a/VisionBuilder.UI.Windows.Test/Form1.Designer.cs b/VisionBuilder.UI.Windows.Test/Form1.Designer.cs index 85cd7e9..e85f92e 100644 --- a/VisionBuilder.UI.Windows.Test/Form1.Designer.cs +++ b/VisionBuilder.UI.Windows.Test/Form1.Designer.cs @@ -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; } } diff --git a/VisionBuilder.UI.Windows.Test/Form1.cs b/VisionBuilder.UI.Windows.Test/Form1.cs index 698946f..afac4b7 100644 --- a/VisionBuilder.UI.Windows.Test/Form1.cs +++ b/VisionBuilder.UI.Windows.Test/Form1.cs @@ -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); + } } } diff --git a/VisionBuilder.UI.Windows.Test/Program.cs b/VisionBuilder.UI.Windows.Test/Program.cs index b3321fa..ab793b2 100644 --- a/VisionBuilder.UI.Windows.Test/Program.cs +++ b/VisionBuilder.UI.Windows.Test/Program.cs @@ -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(); diff --git a/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj b/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj index 1349041..5b142f6 100644 --- a/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj +++ b/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj @@ -21,6 +21,8 @@ + + diff --git a/VisionBuilder.UI.sln b/VisionBuilder.UI.sln index b3635c2..3c5a3c8 100644 --- a/VisionBuilder.UI.sln +++ b/VisionBuilder.UI.sln @@ -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} diff --git a/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs b/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs index d06b2f5..e26b4ce 100644 --- a/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs +++ b/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs @@ -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 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 valueChangedCallback, OptionsWindow optionsWindow, string settingDescription, + Label previewLabel, Func 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(); + 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 valueChangedCallback, OptionsWindow optionsWindow, string settingDescription, Label previewLabel, Func getPreview) { @@ -186,3 +250,9 @@ public class DefaultControlFactory : IControlFactory return outerPanel; } } + +// List Editor Dialog + +// Item Editor Dialog + +// Helper class for simple property info \ No newline at end of file diff --git a/framework/Inspectron.Settings.Windows/Configuration/ItemEditorDialog.cs b/framework/Inspectron.Settings.Windows/Configuration/ItemEditorDialog.cs new file mode 100644 index 0000000..2eab075 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/ItemEditorDialog.cs @@ -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; + } + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.Windows/Configuration/ListEditorDialog.cs b/framework/Inspectron.Settings.Windows/Configuration/ListEditorDialog.cs new file mode 100644 index 0000000..4c6e7a2 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/ListEditorDialog.cs @@ -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 + } + } +} \ No newline at end of file diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs index 9e13ee4..4bf6bf3 100644 --- a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs @@ -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); diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs index 769ad0b..520fad0 100644 --- a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.cs @@ -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); diff --git a/framework/Inspectron.Settings.Windows/Configuration/SimplePropertyInfo.cs b/framework/Inspectron.Settings.Windows/Configuration/SimplePropertyInfo.cs new file mode 100644 index 0000000..6c32153 --- /dev/null +++ b/framework/Inspectron.Settings.Windows/Configuration/SimplePropertyInfo.cs @@ -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; +} \ No newline at end of file diff --git a/framework/Inspectron.Settings/InspectronSettings.cs b/framework/Inspectron.Settings/InspectronSettings.cs index 357ec14..07c6c38 100644 --- a/framework/Inspectron.Settings/InspectronSettings.cs +++ b/framework/Inspectron.Settings/InspectronSettings.cs @@ -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)