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 0000000..2cb08de Binary files /dev/null and b/VisionBuilder.UI.IOCommander.Windows/sample.png differ diff --git a/VisionBuilder.UI.IOCommander/EGPIOCommand.cs b/VisionBuilder.UI.IOCommander/EGPIOCommand.cs new file mode 100644 index 0000000..36995c7 --- /dev/null +++ b/VisionBuilder.UI.IOCommander/EGPIOCommand.cs @@ -0,0 +1,7 @@ +namespace VisionBuilder.UI.IOCommander; + +public enum EGPIOCommand +{ + Start, + Stop, +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/EProcessingEvent.cs b/VisionBuilder.UI.IOCommander/EProcessingEvent.cs new file mode 100644 index 0000000..73ac9cf --- /dev/null +++ b/VisionBuilder.UI.IOCommander/EProcessingEvent.cs @@ -0,0 +1,12 @@ +namespace VisionBuilder.UI.IOCommander; + +public enum EProcessingEvent +{ + ProgramStarted, + ProgramEnded, + SessionStarted, + SessionEnded, + ErrorOccurred, + GoodOccured, + TestMode +} \ No newline at end of file diff --git a/VisionBuilder.UI.IOCommander/IOCommander.cs b/VisionBuilder.UI.IOCommander/IOCommander.cs new file mode 100644 index 0000000..4de7a2f --- /dev/null +++ b/VisionBuilder.UI.IOCommander/IOCommander.cs @@ -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 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)