From 40d74d6da609bb8cb28b08da4bc690cf1817452a Mon Sep 17 00:00:00 2001 From: meelstorm <8780022+TesAnti@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:13:46 +0200 Subject: [PATCH] ringbuffer and statistics --- .../EmulationCameraImageSource.cs | 5 +- ....VisionBuilder.UI.Sources.Emulation.csproj | 4 + VisionBuilder.UI.Camera/ModuleExtensions.cs | 41 ++ .../VisionBuilder.UI.Camera.csproj | 14 + VisionBuilder.UI.Common/CameraSettings.cs | 8 +- VisionBuilder.UI.Common/Extensions.cs | 26 +- .../IVisionBuilderModule.cs | 6 + VisionBuilder.UI.Common/PathSettingsUtils.cs | 33 +- .../ViewModel/SingleCameraVM.cs | 4 +- .../VisionBuilder.UI.Common.csproj | 1 + VisionBuilder.UI.Common/VisionBuilder.cs | 23 + VisionBuilder.UI.Console/ModuleExtensions.cs | 15 + .../VisionBuilder.UI.Console.csproj | 17 + .../VisionBuilderConsole.cs | 36 ++ .../VisionBuilderConsoleSettings.cs | 18 + VisionBuilder.UI.Console/WinConsole.cs | 118 +++++ .../ModuleExtensions.cs | 15 + .../ModuleExtensions.cs | 14 + .../VisionBuilderRingbuffer.cs | 15 +- .../VisionBuilderRingbufferSettings.cs | 38 +- .../ModuleExtensions.cs | 14 + .../VisionBuilderStatistics.cs | 86 ++-- .../VisionBuilderStatisticsSettings.cs | 34 +- .../VisionBuilder.UI.Tests.csproj | 1 + .../VisionBuilderStatisticsTests.cs | 471 ++++++++++++++++++ VisionBuilder.UI.Windows.Test/Program.cs | 81 ++- .../VisionBuilder.UI.Windows.Test.csproj | 4 + .../Components/PreviewWindow.resx | 120 +++++ .../Components/SingleCameraControl.cs | 2 +- VisionBuilder.UI.Windows/ModuleExtensions.cs | 18 + .../Services/LoadingService.cs | 17 - ...rvice.cs => WindowsImagePreviewService.cs} | 4 +- .../Services/WindowsLoadingService.cs | 28 ++ ...=> WindowsRecipeSelectionDialogService.cs} | 2 +- ...gsService.cs => WindowsSettingsService.cs} | 4 +- VisionBuilder.UI.sln | 21 + .../Configuration/DefaultControlFactory.cs | 5 +- .../Configuration/OptionsWindow.Designer.cs | 1 + .../Inspectron.Statistics/CsvStatistics.cs | 2 +- .../Inspectron.Statistics/StatisticsRecord.cs | 1 + .../Controls/MaterialLoader.Designer.cs | 1 + 41 files changed, 1188 insertions(+), 180 deletions(-) create mode 100644 VisionBuilder.UI.Camera/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj create mode 100644 VisionBuilder.UI.Common/IVisionBuilderModule.cs create mode 100644 VisionBuilder.UI.Common/VisionBuilder.cs create mode 100644 VisionBuilder.UI.Console/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.Console/VisionBuilder.UI.Console.csproj create mode 100644 VisionBuilder.UI.Console/VisionBuilderConsole.cs create mode 100644 VisionBuilder.UI.Console/VisionBuilderConsoleSettings.cs create mode 100644 VisionBuilder.UI.Console/WinConsole.cs create mode 100644 VisionBuilder.UI.Recipes.HawkeyeRecipe/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.Ringbuffer/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.Statistics/ModuleExtensions.cs create mode 100644 VisionBuilder.UI.Tests/VisionBuilderStatisticsTests.cs create mode 100644 VisionBuilder.UI.Windows/Components/PreviewWindow.resx create mode 100644 VisionBuilder.UI.Windows/ModuleExtensions.cs delete mode 100644 VisionBuilder.UI.Windows/Services/LoadingService.cs rename VisionBuilder.UI.Windows/Services/{ImagePreviewService.cs => WindowsImagePreviewService.cs} (80%) create mode 100644 VisionBuilder.UI.Windows/Services/WindowsLoadingService.cs rename VisionBuilder.UI.Windows/Services/{RecipeSelectionDialogService.cs => WindowsRecipeSelectionDialogService.cs} (85%) rename VisionBuilder.UI.Windows/Services/{SettingsService.cs => WindowsSettingsService.cs} (86%) diff --git a/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs index 8a2cd72..e9d2cf7 100644 --- a/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs +++ b/Hawkeye.VisionBuilder.UI.Sources.Emulation/EmulationCameraImageSource.cs @@ -1,11 +1,12 @@ using OpenCvSharp; using System.Drawing; using Ninject; +using VisionBuilder.UI.Common; using VisionBuilder.UI.Common.RecipeProcessing; namespace Hawkeye.VisionBuilder.UI.Sources.Emulation { - public class EmulationCameraImageSource:IImageSource, IInitializable + public class EmulationCameraImageSource:IImageSource, IVisionBuilderModule { private readonly EmulationSettings _settings; private readonly string _name; @@ -91,7 +92,7 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Emulation private int _index = 0; - public void Initialize() + public void InitializeModule() { if(string.IsNullOrEmpty(_settings.EmulationPath)) return; SetFolderAbsolute(_settings.EmulationPath); diff --git a/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj b/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj index d475293..d890e47 100644 --- a/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj +++ b/Hawkeye.VisionBuilder.UI.Sources.Emulation/Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj @@ -6,6 +6,10 @@ enable + + + + diff --git a/VisionBuilder.UI.Camera/ModuleExtensions.cs b/VisionBuilder.UI.Camera/ModuleExtensions.cs new file mode 100644 index 0000000..d9181a6 --- /dev/null +++ b/VisionBuilder.UI.Camera/ModuleExtensions.cs @@ -0,0 +1,41 @@ +using Hawkeye.VisionBuilder.UI.Sources.Emulation; +using Ninject; +using Ninject.Extensions.ChildKernel; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace Lindt.Colorballs.Duo.Utils; + +public static class ModuleExtensions +{ + public static IChildKernel RegisterCamera(this IChildKernel self, string cameraName) + { + self.Bind().ToConstant(new CameraSettings(cameraName)); + self.Bind().ToConstant(new EmulationSettings(cameraName)); + return self; + } + private static void BindCameras(CameraSettings cameraSettings, IChildKernel kernel) + { + switch (cameraSettings.CameraSource) + { + case CameraSettings.EImageSource.Emulation: + + kernel.Bind().To().InSingletonScope(); + break; + case CameraSettings.EImageSource.Hawkeye: + break; + case CameraSettings.EImageSource.IDS: + break; + case CameraSettings.EImageSource.Basler: + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + public static IChildKernel UseCamera(this IChildKernel self) + { + var cameraSettings = self.Get(); + BindCameras(cameraSettings, self); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj b/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj new file mode 100644 index 0000000..d29c8c2 --- /dev/null +++ b/VisionBuilder.UI.Camera/VisionBuilder.UI.Camera.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/VisionBuilder.UI.Common/CameraSettings.cs b/VisionBuilder.UI.Common/CameraSettings.cs index c53e067..27eb927 100644 --- a/VisionBuilder.UI.Common/CameraSettings.cs +++ b/VisionBuilder.UI.Common/CameraSettings.cs @@ -20,12 +20,12 @@ public class CameraSettings : ISettings } - public EImageSource Camera1Source { get; set; } - public string Camera1Label { get; set; } + public EImageSource CameraSource { get; set; } + public string CameraLabel { get; set; } public void RegisterSettings(InspectronSettings settings) { - settings.RegisterSimple(this, () => this.Camera1Source, CameraName, "Image source"); - settings.RegisterSimple(this, () => this.Camera1Label, CameraName, "Label"); + settings.RegisterSimple(this, () => this.CameraSource, CameraName, "Image source"); + settings.RegisterSimple(this, () => this.CameraLabel, CameraName, "Label"); } } \ No newline at end of file diff --git a/VisionBuilder.UI.Common/Extensions.cs b/VisionBuilder.UI.Common/Extensions.cs index d20e1ae..47c61b5 100644 --- a/VisionBuilder.UI.Common/Extensions.cs +++ b/VisionBuilder.UI.Common/Extensions.cs @@ -1,5 +1,6 @@ using Inspectron.Settings; using Ninject; +using Ninject.Extensions.ChildKernel; namespace VisionBuilder.UI.Common; @@ -14,6 +15,29 @@ public static class Extensions { settingsInstance.RegisterSettings(self.Get()); } - } + + public static void RegisterModule(this IKernel self) where T : IVisionBuilderModule + { + self.Bind().To().InSingletonScope(); + } + + public static void InitializeModules(this IKernel self) + { + + var serviceTypes = self.GetAll(typeof(IVisionBuilderModule)).Select(x => (IVisionBuilderModule)x).ToList(); + foreach (var service in serviceTypes) + { + if (service != null) + { + service.InitializeModule(); + } + } + } + + + + + + } \ No newline at end of file diff --git a/VisionBuilder.UI.Common/IVisionBuilderModule.cs b/VisionBuilder.UI.Common/IVisionBuilderModule.cs new file mode 100644 index 0000000..df80da8 --- /dev/null +++ b/VisionBuilder.UI.Common/IVisionBuilderModule.cs @@ -0,0 +1,6 @@ +namespace VisionBuilder.UI.Common; + +public interface IVisionBuilderModule +{ + public void InitializeModule(); +} \ No newline at end of file diff --git a/VisionBuilder.UI.Common/PathSettingsUtils.cs b/VisionBuilder.UI.Common/PathSettingsUtils.cs index b846066..ebee8e0 100644 --- a/VisionBuilder.UI.Common/PathSettingsUtils.cs +++ b/VisionBuilder.UI.Common/PathSettingsUtils.cs @@ -1,7 +1,32 @@ -namespace VisionBuilder.UI.Common; +namespace Lindt.Colorballs.Duo.Utils; public static class PathSettingsUtils { + public const string INSTUCTIONS = @" +You can use the following system variables: + +$RECIPE_NAME - name of the current recipe +$DEFECT_NAME - name of the current defect(for bad images) +$CAMERA_NAME - name of the camera + +$HOUR - hour of the image +$MINUTE - minute of the image +$SECOND - second of the image +$MS - milliseconds of the image + +$DAY - day of the image +$MONTH_NUM - number of the month of the image +$MONTH_NAME - name of the month of the image +$YEAR - year of the image + +$SS_HOUR - hour of the session start +$SS_MINUTE - minute of the session start +$SS_SECOND - second of the session start + +$SS_DAY - day of the session start +$SS_MONTH_NUM - number of the month of the session start +$SS_MONTH_NAME - name of the month of the session start +$SS_YEAR - year of the session start"; public static string ConvertVariables( string text, DateTime imageTime, @@ -25,7 +50,8 @@ public static class PathSettingsUtils ["$MS"] = imageTime.Millisecond.ToString("D3"), ["$DAY"] = imageTime.Day.ToString("D2"), - ["$MONTH"] = imageTime.Month.ToString("D2"), + ["$MONTH_NUM"] = imageTime.Month.ToString("D2"), + ["$MONTH_NAME"] = imageTime.ToString("MMMM"), ["$YEAR"] = imageTime.Year.ToString(), ["$SS_HOUR"] = sessionTime.Hour.ToString("D2"), @@ -33,7 +59,8 @@ public static class PathSettingsUtils ["$SS_SECOND"] = sessionTime.Second.ToString("D2"), ["$SS_DAY"] = sessionTime.Day.ToString("D2"), - ["$SS_MONTH"] = sessionTime.ToString("MMMM"), + ["$SS_MONTH_NUM"] = sessionTime.Month.ToString("D2"), + ["$SS_MONTH_NAME"] = sessionTime.ToString("MMMM"), ["$SS_YEAR"] = sessionTime.Year.ToString() }; diff --git a/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs b/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs index f4796e9..8da8a29 100644 --- a/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs +++ b/VisionBuilder.UI.Common/ViewModel/SingleCameraVM.cs @@ -30,7 +30,7 @@ namespace VisionBuilder.UI.Common [NotifyCanExecuteChangedFor(nameof(StopCommand))] private bool _isRunning; [ObservableProperty] - private string _cameraName; + private string _cameraLabel; public bool CanStop => IsRunning; @@ -56,7 +56,7 @@ namespace VisionBuilder.UI.Common StatisticsVm = new StatisticsVM(); PreviewVm = new PreviewVM(); - CameraName = cameraSettings.CameraName; + CameraLabel = cameraSettings.CameraLabel; recognitionControl.ImageProcessed += Handle; diff --git a/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj b/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj index 9c400a2..b0a2cfb 100644 --- a/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj +++ b/VisionBuilder.UI.Common/VisionBuilder.UI.Common.csproj @@ -9,6 +9,7 @@ + diff --git a/VisionBuilder.UI.Common/VisionBuilder.cs b/VisionBuilder.UI.Common/VisionBuilder.cs new file mode 100644 index 0000000..dc13ea2 --- /dev/null +++ b/VisionBuilder.UI.Common/VisionBuilder.cs @@ -0,0 +1,23 @@ +using Inspectron.Settings; +using Ninject; +using Ninject.Extensions.ChildKernel; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; + +namespace VisionBuilder.UI.Common; + +public static class VisionBuilder +{ + public static IKernel CreateMainKernel(InspectronSettings settings) + { + StandardKernel mainKernel = new StandardKernel(); + + // GLOBAL SETTINGS // + mainKernel.Bind().ToConstant(settings); + mainKernel.Bind().ToConstant(new UIConfiguration()); + mainKernel.Bind().To().InSingletonScope(); + // --- // + + return mainKernel; + } + +} \ No newline at end of file diff --git a/VisionBuilder.UI.Console/ModuleExtensions.cs b/VisionBuilder.UI.Console/ModuleExtensions.cs new file mode 100644 index 0000000..8f21f53 --- /dev/null +++ b/VisionBuilder.UI.Console/ModuleExtensions.cs @@ -0,0 +1,15 @@ +using Ninject.Extensions.ChildKernel; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Console; + +namespace VisionBuilder.UI.Statistics; + +public static class ModuleExtensions +{ + public static IChildKernel UseConsole(this IChildKernel self) + { + self.Bind().ToConstant(new VisionBuilderConsoleSettings()); + self.RegisterModule(); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Console/VisionBuilder.UI.Console.csproj b/VisionBuilder.UI.Console/VisionBuilder.UI.Console.csproj new file mode 100644 index 0000000..951ee0f --- /dev/null +++ b/VisionBuilder.UI.Console/VisionBuilder.UI.Console.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/VisionBuilder.UI.Console/VisionBuilderConsole.cs b/VisionBuilder.UI.Console/VisionBuilderConsole.cs new file mode 100644 index 0000000..cda569f --- /dev/null +++ b/VisionBuilder.UI.Console/VisionBuilderConsole.cs @@ -0,0 +1,36 @@ +using Ninject; +using Serilog; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Console +{ + public class VisionBuilderConsole: IVisionBuilderModule + { + private readonly VisionBuilderConsoleSettings _settings; + + public VisionBuilderConsole(VisionBuilderConsoleSettings settings) + { + _settings = settings; + } + + public void InitializeModule() + { + if (_settings.CreateConsoleWindow) + { + WinConsole.Initialize(); + } + // configure Serilog to use console + if (_settings.UseConsole) + { + Serilog.Log.Logger = new Serilog.LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + Log.Information("Console initialized"); + } + + + } + } +} diff --git a/VisionBuilder.UI.Console/VisionBuilderConsoleSettings.cs b/VisionBuilder.UI.Console/VisionBuilderConsoleSettings.cs new file mode 100644 index 0000000..d4225e7 --- /dev/null +++ b/VisionBuilder.UI.Console/VisionBuilderConsoleSettings.cs @@ -0,0 +1,18 @@ +using Inspectron.Settings; +using Inspectron.Settings.Attributes; +using Serilog; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Console; + +public class VisionBuilderConsoleSettings:ISettings +{ + public bool UseConsole { get; set; } = false; + [SettingDescription("Create a console window for non-console application")] + public bool CreateConsoleWindow { get; set; } + public void RegisterSettings(InspectronSettings settings) + { + settings.RegisterSimple(this, () => this.UseConsole, "Console", nameof(UseConsole)); + settings.RegisterSimple(this, () => this.CreateConsoleWindow, "Console", nameof(CreateConsoleWindow)); + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Console/WinConsole.cs b/VisionBuilder.UI.Console/WinConsole.cs new file mode 100644 index 0000000..bc05978 --- /dev/null +++ b/VisionBuilder.UI.Console/WinConsole.cs @@ -0,0 +1,118 @@ +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace VisionBuilder.UI.Console +{ + public static class WinConsole + { + public static void Initialize(bool alwaysCreateNewConsole = true, bool minimizeWindow = false) + { + bool consoleAttached = true; + if (alwaysCreateNewConsole + || (AttachConsole(ATTACH_PARRENT) == 0 + && Marshal.GetLastWin32Error() != ERROR_ACCESS_DENIED)) + { + consoleAttached = AllocConsole() != 0; + } + + if (consoleAttached) + { + InitializeOutStream(); + InitializeInStream(); + } + + if (minimizeWindow) + { + const int SW_MINIMIZE = 6; + var handle = GetConsoleWindow(); + ShowWindow(handle, SW_MINIMIZE); + } + } + + public static void MinimizeWindow() + { + const int SW_MINIMIZE = 6; + var handle = GetConsoleWindow(); + ShowWindow(handle, SW_MINIMIZE); + } + + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + private static void InitializeOutStream() + { + var fs = CreateFileStream("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, FileAccess.Write); + if (fs != null) + { + var writer = new StreamWriter(fs) { AutoFlush = true }; + System.Console.SetOut(writer); + System.Console.SetError(writer); + } + } + + private static void InitializeInStream() + { + var fs = CreateFileStream("CONIN$", GENERIC_READ, FILE_SHARE_READ, FileAccess.Read); + if (fs != null) + { + System.Console.SetIn(new StreamReader(fs)); + } + } + + private static FileStream CreateFileStream(string name, uint win32DesiredAccess, uint win32ShareMode, + FileAccess dotNetFileAccess) + { + var file = new SafeFileHandle(CreateFileW(name, win32DesiredAccess, win32ShareMode, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero), true); + if (!file.IsInvalid) + { + var fs = new FileStream(file, dotNetFileAccess); + return fs; + } + return null; + } + + #region Win API Functions and Constants + [DllImport("kernel32.dll", + EntryPoint = "AllocConsole", + SetLastError = true, + CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + private static extern int AllocConsole(); + + [DllImport("kernel32.dll", + EntryPoint = "AttachConsole", + SetLastError = true, + CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + private static extern UInt32 AttachConsole(UInt32 dwProcessId); + + [DllImport("kernel32.dll", + EntryPoint = "CreateFileW", + SetLastError = true, + CharSet = CharSet.Auto, + CallingConvention = CallingConvention.StdCall)] + private static extern IntPtr CreateFileW( + string lpFileName, + UInt32 dwDesiredAccess, + UInt32 dwShareMode, + IntPtr lpSecurityAttributes, + UInt32 dwCreationDisposition, + UInt32 dwFlagsAndAttributes, + IntPtr hTemplateFile + ); + + private const UInt32 GENERIC_WRITE = 0x40000000; + private const UInt32 GENERIC_READ = 0x80000000; + private const UInt32 FILE_SHARE_READ = 0x00000001; + private const UInt32 FILE_SHARE_WRITE = 0x00000002; + private const UInt32 OPEN_EXISTING = 0x00000003; + private const UInt32 FILE_ATTRIBUTE_NORMAL = 0x80; + private const UInt32 ERROR_ACCESS_DENIED = 5; + + private const UInt32 ATTACH_PARRENT = 0xFFFFFFFF; + + #endregion + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Recipes.HawkeyeRecipe/ModuleExtensions.cs b/VisionBuilder.UI.Recipes.HawkeyeRecipe/ModuleExtensions.cs new file mode 100644 index 0000000..b5dd3f0 --- /dev/null +++ b/VisionBuilder.UI.Recipes.HawkeyeRecipe/ModuleExtensions.cs @@ -0,0 +1,15 @@ +using Ninject.Extensions.ChildKernel; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace VisionBuilder.UI.Recipes.HawkeyeRecipe; + +public static class ModuleExtensions +{ + public static IChildKernel UseHawkeyeRecipes(this IChildKernel self, string cameraName) + { + self.Bind().ToConstant(new HawkeyeRecognitionSettings(cameraName)); + self.Bind().To().InSingletonScope(); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Ringbuffer/ModuleExtensions.cs b/VisionBuilder.UI.Ringbuffer/ModuleExtensions.cs new file mode 100644 index 0000000..17cf0e8 --- /dev/null +++ b/VisionBuilder.UI.Ringbuffer/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Ninject.Extensions.ChildKernel; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Ringbuffer; + +public static class ModuleExtensions +{ + public static IChildKernel UseRingbuffer(this IChildKernel self, string cameraName) + { + self.Bind().ToConstant(new VisionBuilderRingbufferSettings(cameraName)); + self.RegisterModule(); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs index b7dfd89..f351bfc 100644 --- a/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs +++ b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbuffer.cs @@ -1,14 +1,14 @@ using Inspectron.Fastbuffer; using Inspectron.Fastbuffer.Filesystems; -using System.Drawing; -using System.Drawing.Imaging; +using Lindt.Colorballs.Duo.Utils; using OpenCvSharp; +using VisionBuilder.UI.Common; using VisionBuilder.UI.Common.Commands; using VisionBuilder.UI.Common.RecipeProcessing; -namespace Lindt.Colorballs.Duo.Utils; +namespace VisionBuilder.UI.Ringbuffer; -public class VisionBuilderRingbuffer +public class VisionBuilderRingbuffer:IVisionBuilderModule { private readonly VisionBuilderRingbufferSettings _settings; private readonly IRecognitionControl _recognitionControl; @@ -17,10 +17,13 @@ public class VisionBuilderRingbuffer { _settings = settings; _recognitionControl = recognitionControl; + + } + public void InitializeModule() + { _recognitionControl.SessionStarted += _recognitionControl_SessionStarted; _recognitionControl.ImageProcessed += _recognitionControl_ImageProcessed; } - private void _recognitionControl_ImageProcessed(VisionBuilder.UI.Common.Commands.ImageProcessedEvent obj) { if (obj.HasError) @@ -124,4 +127,6 @@ public class VisionBuilderRingbuffer _badRingbuffers.Clear(); } + + } \ No newline at end of file diff --git a/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs index d802275..686a40e 100644 --- a/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs +++ b/VisionBuilder.UI.Ringbuffer/VisionBuilderRingbufferSettings.cs @@ -1,10 +1,9 @@ using Inspectron.Settings; using Inspectron.Settings.Attributes; -using System.ComponentModel; -using System.Security.Cryptography.X509Certificates; +using Lindt.Colorballs.Duo.Utils; using VisionBuilder.UI.Common; -namespace Lindt.Colorballs.Duo.Utils; +namespace VisionBuilder.UI.Ringbuffer; public class VisionBuilderRingbufferSettings : ISettings { @@ -23,48 +22,25 @@ public class VisionBuilderRingbufferSettings : ISettings [SettingDescription( @" Specifies directory, in which MaxBad and MaxGood are applied. -You can use the following system variables: - -$RECIPE_NAME - name of the current recipe -$DEFECT_NAME - name of the current defect(for bad images) -$CAMERA_NAME - name of the camera - -$HOUR - hour of the image -$MINUTE - minute of the image -$SECOND - second of the image -$MS - milliseconds of the image - -$DAY - day of the image -$MONTH - month of the image -$YEAR - year of the image - -$SS_HOUR - hour of the session start -$SS_MINUTE - minute of the session start -$SS_SECOND - second of the session start - -$SS_DAY - day of the session start -$SS_MONTH - month of the session start -$SS_YEAR - year of the session start - -" +"+ PathSettingsUtils.INSTUCTIONS )] [SettingPreview(typeof(VisionBuilderRingbufferSettings), nameof(ImagesPathTemplatePreview))] - public string Root { get; set; } = @"..\Data\Ringbuffer\$SS_MONTH $SS_YEAR"; + public string Root { get; set; } = @"..\Data\Ringbuffer\$SS_MONTH_NAME $SS_YEAR"; - [SettingDescriptionAttribute("Specifies directory, relative to the Root")] + [SettingDescription("Specifies directory, relative to the Root")] [SettingPreview(typeof(VisionBuilderRingbufferSettings), nameof(ImagesPathTemplatePreview))] public string GoodImagesPathTemplate { get; set; } = @"\$RECIPE_NAME"; - [SettingDescriptionAttribute("Specifies directory, relative to the Root")] + [SettingDescription("Specifies directory, relative to the Root")] [SettingPreview(typeof(VisionBuilderRingbufferSettings),nameof(ImagesPathTemplatePreview))] public string BadImagesPathTemplate { get; set; } = @"\$RECIPE_NAME\$DEFECT_NAME"; [SettingPreview(typeof(VisionBuilderRingbufferSettings), nameof(ImagesPathTemplatePreview))] - public string FilenameTemplate { get; set; } = "$YEAR-$MONTH-$DAY $HOUR-$MINUTE-$SECOND $MS"; + public string FilenameTemplate { get; set; } = "$YEAR-$MONTH_NUM-$DAY $HOUR-$MINUTE-$SECOND $MS"; public void RegisterSettings(InspectronSettings settings) diff --git a/VisionBuilder.UI.Statistics/ModuleExtensions.cs b/VisionBuilder.UI.Statistics/ModuleExtensions.cs new file mode 100644 index 0000000..a5b2146 --- /dev/null +++ b/VisionBuilder.UI.Statistics/ModuleExtensions.cs @@ -0,0 +1,14 @@ +using Ninject.Extensions.ChildKernel; +using VisionBuilder.UI.Common; + +namespace VisionBuilder.UI.Statistics; + +public static class ModuleExtensions +{ + public static IChildKernel UseStatistics(this IChildKernel self, string cameraName) + { + self.Bind().ToConstant(new VisionBuilderStatisticsSettings(cameraName)); + self.RegisterModule(); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs b/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs index 967ce90..3aa90b9 100644 --- a/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs +++ b/VisionBuilder.UI.Statistics/VisionBuilderStatistics.cs @@ -13,9 +13,9 @@ using VisionBuilder.UI.Common.RecipeProcessing; namespace VisionBuilder.UI.Statistics { - public class VisionBuilderStatistics:IInitializable + public class VisionBuilderStatistics:IVisionBuilderModule,IDisposable { - private CsvStatistics _csvStatistics; + private CsvStatistics? _csvStatistics; private readonly VisionBuilderStatisticsSettings _settings; private string _filePath; private readonly IRecognitionControl _recognitionControl; @@ -29,7 +29,7 @@ namespace VisionBuilder.UI.Statistics private const string MONTHLY_FILE_NAME = "statistics_month.csv"; private const string EMPTY_DATE = ""; private const string EMPTY_TIME = ""; - + public bool DoNotDeleteTempFiles { get; set; } = false; public VisionBuilderStatistics(VisionBuilderStatisticsSettings settings, IRecognitionControl recognitionControl, ILoadingService loadingService) { @@ -37,18 +37,20 @@ namespace VisionBuilder.UI.Statistics _recognitionControl = recognitionControl; _loadingService = loadingService; - _recognitionControl.SessionStarted += _recognitionControl_SessionStarted; - _recognitionControl.ImageProcessed += _recognitionControl_ImageProcessed; - _recognitionControl.SessionEnded += _recognitionControl_SessionEnded; + } private void _recognitionControl_SessionEnded(SessionEndedEvent obj) { + var endedAt = obj.SessionEnded; + _endTime = endedAt; _loadingService.StartLoading("Writing statistics for " + _settings.CameraName); try { - _csvStatistics.WriteLine([_startTime.ToString("d"), _startTime.ToString("T"), DateTime.Now.ToString("d"), DateTime.Now.ToString("T"), "", "", ""]); + _csvStatistics.WriteLine([_startTime.ToString("d"), _startTime.ToString("T"), endedAt.ToString("d"), endedAt.ToString("T"), "", "", ""]); _csvStatistics.Dispose(); + _csvStatistics = null; + WriteFinalStatistics(); } finally @@ -67,10 +69,10 @@ namespace VisionBuilder.UI.Statistics private void _recognitionControl_SessionStarted(SessionStartedEvent obj) { - _startTime = DateTime.Now; + _startTime = obj.SessionStarted; _currentRecipe = obj.RecipeName; _fileName = "tmp_"+Guid.NewGuid().ToString()+".csv"; - _directoryPath = PathSettingsUtils.ConvertVariables(_settings.PathTemplate, DateTime.Now, DateTime.Now, + _directoryPath = PathSettingsUtils.ConvertVariables(_settings.PathTemplate, _startTime, _startTime, obj.RecipeName, cameraName: _settings.CameraName); _filePath = Path.Combine(_directoryPath, _fileName); var headers = new[] { _settings.StartDateColumnName, _settings.StartTimeColumnName, _settings.EndDateColumnName, _settings.EndTimeColumnName, _settings.ErrorTimeColumnName, _settings.ProductColumnName, _settings.ErrorColumnName }; @@ -84,6 +86,8 @@ namespace VisionBuilder.UI.Statistics } ConcurrentDictionary _defects = new(); + private DateTime _endTime; + private void RecordBadImage(string defectName) { @@ -96,50 +100,60 @@ namespace VisionBuilder.UI.Statistics _defects.AddOrUpdate(defectName, 1, (key, oldValue) => oldValue + 1); } - private void WriteMonthStatistics(string filename) + private void WriteMonthStatistics() { - // read second line to get the start time - var lines = File.ReadAllLines(filename); - var lastLine = lines[^1]; - if (lines.Length < 2) return; // no data to write - var startDate = DateTime.Parse(lastLine.Split(',')[0]); - var startTime = DateTime.Parse(lastLine.Split(',')[1]); - var endDate = DateTime.Parse(lastLine.Split(',')[2]); - var endTime = DateTime.Parse(lastLine.Split(',')[3]); - // all must be non-empty - if (startDate == DateTime.MinValue || startTime == DateTime.MinValue || endDate == DateTime.MinValue || endTime == DateTime.MinValue) + var startDate = _startTime.Date; + var startTime = _startTime; + var endDate = _endTime.Date; + var endTime = _endTime; + + var monthFilePath = Path.Combine(_directoryPath, MONTHLY_FILE_NAME); + + var monthStatistics = new CsvStatistics(monthFilePath, [ + "Date start","Time start","Date end", "Time end" ,"Product","Error type","Error count" + ]); + + foreach (var defect in _defects) { - Log.Error("Invalid date or time in statistics file: " + filename); - throw new InvalidOperationException("Invalid date or time in statistics file: " + filename); + monthStatistics.WriteLine( + [startDate.ToString("d"), startTime.ToString("T"), endDate.ToString("d"), endTime.ToString("T"), _currentRecipe, defect.Key, defect.Value.ToString()] + ); } + + monthStatistics.Dispose(); + + } private void WriteFinalStatistics() { try { - var tmpFiles = Directory.GetFiles(_directoryPath, "tmp_*"); - // order by creation time - Array.Sort(tmpFiles, (x, y) => File.GetCreationTime(x).CompareTo(File.GetCreationTime(y))); - foreach (var file in tmpFiles) - { - var finalFileName = Path.Combine(_directoryPath, BY_EVENTS_FILE_NAME); - var lines = File.ReadAllLines(file); - File.AppendAllLines(finalFileName, lines.Skip(1)); // skip header - WriteMonthStatistics(file); - File.Delete(file); - } + Log.Debug("Writing month statistics"); + WriteMonthStatistics(); } catch (Exception e) { - Log.Error("Error writing statistics for " + _settings.CameraName+ e); + Log.Error("Error writing statistics for " + _settings.CameraName+ "\n"+ e); } + _csvStatistics?.Dispose(); + _csvStatistics = null; } - public void Initialize() + public void InitializeModule() { - WriteFinalStatistics(); + _recognitionControl.SessionStarted += _recognitionControl_SessionStarted; + _recognitionControl.ImageProcessed += _recognitionControl_ImageProcessed; + _recognitionControl.SessionEnded += _recognitionControl_SessionEnded; + } + + public void Dispose() + { + _csvStatistics?.Dispose(); + _recognitionControl.SessionStarted -= _recognitionControl_SessionStarted; + _recognitionControl.ImageProcessed -= _recognitionControl_ImageProcessed; + _recognitionControl.SessionEnded -= _recognitionControl_SessionEnded; } } } diff --git a/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs b/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs index 89a39c6..bf2725d 100644 --- a/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs +++ b/VisionBuilder.UI.Statistics/VisionBuilderStatisticsSettings.cs @@ -1,5 +1,6 @@ using Inspectron.Settings; using Inspectron.Settings.Attributes; +using Lindt.Colorballs.Duo.Utils; using VisionBuilder.UI.Common; namespace VisionBuilder.UI.Statistics; @@ -16,31 +17,10 @@ public class VisionBuilderStatisticsSettings: ISettings [SettingDescription( @" Specifies directory, in which MaxBad and MaxGood are applied. -You can use the following system variables: - -$RECIPE_NAME - name of the current recipe -$DEFECT_NAME - name of the current defect(for bad images) -$CAMERA_NAME - name of the camera - -$HOUR - hour of the image -$MINUTE - minute of the image -$SECOND - second of the image -$MS - milliseconds of the image - -$DAY - day of the image -$MONTH - month of the image -$YEAR - year of the image - -$SS_HOUR - hour of the session start -$SS_MINUTE - minute of the session start -$SS_SECOND - second of the session start - -$SS_DAY - day of the session start -$SS_MONTH - month of the session start -$SS_YEAR - year of the session start -" +"+PathSettingsUtils.INSTUCTIONS )] - public string PathTemplate { get; set; }= @$"..\Data\Statistics\$SS_MONTH $SS_YEAR\$RECIPE_NAME\"; + [SettingPreview(typeof(VisionBuilderStatisticsSettings), nameof(PathTemplatePreview))] + public string PathTemplate { get; set; }= @$"..\Data\Statistics\$SS_MONTH_NUM $SS_YEAR\$RECIPE_NAME\"; public string StartDateColumnName { get; set; } = "Start Date"; public string StartTimeColumnName { get; set; } = "Start time"; @@ -61,4 +41,10 @@ $SS_YEAR - year of the session start settings.RegisterSimple(this,()=>this.ProductColumnName,CameraName+"/"+"Statistics",nameof(ProductColumnName)); settings.RegisterSimple(this,()=>this.ErrorColumnName,CameraName+"/"+"Statistics",nameof(ErrorColumnName)); } + + public static string PathTemplatePreview(object template) + { + return PathSettingsUtils.ConvertVariables((string)template, DateTime.Now, DateTime.Now, "my recipe", + "my defect", "my camera"); + } } \ No newline at end of file diff --git a/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj b/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj index d43e20c..3baad5c 100644 --- a/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj +++ b/VisionBuilder.UI.Tests/VisionBuilder.UI.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/VisionBuilder.UI.Tests/VisionBuilderStatisticsTests.cs b/VisionBuilder.UI.Tests/VisionBuilderStatisticsTests.cs new file mode 100644 index 0000000..f92456e --- /dev/null +++ b/VisionBuilder.UI.Tests/VisionBuilderStatisticsTests.cs @@ -0,0 +1,471 @@ +using System.Collections.Concurrent; +using System.Reflection; +using VisionBuilder.UI.Common; +using VisionBuilder.UI.Common.Commands; +using VisionBuilder.UI.Common.RecipeProcessing; +using VisionBuilder.UI.Common.ViewModel.Classes; +using VisionBuilder.UI.Statistics; + +namespace VisionBuilder.UI.Tests; + +[TestClass] +public class VisionBuilderStatisticsTests +{ + private string _tempDirectory; + private VisionBuilderStatisticsSettings _settings; + private TestRecognitionControl _recognitionControl; + private TestLoadingService _loadingService; + + [TestInitialize] + public void Setup() + { + _tempDirectory = Path.Combine(Path.GetTempPath(), "VisionBuilderStatisticsTests", Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDirectory); + + _settings = new VisionBuilderStatisticsSettings("TestCamera") + { + PathTemplate = _tempDirectory, + StartDateColumnName = "Start Date", + StartTimeColumnName = "Start time", + EndDateColumnName = "End Date", + EndTimeColumnName = "End time", + ErrorTimeColumnName = "Error time", + ProductColumnName = "Product", + ErrorColumnName = "Error" + }; + + _recognitionControl = new TestRecognitionControl(); + _loadingService = new TestLoadingService(); + } + + [TestCleanup] + public void Cleanup() + { + if (Directory.Exists(_tempDirectory)) + { + Directory.Delete(_tempDirectory, true); + } + } + + [TestMethod] + public void SessionStarted_CreatesTemporaryFile_WithCorrectHeaders() + { + // Arrange + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); + statistics.InitializeModule(); + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + statistics.Dispose(); // Ensure file is written and closed + + // Assert + var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); + Assert.AreEqual(1, tmpFiles.Length, "Should create exactly one temporary file"); + + var lines = File.ReadAllLines(tmpFiles[0]); + Assert.IsTrue(lines.Length >= 2, "Should have header and at least one data line"); + + var expectedHeaders = new[] { "Start Date", "Start time", "End Date", "End time", "Error time", "Product", "Error" }; + var actualHeaders = lines[0].Split(','); + CollectionAssert.AreEqual(expectedHeaders, actualHeaders, "Headers should match settings"); + + var firstDataLine = lines[1].Split(','); + Assert.IsTrue(DateTime.TryParse(firstDataLine[0], out _), "First column should be a valid date"); + Assert.IsTrue(DateTime.TryParse(firstDataLine[1], out _), "Second column should be a valid time"); + } + + [TestMethod] + public void ImageProcessed_WithError_RecordsDefectInFile() + { + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); + statistics.InitializeModule(); + // Arrange + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + var imageProcessedEvent = new ImageProcessedEvent + { + HasError = true, + ErrorNames = new List { "TestDefect" }, + RecipeName = "TestRecipe", + SessionStart = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + _recognitionControl.TriggerImageProcessed(imageProcessedEvent); + statistics.Dispose(); // Ensure file is written and closed + // Assert + var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); + var lines = File.ReadAllLines(tmpFiles[0]); + + Assert.IsTrue(lines.Length >= 3, "Should have header, session start, and error record"); + + var errorLine = lines[2].Split(','); + Assert.AreEqual("TestDefect", errorLine[6], "Error name should be recorded in correct column"); + Assert.AreEqual("TestRecipe", errorLine[5], "Recipe name should be recorded in correct column"); + Assert.IsTrue(DateTime.TryParse(errorLine[4], out _), "Error time should be valid"); + + // Verify defect counting using reflection + var defectsField = typeof(VisionBuilderStatistics).GetField("_defects", BindingFlags.NonPublic | BindingFlags.Instance); + var defects = (ConcurrentDictionary)defectsField.GetValue(statistics); + Assert.AreEqual(1, defects["TestDefect"], "Defect count should be incremented"); + } + + [TestMethod] + public void ImageProcessed_WithoutError_DoesNotRecordDefect() + { + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); + statistics.InitializeModule(); + // Arrange + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + var imageProcessedEvent = new ImageProcessedEvent + { + HasError = false, + ErrorNames = new List(), + RecipeName = "TestRecipe", + SessionStart = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + _recognitionControl.TriggerImageProcessed(imageProcessedEvent); + statistics.Dispose(); // Ensure file is written and closed + // Assert + var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); + var lines = File.ReadAllLines(tmpFiles[0]); + + Assert.AreEqual(2, lines.Length, "Should only have header and session start line"); + + // Verify defect counting using reflection + var defectsField = typeof(VisionBuilderStatistics).GetField("_defects", BindingFlags.NonPublic | BindingFlags.Instance); + var defects = (ConcurrentDictionary)defectsField.GetValue(statistics); + Assert.AreEqual(0, defects.Count, "No defects should be recorded"); + } + + [TestMethod] + public void SessionEnded_WritesEndTimeAndDisposesCsv() + { + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService) + { + DoNotDeleteTempFiles = true + }; + statistics.InitializeModule(); + // Arrange + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + var sessionEndEvent = new SessionEndedEvent + { + SessionEnded = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + _recognitionControl.TriggerSessionEnded(sessionEndEvent); + statistics.Dispose(); // Ensure file is written and closed + + // Assert + var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); + var lines = File.ReadAllLines(tmpFiles[0]); + + Assert.IsTrue(lines.Length >= 3, "Should have header, session start, and session end"); + + var endLine = lines[2].Split(','); + Assert.IsTrue(DateTime.TryParse(endLine[2], out _), "End date should be valid"); + Assert.IsTrue(DateTime.TryParse(endLine[3], out _), "End time should be valid"); + + Assert.IsTrue(_loadingService.StartLoadingCalled, "Should start loading during session end"); + Assert.IsTrue(_loadingService.StopLoadingCalled, "Should stop loading after session end"); + } + + [TestMethod] + public void WriteFinalStatistics_ConsolidatesTemporaryFiles() + { + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); + statistics.InitializeModule(); + // Arrange + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + var imageProcessedEvent = new ImageProcessedEvent + { + HasError = true, + ErrorNames = new List { "TestDefect" }, + RecipeName = "TestRecipe", + SessionStart = DateTime.Now + }; + + var sessionEndEvent = new SessionEndedEvent + { + SessionEnded = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + _recognitionControl.TriggerImageProcessed(imageProcessedEvent); + _recognitionControl.TriggerSessionEnded(sessionEndEvent); + + statistics.Dispose(); // Ensure file is written and closed + + // Assert + var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); + Assert.AreEqual(0, tmpFiles.Length, "Temporary files should be deleted after consolidation"); + + var finalFile = Path.Combine(_tempDirectory, "statistics_by_events.csv"); + Assert.IsTrue(File.Exists(finalFile), "Final statistics file should be created"); + + var finalLines = File.ReadAllLines(finalFile); + Assert.IsTrue(finalLines.Length >= 2, "Final file should contain session data"); + + // Verify the defect was recorded in the final file + var defectLine = finalLines.FirstOrDefault(l => l.Contains("TestDefect")); + Assert.IsNotNull(defectLine, "Defect should be recorded in final file"); + } + + [TestMethod] + public void MultipleDefects_CountedCorrectly() + { + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); + statistics.InitializeModule(); + // Arrange + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + + // Process multiple images with same defect + for (int i = 0; i < 3; i++) + { + _recognitionControl.TriggerImageProcessed(new ImageProcessedEvent + { + HasError = true, + ErrorNames = new List { "DefectA" }, + RecipeName = "TestRecipe", + SessionStart = DateTime.Now + }); + } + + // Process images with different defect + for (int i = 0; i < 2; i++) + { + _recognitionControl.TriggerImageProcessed(new ImageProcessedEvent + { + HasError = true, + ErrorNames = new List { "DefectB" }, + RecipeName = "TestRecipe", + SessionStart = DateTime.Now + }); + } + statistics.Dispose(); // Ensure file is written and closed + // Assert + var defectsField = typeof(VisionBuilderStatistics).GetField("_defects", BindingFlags.NonPublic | BindingFlags.Instance); + var defects = (ConcurrentDictionary)defectsField.GetValue(statistics); + + Assert.AreEqual(3, defects["DefectA"], "DefectA should be counted correctly"); + Assert.AreEqual(2, defects["DefectB"], "DefectB should be counted correctly"); + } + + [TestMethod] + public void PathTemplate_WithVariables_ReplacedCorrectly() + { + // Arrange + var customSettings = new VisionBuilderStatisticsSettings("TestCamera") + { + PathTemplate = Path.Combine(_tempDirectory, "$RECIPE_NAME", "$SS_YEAR", "$SS_MONTH") + }; + + var customStatistics = new VisionBuilderStatistics(customSettings, _recognitionControl, _loadingService); + customStatistics.InitializeModule(); + var testDate = new DateTime(2023, 5, 15, 10, 30, 0); + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "MyRecipe", + SessionStarted = new DateTime(2023, 5, 15, 10, 30, 0) + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + customStatistics.Dispose(); // Ensure file is written and closed + // Assert + var expectedPath = Path.Combine(_tempDirectory, "MyRecipe", testDate.ToString("yyyy"), testDate.ToString("MMMM")); + Assert.IsTrue(Directory.Exists(expectedPath), "Directory should be created with expanded variables"); + + var tmpFiles = Directory.GetFiles(expectedPath, "tmp_*.csv"); + Assert.AreEqual(1, tmpFiles.Length, "Temporary file should be created in correct directory"); + } + + [TestMethod] + public void CustomColumnNames_UsedInHeaders() + { + // Arrange + var customSettings = new VisionBuilderStatisticsSettings("TestCamera") + { + PathTemplate = _tempDirectory, + StartDateColumnName = "Custom Start Date", + StartTimeColumnName = "Custom Start Time", + EndDateColumnName = "Custom End Date", + EndTimeColumnName = "Custom End Time", + ErrorTimeColumnName = "Custom Error Time", + ProductColumnName = "Custom Product", + ErrorColumnName = "Custom Error" + }; + + var customStatistics = new VisionBuilderStatistics(customSettings, _recognitionControl, _loadingService); + customStatistics.InitializeModule(); + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = DateTime.Now + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + + customStatistics.Dispose(); + + // Assert + var tmpFiles = Directory.GetFiles(_tempDirectory, "tmp_*.csv"); + var lines = File.ReadAllLines(tmpFiles[0]); + + var expectedHeaders = new[] { "Custom Start Date", "Custom Start Time", "Custom End Date", "Custom End Time", "Custom Error Time", "Custom Product", "Custom Error" }; + var actualHeaders = lines[0].Split(','); + CollectionAssert.AreEqual(expectedHeaders, actualHeaders, "Custom column names should be used in headers"); + } + + [TestMethod] + public void WriteMonthStatistics_CreatesMonthlyFileWithDefectCounts() + { + var statistics = new VisionBuilderStatistics(_settings, _recognitionControl, _loadingService); + statistics.InitializeModule(); + // Arrange + var sessionStartEvent = new SessionStartedEvent + { + RecipeName = "TestRecipe", + SessionStarted = new DateTime(2023, 5, 15, 10, 30, 0) + }; + + var imageProcessedEvent1 = new ImageProcessedEvent + { + HasError = true, + ErrorNames = new List { "DefectA" }, + RecipeName = "TestRecipe", + SessionStart = new DateTime(2023, 5, 15, 10, 30, 0) + }; + + var imageProcessedEvent2 = new ImageProcessedEvent + { + HasError = true, + ErrorNames = new List { "DefectB" }, + RecipeName = "TestRecipe", + SessionStart = new DateTime(2023, 5, 15, 10, 30, 0) + }; + + var sessionEndEvent = new SessionEndedEvent + { + SessionEnded = new DateTime(2023, 5, 15, 11, 45, 0) + }; + + // Act + _recognitionControl.TriggerSessionStarted(sessionStartEvent); + _recognitionControl.TriggerImageProcessed(imageProcessedEvent1); + _recognitionControl.TriggerImageProcessed(imageProcessedEvent1); // Same defect again + _recognitionControl.TriggerImageProcessed(imageProcessedEvent2); + _recognitionControl.TriggerSessionEnded(sessionEndEvent); + statistics.Dispose(); + + // Assert + var monthlyFile = Path.Combine(_tempDirectory, "statistics_month.csv"); + Assert.IsTrue(File.Exists(monthlyFile), "Monthly statistics file should be created"); + + var monthlyLines = File.ReadAllLines(monthlyFile); + Assert.IsTrue(monthlyLines.Length >= 3, "Should have header and defect records"); + + var expectedHeaders = new[] { "Date start", "Time start", "Date end", "Time end", "Product", "Error type", "Error count" }; + var actualHeaders = monthlyLines[0].Split(','); + CollectionAssert.AreEqual(expectedHeaders, actualHeaders, "Monthly file should have correct headers"); + + // Verify defect counts + var defectALine = monthlyLines.FirstOrDefault(l => l.Contains("DefectA")); + var defectBLine = monthlyLines.FirstOrDefault(l => l.Contains("DefectB")); + + Assert.IsNotNull(defectALine, "DefectA should be recorded in monthly file"); + Assert.IsNotNull(defectBLine, "DefectB should be recorded in monthly file"); + + var defectAData = defectALine.Split(','); + var defectBData = defectBLine.Split(','); + + Assert.AreEqual("2", defectAData[6], "DefectA count should be 2"); + Assert.AreEqual("1", defectBData[6], "DefectB count should be 1"); + Assert.AreEqual("TestRecipe", defectAData[4], "Product name should be correct"); + Assert.AreEqual("TestRecipe", defectBData[4], "Product name should be correct"); + + // Verify dates and times + Assert.IsTrue(DateTime.TryParse(defectAData[0], out var startDate), "Start date should be valid"); + Assert.IsTrue(DateTime.TryParse(defectAData[1], out var startTime), "Start time should be valid"); + Assert.IsTrue(DateTime.TryParse(defectAData[2], out var endDate), "End date should be valid"); + Assert.IsTrue(DateTime.TryParse(defectAData[3], out var endTime), "End time should be valid"); + } + + +} + +// Test implementation of IRecognitionControl using reflection approach +public class TestRecognitionControl : IRecognitionControl +{ + public event Action ImageProcessed = delegate { }; + public event Action SessionStarted = delegate { }; + public event Action SessionEnded = delegate { }; + + public List GetRecipesData() => new List(); + public void SetRecipe(RecipeData recipe) { } + public void Start() { } + public void Stop() { } + + public void TriggerImageProcessed(ImageProcessedEvent eventArgs) => ImageProcessed(eventArgs); + public void TriggerSessionStarted(SessionStartedEvent eventArgs) => SessionStarted(eventArgs); + public void TriggerSessionEnded(SessionEndedEvent eventArgs) => SessionEnded(eventArgs); +} + +// Test implementation of ILoadingService +public class TestLoadingService : ILoadingService +{ + public bool StartLoadingCalled { get; private set; } + public bool StopLoadingCalled { get; private set; } + public string LastLoadingMessage { get; private set; } + + public void StartLoading(string message) + { + StartLoadingCalled = true; + LastLoadingMessage = message; + } + + public void StopLoading(string message) + { + StopLoadingCalled = true; + LastLoadingMessage = message; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows.Test/Program.cs b/VisionBuilder.UI.Windows.Test/Program.cs index 1123f29..b3321fa 100644 --- a/VisionBuilder.UI.Windows.Test/Program.cs +++ b/VisionBuilder.UI.Windows.Test/Program.cs @@ -11,14 +11,19 @@ using Inspectron.Settings; using Inspectron.Settings.Windows.Configuration; using Lindt.Colorballs.Duo.Utils; using Ninject; +using Ninject.Extensions.ChildKernel; using OpenCvSharp; using VisionBuilder.UI.Common.RecipeProcessing; +using VisionBuilder.UI.Console; using VisionBuilder.UI.Recipes.HawkeyeRecipe; +using VisionBuilder.UI.Ringbuffer; +using VisionBuilder.UI.Statistics; namespace VisionBuilder.UI.Windows.Test { internal static class Program { + private const string CAMERA1 = "Camera 1"; /// /// The main entry point for the application. /// @@ -29,59 +34,43 @@ namespace VisionBuilder.UI.Windows.Test // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); + InspectronSettings settings = new InspectronSettings("..\\Config\\AppSettings.xml"); - StandardKernel kernelCamera1 = new StandardKernel(); + var mainKernel = Common.VisionBuilder.CreateMainKernel(settings); + + + + // CAMERA 1 // + ChildKernel kernelCamera1 = new ChildKernel(mainKernel); + mainKernel.UseWindowsServices(); + + // MODULES // + kernelCamera1 + .RegisterCamera(CAMERA1) + .UseStatistics(CAMERA1) + .UseRingbuffer(CAMERA1) + .UseHawkeyeRecipes(CAMERA1) + .UseConsole(); - // SETTINGS // - InspectronSettings settings = new InspectronSettings("..\\Config\\AppSettings.xml"); - kernelCamera1.Bind().ToConstant(settings); - kernelCamera1.Bind().ToConstant(new UIConfiguration()); - kernelCamera1.Bind().ToConstant(new CameraSettings("Camera 1")); - kernelCamera1.Bind().ToConstant(new EmulationSettings("Camera 1")); - kernelCamera1.Bind().ToConstant(new HawkeyeRecognitionSettings("Camera 1")); - kernelCamera1.Bind().ToConstant(new VisionBuilderRingbufferSettings("Camera 1")); - + // LOAD SETTINGS // kernelCamera1.RegisterSettings(); settings.LoadSettings(); + + // Create camera and bind it + // Must be done after settings are loaded + kernelCamera1.UseCamera(); + + + + kernelCamera1.InitializeModules(); + + var mainWindowVm = mainKernel.Get(); + mainWindowVm.SingleCameraVms = [ + kernelCamera1.Get() + ]; - // --- // - - // SOURCES // - var cameraSettings = kernelCamera1.Get(); - switch (cameraSettings.Camera1Source) - { - case CameraSettings.EImageSource.Emulation: - kernelCamera1.Bind().To().InSingletonScope(); - break; - case CameraSettings.EImageSource.Hawkeye: - break; - case CameraSettings.EImageSource.IDS: - break; - case CameraSettings.EImageSource.Basler: - break; - default: - throw new ArgumentOutOfRangeException(); - } - // --- // - - // SERVICES // - kernelCamera1.Bind().To().InSingletonScope(); - kernelCamera1.Bind().To().InSingletonScope(); - kernelCamera1.Bind().ToSelf().InSingletonScope(); - kernelCamera1.Bind().To().InSingletonScope(); - - kernelCamera1.Get(); // initialize the ringbuffer - - var mainWindowVm = new MainWindowVM(kernelCamera1.Get()) - { - SingleCameraVms = new ObservableCollection() - { - new SingleCameraVM(kernelCamera1.Get(),kernelCamera1.Get(),new RecipeSelectionDialogService(),kernelCamera1.Get(), new ImagePreviewService(), new NoLearning()) - } - }; - settings.LoadSettings(); Application.Run(new Form1(mainWindowVm)); } diff --git a/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj b/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj index 90e6dab..1349041 100644 --- a/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj +++ b/VisionBuilder.UI.Windows.Test/VisionBuilder.UI.Windows.Test.csproj @@ -9,6 +9,7 @@ + @@ -17,9 +18,12 @@ + + + diff --git a/VisionBuilder.UI.Windows/Components/PreviewWindow.resx b/VisionBuilder.UI.Windows/Components/PreviewWindow.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/VisionBuilder.UI.Windows/Components/PreviewWindow.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.Windows/Components/SingleCameraControl.cs b/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs index 977bed1..ed33986 100644 --- a/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs +++ b/VisionBuilder.UI.Windows/Components/SingleCameraControl.cs @@ -31,7 +31,7 @@ namespace VisionBuilder.UI.Windows.Components btnStop.Command=_singleCameraVm.StopCommand; btnSelectRecipe.Command=_singleCameraVm.SelectRecipeCommand; - lblCameraName.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CameraName), true, DataSourceUpdateMode.OnPropertyChanged); + lblCameraName.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CameraLabel), true, DataSourceUpdateMode.OnPropertyChanged); btnSelectRecipe.DataBindings.Add("Text", _singleCameraVm, nameof(_singleCameraVm.CurrentRecipeName), true, DataSourceUpdateMode.OnPropertyChanged); } diff --git a/VisionBuilder.UI.Windows/ModuleExtensions.cs b/VisionBuilder.UI.Windows/ModuleExtensions.cs new file mode 100644 index 0000000..f1fb4a8 --- /dev/null +++ b/VisionBuilder.UI.Windows/ModuleExtensions.cs @@ -0,0 +1,18 @@ +using Ninject; +using VisionBuilder.UI.Common.RecipeProcessing; +using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; +using VisionBuilder.UI.Windows.Settings; + +namespace VisionBuilder.UI.Windows; + +public static class ModuleExtensions +{ + public static IKernel UseWindowsServices(this IKernel self) + { + self.Bind().To().InSingletonScope(); + self.Bind().To().InSingletonScope(); + self.Bind().To().InSingletonScope(); + self.Bind().To().InSingletonScope(); + return self; + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/LoadingService.cs b/VisionBuilder.UI.Windows/Services/LoadingService.cs deleted file mode 100644 index bfe3a76..0000000 --- a/VisionBuilder.UI.Windows/Services/LoadingService.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MaterialSkin.Controls; -using VisionBuilder.UI.Common.RecipeProcessing; - -namespace VisionBuilder.UI.Windows.Settings; - -public class LoadingService: ILoadingService -{ - public void StartLoading(string title) - { - MaterialLoader.Instance.StartLoading(title); - } - - public void StopLoading(string title) - { - MaterialLoader.Instance.FinishLoading(title); - } -} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/ImagePreviewService.cs b/VisionBuilder.UI.Windows/Services/WindowsImagePreviewService.cs similarity index 80% rename from VisionBuilder.UI.Windows/Services/ImagePreviewService.cs rename to VisionBuilder.UI.Windows/Services/WindowsImagePreviewService.cs index 14fe3e6..916095f 100644 --- a/VisionBuilder.UI.Windows/Services/ImagePreviewService.cs +++ b/VisionBuilder.UI.Windows/Services/WindowsImagePreviewService.cs @@ -5,10 +5,10 @@ using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; namespace VisionBuilder.UI.Windows.Settings; -public class ImagePreviewService: IImagePreviewService +public class WindowsImagePreviewService: IImagePreviewService { - public ImagePreviewService() + public WindowsImagePreviewService() { } diff --git a/VisionBuilder.UI.Windows/Services/WindowsLoadingService.cs b/VisionBuilder.UI.Windows/Services/WindowsLoadingService.cs new file mode 100644 index 0000000..b3e3670 --- /dev/null +++ b/VisionBuilder.UI.Windows/Services/WindowsLoadingService.cs @@ -0,0 +1,28 @@ +using MaterialSkin.Controls; +using VisionBuilder.UI.Common.RecipeProcessing; + +namespace VisionBuilder.UI.Windows.Settings; + +public class WindowsLoadingService: ILoadingService +{ + public void StartLoading(string title) + { + lock (this) + { + MaterialLoader.Instance.StartLoading(title); + Application.DoEvents(); // Ensure the UI updates immediately + Thread.Sleep(10); + } + + } + + public void StopLoading(string title) + { + lock (this) + { + MaterialLoader.Instance.FinishLoading(title); + Application.DoEvents(); // Ensure the UI updates immediately + Thread.Sleep(10); + } + } +} \ No newline at end of file diff --git a/VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs b/VisionBuilder.UI.Windows/Services/WindowsRecipeSelectionDialogService.cs similarity index 85% rename from VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs rename to VisionBuilder.UI.Windows/Services/WindowsRecipeSelectionDialogService.cs index f177581..023ba2e 100644 --- a/VisionBuilder.UI.Windows/Services/RecipeSelectionDialogService.cs +++ b/VisionBuilder.UI.Windows/Services/WindowsRecipeSelectionDialogService.cs @@ -4,7 +4,7 @@ using VisionBuilder.UI.Windows.Dialogs; namespace VisionBuilder.UI.Windows.Settings; -public class RecipeSelectionDialogService: IRecipeSelectionDialogService +public class WindowsRecipeSelectionDialogService: IRecipeSelectionDialogService { public bool SelectRecipe(RecipeSelectionVM recipeSelectionVm) { diff --git a/VisionBuilder.UI.Windows/Services/SettingsService.cs b/VisionBuilder.UI.Windows/Services/WindowsSettingsService.cs similarity index 86% rename from VisionBuilder.UI.Windows/Services/SettingsService.cs rename to VisionBuilder.UI.Windows/Services/WindowsSettingsService.cs index 2e1c78b..1b446b1 100644 --- a/VisionBuilder.UI.Windows/Services/SettingsService.cs +++ b/VisionBuilder.UI.Windows/Services/WindowsSettingsService.cs @@ -5,12 +5,12 @@ using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; namespace VisionBuilder.UI.Windows.Settings; -public class SettingsService: ISettingsService +public class WindowsSettingsService: ISettingsService { private readonly UIConfiguration _uiConfiguration; private readonly InspectronSettings _settings; - public SettingsService(UIConfiguration uiConfiguration, InspectronSettings settings) + public WindowsSettingsService(UIConfiguration uiConfiguration, InspectronSettings settings) { _uiConfiguration = uiConfiguration ?? throw new ArgumentNullException(nameof(uiConfiguration)); _settings = settings ?? throw new ArgumentNullException(nameof(settings)); diff --git a/VisionBuilder.UI.sln b/VisionBuilder.UI.sln index 8f2d432..b3635c2 100644 --- a/VisionBuilder.UI.sln +++ b/VisionBuilder.UI.sln @@ -51,6 +51,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Fastbuffer", "fr EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{C828783C-1CE1-4245-8731-4AE18A28F590}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Console", "VisionBuilder.UI.Console\VisionBuilder.UI.Console.csproj", "{4B1817EF-A3D1-48CC-BE22-4138740E450F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Camera", "VisionBuilder.UI.Camera\VisionBuilder.UI.Camera.csproj", "{FBDCC120-B6B3-4A28-8845-DF1690F619C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.IOCommander", "VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj", "{E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -137,6 +143,18 @@ Global {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Release|Any CPU.ActiveCfg = Release|Any CPU {0A23E56A-DF4C-5174-18DF-18A771957AF4}.Release|Any CPU.Build.0 = Release|Any CPU + {4B1817EF-A3D1-48CC-BE22-4138740E450F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B1817EF-A3D1-48CC-BE22-4138740E450F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B1817EF-A3D1-48CC-BE22-4138740E450F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B1817EF-A3D1-48CC-BE22-4138740E450F}.Release|Any CPU.Build.0 = Release|Any CPU + {FBDCC120-B6B3-4A28-8845-DF1690F619C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBDCC120-B6B3-4A28-8845-DF1690F619C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBDCC120-B6B3-4A28-8845-DF1690F619C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBDCC120-B6B3-4A28-8845-DF1690F619C1}.Release|Any CPU.Build.0 = Release|Any CPU + {E0F75E78-F7FA-4B7D-BA5F-A8A91C0A9AD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -155,6 +173,9 @@ Global {51C2BC65-5E5D-E72A-988D-215E192DE889} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} {1F9BB3BD-65C3-4B96-A880-4908CF6D9754} = {C828783C-1CE1-4245-8731-4AE18A28F590} {0A23E56A-DF4C-5174-18DF-18A771957AF4} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1} + {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} 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 b90bb2e..d06b2f5 100644 --- a/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs +++ b/framework/Inspectron.Settings.Windows/Configuration/DefaultControlFactory.cs @@ -28,7 +28,8 @@ public class DefaultControlFactory : IControlFactory getPreview = value => (string)method.Invoke(null, new[] { value }); previewLabel = new Label { - AutoSize = true, + AutoSize = false, + Size = new System.Drawing.Size(600, 30), Padding = new Padding(15, 0, 0, 0), Font = new System.Drawing.Font("Segoe UI", 10, System.Drawing.FontStyle.Italic), Text = getPreview(initialValue) @@ -95,7 +96,7 @@ public class DefaultControlFactory : IControlFactory private Control CreateStringControl(string description, object initialValue, Action valueChangedCallback, string settingDescription, Label previewLabel, Func getPreview) { - var textBox = new TextBox { Text = initialValue as string, Width = 400 }; + var textBox = new TextBox { Text = initialValue as string, Width = 600 }; var label = new Label { Text = description, AutoSize = true, Padding = new Padding(0, 5, 0, 0) }; textBox.TextChanged += (s, e) => diff --git a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs index 2f142fd..9e13ee4 100644 --- a/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs +++ b/framework/Inspectron.Settings.Windows/Configuration/OptionsWindow.Designer.cs @@ -33,6 +33,7 @@ public partial class OptionsWindow flowLayoutPanelSettings.FlowDirection = FlowDirection.TopDown; flowLayoutPanelSettings.Location = new Point(220, 12); flowLayoutPanelSettings.Name = "flowLayoutPanelSettings"; + flowLayoutPanelSettings.Padding = new Padding(0, 0, 0, 15); flowLayoutPanelSettings.Size = new Size(852, 582); flowLayoutPanelSettings.TabIndex = 1; flowLayoutPanelSettings.WrapContents = false; diff --git a/framework/Inspectron.Statistics/CsvStatistics.cs b/framework/Inspectron.Statistics/CsvStatistics.cs index 9e0e190..df9376b 100644 --- a/framework/Inspectron.Statistics/CsvStatistics.cs +++ b/framework/Inspectron.Statistics/CsvStatistics.cs @@ -42,7 +42,7 @@ public class CsvStatistics { _csv.Dispose(); _stream.Dispose(); + _fileStream.Close(); _fileStream.Dispose(); - } } \ No newline at end of file diff --git a/framework/Inspectron.Statistics/StatisticsRecord.cs b/framework/Inspectron.Statistics/StatisticsRecord.cs index 8e2f1cf..13335c9 100644 --- a/framework/Inspectron.Statistics/StatisticsRecord.cs +++ b/framework/Inspectron.Statistics/StatisticsRecord.cs @@ -2,5 +2,6 @@ public class StatisticsRecord { + public DateTime Time { get; set; } public string[] Values { get; set; } } \ No newline at end of file diff --git a/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs b/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs index 16f5b89..a2ac9a4 100644 --- a/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs +++ b/framework/MaterialSkin.Core/Controls/MaterialLoader.Designer.cs @@ -38,6 +38,7 @@ ControlBox = false; Name = "MaterialLoader"; StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + TopMost = true; ResumeLayout(false); }