From 913805eb7029bef8838795f42f57b1142771ddbc Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Wed, 17 Jun 2026 10:35:36 +0300 Subject: [PATCH] feat: mock run mode extract IImageCapturerService interface from ImageCapturerService add ImageCapturerMockService as implementation of IImageCapturerService add --mock flag in command line interface and implement its usage --- .../AmineContent/IImageCapturerService.cs | 10 ++++ .../AmineContent/ImageCapturerMockService.cs | 57 +++++++++++++++++++ .../AmineContent/ImageCapturerService.cs | 2 +- GSS2.Core/DependencyInjectionHelper.cs | 18 ++++-- GSS2/Commands/UI.cs | 45 ++++++++------- 5 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 GSS2.Core/Analysis/AmineContent/IImageCapturerService.cs create mode 100644 GSS2.Core/Analysis/AmineContent/ImageCapturerMockService.cs diff --git a/GSS2.Core/Analysis/AmineContent/IImageCapturerService.cs b/GSS2.Core/Analysis/AmineContent/IImageCapturerService.cs new file mode 100644 index 0000000..c77e346 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/IImageCapturerService.cs @@ -0,0 +1,10 @@ + +using OpenCvSharp; + +namespace GSS2.Core.Analysis.AmineContent; + +public interface IImageCapturerService +{ + public Task CaptureImages(Action<(int i, double visible, double uv365, double uv254, Mat image)> callback, CancellationToken cancellationToken); + public Task CaptureImages(Func<(int i, double visible, double uv365, double uv254, Mat image), Task> callback, CancellationToken cancellationToken); +} diff --git a/GSS2.Core/Analysis/AmineContent/ImageCapturerMockService.cs b/GSS2.Core/Analysis/AmineContent/ImageCapturerMockService.cs new file mode 100644 index 0000000..8047a78 --- /dev/null +++ b/GSS2.Core/Analysis/AmineContent/ImageCapturerMockService.cs @@ -0,0 +1,57 @@ + +using System.Collections.ObjectModel; + +using Microsoft.Extensions.Logging; + +using OpenCvSharp; + +using GSS2.Core.Extensions; + +namespace GSS2.Core.Analysis.AmineContent; + +public class ImageCapturerMockService : IImageCapturerService +{ + // Список интенсивностей освещения для каждого кадра + public static readonly ReadOnlyCollection<(double visible, double uv365, double uv254)> IlluminatorIntensities = + [ + (0.02500, 0.0, 0.0), + (0.01250, 0.0, 0.0), + (0.00625, 0.0, 0.0), + (0.00000, 1.0, 0.0), + (0.00000, 0.0, 1.0), + (0.00000, 1.0, 1.0), + (0.01250, 1.0, 0.0), + (0.01250, 0.0, 1.0), + (0.01250, 1.0, 1.0), + (0.00625, 1.0, 0.0), + (0.00625, 0.0, 1.0), + (0.00625, 1.0, 1.0) + ]; + + private readonly string _imagesDirectoryPath; + private readonly ILogger _logger; + + public ImageCapturerMockService(string imagesDirectoryPath, ILogger logger) + { + _imagesDirectoryPath = imagesDirectoryPath; + _logger = logger; + } + + public async Task CaptureImages(Action<(int i, double visible, double uv365, double uv254, Mat image)> callback, CancellationToken cancellationToken) => + await CaptureImages(async (data) => callback(data), cancellationToken); + + public async Task CaptureImages(Func<(int i, double visible, double uv365, double uv254, Mat image), Task> callback, CancellationToken cancellationToken) + { + foreach (var (i, (visible, uv365, uv254)) in IlluminatorIntensities.Enumerate()) + { + cancellationToken.ThrowIfCancellationRequested(); + _logger.LogInformation("Capturing images: {} / {} ({};{};{})", i + 1, IlluminatorIntensities.Count(), visible, uv365, uv254); + + Mat? image = Cv2.ImRead(Path.Join(_imagesDirectoryPath, $"{visible}-{uv365}-{uv254}.png")); + if (image is null) + throw new Exception("Image capture failed"); + + await callback.Invoke((i, visible, uv365, uv254, image)); + } + } +} diff --git a/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs b/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs index 2bb7ba3..6a97a78 100644 --- a/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs +++ b/GSS2.Core/Analysis/AmineContent/ImageCapturerService.cs @@ -11,7 +11,7 @@ using OpenCvSharp; namespace GSS2.Core.Analysis.AmineContent; -public class ImageCapturerService +public class ImageCapturerService : IImageCapturerService { // Список интенсивностей освещения для каждого кадра public static readonly ReadOnlyCollection<(double visible, double uv365, double uv254)> IlluminatorIntensities = diff --git a/GSS2.Core/DependencyInjectionHelper.cs b/GSS2.Core/DependencyInjectionHelper.cs index f629ee4..cf6c093 100644 --- a/GSS2.Core/DependencyInjectionHelper.cs +++ b/GSS2.Core/DependencyInjectionHelper.cs @@ -7,8 +7,7 @@ using Microsoft.Extensions.Logging.Console; using GSS2.Core.Logging; using GSS2.Core.Hardware; -using GSS2.Core.Analysis.AmineContent.Database.Calibration; -using GSS2.Core.Analysis.AmineContent.Database.Results; +using GSS2.Core.Analysis.AmineContent.Database; using GSS2.Core.Analysis.AmineContent; using ConsoleFormatter = GSS2.Core.Logging.ConsoleFormatter; @@ -128,8 +127,19 @@ public static class DependencyInjectionHelper ServiceLifetime.Singleton ); - public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services) => services - .AddSingleton(); + public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services, bool mock = false) + { + if (!mock) + return services.AddSingleton(); + + return services.AddSingleton(serviceProvider => + { + var logger = serviceProvider.GetService>(); + if (logger is null) + throw new Exception("Cannot get logger"); + return new ImageCapturerMockService("./", logger); + }); + } public static IServiceCollection AddAmineContentAnalyzerService(this IServiceCollection services) => services .AddSingleton(); diff --git a/GSS2/Commands/UI.cs b/GSS2/Commands/UI.cs index 1c440ef..3b31b52 100644 --- a/GSS2/Commands/UI.cs +++ b/GSS2/Commands/UI.cs @@ -10,9 +10,7 @@ using Avalonia.OpenGL.Egl; using GSS2.Core; using GSS2.Core.Logging; -using GSS2.Core.Analysis.AmineContent.Database.Calibration; -using GSS2.Core.Analysis.AmineContent.Database.Results; -using GSS2.Core.Hardware; +using GSS2.Core.Analysis.AmineContent.Database; namespace GSS2.Commands; @@ -29,6 +27,7 @@ public static class UI public static readonly Option Fullscreen; public static readonly Option RenderingMode; public static readonly Option HideConsole; + public static readonly Option Mock; static UI() { @@ -52,6 +51,11 @@ public static class UI Description = "Отключить вывод в консоль", DefaultValueFactory = (result) => result.GetValue(RenderingMode) == RenderingModes.DRM }; + Mock = new("--mock") + { + Description = "Запуск в фиктивном режиме без инициализации и использования периферии", + DefaultValueFactory = (result) => false + }; // Изменяем описания стандартных опций Helper.TranslateDefaultOptionDescriptions(RootCommand); @@ -60,11 +64,13 @@ public static class UI RootCommand.Add(Fullscreen); RootCommand.Add(RenderingMode); RootCommand.Add(HideConsole); + RootCommand.Add(Mock); RootCommand.SetAction(CommandAction); Command.Add(Fullscreen); Command.Add(RenderingMode); Command.Add(HideConsole); + Command.Add(Mock); Command.SetAction(CommandAction); } @@ -85,6 +91,7 @@ public static class UI var fullscreen = result.GetValue(Fullscreen); var renderingMode = result.GetValue(RenderingMode); var hideConsole = result.GetValue(HideConsole); + var mock = result.GetValue(Mock); // Если выбран режим рендеринга DRM, то приложение будет рисоваться вместе с консолью, // поэтому если не перехватить консоль, то вывод в неё будет рисоваться вместе с интерфейсом @@ -92,7 +99,7 @@ public static class UI SilenceConsole(); // Собираем хост - var host = BuildHost(); + var host = BuildHost(mock); // Запускаем хост асинхронно без блокировки потока _ = host.RunAsync(); @@ -137,7 +144,7 @@ public static class UI thread.Start(); } - private static IHost BuildHost() + private static IHost BuildHost(bool mock) { // Создаём хост var builder = Host.CreateApplicationBuilder(); @@ -150,17 +157,19 @@ public static class UI builder.Services.AddSingleton(); // Добавляем сервисы баз данных - builder.Services.AddAmineContentCalibrationContext(builder.Configuration); - builder.Services.AddAmineContentResultsContext(builder.Configuration); + builder.Services.AddAmineContentContext(builder.Configuration); // Добавляем сервисы аппаратной части и прочее - builder.Services.AddLightsService("Hardware:Lights"); - builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity"); - builder.Services.AddIlluminatorService("Hardware:Illuminator"); - builder.Services.AddCameraService("Hardware:Camera", true); - builder.Services.AddComputeResourcesService(); + if (!mock) + { + builder.Services.AddLightsService("Hardware:Lights"); + builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity"); + builder.Services.AddIlluminatorService("Hardware:Illuminator"); + builder.Services.AddCameraService("Hardware:Camera", true); + builder.Services.AddComputeResourcesService(); + } builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue("ImageStorage") ?? "images")); - builder.Services.AddAmineContentImageCapturerService(); + builder.Services.AddAmineContentImageCapturerService(mock); builder.Services.AddAmineContentAnalyzerService(); // Добавляем элементы интерфейса как сервисы @@ -171,17 +180,13 @@ public static class UI // Поскольку LibCameraLogSink, который перенаправляет логи из libcamera в хост, // не будет создан ни одним из других сервисов, его необходимо создать превентивно - host.Services.GetRequiredService(); + if (!mock) + host.Services.GetRequiredService(); // Применяем миграции баз данных если их схема данных изменялась using (var scope = host.Services.CreateScope()) { - var context = scope.ServiceProvider.GetRequiredService(); - context.Database.Migrate(); - } - using (var scope = host.Services.CreateScope()) - { - var context = scope.ServiceProvider.GetRequiredService(); + var context = scope.ServiceProvider.GetRequiredService(); context.Database.Migrate(); }