io commander

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
using Ninject.Extensions.ChildKernel;
using System.ComponentModel;
using Ninject;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.IOCommander.Interfaces;
using VisionBuilder.UI.IOCommander.Modules;
using VisionBuilder.UI.IOCommander.Settings;
namespace VisionBuilder.UI.IOCommander;
public static class ModuleExtensions
{
public static IKernel UseIOCommander(this IKernel self, string[] cameraNames)
{
// for settings to work properly
TypeDescriptor.AddAttributes(typeof(List<IOCommanderPinAction>), new TypeConverterAttribute(typeof(IOCommanderPinActionListConverter)));
TypeDescriptor.AddAttributes(typeof(List<IOCommanderCommand>), new TypeConverterAttribute(typeof(IOCommanderCommandListConverter)));
List<IOCommanderCameraSettings> cameras = cameraNames.Select(name => new IOCommanderCameraSettings(name)).ToList();
self.Bind<IOCommanderSettings, ISettings>().ToConstant(new IOCommanderSettings(cameras.ToArray()));
self.Bind<IOCommanderModule,IVisionBuilderModule>().To<IOCommanderModule>().InSingletonScope();
self.Bind<IOCommanderProgramModule, IVisionBuilderModule>().To<IOCommanderProgramModule>().InSingletonScope();
self.Bind<IIOCommanderDebugViewService>().To<NoDebugView>().InSingletonScope();
return self;
}
public static IChildKernel UseCameraIOCommander(this IChildKernel self, string cameraName)
{
var ioCommanderSettings = self.Get<IOCommanderSettings>();
var cameraSettings=ioCommanderSettings.Cameras.First(x => x.CameraName == cameraName);
self.Bind<IOCommanderCameraSettings, ISettings>().ToConstant(cameraSettings);
self.Bind<IOCommanderCameraModule, IVisionBuilderModule>().To<IOCommanderCameraModule>().InSingletonScope();
return self;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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