comprehensive update

1) remove unused components: camera view (due it causes segmentation faults), compute resources and all related, illuminator controller (due in useless without camera view), image storage service, temperature and humidity service
2) database schema - reduce tables references, features storing in records themselves in compressed form, add records creation and editing date and time, add separator comment column
3) analysis - rework of pipeline and ui, now database storing only raw data and all display values calculated from it
4) lights service - add reconnection if disconnected
5) add width and height command line arguments
6) fix some typos and other issues
This commit is contained in:
2026-06-29 15:13:29 +03:00
parent caee9fafd5
commit 72ae26eee7
74 changed files with 5219 additions and 4248 deletions
@@ -84,6 +84,8 @@ public partial class AnalyzerService
private PredictionEngine<RegressionData, RegressionPrediction>? _regressionEngine = null; private PredictionEngine<RegressionData, RegressionPrediction>? _regressionEngine = null;
public bool Initialized { get; private set; } = false; public bool Initialized { get; private set; } = false;
public int InitializedBrandId { get; private set; } = 0;
public int InitializedSeparatorId { get; private set; } = 0;
public AnalyzerService(ILogger<AnalyzerService> logger, Context Context) public AnalyzerService(ILogger<AnalyzerService> logger, Context Context)
{ {
@@ -93,6 +95,14 @@ public partial class AnalyzerService
public async Task Initialize(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken = default) public async Task Initialize(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken = default)
{ {
// TODO: Повторная инициализация если изменились калибровочные данные
if (Initialized &&
InitializedBrandId == brand.Id &&
InitializedSeparatorId == separator.Id
)
return;
Initialized = false; Initialized = false;
_logger.LogInformation("Инициализация"); _logger.LogInformation("Инициализация");
@@ -116,6 +126,8 @@ public partial class AnalyzerService
} }
Initialized = true; Initialized = true;
InitializedBrandId = brand.Id;
InitializedSeparatorId = separator.Id;
} }
private async Task InitializeMlClusterizationEngine(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken) private async Task InitializeMlClusterizationEngine(BrandRecord brand, SeparatorRecord separator, CancellationToken cancellationToken)
{ {
@@ -126,35 +138,49 @@ public partial class AnalyzerService
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Подготовка данных"); _logger.LogInformation("Подготовка данных");
var sampleRecordIds = _context.SampleRecords var sampleRecords = _context.SampleRecords
.Where(r => r.BrandId == brand.Id) .Where(r => r.BrandId == brand.Id);
.Select(r => r.Id);
_logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecordIds.Count()); _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count());
if (sampleRecordIds.Count() == 0) if (sampleRecords.Count() == 0)
throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\"");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var separatorDataSet = _context.FeatureRecords if (separator.Features.Count() == 0)
.Where(r => r.SeparatorRecordId == separator.Id) throw new InvalidDataException($"Не найдено калибровочных признаков для сепаратора: \"{separator.Type}\"");
.Select(r => new ClusterizationData if (separator.Features.Count() % FEATURES_LENGTH != 0)
throw new InvalidDataException($"Неожидаемое количество калибровочных признаков для сепаратора: \"{separator.Type}\"");
var separatorDataSet = separator.Features.Chunk(FEATURES_LENGTH)
.Select(v => new ClusterizationData
{ {
Label = "separator", Label = "separator",
Features = r.Values.ToArray() Features = v.ToArray()
}); })
.Shuffle();
_logger.LogInformation("Загружено векторов для сепаратора: {}", separatorDataSet.Count()); _logger.LogInformation("Загружено векторов для сепаратора: {}", separatorDataSet.Count());
if (separatorDataSet.Count() == 0) if (separatorDataSet.Count() == 0)
throw new InvalidDataException($"Не найдено калибровочных данных для сепаратора: \"{separator.Type}\""); throw new InvalidDataException($"Не найдено калибровочных данных для сепаратора: \"{separator.Type}\"");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var sampleDataSet = _context.FeatureRecords foreach (var sampleRecord in sampleRecords)
.Where(r => r.SeparatorRecordId != 0) {
.Where(r => sampleRecordIds.Contains(r.SampleRecordId)) if (sampleRecord.Features.Count() == 0)
.Select(r => new ClusterizationData _logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName);
if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0)
_logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName);
}
var sampleDataSet = sampleRecords
.Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0)
.SelectMany(r => r.Features.Chunk(FEATURES_LENGTH))
.Select(f => new ClusterizationData
{ {
Label = "sample", Label = "sample",
Features = r.Values.ToArray() Features = f.ToArray()
}) })
.Shuffle(); .Shuffle();
_logger.LogInformation("Загружено векторов для пробы: {}", sampleDataSet.Count()); _logger.LogInformation("Загружено векторов для пробы: {}", sampleDataSet.Count());
if (separatorDataSet.Count() == 0) if (separatorDataSet.Count() == 0)
throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\"");
@@ -187,32 +213,40 @@ public partial class AnalyzerService
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Подготовка данных"); _logger.LogInformation("Подготовка данных");
var sampleRecordIds = _context.SampleRecords var sampleRecords = _context.SampleRecords
.Where(r => r.BrandId == brand.Id) .Where(r => r.BrandId == brand.Id);
.Select(r => r.Id);
_logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecordIds.Count()); _logger.LogInformation("Найдено калибровочных записей проб для марки: {}", sampleRecords.Count());
if (sampleRecordIds.Count() == 0) if (sampleRecords.Count() == 0)
throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\""); throw new InvalidDataException($"Не найдено калибровочных записей для марки: \"{brand.BrandName} - {brand.MixtureName}\"");
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
foreach (var sampleRecord in sampleRecords)
{
if (sampleRecord.Features.Count() == 0)
_logger.LogError("Не найдено калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName);
if (sampleRecord.Features.Count() % FEATURES_LENGTH != 0)
_logger.LogError("Неожидаемое количество калибровочных признаков для пробы: \"{}\"", sampleRecord.SampleName);
}
_logger.LogInformation("Подготовка данных"); _logger.LogInformation("Подготовка данных");
var trainDataSet = _context.FeatureRecords var trainDataSet = sampleRecords
.Where(r => r.SeparatorRecordId != 0) .Where(r => r.Features.Count() > 0 && r.Features.Count() % FEATURES_LENGTH == 0)
.Where(r => sampleRecordIds.Contains(r.SampleRecordId)) .Where(r => r.MeasuredContent >= 0 || r.MixtureActualRate >= 0)
.ToList() .SelectMany(r =>
.Select(r => r.Features
{ .Chunk(FEATURES_LENGTH)
var sample = _context.SampleRecords.First(s => s.Id == r.SampleRecordId); .Select(f =>
return new RegressionData new RegressionData
{ {
Value = (float)( Value = (float)(r.MeasuredContent >= 0 ? r.MeasuredContent : r.MixtureActualRate),
sample.MeasuredContent >= 0 ? sample.MeasuredContent : Features = f.ToArray()
sample.MixtureActualRate }
), )
Features = r.Values.ToArray() )
}; .Shuffle()
}) .ToList();
.Shuffle();
_logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count()); _logger.LogInformation("Загружено векторов для обучения: {}", trainDataSet.Count());
if (trainDataSet.Count() == 0) if (trainDataSet.Count() == 0)
throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\""); throw new InvalidDataException($"Не найдено калибровочных данных для марки: \"{brand.BrandName} - {brand.MixtureName}\"");
@@ -232,13 +266,15 @@ public partial class AnalyzerService
Vector<double> meanVector = matrix.ColumnSums() / matrix.RowCount; Vector<double> meanVector = matrix.ColumnSums() / matrix.RowCount;
Matrix<double> covarianceMatrix = MatrixCovariance(matrix, meanVector); Matrix<double> covarianceMatrix = MatrixCovariance(matrix, meanVector);
Matrix<double> invCovarianceMatrix = covarianceMatrix.Inverse(); Matrix<double> invCovarianceMatrix = covarianceMatrix.Inverse();
trainDataSet = trainDataSet.Where((data, index) => trainDataSet = trainDataSet
{ .Where((data, index) =>
var row = matrix.Row(index); {
double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix); var row = matrix.Row(index);
double distance = CalculateMahalanobis(row, meanVector, invCovarianceMatrix);
return distance <= FILTERING_THRESHOLD; return distance <= FILTERING_THRESHOLD;
}).ToList(); })
.ToList();
_logger.LogInformation("Векторов для обучения после фильтрации: {}", trainDataSet.Count()); _logger.LogInformation("Векторов для обучения после фильтрации: {}", trainDataSet.Count());
var trainData = _ml.Data.LoadFromEnumerable(trainDataSet); var trainData = _ml.Data.LoadFromEnumerable(trainDataSet);
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -11,6 +11,10 @@ public class BrandRecord
[Comment("Уникальный идентификатор")] [Comment("Уникальный идентификатор")]
[MaxLength(36)] [MaxLength(36)]
public string Uuid { get; set; } public string Uuid { get; set; }
[Comment("Дата и время создания")]
public DateTime CreateDateTime { get; set; }
[Comment("Дата и время редактирования")]
public DateTime EditDateTime { get; set; }
[Comment("Марка пробы")] [Comment("Марка пробы")]
[MaxLength(256)] [MaxLength(256)]
public string BrandName { get; set; } public string BrandName { get; set; }
@@ -19,25 +23,41 @@ public class BrandRecord
public string MixtureName { get; set; } public string MixtureName { get; set; }
[Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")] [Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")]
public double MixtureNormalRate { get; set; } public double MixtureNormalRate { get; set; }
[Comment("Дополнительная информация")]
[MaxLength(1024)]
public string Comment { get; set; }
public BrandRecord() public BrandRecord()
: this(0, null, "", "", 0) : this(0, null, null, null, "", "", 0, "")
{ } { }
public BrandRecord( public BrandRecord(
int id = 0, int id = 0,
string? uuid = null, string? uuid = null,
DateTime? createDateTime = null,
DateTime? editDateTime = null,
string brandName = "", string brandName = "",
string mixtureName = "", string mixtureName = "",
double mixtureNormalRate = 0) double mixtureNormalRate = 0,
string comment = ""
)
{ {
Id = id; Id = id;
if (uuid is null) if (uuid is null)
Uuid = Guid.NewGuid().ToString(); Uuid = Guid.NewGuid().ToString();
else else
Uuid = uuid; Uuid = uuid;
if (createDateTime is null)
CreateDateTime = DateTime.UtcNow;
else
CreateDateTime = createDateTime.Value;
if (editDateTime is null)
EditDateTime = DateTime.UtcNow;
else
EditDateTime = editDateTime.Value;
BrandName = brandName; BrandName = brandName;
MixtureName = mixtureName; MixtureName = mixtureName;
MixtureNormalRate = mixtureNormalRate; MixtureNormalRate = mixtureNormalRate;
Comment = comment;
} }
} }
@@ -2,16 +2,26 @@ using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database; namespace GSS2.Core.Analysis.AmineContent.Database;
// TODO: Добавить внешние ключи в таблицы и правило что при удалении ключа ключ должен сбрасываться на 0
public class Context : DbContext public class Context : DbContext
{ {
public DbSet<BrandRecord> BrandRecords => Set<BrandRecord>(); public DbSet<BrandRecord> BrandRecords => Set<BrandRecord>();
public DbSet<FeatureRecord> FeatureRecords => Set<FeatureRecord>();
public DbSet<ImageRecord> ImageRecords => Set<ImageRecord>(); public DbSet<ImageRecord> ImageRecords => Set<ImageRecord>();
// public DbSet<ResultRecord> ResultRecords => Set<ResultRecord>(); public DbSet<ResultRecord> ResultRecords => Set<ResultRecord>();
public DbSet<SeparatorRecord> SeparatorRecords => Set<SeparatorRecord>(); public DbSet<SeparatorRecord> SeparatorRecords => Set<SeparatorRecord>();
public DbSet<SampleRecord> SampleRecords => Set<SampleRecord>(); public DbSet<SampleRecord> SampleRecords => Set<SampleRecord>();
public Context(DbContextOptions<Context> options) public Context(DbContextOptions<Context> options)
: base(options) : base(options)
{ } { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SeparatorRecord>()
.Property(e => e.Features)
.HasConversion(new CompressedFloatListConverter());
modelBuilder.Entity<SampleRecord>()
.Property(e => e.Features)
.HasConversion(new CompressedFloatListConverter());
}
} }
@@ -1,37 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
namespace GSS2.Core.Analysis.AmineContent.Database;
public class FeatureRecord
{
[Comment("Идентификатор")]
public int Id { get; set; }
[Comment("Значения вектора")]
public List<float> Values { get; set; }
[Comment("Сепаратор")]
public int SeparatorRecordId { get; set; }
[Comment("Проба")]
public int SampleRecordId { get; set; }
public FeatureRecord()
: this(0, null, 0, 0)
{ }
public FeatureRecord(
int id = 0,
List<float>? values = null,
int separatorRecordId = 0,
int sampleRecordId = 0
)
{
Id = id;
if (values is null)
Values = new List<float>();
else
Values = values;
SeparatorRecordId = separatorRecordId;
SampleRecordId = sampleRecordId;
}
}
@@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace GSS2.Core.Analysis.AmineContent.Database.Migrations namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
{ {
[DbContext(typeof(Context))] [DbContext(typeof(Context))]
[Migration("20260615113305_Migration0")] [Migration("20260624080642_Migration0")]
partial class Migration0 partial class Migration0
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -33,6 +33,20 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Марка пробы"); .HasComment("Марка пробы");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("MixtureName") b.Property<string>("MixtureName")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
@@ -54,31 +68,6 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
b.ToTable("BrandRecords"); b.ToTable("BrandRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.FeatureRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<int>("SampleRecordId")
.HasColumnType("INTEGER")
.HasComment("Проба");
b.Property<int>("SeparatorRecordId")
.HasColumnType("INTEGER")
.HasComment("Сепаратор");
b.PrimitiveCollection<string>("Values")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения вектора");
b.HasKey("Id");
b.ToTable("FeatureRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -96,6 +85,125 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
b.ToTable("ImageRecords"); b.ToTable("ImageRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<string>("BrandName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Марка пробы");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<int>("ImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Изображение");
b.Property<int>("MaskImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Маска");
b.Property<string>("MixtureName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название кондиционирующей смеси");
b.Property<double>("MixtureNormalRate")
.HasColumnType("REAL")
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
b.Property<int>("ParticlesCount")
.HasColumnType("INTEGER")
.HasComment("Количество частиц");
b.Property<int>("ResultImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Изображение результата");
b.Property<string>("SampleComment")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе");
b.Property<string>("SampleName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название пробы");
b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Место отбора пробы");
b.Property<string>("SeparatorType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Тип сепаратора");
b.Property<string>("Uuid")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT")
.HasComment("Уникальный идентификатор");
b.PrimitiveCollection<string>("ValueCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения частиц (количество)");
b.PrimitiveCollection<string>("ValueData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения частиц (данные)");
b.PrimitiveCollection<string>("XCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, X координаты (количество)");
b.PrimitiveCollection<string>("XData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, X координаты (данные)");
b.PrimitiveCollection<string>("YCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, Y координаты (количество)");
b.PrimitiveCollection<string>("YData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, Y координаты (данные)");
b.HasKey("Id");
b.ToTable("ResultRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -107,7 +215,15 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasComment("Марка"); .HasComment("Марка");
b.PrimitiveCollection<string>("FeatureIds") b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.PrimitiveCollection<string>("Features")
.IsRequired() .IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Признаки"); .HasComment("Признаки");
@@ -126,7 +242,7 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
b.Property<string>("SampleComment") b.Property<string>("SampleComment")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(1024)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе"); .HasComment("Дополнительная информация о пробе");
@@ -164,7 +280,21 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasComment("Идентификатор"); .HasComment("Идентификатор");
b.PrimitiveCollection<string>("FeatureIds") b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.PrimitiveCollection<string>("Features")
.IsRequired() .IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Признаки"); .HasComment("Признаки");
@@ -18,30 +18,18 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор") Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
BrandName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"), BrandName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"),
MixtureName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"), MixtureName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"),
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)") MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"),
Comment = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация")
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_BrandRecords", x => x.Id); table.PrimaryKey("PK_BrandRecords", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "FeatureRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
.Annotation("Sqlite:Autoincrement", true),
Values = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения вектора"),
SeparatorRecordId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Сепаратор"),
SampleRecordId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Проба")
},
constraints: table =>
{
table.PrimaryKey("PK_FeatureRecords", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ImageRecords", name: "ImageRecords",
columns: table => new columns: table => new
@@ -55,6 +43,39 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
table.PrimaryKey("PK_ImageRecords", x => x.Id); table.PrimaryKey("PK_ImageRecords", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "ResultRecords",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
.Annotation("Sqlite:Autoincrement", true),
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
SampleName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"),
SampleComment = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"),
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы"),
SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"),
SeparatorType = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип сепаратора"),
BrandName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Марка пробы"),
MixtureName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название кондиционирующей смеси"),
MixtureNormalRate = table.Column<double>(type: "REAL", nullable: false, comment: "Норма расхода кондиционирующей смеси (кг/т | гр/кг)"),
ImageId = table.Column<int>(type: "INTEGER", maxLength: 256, nullable: false, comment: "Изображение"),
MaskImageId = table.Column<int>(type: "INTEGER", maxLength: 256, nullable: false, comment: "Маска"),
ResultImageId = table.Column<int>(type: "INTEGER", maxLength: 256, nullable: false, comment: "Изображение результата"),
ParticlesCount = table.Column<int>(type: "INTEGER", nullable: false, comment: "Количество частиц"),
ValueCount = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения частиц (количество)"),
ValueData = table.Column<string>(type: "TEXT", nullable: false, comment: "Значения частиц (данные)"),
XCount = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, X координаты (количество)"),
XData = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, X координаты (данные)"),
YCount = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, Y координаты (количество)"),
YData = table.Column<string>(type: "TEXT", nullable: false, comment: "Контура частиц, Y координаты (данные)")
},
constraints: table =>
{
table.PrimaryKey("PK_ResultRecords", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "SampleRecords", name: "SampleRecords",
columns: table => new columns: table => new
@@ -62,15 +83,17 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор") Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
SampleName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"), SampleName = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Название пробы"),
SampleComment = table.Column<string>(type: "TEXT", maxLength: 512, nullable: false, comment: "Дополнительная информация о пробе"), SampleComment = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация о пробе"),
SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы (utc)"), SamplingDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время отбора пробы (utc)"),
BrandId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Марка"), BrandId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Марка"),
SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"), SamplingPlace = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Место отбора пробы"),
MixtureActualRate = table.Column<double>(type: "REAL", nullable: false, comment: "Фактический расход кондиционирующей смеси (кг/т | гр/кг)"), MixtureActualRate = table.Column<double>(type: "REAL", nullable: false, comment: "Фактический расход кондиционирующей смеси (кг/т | гр/кг)"),
MeasuredContent = table.Column<double>(type: "REAL", nullable: false, comment: "Измеренное количество кондиционирующей смеси (кг/т | гр/кг)"), MeasuredContent = table.Column<double>(type: "REAL", nullable: false, comment: "Измеренное количество кондиционирующей смеси (кг/т | гр/кг)"),
ImageId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Изображение"), ImageId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Изображение"),
FeatureIds = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки") Features = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки")
}, },
constraints: table => constraints: table =>
{ {
@@ -84,9 +107,12 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор") Id = table.Column<int>(type: "INTEGER", nullable: false, comment: "Идентификатор")
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"), Uuid = table.Column<string>(type: "TEXT", maxLength: 36, nullable: false, comment: "Уникальный идентификатор"),
CreateDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время создания"),
EditDateTime = table.Column<DateTime>(type: "TEXT", nullable: false, comment: "Дата и время редактирования"),
Type = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип"), Type = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false, comment: "Тип"),
Comment = table.Column<string>(type: "TEXT", maxLength: 1024, nullable: false, comment: "Дополнительная информация"),
ImageId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Изображение"), ImageId = table.Column<int>(type: "INTEGER", nullable: false, comment: "Изображение"),
FeatureIds = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки") Features = table.Column<string>(type: "TEXT", nullable: false, comment: "Признаки")
}, },
constraints: table => constraints: table =>
{ {
@@ -101,10 +127,10 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
name: "BrandRecords"); name: "BrandRecords");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "FeatureRecords"); name: "ImageRecords");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ImageRecords"); name: "ResultRecords");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "SampleRecords"); name: "SampleRecords");
@@ -0,0 +1,325 @@
// <auto-generated />
using System;
using GSS2.Core.Analysis.AmineContent.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
{
[DbContext(typeof(Context))]
[Migration("20260624154152_Migration1")]
partial class Migration1
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.BrandRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<string>("BrandName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Марка пробы");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("MixtureName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название кондиционирующей смеси");
b.Property<double>("MixtureNormalRate")
.HasColumnType("REAL")
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
b.Property<string>("Uuid")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT")
.HasComment("Уникальный идентификатор");
b.HasKey("Id");
b.ToTable("BrandRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<string>("Base64")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения вектора");
b.HasKey("Id");
b.ToTable("ImageRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<string>("BrandName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Марка пробы");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<int>("ImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Изображение");
b.Property<int>("MaskImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Маска");
b.Property<string>("MixtureName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название кондиционирующей смеси");
b.Property<double>("MixtureNormalRate")
.HasColumnType("REAL")
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
b.Property<int>("ParticlesCount")
.HasColumnType("INTEGER")
.HasComment("Количество частиц");
b.Property<int>("ResultImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Изображение результата");
b.Property<string>("SampleComment")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе");
b.Property<string>("SampleName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название пробы");
b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Место отбора пробы");
b.Property<string>("SeparatorType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Тип сепаратора");
b.Property<string>("Uuid")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT")
.HasComment("Уникальный идентификатор");
b.PrimitiveCollection<string>("ValueCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения частиц (количество)");
b.PrimitiveCollection<string>("ValueData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения частиц (данные)");
b.PrimitiveCollection<string>("XCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, X координаты (количество)");
b.PrimitiveCollection<string>("XData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, X координаты (данные)");
b.PrimitiveCollection<string>("YCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, Y координаты (количество)");
b.PrimitiveCollection<string>("YData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, Y координаты (данные)");
b.HasKey("Id");
b.ToTable("ResultRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<int>("BrandId")
.HasColumnType("INTEGER")
.HasComment("Марка");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("Features")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Признаки");
b.Property<int>("ImageId")
.HasColumnType("INTEGER")
.HasComment("Изображение");
b.Property<double>("MeasuredContent")
.HasColumnType("REAL")
.HasComment("Измеренное количество кондиционирующей смеси (кг/т | гр/кг)");
b.Property<double>("MixtureActualRate")
.HasColumnType("REAL")
.HasComment("Фактический расход кондиционирующей смеси (кг/т | гр/кг)");
b.Property<string>("SampleComment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе");
b.Property<string>("SampleName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название пробы");
b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы (utc)");
b.Property<string>("SamplingPlace")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Место отбора пробы");
b.Property<string>("Uuid")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT")
.HasComment("Уникальный идентификатор");
b.HasKey("Id");
b.ToTable("SampleRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SeparatorRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("Features")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Признаки");
b.Property<int>("ImageId")
.HasColumnType("INTEGER")
.HasComment("Изображение");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Тип");
b.Property<string>("Uuid")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT")
.HasComment("Уникальный идентификатор");
b.HasKey("Id");
b.ToTable("SeparatorRecords");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
{
/// <inheritdoc />
public partial class Migration1 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -30,6 +30,20 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Марка пробы"); .HasComment("Марка пробы");
b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("MixtureName") b.Property<string>("MixtureName")
.IsRequired() .IsRequired()
.HasMaxLength(256) .HasMaxLength(256)
@@ -51,31 +65,6 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
b.ToTable("BrandRecords"); b.ToTable("BrandRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.FeatureRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<int>("SampleRecordId")
.HasColumnType("INTEGER")
.HasComment("Проба");
b.Property<int>("SeparatorRecordId")
.HasColumnType("INTEGER")
.HasComment("Сепаратор");
b.PrimitiveCollection<string>("Values")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения вектора");
b.HasKey("Id");
b.ToTable("FeatureRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ImageRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -93,6 +82,125 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
b.ToTable("ImageRecords"); b.ToTable("ImageRecords");
}); });
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.ResultRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasComment("Идентификатор");
b.Property<string>("BrandName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Марка пробы");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<int>("ImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Изображение");
b.Property<int>("MaskImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Маска");
b.Property<string>("MixtureName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название кондиционирующей смеси");
b.Property<double>("MixtureNormalRate")
.HasColumnType("REAL")
.HasComment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)");
b.Property<int>("ParticlesCount")
.HasColumnType("INTEGER")
.HasComment("Количество частиц");
b.Property<int>("ResultImageId")
.HasMaxLength(256)
.HasColumnType("INTEGER")
.HasComment("Изображение результата");
b.Property<string>("SampleComment")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе");
b.Property<string>("SampleName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Название пробы");
b.Property<DateTime>("SamplingDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время отбора пробы");
b.Property<string>("SamplingPlace")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Место отбора пробы");
b.Property<string>("SeparatorType")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasComment("Тип сепаратора");
b.Property<string>("Uuid")
.IsRequired()
.HasMaxLength(36)
.HasColumnType("TEXT")
.HasComment("Уникальный идентификатор");
b.PrimitiveCollection<string>("ValueCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения частиц (количество)");
b.PrimitiveCollection<string>("ValueData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Значения частиц (данные)");
b.PrimitiveCollection<string>("XCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, X координаты (количество)");
b.PrimitiveCollection<string>("XData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, X координаты (данные)");
b.PrimitiveCollection<string>("YCount")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, Y координаты (количество)");
b.PrimitiveCollection<string>("YData")
.IsRequired()
.HasColumnType("TEXT")
.HasComment("Контура частиц, Y координаты (данные)");
b.HasKey("Id");
b.ToTable("ResultRecords");
});
modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b => modelBuilder.Entity("GSS2.Core.Analysis.AmineContent.Database.SampleRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -104,7 +212,15 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasComment("Марка"); .HasComment("Марка");
b.PrimitiveCollection<string>("FeatureIds") b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("Features")
.IsRequired() .IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Признаки"); .HasComment("Признаки");
@@ -123,7 +239,7 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
b.Property<string>("SampleComment") b.Property<string>("SampleComment")
.IsRequired() .IsRequired()
.HasMaxLength(512) .HasMaxLength(1024)
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Дополнительная информация о пробе"); .HasComment("Дополнительная информация о пробе");
@@ -161,7 +277,21 @@ namespace GSS2.Core.Analysis.AmineContent.Database.Migrations
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasComment("Идентификатор"); .HasComment("Идентификатор");
b.PrimitiveCollection<string>("FeatureIds") b.Property<string>("Comment")
.IsRequired()
.HasMaxLength(1024)
.HasColumnType("TEXT")
.HasComment("Дополнительная информация");
b.Property<DateTime>("CreateDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время создания");
b.Property<DateTime>("EditDateTime")
.HasColumnType("TEXT")
.HasComment("Дата и время редактирования");
b.Property<string>("Features")
.IsRequired() .IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasComment("Признаки"); .HasComment("Признаки");
@@ -1,60 +1,149 @@
// using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
// using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
// namespace GSS2.Core.Analysis.AmineContent.Database; namespace GSS2.Core.Analysis.AmineContent.Database;
// public class ResultRecord public class ResultRecord
// { {
// [Comment("Идентификатор")] [Comment("Идентификатор")]
// public int Id { get; set; } public int Id { get; set; }
// [Comment("Уникальный идентификатор")] [Comment("Уникальный идентификатор")]
// [MaxLength(36)] [MaxLength(36)]
// public required string Uuid { get; set; } public string Uuid { get; set; }
// [Comment("Название пробы")] [Comment("Дата и время создания")]
// [MaxLength(256)] public DateTime CreateDateTime { get; set; }
// public required string SampleName { get; set; } [Comment("Дата и время редактирования")]
// [Comment("Дополнительная информация о пробе")] public DateTime EditDateTime { get; set; }
// [MaxLength(512)] [Comment("Название пробы")]
// public required string SampleComment { get; set; } [MaxLength(256)]
// [Comment("Дата и время отбора пробы")] public string SampleName { get; set; }
// public required DateTime SamplingDateTime { get; set; } [Comment("Дополнительная информация о пробе")]
// [Comment("Место отбора пробы")] [MaxLength(512)]
// [MaxLength(256)] public string SampleComment { get; set; }
// public required string SamplingPlace { get; set; } [Comment("Дата и время отбора пробы")]
// [Comment("Изображение")] public DateTime SamplingDateTime { get; set; }
// [MaxLength(256)] [Comment("Место отбора пробы")]
// public required string Image { get; set; } [MaxLength(256)]
// [Comment("Тип сепаратора")] public string SamplingPlace { get; set; }
// [MaxLength(256)] [Comment("Тип сепаратора")]
// public required string SeparatorType { get; set; } [MaxLength(256)]
// [Comment("Марка пробы")] public string SeparatorType { get; set; }
// [MaxLength(256)] [Comment("Марка пробы")]
// public required string SampleBrand { get; set; } [MaxLength(256)]
// [Comment("Маска")] public string BrandName { get; set; }
// [MaxLength(256)] [Comment("Название кондиционирующей смеси")]
// public required string MaskImage { get; set; } [MaxLength(256)]
// [Comment("Изображение результата")] public string MixtureName { get; set; }
// [MaxLength(256)] [Comment("Норма расхода кондиционирующей смеси (кг/т | гр/кг)")]
// public required string ResultImage { get; set; } public double MixtureNormalRate { get; set; }
// [Comment("Минимальное значение")] [Comment("Изображение")]
// public required double MinValue { get; set; } [MaxLength(256)]
// [Comment("Максимальное значение")] public int ImageId { get; set; }
// public required double MaxValue { get; set; } [Comment("Маска")]
// [Comment("Количество частиц")] [MaxLength(256)]
// public required int ParticlesCount { get; set; } public int MaskImageId { get; set; }
// [Comment("Среднее значение")] [Comment("Изображение результата")]
// public required double MeanValue { get; set; } [MaxLength(256)]
// [Comment("Средняя дисперсия")] public int ResultImageId { get; set; }
// public required double MeanVariance { get; set; } [Comment("Количество частиц")]
// [Comment("Дисперсия среднего")] public int ParticlesCount { get; set; }
// public required double VarianceOfMean { get; set; } [Comment("Значения частиц (количество)")]
// [Comment("Среднее качество обработки")] public List<int> ValueCount { get; set; }
// public required double? MeanQuality { get; set; } [Comment("Значения частиц (данные)")]
// [Comment("Средние значения")] public List<float> ValueData { get; set; }
// public required double[] MeanValues { get; set; } [Comment("Контура частиц, X координаты (количество)")]
// [Comment("Дисперсии")] public List<int> XCount { get; set; }
// public required double[] Variances { get; set; } [Comment("Контура частиц, X координаты (данные)")]
// [Comment("Качества обработки")] public List<float> XData { get; set; }
// public required double[]? Qualities { get; set; } [Comment("Контура частиц, Y координаты (количество)")]
// } public List<int> YCount { get; set; }
[Comment("Контура частиц, Y координаты (данные)")]
public List<float> YData { get; set; }
public ResultRecord()
: this(0, null, null, null, "", "", null, "", "", "", "", -1, 0, 0, 0, -1, null, null, null)
{ }
public ResultRecord(
int id = 0,
string? uuid = null,
DateTime? createDateTime = null,
DateTime? editDateTime = null,
string sampleName = "",
string sampleComment = "",
DateTime? samplingDateTime = null,
string samplingPlace = "",
string separatorType = "",
string brandName = "",
string mixtureName = "",
double mixtureNormalRate = -1,
int imageId = 0,
int maskImageId = 0,
int resultImageId = 0,
int particlesCount = -1,
List<float[]>? values = null,
List<float[]>? xs = null,
List<float[]>? ys = null
)
{
Id = id;
if (uuid is null)
Uuid = Guid.NewGuid().ToString();
else
Uuid = uuid;
if (createDateTime is null)
CreateDateTime = DateTime.UtcNow;
else
CreateDateTime = createDateTime.Value;
if (editDateTime is null)
EditDateTime = DateTime.UtcNow;
else
EditDateTime = editDateTime.Value;
SampleName = sampleName;
SampleComment = sampleComment;
if (samplingDateTime is null)
SamplingDateTime = DateTime.UtcNow;
else
SamplingDateTime = samplingDateTime.Value;
SamplingPlace = samplingPlace;
SeparatorType = separatorType;
BrandName = brandName;
MixtureName = mixtureName;
MixtureNormalRate = mixtureNormalRate;
ImageId = imageId;
MaskImageId = maskImageId;
ResultImageId = resultImageId;
ParticlesCount = particlesCount;
if (values is null)
{
ValueCount = new List<int>();
ValueData = new List<float>();
}
else
{
ValueCount = values.Select(fs => fs.Count()).ToList();
ValueData = values.SelectMany(fs => fs).ToList();
}
if (xs is null)
{
XCount = new List<int>();
XData = new List<float>();
}
else
{
XCount = xs.Select(fs => fs.Count()).ToList();
XData = xs.SelectMany(fs => fs).ToList();
}
if (ys is null)
{
YCount = new List<int>();
YData = new List<float>();
}
else
{
YCount = ys.Select(fs => fs.Count()).ToList();
YData = ys.SelectMany(fs => fs).ToList();
}
}
}
@@ -11,11 +11,15 @@ public class SampleRecord
[Comment("Уникальный идентификатор")] [Comment("Уникальный идентификатор")]
[MaxLength(36)] [MaxLength(36)]
public string Uuid { get; set; } public string Uuid { get; set; }
[Comment("Дата и время создания")]
public DateTime CreateDateTime { get; set; }
[Comment("Дата и время редактирования")]
public DateTime EditDateTime { get; set; }
[Comment("Название пробы")] [Comment("Название пробы")]
[MaxLength(256)] [MaxLength(256)]
public string SampleName { get; set; } public string SampleName { get; set; }
[Comment("Дополнительная информация о пробе")] [Comment("Дополнительная информация о пробе")]
[MaxLength(512)] [MaxLength(1024)]
public string SampleComment { get; set; } public string SampleComment { get; set; }
[Comment("Дата и время отбора пробы (utc)")] [Comment("Дата и время отбора пробы (utc)")]
public DateTime SamplingDateTime { get; set; } public DateTime SamplingDateTime { get; set; }
@@ -31,14 +35,16 @@ public class SampleRecord
[Comment("Изображение")] [Comment("Изображение")]
public int ImageId { get; set; } public int ImageId { get; set; }
[Comment("Признаки")] [Comment("Признаки")]
public List<int> FeatureIds { get; set; } public List<float> Features { get; set; }
public SampleRecord() public SampleRecord()
: this(0, null, "", "", null, 0, "", -1, -1, 0, null) : this(0, null, null, null, "", "", null, 0, "", -1, -1, 0, null)
{ } { }
public SampleRecord( public SampleRecord(
int id = 0, int id = 0,
string? uuid = null, string? uuid = null,
DateTime? createDateTime = null,
DateTime? editDateTime = null,
string sampleName = "", string sampleName = "",
string sampleComment = "", string sampleComment = "",
DateTime? samplingDateTime = null, DateTime? samplingDateTime = null,
@@ -47,7 +53,7 @@ public class SampleRecord
double mixtureActualRate = -1, double mixtureActualRate = -1,
double measuredContent = -1, double measuredContent = -1,
int imageId = 0, int imageId = 0,
List<int>? featureIds = null List<float[]>? features = null
) )
{ {
Id = id; Id = id;
@@ -55,6 +61,14 @@ public class SampleRecord
Uuid = Guid.NewGuid().ToString(); Uuid = Guid.NewGuid().ToString();
else else
Uuid = uuid; Uuid = uuid;
if (createDateTime is null)
CreateDateTime = DateTime.UtcNow;
else
CreateDateTime = createDateTime.Value;
if (editDateTime is null)
EditDateTime = DateTime.UtcNow;
else
EditDateTime = editDateTime.Value;
SampleName = sampleName; SampleName = sampleName;
SampleComment = sampleComment; SampleComment = sampleComment;
if (samplingDateTime is null) if (samplingDateTime is null)
@@ -66,9 +80,9 @@ public class SampleRecord
MixtureActualRate = mixtureActualRate; MixtureActualRate = mixtureActualRate;
MeasuredContent = measuredContent; MeasuredContent = measuredContent;
ImageId = imageId; ImageId = imageId;
if (featureIds is null) if (features is null)
FeatureIds = new List<int>(); Features = new List<float>();
else else
FeatureIds = featureIds; Features = features.SelectMany(fs => fs).ToList();
} }
} }
@@ -11,24 +11,34 @@ public class SeparatorRecord
[Comment("Уникальный идентификатор")] [Comment("Уникальный идентификатор")]
[MaxLength(36)] [MaxLength(36)]
public string Uuid { get; set; } public string Uuid { get; set; }
[Comment("Дата и время создания")]
public DateTime CreateDateTime { get; set; }
[Comment("Дата и время редактирования")]
public DateTime EditDateTime { get; set; }
[Comment("Тип")] [Comment("Тип")]
[MaxLength(256)] [MaxLength(256)]
public string Type { get; set; } public string Type { get; set; }
[Comment("Дополнительная информация")]
[MaxLength(1024)]
public string Comment { get; set; }
[Comment("Изображение")] [Comment("Изображение")]
public int ImageId { get; set; } public int ImageId { get; set; }
[Comment("Признаки")] [Comment("Признаки")]
public List<int> FeatureIds { get; set; } public List<float> Features { get; set; }
public SeparatorRecord() public SeparatorRecord()
: this(0, "", "", 0, null) : this(0, null, null, null, "", "", 0, null)
{ } { }
public SeparatorRecord( public SeparatorRecord(
int id = 0, int id = 0,
string? uuid = null, string? uuid = null,
DateTime? createDateTime = null,
DateTime? editDateTime = null,
string type = "", string type = "",
string comment = "",
int imageId = 0, int imageId = 0,
List<int>? featureIds = null List<float[]>? features = null
) )
{ {
Id = id; Id = id;
@@ -36,11 +46,20 @@ public class SeparatorRecord
Uuid = Guid.NewGuid().ToString(); Uuid = Guid.NewGuid().ToString();
else else
Uuid = uuid; Uuid = uuid;
Type = type; if (createDateTime is null)
ImageId = imageId; CreateDateTime = DateTime.UtcNow;
if (featureIds is null)
FeatureIds = new List<int>();
else else
FeatureIds = featureIds; CreateDateTime = createDateTime.Value;
if (editDateTime is null)
EditDateTime = DateTime.UtcNow;
else
EditDateTime = editDateTime.Value;
Type = type;
Comment = comment;
ImageId = imageId;
if (features is null)
Features = new List<float>();
else
Features = features.SelectMany(fs => fs).ToList();
} }
} }
@@ -59,8 +59,6 @@ public class ImageCapturerService : IImageCapturerService
// Если ViewFinder запущен, то для съёмки изображений его нужно остановить // Если ViewFinder запущен, то для съёмки изображений его нужно остановить
// потом после съёмки снова запустить // потом после съёмки снова запустить
var needRestartViewFinder = _camera.ViewFinderStarted; var needRestartViewFinder = _camera.ViewFinderStarted;
if (needRestartViewFinder)
_camera.StopViewFinder();
try try
{ {
@@ -117,9 +115,6 @@ public class ImageCapturerService : IImageCapturerService
{ {
// В любом случае отключаем осветитель // В любом случае отключаем осветитель
_illuminator.TurnOff(); _illuminator.TurnOff();
// Запускаем обратно ViewFinder, если он был запущен и ещё не запущен обратно
if (needRestartViewFinder && !_camera.ViewFinderStarted)
_camera.StartViewFinder();
} }
} }
} }
+18 -30
View File
@@ -28,22 +28,22 @@ public static class DependencyInjectionHelper
logging.AddProvider(new FileLoggerProvider("last_run.log", false)); logging.AddProvider(new FileLoggerProvider("last_run.log", false));
return logging; return logging;
} }
public static IServiceCollection AddTemperatureHumidityService(this IServiceCollection services, string section) => services // public static IServiceCollection AddTemperatureHumidityService(this IServiceCollection services, string section) => services
.AddSingleton( // .AddSingleton(
serviceProvider => // serviceProvider =>
{ // {
var configuration = serviceProvider.GetService<IConfiguration>(); // var configuration = serviceProvider.GetService<IConfiguration>();
if (configuration is null) // if (configuration is null)
throw new Exception("Cannot get configuration"); // throw new Exception("Cannot get configuration");
var serviceConfiguration = configuration.GetSection(section).Get<TemperatureHumidityService.TemperatureHumidityServiceConfiguration>(); // var serviceConfiguration = configuration.GetSection(section).Get<TemperatureHumidityService.TemperatureHumidityServiceConfiguration>();
if (serviceConfiguration is null) // if (serviceConfiguration is null)
throw new InvalidConfigurationException($"Cannot get configuration {section}"); // throw new InvalidConfigurationException($"Cannot get configuration {section}");
var logger = serviceProvider.GetService<ILogger<TemperatureHumidityService>>(); // var logger = serviceProvider.GetService<ILogger<TemperatureHumidityService>>();
if (logger is null) // if (logger is null)
throw new Exception("Cannot get logger"); // throw new Exception("Cannot get logger");
return new TemperatureHumidityService(logger, serviceConfiguration); // return new TemperatureHumidityService(logger, serviceConfiguration);
} // }
); // );
public static IServiceCollection AddLightsService(this IServiceCollection services, string section) => services public static IServiceCollection AddLightsService(this IServiceCollection services, string section) => services
.AddSingleton( .AddSingleton(
serviceProvider => serviceProvider =>
@@ -92,21 +92,9 @@ public static class DependencyInjectionHelper
return new CameraService(logger, serviceConfiguration, autoInitialize); return new CameraService(logger, serviceConfiguration, autoInitialize);
} }
); );
public static IServiceCollection AddComputeResourcesService(this IServiceCollection services) => services
.AddSingleton<ComputeResourcesService>();
[Obsolete] [Obsolete]
public static IServiceCollection AddUsbService(this IServiceCollection services) => services public static IServiceCollection AddUsbService(this IServiceCollection services) => services
.AddSingleton<UsbService>(); .AddSingleton<UsbService>();
public static IServiceCollection AddImageStorageService(this IServiceCollection services, DirectoryInfo rootDirectory) => services
.AddSingleton<ImageStorageService>(
serviceProvider =>
{
var logger = serviceProvider.GetService<ILogger<ImageStorageService>>();
if (logger is null)
throw new Exception("Cannot get logger");
return new ImageStorageService(logger, rootDirectory);
}
);
public static IServiceCollection AddAmineContentContext(this IServiceCollection services, IConfigurationManager configuration) => services public static IServiceCollection AddAmineContentContext(this IServiceCollection services, IConfigurationManager configuration) => services
.AddDbContext<Context>( .AddDbContext<Context>(
@@ -121,9 +109,9 @@ public static class DependencyInjectionHelper
public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services, bool mock = false) public static IServiceCollection AddAmineContentImageCapturerService(this IServiceCollection services, bool mock = false)
{ {
if (!mock) if (!mock)
return services.AddSingleton<ImageCapturerService>(); return services.AddSingleton<IImageCapturerService, ImageCapturerService>();
return services.AddSingleton<ImageCapturerMockService>(serviceProvider => return services.AddSingleton<IImageCapturerService, ImageCapturerMockService>(serviceProvider =>
{ {
var logger = serviceProvider.GetService<ILogger<ImageCapturerMockService>>(); var logger = serviceProvider.GetService<ILogger<ImageCapturerMockService>>();
if (logger is null) if (logger is null)
@@ -13,4 +13,3 @@ public static class ConfigurationSectionExtensions
return dict; return dict;
} }
} }
@@ -46,4 +46,32 @@ public static class EnumerableExtensions
variance += Math.Pow(value - mean, 2.0); variance += Math.Pow(value - mean, 2.0);
return variance / values.Count(); return variance / values.Count();
} }
public static double StdDev(this IEnumerable<double> values) => Math.Sqrt(Variance(values));
public static float Variance(this IEnumerable<float> values)
{
if (values.Count() == 1)
return 0;
float mean = values.Average();
float variance = 0.0f;
foreach (int value in values)
variance += MathF.Pow(value - mean, 2.0f);
return variance / values.Count();
}
public static float StdDev(this IEnumerable<float> values) => MathF.Sqrt(Variance(values));
public static IEnumerable<IEnumerable<TValue>> Chunk<TValue>(this IEnumerable<TValue> values, IEnumerable<int> chunks)
{
if (chunks.Count() == 0)
throw new InvalidDataException();
if (values.Count() != chunks.Sum())
throw new InvalidDataException();
var start = 0;
foreach (var chunk in chunks)
{
yield return values.Skip(start).Take(chunk);
start += chunk;
}
}
} }
+3 -243
View File
@@ -41,7 +41,6 @@ public class CameraService : IDisposable
CameraManagerStarted = 1 << 1, CameraManagerStarted = 1 << 1,
CameraAcquired = 1 << 2, CameraAcquired = 1 << 2,
CameraStarted = 1 << 3, CameraStarted = 1 << 3,
ViewFinderFrameBufferAllocated = 1 << 4,
ImageCaptureFrameBufferAllocated = 1 << 5, ImageCaptureFrameBufferAllocated = 1 << 5,
ImageCaptureFrameBuffersMapped = 1 << 6 ImageCaptureFrameBuffersMapped = 1 << 6
} }
@@ -51,10 +50,6 @@ public class CameraService : IDisposable
public static CameraServiceConfiguration Default => new CameraServiceConfiguration public static CameraServiceConfiguration Default => new CameraServiceConfiguration
{ {
CameraId = "", CameraId = "",
ViewFinderFrameBufferCount = 5,
ViewFinderWidth = 4056,
ViewFinderHeight = 3080,
ViewFinderFps = 30,
ImageCaptureFrameBufferCount = 2, ImageCaptureFrameBufferCount = 2,
ImageCaptureWidth = 4056, ImageCaptureWidth = 4056,
ImageCaptureHeight = 3080, ImageCaptureHeight = 3080,
@@ -66,10 +61,6 @@ public class CameraService : IDisposable
}; };
public string CameraId { get; init; } = ""; public string CameraId { get; init; } = "";
public int ViewFinderFrameBufferCount { get; init; }
public int ViewFinderWidth { get; init; }
public int ViewFinderHeight { get; init; }
public int ViewFinderFps { get; init; }
public int ImageCaptureFrameBufferCount { get; init; } public int ImageCaptureFrameBufferCount { get; init; }
public int ImageCaptureWidth { get; init; } public int ImageCaptureWidth { get; init; }
public int ImageCaptureHeight { get; init; } public int ImageCaptureHeight { get; init; }
@@ -87,16 +78,11 @@ public class CameraService : IDisposable
private CameraManager? _cameraManager; private CameraManager? _cameraManager;
private Camera? _camera; private Camera? _camera;
private CameraConfiguration? _cameraConfiguration; private CameraConfiguration? _cameraConfiguration;
private StreamConfiguration? _viewFinderStreamConfiguration;
private StreamConfiguration? _imageCaptureStreamConfiguration; private StreamConfiguration? _imageCaptureStreamConfiguration;
private FrameBufferAllocator? _viewFinderFrameBufferAllocator;
private FrameBufferAllocator? _imageCaptureFrameBufferAllocator; private FrameBufferAllocator? _imageCaptureFrameBufferAllocator;
private Stream? _viewFinderStream;
private Stream? _imageCaptureStream; private Stream? _imageCaptureStream;
private List<MappedBuffer>? _imageCaptureMappedBuffers; private List<MappedBuffer>? _imageCaptureMappedBuffers;
private System.Timers.Timer? _viewFinderTimer = null;
private System.Timers.ElapsedEventHandler? _viewFinderTimerElapsed = null; private System.Timers.ElapsedEventHandler? _viewFinderTimerElapsed = null;
private EventHandler<Camera.RequestCompletedEventArgs>? _viewFinderRequestCompletedHandler = null;
private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource(); private readonly CancellationTokenSource _disposeCts = new CancellationTokenSource();
private readonly Dictionary<string, object> _cameraHardSettings = new Dictionary<string, object>(); private readonly Dictionary<string, object> _cameraHardSettings = new Dictionary<string, object>();
@@ -176,10 +162,8 @@ public class CameraService : IDisposable
CameraSelect(effectiveCancellationToken); CameraSelect(effectiveCancellationToken);
CameraAcquire(effectiveCancellationToken); CameraAcquire(effectiveCancellationToken);
CameraConfigure(effectiveCancellationToken); CameraConfigure(effectiveCancellationToken);
ViewFinderFrameBuffersAllocate(effectiveCancellationToken);
ImageCaptureFrameBuffersAllocate(effectiveCancellationToken); ImageCaptureFrameBuffersAllocate(effectiveCancellationToken);
CameraStart(effectiveCancellationToken); CameraStart(effectiveCancellationToken);
ViewFinderFrameBuffersInitialize(effectiveCancellationToken);
ImageCaptureFrameBuffersInitialize(effectiveCancellationToken); ImageCaptureFrameBuffersInitialize(effectiveCancellationToken);
ImageCaptureFrameBuffersMap(effectiveCancellationToken); ImageCaptureFrameBuffersMap(effectiveCancellationToken);
} }
@@ -251,16 +235,8 @@ public class CameraService : IDisposable
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_cameraConfiguration = _camera.GenerateConfiguration( _cameraConfiguration = _camera.GenerateConfiguration(StreamRoleEnum.Raw);
StreamRoleEnum.Viewfinder,
StreamRoleEnum.Raw);
_viewFinderStreamConfiguration = _cameraConfiguration.First();
_viewFinderStreamConfiguration.BufferCount = checked((uint)_configuration.ViewFinderFrameBufferCount);
_viewFinderStreamConfiguration.Size = new Size(
checked((uint)_configuration.ViewFinderWidth),
checked((uint)_configuration.ViewFinderHeight)
);
_imageCaptureStreamConfiguration = _cameraConfiguration.Skip(1).First(); _imageCaptureStreamConfiguration = _cameraConfiguration.Skip(1).First();
_imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount); _imageCaptureStreamConfiguration.BufferCount = checked((uint)_configuration.ImageCaptureFrameBufferCount);
@@ -275,13 +251,6 @@ public class CameraService : IDisposable
throw new Exception("Error while camera configuration validation"); throw new Exception("Error while camera configuration validation");
} }
_logger.LogDebug(@$"ViewFinder stream configuration:
Size: {_viewFinderStreamConfiguration.Size.ToString(true)}
FrameSize: {_viewFinderStreamConfiguration.FrameSize}
Stride: {_viewFinderStreamConfiguration.Stride}
ColorSpace: {_viewFinderStreamConfiguration.ColorSpace?.ToString(true)}
PixelFormat: {_viewFinderStreamConfiguration.PixelFormat?.ToString(true)}");
_logger.LogDebug(@$"ImageCapture stream configuration: _logger.LogDebug(@$"ImageCapture stream configuration:
Size: {_imageCaptureStreamConfiguration.Size.ToString(true)} Size: {_imageCaptureStreamConfiguration.Size.ToString(true)}
FrameSize: {_imageCaptureStreamConfiguration.FrameSize} FrameSize: {_imageCaptureStreamConfiguration.FrameSize}
@@ -298,7 +267,6 @@ public class CameraService : IDisposable
throw new Exception($"Error while camera configuration {result}"); throw new Exception($"Error while camera configuration {result}");
} }
_viewFinderStream = _viewFinderStreamConfiguration.Stream();
_imageCaptureStream = _imageCaptureStreamConfiguration.Stream(); _imageCaptureStream = _imageCaptureStreamConfiguration.Stream();
} }
private void CameraStart(CancellationToken cancellationToken) private void CameraStart(CancellationToken cancellationToken)
@@ -320,28 +288,6 @@ public class CameraService : IDisposable
_state |= State.CameraStarted; _state |= State.CameraStarted;
} }
private void ViewFinderFrameBuffersAllocate(CancellationToken cancellationToken)
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_viewFinderStream is null)
throw new InvalidOperationException("ViewFinderStream not initialized");
cancellationToken.ThrowIfCancellationRequested();
_viewFinderFrameBufferAllocator = new FrameBufferAllocator(_camera);
cancellationToken.ThrowIfCancellationRequested();
int result = _viewFinderFrameBufferAllocator.Allocate(_viewFinderStream);
if (result < 0)
{
_logger.LogCritical("Error while view finder frame buffers allocation {Result}", result);
throw new Exception($"Error while view finder frame buffers allocation {result}");
}
_state |= State.ViewFinderFrameBufferAllocated;
}
private void ImageCaptureFrameBuffersAllocate(CancellationToken cancellationToken) private void ImageCaptureFrameBuffersAllocate(CancellationToken cancellationToken)
{ {
if (_camera is null) if (_camera is null)
@@ -364,55 +310,6 @@ public class CameraService : IDisposable
_state |= State.ImageCaptureFrameBufferAllocated; _state |= State.ImageCaptureFrameBufferAllocated;
} }
private void ViewFinderFrameBuffersInitialize(CancellationToken cancellationToken)
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_viewFinderStream is null)
throw new InvalidOperationException("ViewFinderStream not initialized");
if (_viewFinderFrameBufferAllocator is null)
throw new InvalidOperationException("ViewFinderFrameBufferAllocator not initialized");
cancellationToken.ThrowIfCancellationRequested();
var buffers = _viewFinderFrameBufferAllocator.Buffers(_viewFinderStream);
_logger.LogDebug("View finder frame buffers initialization");
foreach (var buffer in buffers)
{
cancellationToken.ThrowIfCancellationRequested();
var request = _camera.CreateRequest(0);
var result = request.AddBuffer(_viewFinderStream, buffer);
if (result < 0)
throw new Exception("Error while request create");
var cts = new TaskCompletionSource();
void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e)
{
if (e.Request.Handle == request.Handle)
cts.SetResult();
}
_camera.RequestCompleted += RequestCompleted;
result = _camera.QueueRequest(request);
if (result < 0)
{
_camera.RequestCompleted -= RequestCompleted;
throw new Exception("Error while request queue");
}
cts.Task.WaitAsync(cancellationToken).GetAwaiter().GetResult();
_camera.RequestCompleted -= RequestCompleted;
_logger.LogDebug("Request completed: \n\tResult: {} \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers);
_logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
_logger.LogDebug("Request buffer planes: ");
foreach (var (i, plane) in buffer.Metadata.Planes.Enumerate())
_logger.LogDebug("Plane[{}]: bytes used: 0x{:X}", i, plane.BytesUsed);
foreach (var (i, plane) in buffer.Planes.Enumerate())
_logger.LogDebug("Plane[{}]: \n\tFd: 0x{:X} \n\tOffset: 0x{:X} \n\tLength: 0x{:X} \n\tKInvalidOffset: 0x{:X}", i, plane.Fd.Get(), plane.Offset, plane.Length, plane.KInvalidOffset);
}
}
private void ImageCaptureFrameBuffersInitialize(CancellationToken cancellationToken) private void ImageCaptureFrameBuffersInitialize(CancellationToken cancellationToken)
{ {
if (_camera is null) if (_camera is null)
@@ -826,135 +723,6 @@ public class CameraService : IDisposable
return image; return image;
} }
public void StartViewFinder()
{
if (_camera is null)
throw new InvalidOperationException("Camera not initialized");
if (_viewFinderStream is null)
throw new InvalidOperationException("ViewFinderStream not initialized");
if (_viewFinderStreamConfiguration is null)
throw new InvalidOperationException("ViewFinderStreamConfiguration not initialized");
if (_viewFinderFrameBufferAllocator is null)
throw new InvalidOperationException("ViewFinderFrameBufferAllocator not initialized");
if (_viewFinderTimer is not null ||
_viewFinderTimerElapsed is not null ||
_viewFinderRequestCompletedHandler is not null)
throw new InvalidOperationException();
ViewFinderStarted = true;
_viewFinderTimer = new System.Timers.Timer()
{
Interval = 1000 / _configuration.ViewFinderFps,
AutoReset = true,
};
try
{
var buffers = _viewFinderFrameBufferAllocator.Buffers(_viewFinderStream);
lock (ViewFinderBufferQueue)
foreach (var buffer in buffers)
ViewFinderBufferQueue.Add(buffer);
void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs ea)
{
if (ea.Request.Cookie != 2)
return;
var buffer = ea.Request.FindBuffer(_viewFinderStream);
// _logger.LogInformation("Completed request for frame buffer {}", buffer);
lock (ViewFinderBufferQueue)
ViewFinderBufferQueue.Add(buffer);
}
_viewFinderRequestCompletedHandler = RequestCompleted;
_camera.RequestCompleted += _viewFinderRequestCompletedHandler;
void TimerElapsed(object? sender, System.Timers.ElapsedEventArgs ea)
{
if (IsImageCapturing)
return;
try
{
FrameBuffer buffer;
lock (ViewFinderBufferQueue)
{
if (ViewFinderBufferQueue.Count() > 0 && (
ViewFinderBufferQueue.First().Request.Cookie != 2 ||
ViewFinderBufferQueue.First().Request.Status == Request.StatusEnum.RequestComplete ||
ViewFinderBufferQueue.First().Request.Status == Request.StatusEnum.RequestCancelled))
{
buffer = ViewFinderBufferQueue.First();
ViewFinderBufferQueue.Remove(buffer);
}
else
return;
}
var request = _camera.CreateRequest(2);
request.AddBuffer(_viewFinderStream, buffer);
_camera.QueueRequest(request);
// _logger.LogInformation("Queued request for frame buffer {}", buffer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while ViewFinder tick");
}
}
_viewFinderTimerElapsed = TimerElapsed;
_viewFinderTimer.Elapsed += _viewFinderTimerElapsed;
_viewFinderTimer.Start();
_logger.LogInformation("ViewFinder started");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while ViewFinder start");
_viewFinderTimer.Stop();
_viewFinderTimer.Dispose();
_viewFinderTimer = null;
_viewFinderTimerElapsed = null;
_camera.RequestCompleted -= _viewFinderRequestCompletedHandler;
_viewFinderRequestCompletedHandler = null;
lock (ViewFinderBufferQueue)
ViewFinderBufferQueue.Clear();
}
}
public void StopViewFinder()
{
ViewFinderStarted = false;
_viewFinderTimer?.Stop();
_viewFinderTimer?.Dispose();
_viewFinderTimer = null;
_viewFinderTimerElapsed = null;
_camera?.RequestCompleted -= _viewFinderRequestCompletedHandler;
_viewFinderRequestCompletedHandler = null;
lock (ViewFinderBufferQueue)
ViewFinderBufferQueue.Clear();
}
public (FrameBuffer, StreamConfiguration)? GrabViewFinderFrame()
{
if (IsImageCapturing)
return null;
FrameBuffer? buffer = null;
lock (ViewFinderBufferQueue)
if (ViewFinderBufferQueue.Count() > 0)
{
buffer = ViewFinderBufferQueue.LastOrDefault();
if (buffer is not null)
ViewFinderBufferQueue.Remove(buffer);
}
if (buffer is not null && _viewFinderStreamConfiguration is not null)
return (buffer, _viewFinderStreamConfiguration);
return null;
}
public void ReturnViewFinderFrame(FrameBuffer frameBuffer)
{
lock (ViewFinderBufferQueue)
ViewFinderBufferQueue.Insert(0, frameBuffer);
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
if (!_disposedValue) if (!_disposedValue)
@@ -986,8 +754,6 @@ public class CameraService : IDisposable
} }
_imageCaptureMappedBuffers.Clear(); _imageCaptureMappedBuffers.Clear();
} }
if (_state.HasFlag(State.ViewFinderFrameBufferAllocated) && _viewFinderStream is not null)
_viewFinderFrameBufferAllocator?.Free(_viewFinderStream);
if (_state.HasFlag(State.ImageCaptureFrameBufferAllocated) && _imageCaptureStream is not null) if (_state.HasFlag(State.ImageCaptureFrameBufferAllocated) && _imageCaptureStream is not null)
_imageCaptureFrameBufferAllocator?.Free(_imageCaptureStream); _imageCaptureFrameBufferAllocator?.Free(_imageCaptureStream);
if (_state.HasFlag(State.CameraStarted)) if (_state.HasFlag(State.CameraStarted))
@@ -997,23 +763,17 @@ public class CameraService : IDisposable
if (_state.HasFlag(State.CameraManagerStarted)) if (_state.HasFlag(State.CameraManagerStarted))
_cameraManager?.Stop(); _cameraManager?.Stop();
// _viewFinderStream?.Dispose();
// _imageCaptureStream?.Dispose();
// _viewFinderFrameBufferAllocator?.Dispose();
// _imageCaptureFrameBufferAllocator?.Dispose();
_cameraManager?.Dispose(); _cameraManager?.Dispose();
} }
catch (Exception e) catch (Exception ex)
{ {
_logger.LogError(e, "Error while CameraService cleanup"); _logger.LogError(ex, "Error while CameraService cleanup");
} }
finally finally
{ {
_cameraManager = null; _cameraManager = null;
_camera = null; _camera = null;
_viewFinderFrameBufferAllocator = null;
_imageCaptureFrameBufferAllocator = null; _imageCaptureFrameBufferAllocator = null;
_viewFinderStream = null;
_imageCaptureStream = null; _imageCaptureStream = null;
_imageCaptureMappedBuffers = null; _imageCaptureMappedBuffers = null;
_state = State.NotInitialized; _state = State.NotInitialized;
@@ -1,143 +0,0 @@
using System.Globalization;
using GSS2.Core.Extensions;
using Microsoft.Extensions.Logging;
namespace GSS2.Core.Hardware;
public class ComputeResourcesService
{
public record CpuUsage(double AllUsagePercent, List<double> CoreUsagePercents);
public record CpuTemperature(double Current, double Critical);
public record MemoryUsage(
long TotalB, long UsedB, long AvailableB,
double TotalkB, double UsedkB, double AvailablekB,
double TotalMB, double UsedMB, double AvailableMB,
double TotalGB, double UsedGB, double AvailableGB)
{
public MemoryUsage(long totalB, long usedB, long availableB) :
this(
TotalB: totalB, UsedB: usedB, AvailableB: availableB,
TotalkB: totalB / 1000, UsedkB: usedB / 1000, AvailablekB: availableB / 1000,
TotalMB: totalB / Math.Pow(1000.0, 2), UsedMB: usedB / Math.Pow(1000.0, 2), AvailableMB: availableB / Math.Pow(1000.0, 2),
TotalGB: totalB / Math.Pow(1000.0, 3), UsedGB: usedB / Math.Pow(1000.0, 3), AvailableGB: availableB / Math.Pow(1000.0, 3)
)
{ }
}
public record ComputeResourcesSnapshot(CpuUsage CpuUsage, MemoryUsage MemoryUsage, CpuTemperature CpuTemperature, DateTime Timestamp);
private readonly ILogger<ComputeResourcesService> _logger;
private (long idle, long total)? _prevAll = null;
private Dictionary<int, (long idle, long total)?>? _prevCores = null;
public ComputeResourcesService(ILogger<ComputeResourcesService> logger)
{
_logger = logger;
_logger.LogInformation("Initialization");
_logger.LogInformation("Initialized");
}
public ComputeResourcesSnapshot GetSnapshot()
{
return new ComputeResourcesSnapshot(
CpuUsage: GetCpuUsage(),
CpuTemperature: GetCpuTemperature(),
MemoryUsage: GetMemoryUsage(),
Timestamp: DateTime.UtcNow
);
}
private CpuUsage GetCpuUsage()
{
static (double usage, long idle, long total) ParseLines(string line, (long idle, long total)? prev)
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
long user = long.Parse(parts[1]);
long nice = long.Parse(parts[2]);
long system = long.Parse(parts[3]);
long idle = long.Parse(parts[4]);
long iowait = line.Length > 5 ? long.Parse(parts[5]) : 0;
long irq = line.Length > 6 ? long.Parse(parts[6]) : 0;
long softirq = line.Length > 7 ? long.Parse(parts[7]) : 0;
long idleTime = idle + iowait;
long totalTime = idleTime + user + nice + system + irq + softirq;
double usage = 0;
if (prev is not null)
{
long totalDiff = totalTime - prev.Value.total;
long idleDiff = idleTime - prev.Value.idle;
if (totalDiff > 0)
usage = (double)(totalDiff - idleDiff) / totalDiff * 100.0;
}
return (usage, idleTime, totalTime);
}
var lines = File.ReadLines("/proc/stat").Where(l => l.StartsWith("cpu"));
if (lines.Count() == 0)
return new CpuUsage(0, new List<double>());
var (allUsage, allIdle, allTotal) = ParseLines(lines.First(), _prevAll);
_prevAll = (allIdle, allTotal);
var coreUsages = new List<double>();
foreach (var (i, line) in lines.Skip(1).Enumerate())
{
if (_prevCores is null)
_prevCores = new Dictionary<int, (long idle, long total)?>();
if (!_prevCores.ContainsKey(i))
_prevCores.Add(i, null);
var (coreUsage, coreIdle, coreTotal) = ParseLines(line, _prevCores[i]);
_prevCores[i] = (coreIdle, coreTotal);
coreUsages.Add(coreUsage);
}
return new CpuUsage(
AllUsagePercent: allUsage,
CoreUsagePercents: coreUsages
);
}
private CpuTemperature GetCpuTemperature()
{
var currentStr = File.ReadAllText("/sys/class/thermal/thermal_zone0/temp").Trim();
var current = int.Parse(currentStr) / 1000.0;
var criticalFile = Directory.GetFiles("/sys/class/thermal/thermal_zone0/", "trip_point_*_type")
.FirstOrDefault(f => File.ReadAllText(f).Trim() == "critical")?
.Replace("_type", "_temp");
double critical = 100.0;
if (criticalFile is not null)
{
var criticalStr = File.ReadAllText(criticalFile).Trim();
critical = int.Parse(criticalStr) / 1000.0;
}
return new CpuTemperature(
Current: current,
Critical: critical
);
}
private MemoryUsage GetMemoryUsage()
{
var memInfo = File.ReadAllLines("/proc/meminfo")
.Select(l => l.Split(':', 2))
.ToDictionary(
p => p[0],
p => long.Parse(p[1].Trim().Split(' ')[0], CultureInfo.InvariantCulture) * 1024
);
long total = memInfo["MemTotal"];
long available = memInfo.ContainsKey("MemAvailable") ? memInfo["MemAvailable"] : memInfo["MemFree"];
long used = total - available;
return new MemoryUsage(totalB: total, usedB: used, availableB: available);
}
}
+8 -2
View File
@@ -38,12 +38,18 @@ public class LightsService
if (configuration is null) if (configuration is null)
{ {
configuration = LightsServiceConfiguration.Default; configuration = LightsServiceConfiguration.Default;
_logger.LogWarning("Configuration not set. Using default configuration."); _logger.LogWarning("Configuration not set. Using default configuration.");
} }
_configuration = configuration; _configuration = configuration;
_logger.LogInformation("Using following configuration: \n{}", _configuration.ToString("\n", 4)); _logger.LogInformation("Using following configuration: \n{}", _configuration.ToString("\n", 4));
var channelOptions = new GrpcChannelOptions
_client = new Lights.LightsClient(GrpcChannel.ForAddress(_configuration.ServerAddress)); {
Credentials = ChannelCredentials.Insecure,
HttpHandler = new SocketsHttpHandler(),
MaxReconnectBackoff = TimeSpan.FromSeconds(10)
};
_client = new Lights.LightsClient(GrpcChannel.ForAddress(_configuration.ServerAddress, channelOptions));
} }
catch (Exception e) catch (Exception e)
{ {
@@ -1,172 +0,0 @@
using System.Device.I2c;
using Microsoft.Extensions.Logging;
using GSS2.Core.Abstractions;
namespace GSS2.Core.Hardware;
public class TemperatureHumidityService : IDisposable
{
public record TemperatureHumidityServiceConfiguration : ServiceConfigurationBase
{
public static TemperatureHumidityServiceConfiguration Default => new TemperatureHumidityServiceConfiguration
{
Bus = 6,
Address = 0x45
};
public int Bus { get; init; }
public int Address { get; init; }
}
private bool _disposedValue;
private readonly ILogger<TemperatureHumidityService> _logger;
private readonly TemperatureHumidityServiceConfiguration _configuration;
private readonly I2cDevice _sensor;
public TemperatureHumidityService(ILogger<TemperatureHumidityService> logger, TemperatureHumidityServiceConfiguration? configuration = null)
{
_logger = logger;
_logger.LogInformation("Initialization");
try
{
if (configuration is null)
{
configuration = TemperatureHumidityServiceConfiguration.Default;
_logger.LogWarning("Configuration not set. Using default configuration.");
}
_configuration = configuration;
_logger.LogInformation("Using following configuration: \n{}", _configuration.ToString("\n", 4));
_sensor = I2cDevice.Create(new I2cConnectionSettings(_configuration.Bus, _configuration.Address));
}
catch (Exception e)
{
_logger.LogCritical(e, "An exception occurred while initialization");
throw;
}
_logger.LogInformation("Initialized");
}
public (double temperature, double relativeHumidity) Measure(int measurementsCount = 1, bool checkCrc = true, CancellationToken cancellationToken = default)
{
// В соответствии с даташитом на SHT45-AD1B-R2 псевдокод измерения с высокой точностью следующий:
// i2c_write(i2c_addr=0x44, tx_bytes=[0xFD])
// wait_seconds(0.01)
// rx_bytes = i2c_read(i2c_addr=0x44, number_of_bytes=6)
// t_ticks = rx_bytes[0] * 256 + rx_bytes[1]
// checksum_t = rx_bytes[2]
// rh_ticks = rx_bytes[3] * 256 + rx_bytes[4]
// checksum_rh = rx_bytes[5]
// t_degC = -45 + 175 * t_ticks/65535
// rh_pRH = -6 + 125 * rh_ticks/65535
// if (rh_pRH > 100):
// rh_pRH = 100
// if (rh_pRH < 0):
// rh_pRH = 0
if (measurementsCount <= 0)
throw new ArgumentOutOfRangeException(nameof(measurementsCount));
double sumTemperature = 0;
double sumRelativeHumidity = 0;
int successMeasurementsCount = 0;
Span<byte> command = stackalloc byte[] { 0xFD };
Span<byte> data = stackalloc byte[6];
for (int i = 0; i < measurementsCount; i++)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// Отправляем команду
_sensor.Write(command);
// 10 мс ожидания
Task.Delay(10, cancellationToken);
// Читаем данные
_sensor.Read(data);
// Распаковываем данные [2 * 8-bit T-data; 8-bit CRC; 2 * 8-bit RH-data; 8-bit CRC]
byte temperatureChecksum = data[2];
byte relativeHumidityChecksum = data[5];
ushort temperatureTicks = BitConverter.ToUInt16(data.Slice(0, 2));
int relativeHumidityTicks = BitConverter.ToUInt16(data.Slice(3, 2));
// Проверка по CRC
if (checkCrc && (!CheckCrc(data.Slice(0, 2), temperatureChecksum) ||
!CheckCrc(data.Slice(3, 2), relativeHumidityChecksum)))
{
_logger.LogWarning("Temperature and humidity sensor CRC error");
continue;
}
// Пересчёт
double temperature = -45 + 175 * (temperatureTicks / 65535.0);
double relativeHumidity = -6 + 125 * (relativeHumidityTicks / 65535.0);
relativeHumidity = Math.Clamp(relativeHumidity, 0, 100);
// Сумма для усреднения
sumTemperature += temperature;
sumRelativeHumidity += relativeHumidity;
successMeasurementsCount++;
}
catch (Exception e)
{
_logger.LogWarning(e, "Temperature and humidity sensor not available on I2C bus");
}
}
if (successMeasurementsCount == 0)
{
_logger.LogError("Temperature and humidity sensor: no successful measurements");
return (double.NaN, double.NaN);
}
return (sumTemperature / successMeasurementsCount, sumRelativeHumidity / successMeasurementsCount);
}
private static bool CheckCrc(Span<byte> data, byte expectedCrc)
{
byte crc = 0xFF;
foreach (var b in data)
{
crc ^= b;
for (int i = 0; i < 8; i++)
{
if ((crc & 0x80) != 0)
crc = (byte)((crc << 1) ^ 0x31);
else
crc <<= 1;
}
}
return crc == expectedCrc;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
try
{
_sensor?.Dispose();
}
catch { }
}
_disposedValue = true;
}
}
void IDisposable.Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
-113
View File
@@ -1,113 +0,0 @@
using Microsoft.Extensions.Logging;
namespace GSS2.Core;
public class ImageStorageService
{
private readonly ILogger<ImageStorageService> _logger;
private readonly DirectoryInfo _rootDirectory;
public ImageStorageService(ILogger<ImageStorageService> logger, DirectoryInfo rootDirectory)
{
_logger = logger;
_logger.LogInformation("Инициализация");
_rootDirectory = rootDirectory;
if (!_rootDirectory.Exists)
{
_rootDirectory.Create();
_logger.LogInformation("Создана директория: {}", _rootDirectory.FullName);
}
_logger.LogInformation("Инициализировано");
}
public async Task<string> SaveAsync(Stream fileStream, string extension, string subDirectory, CancellationToken cancellationToken = default)
{
var fileName = GenerateFileName(extension);
var fullPath = GetFullPath(fileName, subDirectory);
System.Diagnostics.Debug.Assert(fullPath is not null);
var directory = Path.GetDirectoryName(fullPath)!;
Directory.CreateDirectory(directory);
await using var file = new FileStream(
fullPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
81920,
useAsync: true);
await fileStream.CopyToAsync(file, cancellationToken);
_logger.LogInformation("Файл сохранён: {}", fileName);
return fileName;
}
public Stream OpenRead(string fileName, string? subDirectory = null)
{
var fullPath = GetFullPath(fileName, subDirectory);
if (fullPath is null)
throw new FileNotFoundException(null, fullPath);
return new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
}
public bool Delete(string fileName, string? subDirectory = null)
{
var fullPath = GetFullPath(fileName, subDirectory);
if (fullPath is null)
throw new FileNotFoundException(null, fullPath);
if (!File.Exists(fullPath))
return false;
File.Delete(fullPath);
_logger.LogInformation("Файл удалён: {}", fileName);
var parentDirectory = Directory.GetParent(fullPath);
if (parentDirectory?.GetFiles().Count() == 0)
{
parentDirectory.Delete();
_logger.LogInformation("Удалена пустая директория: {}", parentDirectory.Name);
}
return true;
}
private string GenerateFileName(string extension)
{
var guid = Guid.NewGuid().ToString("N");
return $"{guid}{extension}";
}
public string? GetFullPath(string fileName, string? subDirectory = null)
{
if (fileName.Count() < 4)
return null;
var level1 = fileName.Substring(0, 2);
var level2 = fileName.Substring(2, 2);
if (subDirectory is not null)
return Path.Combine(_rootDirectory.FullName, subDirectory, level1, level2, fileName);
return ListFiles()
.FirstOrDefault(f => Path.GetFileName(f) == fileName);
}
public List<string> ListFiles(string? subDirectory = null)
{
if (subDirectory is not null)
return Directory.GetDirectories(Path.Join(_rootDirectory.FullName, subDirectory))
.SelectMany(Directory.GetDirectories)
.SelectMany(Directory.GetFiles)
.ToList();
return Directory.GetDirectories(_rootDirectory.FullName)
.Select(Path.GetFileName)
.SelectMany(ListFiles)
.ToList();
}
}
+4 -1
View File
@@ -15,5 +15,8 @@ public sealed class FileLoggerProvider : ILoggerProvider
public ILogger CreateLogger(string categoryName) => new FileLogger(categoryName, _filePath); public ILogger CreateLogger(string categoryName) => new FileLogger(categoryName, _filePath);
public void Dispose() { } public void Dispose()
{
return;
}
} }
@@ -0,0 +1,65 @@
using System.IO.Compression;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;
/// <summary>
/// Компрессор для больших списков чисел с плавающей точкой
/// </summary>
public class CompressedFloatListConverter : ValueConverter<List<float>, string>
{
public CompressedFloatListConverter()
: base(
v => Compress(v),
v => Decompress(v)
)
{ }
private static string Compress(List<float> list)
{
if (list is null || list.Count == 0)
return "";
byte[] floatBytes = new byte[list.Count * sizeof(float)];
Buffer.BlockCopy(list.ToArray(), 0, floatBytes, 0, floatBytes.Length);
using var outputStream = new MemoryStream();
using (var compressionStream = new BrotliStream(outputStream, CompressionLevel.Optimal))
compressionStream.Write(floatBytes, 0, floatBytes.Length);
return Convert.ToBase64String(outputStream.ToArray());
}
private static List<float> Decompress(string base64String)
{
if (string.IsNullOrEmpty(base64String))
return new List<float>();
byte[] compressedBytes;
float[]? floatArray;
try
{
compressedBytes = Convert.FromBase64String(base64String);
}
catch (FormatException)
{
floatArray = JsonConvert.DeserializeObject<float[]>(base64String);
if (floatArray is null)
throw;
return new List<float>(floatArray);
}
using var inputStream = new MemoryStream(compressedBytes);
using var decompressionStream = new BrotliStream(inputStream, CompressionMode.Decompress);
using var outputStream = new MemoryStream();
decompressionStream.CopyTo(outputStream);
byte[] decompressedBytes = outputStream.ToArray();
floatArray = new float[decompressedBytes.Length / sizeof(float)];
Buffer.BlockCopy(decompressedBytes, 0, floatArray, 0, decompressedBytes.Length);
return new List<float>(floatArray);
}
}
@@ -4,7 +4,7 @@ using Avalonia.Data.Converters;
namespace GSS2.UI.Core.Converters; namespace GSS2.UI.Core.Converters;
public class GreaterThenOrEqualConverter : IValueConverter public class GreaterThanOrEqualConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
@@ -4,7 +4,7 @@ using Avalonia.Data.Converters;
namespace GSS2.UI.Core.Converters; namespace GSS2.UI.Core.Converters;
public class LessThenConverter : IValueConverter public class LessThanConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
@@ -4,7 +4,7 @@ using Avalonia.Data.Converters;
namespace GSS2.UI.Core.Converters; namespace GSS2.UI.Core.Converters;
public class LessThenOrEqualConverter : IValueConverter public class LessThanOrEqualConverter : IValueConverter
{ {
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
+10 -7
View File
@@ -1,15 +1,18 @@
<Window <Window
Height="{Binding Height}"
Title="{Binding Title}"
Topmost="{Binding Topmost}"
Width="{Binding Width}"
WindowDecorations="{Binding WindowDecorations}"
WindowState="{Binding WindowState}"
x:Class="GSS2.UI.Core.MainWindow" x:Class="GSS2.UI.Core.MainWindow"
xmlns="https://github.com/avaloniaui" x:DataType="vm:MainWindowViewModel"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:GSS2.UI.Core.ViewModels" xmlns:vm="using:GSS2.UI.Core.ViewModels"
Title="{Binding Title}" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:DataType="vm:MainWindowViewModel" xmlns="https://github.com/avaloniaui"
WindowDecorations="{Binding WindowDecorations}" >
Topmost="{Binding Topmost}"
WindowState="{Binding WindowState}">
<ContentControl x:Name="MainContent" /> <ContentControl x:Name="MainContent" />
+5 -1
View File
@@ -431,6 +431,10 @@ public class SectionPanel : Panel
}; };
IsScrolling = true; IsScrolling = true;
return animation.RunAsync(control).ContinueWith(_ => Dispatcher.UIThread.Invoke(() => IsScrolling = false)); return animation
.RunAsync(control)
.ContinueWith(_ =>
Dispatcher.UIThread.Invoke(() => IsScrolling = false)
);
} }
} }
@@ -4,22 +4,24 @@ using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.IdentityModel.Tokens;
namespace GSS2.UI.Core.ViewModels; namespace GSS2.UI.Core.ViewModels;
public partial class AsyncImageRecordViewModel : ViewModelBase public partial class AsyncImageRecordViewModel : ViewModelBase
{ {
private static readonly SemaphoreSlim _semaphore = new(1, 1); private static readonly SemaphoreSlim _semaphore = new(1, 1);
[ObservableProperty] public partial string? Data { get; set; } [ObservableProperty] public partial string Data { get; set; }
[ObservableProperty] public partial IImage? Image { get; set; } [ObservableProperty] public partial IImage? Image { get; set; }
[ObservableProperty] public partial bool IsLoading { get; set; } = false; [ObservableProperty] public partial bool IsLoading { get; set; } = false;
public AsyncImageRecordViewModel(string? data = null) public AsyncImageRecordViewModel(string data = "")
{ {
Data = data; Data = data;
} }
partial void OnDataChanged(string? value) => _ = LoadAsync(); partial void OnDataChanged(string value) => _ = LoadAsync();
private async Task LoadAsync() private async Task LoadAsync()
{ {
@@ -27,7 +29,7 @@ public partial class AsyncImageRecordViewModel : ViewModelBase
try try
{ {
if (Data is not null) if (!string.IsNullOrEmpty(Data))
{ {
IsLoading = true; IsLoading = true;
byte[] imageBytes = Convert.FromBase64String(Data); byte[] imageBytes = Convert.FromBase64String(Data);
@@ -1,74 +0,0 @@
using Microsoft.Extensions.Logging;
using LibCameraSharp;
using GSS2.Core.Hardware;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia.Media;
namespace GSS2.UI.Core.ViewModels;
public partial class CameraViewModel : ViewModelBase, IDisposable
{
private bool _disposedValue;
private readonly ILogger<CameraViewModel> _logger;
private readonly CameraService _cameraService;
private readonly System.Timers.Timer _renderTimer;
public event EventHandler? RenderRequested;
[ObservableProperty] private Color _background = Colors.Transparent;
[ObservableProperty] private Stretch _stretchMode = Stretch.Uniform;
[ObservableProperty] private TileMode _tileMode = TileMode.None;
public CameraViewModel(ILogger<CameraViewModel> logger, CameraService cameraService)
{
_logger = logger;
_logger.LogInformation("Initialization");
_cameraService = cameraService;
_renderTimer = new System.Timers.Timer(33);
_renderTimer.Elapsed += (_, _) =>
{
if (!_cameraService.IsImageCapturing)
RenderRequested?.Invoke(this, new EventArgs());
};
_logger.LogInformation("Initialized");
}
public void StartViewFinder()
{
_cameraService.StartViewFinder();
_renderTimer.Start();
}
public void StopViewFinder()
{
_renderTimer.Stop();
_cameraService.StopViewFinder();
}
public bool CanGrabViewFinderFrame() => !_cameraService.IsImageCapturing;
public (FrameBuffer, StreamConfiguration)? GrabViewFinderFrame() => _cameraService.GrabViewFinderFrame();
public void ReturnViewFinderFrame(FrameBuffer frameBuffer) => _cameraService.ReturnViewFinderFrame(frameBuffer);
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_renderTimer.Stop();
_renderTimer.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
@@ -1,75 +0,0 @@
using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia.Layout;
using GSS2.Core.Hardware;
namespace GSS2.UI.Core.ViewModels;
public partial class IlluminatorControllerViewModel : ViewModelBase
{
private readonly ILogger<IlluminatorControllerViewModel> _logger;
private readonly IlluminatorService _illuminatorService;
private bool _whiteIntensityChanging = false;
private bool _uv365IntensityChanging = false;
private bool _uv254IntensityChanging = false;
[ObservableProperty] private double _whiteIntensity;
[ObservableProperty] private double _uv365Intensity;
[ObservableProperty] private double _uv254Intensity;
[ObservableProperty] private bool _enabled;
[ObservableProperty] Orientation _orientation;
public IlluminatorControllerViewModel(ILogger<IlluminatorControllerViewModel> logger, IlluminatorService illuminatorService)
{
_logger = logger;
_logger.LogInformation("Initialization");
_illuminatorService = illuminatorService;
WhiteIntensity = 0;
Uv365Intensity = 0;
Uv254Intensity = 0;
Orientation = Orientation.Horizontal;
Enabled = false;
_logger.LogInformation("Initialized");
}
partial void OnWhiteIntensityChanged(double value)
{
if (_whiteIntensityChanging)
return;
_whiteIntensityChanging = true;
_illuminatorService.SetIntensity(white: value);
_whiteIntensityChanging = false;
}
partial void OnUv365IntensityChanged(double value)
{
if (_uv365IntensityChanging)
return;
_uv365IntensityChanging = true;
_illuminatorService.SetIntensity(uv365: value);
_uv365IntensityChanging = false;
}
partial void OnUv254IntensityChanged(double value)
{
if (_uv254IntensityChanging)
return;
_uv254IntensityChanging = true;
_illuminatorService.SetIntensity(uv254: value);
_uv254IntensityChanging = false;
}
partial void OnEnabledChanged(bool value)
{
if (value)
_illuminatorService.TurnOn();
else
_illuminatorService.TurnOff();
}
}
@@ -10,6 +10,8 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] public partial WindowDecorations WindowDecorations { get; set; } [ObservableProperty] public partial WindowDecorations WindowDecorations { get; set; }
[ObservableProperty] public partial bool Topmost { get; set; } [ObservableProperty] public partial bool Topmost { get; set; }
[ObservableProperty] public partial WindowState WindowState { get; set; } [ObservableProperty] public partial WindowState WindowState { get; set; }
[ObservableProperty] public partial double Width { get; set; }
[ObservableProperty] public partial double Height { get; set; }
public MainWindowViewModel() public MainWindowViewModel()
{ {
@@ -1,69 +0,0 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using Microsoft.Extensions.Logging;
namespace GSS2.UI.Core.ViewModels;
public partial class ResourceBarViewModel : ViewModelBase
{
private readonly ILogger<ResourceBarViewModel> _logger;
[ObservableProperty] private ObservableCollection<ISeries> _series;
[ObservableProperty] private Rect _bounds;
public ResourceBarViewModel(ILogger<ResourceBarViewModel> logger, IEnumerable<ISeries> series)
{
_logger = logger;
_logger.LogInformation("Initialization");
Series = new ObservableCollection<ISeries>(series);
_logger.LogInformation("Initialized");
}
partial void OnBoundsChanged(Rect value)
{
var size = Math.Min(value.Width, value.Height);
var textSize = size switch
{
< 100 => 8,
< 150 => 10,
< 175 => 12,
_ => 14
};
var innerRadius = size switch
{
< 50 => size / 2,
< 120 => size / 3,
_ => size / 4,
};
foreach (var series in Series.OfType<PieSeries<ObservableValue>>())
{
series.DataLabelsSize = textSize;
series.InnerRadius = innerRadius;
series.OuterRadiusOffset = 0;
series.RelativeOuterRadius = 1;
}
}
public void UpdateValue(double value, double maxValue)
{
if (Series.Count() == 2 &&
Series.Skip(0).First().Values is ObservableCollection<ObservableValue> value1 &&
Series.Skip(1).First().Values is ObservableCollection<ObservableValue> value2)
{
value1.First().Value = value > maxValue ? maxValue : value;
value2.First().Value = value > maxValue ? 0 : maxValue - value;
}
}
}
@@ -1,67 +0,0 @@
using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
namespace GSS2.UI.Core.ViewModels;
public partial class ResourceChartViewModel : ViewModelBase
{
private readonly ILogger<ResourceChartViewModel> _logger;
[ObservableProperty] private ObservableCollection<ISeries> _series;
[ObservableProperty] private ObservableCollection<Axis> _yAxes;
[ObservableProperty] private ObservableCollection<Axis> _xAxes;
[ObservableProperty] private Rect _bounds;
public ResourceChartViewModel(ILogger<ResourceChartViewModel> logger, IEnumerable<ISeries> series, IEnumerable<Axis> yAxes, IEnumerable<Axis> xAxes)
{
_logger = logger;
_logger.LogInformation("Initialization");
Series = new ObservableCollection<ISeries>(series);
YAxes = new ObservableCollection<Axis>(yAxes);
XAxes = new ObservableCollection<Axis>(xAxes);
_logger.LogInformation("Initialized");
}
partial void OnBoundsChanged(Rect value)
{
var textSize = value.Height switch
{
< 100 => 8,
< 150 => 10,
< 200 => 12,
_ => 14
};
var labelsDensity = value.Height switch
{
< 100 => 0.2f,
< 150 => 0.3f,
< 200 => 0.4f,
_ => 0.5f
};
foreach (var axis in YAxes)
{
axis.TextSize = textSize;
axis.NameTextSize = textSize;
axis.LabelsDensity = labelsDensity;
}
}
public void AppendValue(double value, int maxValuesCount)
{
if (Series.Count() != 1 || Series.First().Values is not ObservableCollection<double> values)
return;
values.Add(value);
while (values.Count() > maxValuesCount || values.Count() == 0)
values.RemoveAt(0);
}
}
@@ -1,46 +0,0 @@
using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia;
namespace GSS2.UI.Core.ViewModels;
public partial class ResourceCpuUsageBarsViewModel : ViewModelBase
{
private readonly ILogger<ResourceCpuUsageBarsViewModel> _logger;
[ObservableProperty] private Rect _bounds;
[ObservableProperty] private bool _isCoresColumnVisible;
[ObservableProperty] private ResourceBarViewModel _cpu;
[ObservableProperty] private ResourceBarViewModel _cpu0;
[ObservableProperty] private ResourceBarViewModel _cpu1;
[ObservableProperty] private ResourceBarViewModel _cpu2;
[ObservableProperty] private ResourceBarViewModel _cpu3;
public ResourceCpuUsageBarsViewModel(
ILogger<ResourceCpuUsageBarsViewModel> logger,
ResourceBarViewModel cpu,
ResourceBarViewModel cpu0,
ResourceBarViewModel cpu1,
ResourceBarViewModel cpu2,
ResourceBarViewModel cpu3
)
{
_logger = logger;
_logger.LogInformation("Initialized");
Cpu = cpu;
Cpu0 = cpu0;
Cpu1 = cpu1;
Cpu2 = cpu2;
Cpu3 = cpu3;
_logger.LogInformation("Initialized");
}
partial void OnBoundsChanged(Rect value)
{
IsCoresColumnVisible = Math.Min(value.Width, value.Height * 1.5) > 200;
}
}
@@ -1,229 +0,0 @@
using System.Collections.ObjectModel;
using Microsoft.Extensions.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia;
using Avalonia.Threading;
using SkiaSharp;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.Defaults;
using GSS2.Core.Hardware;
namespace GSS2.UI.Core.ViewModels;
public partial class ResourcesViewModel : ViewModelBase, IDisposable
{
private bool _disposedValue;
private readonly ComputeResourcesService _computeResourcesService;
private readonly ILogger<ResourcesViewModel> _logger;
private readonly int _intervalMs = 500;
private readonly int _chartsHistorySize = 120;
private readonly System.Timers.Timer _updateTimer;
[ObservableProperty] private Rect _bounds;
[ObservableProperty] private bool _isChartsColumnVisible;
[ObservableProperty] private bool _isBarsColumnVisible;
[ObservableProperty] private ResourceChartViewModel _cpuUsageChartViewModel;
[ObservableProperty] private ResourceChartViewModel _cpuTemperatureChartViewModel;
[ObservableProperty] private ResourceChartViewModel _memoryUsageChartViewModel;
[ObservableProperty] private ResourceCpuUsageBarsViewModel _cpuUsageBarsViewModel;
[ObservableProperty] private ResourceBarViewModel _cpuTemperatureBarViewModel;
[ObservableProperty] private ResourceBarViewModel _memoryUsageBarViewModel;
public ResourcesViewModel(
ILogger<ResourcesViewModel> logger,
ILogger<ResourceCpuUsageBarsViewModel> resourceCpuUsageBarsViewModelLogger,
ILogger<ResourceChartViewModel> resourceChartViewModelCpuUsageLogger,
ILogger<ResourceChartViewModel> resourceChartViewModelCpuTemperatureLogger,
ILogger<ResourceChartViewModel> resourceChartViewModelMemoryUsageLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelCpuUsageLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelCpu0UsageLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelCpu1UsageLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelCpu2UsageLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelCpu3UsageLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelCpuTemperatureLogger,
ILogger<ResourceBarViewModel> resourceBarViewModelMemoryUsageLogger,
ComputeResourcesService computeResourcesService
)
{
_logger = logger;
_logger.LogInformation("Initialization");
_computeResourcesService = computeResourcesService;
ResourceChartViewModel CreateChartViewModel(ILogger<ResourceChartViewModel> logger, string name, double minLimit, double maxLimit, double minStep)
{
return new ResourceChartViewModel(
logger: logger,
series:
[
new LineSeries<double>
{
Name = name,
Values = new ObservableCollection<double>(Enumerable.Repeat(0.0, _chartsHistorySize)),
Fill = new SolidColorPaint(SKColors.DeepSkyBlue.WithAlpha(127), 0),
GeometrySize = 0,
ShowDataLabels = false,
Stroke = new SolidColorPaint(SKColors.DeepSkyBlue, 1),
GeometryFill = new SolidColorPaint(SKColors.Transparent, 0),
GeometryStroke = new SolidColorPaint(SKColors.Transparent, 0),
IsHoverable = false,
DataPadding = new LiveChartsCore.Drawing.LvcPoint(0.1f, 0.1f)
}
],
yAxes:
[
new Axis
{
MinLimit = minLimit,
MaxLimit = maxLimit,
MinStep = minStep,
Name = name
}
],
xAxes:
[
new Axis
{
IsVisible = false,
Padding = new LiveChartsCore.Drawing.Padding(0)
}
]
);
}
ResourceBarViewModel CreateBarViewModel(ILogger<ResourceBarViewModel> logger, Func<LiveChartsCore.Kernel.ChartPoint<ObservableValue, LiveChartsCore.SkiaSharpView.Drawing.Geometries.DoughnutGeometry, LiveChartsCore.SkiaSharpView.Drawing.Geometries.LabelGeometry>, string> formatter)
{
return new ResourceBarViewModel(
logger: logger,
series:
[
new PieSeries<ObservableValue>
{
Values = new ObservableCollection<ObservableValue> { new ObservableValue(1) },
Fill = new SolidColorPaint(SKColors.DeepSkyBlue, 0),
ShowDataLabels = true,
IsHoverable = false,
HoverPushout = 0,
DataLabelsFormatter = formatter,
DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.ChartCenter,
DataLabelsPaint = new SolidColorPaint(SKColors.White)
},
new PieSeries<ObservableValue>
{
Values = new ObservableCollection<ObservableValue> { new ObservableValue(1) },
Fill = new SolidColorPaint(SKColors.DeepSkyBlue.WithAlpha(64), 0),
ShowDataLabels = false,
IsHoverable = false,
HoverPushout = 0
}
]
);
}
CpuUsageChartViewModel = CreateChartViewModel(resourceChartViewModelCpuUsageLogger, "Нагрузка ЦПУ, %", 0, 100, 10);
CpuTemperatureChartViewModel = CreateChartViewModel(resourceChartViewModelCpuTemperatureLogger, "Температура ЦПУ, ℃", 0, 100, 10);
MemoryUsageChartViewModel = CreateChartViewModel(resourceChartViewModelMemoryUsageLogger, "Нагрузка ОЗУ, МБ", 0, 8000, 1000);
CpuUsageBarsViewModel = new ResourceCpuUsageBarsViewModel(
resourceCpuUsageBarsViewModelLogger,
CreateBarViewModel(resourceBarViewModelCpuUsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"),
CreateBarViewModel(resourceBarViewModelCpu0UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"),
CreateBarViewModel(resourceBarViewModelCpu1UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"),
CreateBarViewModel(resourceBarViewModelCpu2UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %"),
CreateBarViewModel(resourceBarViewModelCpu3UsageLogger, value => $"{value.Model?.Value?.ToString("00.00")} %")
);
CpuTemperatureBarViewModel = CreateBarViewModel(resourceBarViewModelCpuTemperatureLogger, value => $"{value.Model?.Value?.ToString("00.00")} ℃");
MemoryUsageBarViewModel = CreateBarViewModel(resourceBarViewModelMemoryUsageLogger, value => $"{value.Model?.Value?.ToString("000.00")} МБ");
Update();
_updateTimer = new System.Timers.Timer(_intervalMs);
_updateTimer.Elapsed += UpdateTimerElapsed;
_updateTimer.AutoReset = true;
_updateTimer.Start();
_logger.LogInformation("Initialized");
}
partial void OnBoundsChanged(Rect value)
{
if (value.Width < 300)
{
IsChartsColumnVisible = false;
IsBarsColumnVisible = true;
}
else if (value.Width < 400)
{
IsChartsColumnVisible = true;
IsBarsColumnVisible = false;
}
else
{
IsChartsColumnVisible = true;
IsBarsColumnVisible = true;
}
}
private void UpdateTimerElapsed(object? sender, System.Timers.ElapsedEventArgs ea) => Update();
private void Update()
{
try
{
var snapshot = _computeResourcesService.GetSnapshot();
Dispatcher.UIThread.Post(() =>
{
if (CpuTemperatureChartViewModel.YAxes.Count() == 1)
CpuTemperatureChartViewModel.YAxes.First().MaxLimit = snapshot.CpuTemperature.Critical;
if (MemoryUsageChartViewModel.YAxes.Count() == 1)
MemoryUsageChartViewModel.YAxes.First().MaxLimit = snapshot.MemoryUsage.TotalMB;
CpuUsageChartViewModel.AppendValue(Math.Round(snapshot.CpuUsage.AllUsagePercent, 2), _chartsHistorySize);
CpuTemperatureChartViewModel.AppendValue(Math.Round(snapshot.CpuTemperature.Current, 2), _chartsHistorySize);
MemoryUsageChartViewModel.AppendValue(Math.Round(snapshot.MemoryUsage.UsedMB, 2), _chartsHistorySize);
CpuUsageBarsViewModel.Cpu.UpdateValue(Math.Round(snapshot.CpuUsage.AllUsagePercent, 2), 100);
CpuUsageBarsViewModel.Cpu0.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(0).First(), 2), 100);
CpuUsageBarsViewModel.Cpu1.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(1).First(), 2), 100);
CpuUsageBarsViewModel.Cpu2.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(2).First(), 2), 100);
CpuUsageBarsViewModel.Cpu3.UpdateValue(Math.Round(snapshot.CpuUsage.CoreUsagePercents.Skip(3).First(), 2), 100);
CpuTemperatureBarViewModel.UpdateValue(Math.Round(snapshot.CpuTemperature.Current, 2), snapshot.CpuTemperature.Critical);
MemoryUsageBarViewModel.UpdateValue(Math.Round(snapshot.MemoryUsage.UsedMB, 2), snapshot.MemoryUsage.TotalMB);
});
}
catch (Exception e)
{
_logger.LogWarning(e, "Exception while update");
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
try
{
_updateTimer.Stop();
_updateTimer.Dispose();
}
catch
{ }
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
-13
View File
@@ -1,13 +0,0 @@
<Control
x:Class="GSS2.UI.Core.Views.CameraView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
x:DataType="vm:CameraViewModel"
Background="{Binding Background}"
StretchMode="{Binding StretchMode}"
TileMode="{Binding TileMode}" />
-714
View File
@@ -1,714 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Avalonia;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Controls;
using Avalonia.OpenGL;
using Avalonia.Threading;
using Avalonia.Input;
using GSS2.UI.Core.ViewModels;
using Avalonia.Markup.Xaml;
using GSS2.Core.UI.Extensions;
using Avalonia.VisualTree;
namespace GSS2.UI.Core.Views;
public partial class CameraView : OpenGlControlBase
{
private class OpenGLContext
{
[DllImport("libEGL.so.1")] public static extern IntPtr eglQueryString(IntPtr dpy, int name);
[DllImport("libEGL.so.1")] public static extern IntPtr eglGetCurrentDisplay();
[DllImport("libEGL.so.1")] public static extern IntPtr eglGetProcAddress(string procname);
[DllImport("libGL.so.1")] public static extern void glUniform1i(int location, int falue);
[DllImport("libGL.so.1")] public static extern void glUniformMatrix3x2fv(int location, int count, bool transpose, IntPtr value);
[DllImport("libGL.so.1")] public static extern void glUniformMatrix3fv(int location, int count, bool transpose, IntPtr value);
public const uint DRM_FORMAT_BGRX8888 = 0x34325258; // XR24 XRGB8888
public const int GL_TRIANGLE_STRIP = 0x0005;
public const int GL_TEXTURE_WRAP_S = 0x2802;
public const int GL_TEXTURE_WRAP_T = 0x2803;
public const int GL_CLAMP_TO_EDGE = 0x812F;
public const int EGL_LINUX_DMA_BUF_EXT = 0x3270;
public const int EGL_LINUX_DRM_FOURCC_EXT = 0x3271;
public const int EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272;
public const int EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273;
public const int EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274;
public delegate IntPtr GlEGLImageTargetTexture2DOESDelegate(int target, IntPtr image);
public delegate IntPtr EglCreateImageKHRDelegate(IntPtr dpy, IntPtr ctx, int target, IntPtr buffer, int[] attribs);
public delegate bool EglDestroyImageKHRDelegate(IntPtr dpy, IntPtr image);
private const string VERTEX_SHADER = @"
#version 300 es
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUv;
uniform mat4 uMvp;
uniform mat3 uUv;
out vec2 vUv;
void main()
{
vec3 uv = uUv * vec3(aUv, 1.0);
vUv = uv.xy;
gl_Position = uMvp * vec4(aPos, 0.0, 1.0);
}
";
private const string FRAGMENT_SHADER = @"
#version 300 es
precision mediump float;
in vec2 vUv;
uniform sampler2D uTex;
out vec4 fragColor;
void main()
{
fragColor = texture(uTex, vUv);
}
";
private readonly float[] Vertices =
[
-1f, -1f, 0f, 1f,
1f, -1f, 1f, 1f,
-1f, 1f, 0f, 0f,
1f, 1f, 1f, 0f,
];
public IntPtr EglDisplay { get; }
public int Texture { get; }
public int Program { get; }
public int UTexLocation { get; }
public int UMvpLocation { get; }
public int UUvLocation { get; }
public int VAO { get; }
public int VBO { get; }
public EglCreateImageKHRDelegate EglCreateImageKHR { get; }
public EglDestroyImageKHRDelegate EglDestroyImageKHR { get; }
public GlEGLImageTargetTexture2DOESDelegate GlEGLImageTargetTexture2DOES { get; }
public OpenGLContext(GlInterface gl)
{
var glExtensions = gl.GetString(GlConsts.GL_EXTENSIONS);
if (glExtensions is null)
throw new Exception("GL extensions query returned null string");
if (!glExtensions.Contains("GL_OES_EGL_image") &&
!glExtensions.Contains("GL_OES_EGL_image_external"))
throw new Exception("Required GL extensions \"GL_OES_EGL_image_external\" or \"GL_OES_EGL_image\" not found");
EglDisplay = eglGetCurrentDisplay();
if (EglDisplay == IntPtr.Zero)
throw new Exception("EGL display is nullptr");
var eglExtensions = Marshal.PtrToStringAnsi(eglQueryString(EglDisplay, EglConsts.EGL_EXTENSIONS));
if (eglExtensions is null)
throw new Exception("EGL extensions query returned null");
if (!eglExtensions.Contains("EGL_EXT_image_dma_buf_import"))
throw new Exception("Required EGL extension \"EGL_EXT_image_dma_buf_import\" not found");
var pEglCreateImageKHR = eglGetProcAddress("eglCreateImageKHR");
if (pEglCreateImageKHR == IntPtr.Zero)
throw new Exception("Required EGL method \"eglCreateImageKHR\" not found");
EglCreateImageKHR = Marshal.GetDelegateForFunctionPointer<EglCreateImageKHRDelegate>(pEglCreateImageKHR);
var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR");
if (pEglDestroyImageKHR == IntPtr.Zero)
throw new Exception("Required EGL method \"eglDestroyImageKHR\" not found");
EglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer<EglDestroyImageKHRDelegate>(pEglDestroyImageKHR);
var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES");
if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero)
throw new Exception("Required EGL method \"glEGLImageTargetTexture2DOES\" not found");
GlEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer<GlEGLImageTargetTexture2DOESDelegate>(pGlEGLImageTargetTexture2DOES);
Texture = gl.GenTexture(); ThrowIfOpenGLError(gl);
gl.BindTexture(GlConsts.GL_TEXTURE_2D, Texture); ThrowIfOpenGLError(gl);
gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl);
gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl);
gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MIN_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl);
gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MAG_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl);
int vertexShader = gl.CreateShader(GlConsts.GL_VERTEX_SHADER); ThrowIfOpenGLError(gl);
gl.ShaderSourceString(vertexShader, VERTEX_SHADER); ThrowIfOpenGLError(gl);
gl.CompileShader(vertexShader);
int fragmentShader = gl.CreateShader(GlConsts.GL_FRAGMENT_SHADER); ThrowIfOpenGLError(gl);
gl.ShaderSourceString(fragmentShader, FRAGMENT_SHADER); ThrowIfOpenGLError(gl);
gl.CompileShader(fragmentShader); ThrowIfOpenGLError(gl);
Program = gl.CreateProgram(); ThrowIfOpenGLError(gl);
gl.AttachShader(Program, vertexShader); ThrowIfOpenGLError(gl);
gl.AttachShader(Program, fragmentShader); ThrowIfOpenGLError(gl);
gl.LinkProgram(Program); ThrowIfOpenGLError(gl);
gl.DeleteShader(vertexShader); ThrowIfOpenGLError(gl);
gl.DeleteShader(fragmentShader); ThrowIfOpenGLError(gl);
UTexLocation = gl.GetUniformLocationString(Program, "uTex"); ThrowIfOpenGLError(gl);
UMvpLocation = gl.GetUniformLocationString(Program, "uMvp"); ThrowIfOpenGLError(gl);
UUvLocation = gl.GetUniformLocationString(Program, "uUv"); ThrowIfOpenGLError(gl);
VAO = gl.GenVertexArray(); ThrowIfOpenGLError(gl);
VBO = gl.GenBuffer(); ThrowIfOpenGLError(gl);
gl.BindVertexArray(VAO); ThrowIfOpenGLError(gl);
gl.BindBuffer(GlConsts.GL_ARRAY_BUFFER, VBO); ThrowIfOpenGLError(gl);
unsafe
{
fixed (float* pVertices = Vertices)
gl.BufferData(GlConsts.GL_ARRAY_BUFFER, Vertices.Length * sizeof(float), new IntPtr(pVertices), GlConsts.GL_STATIC_DRAW); ThrowIfOpenGLError(gl);
}
gl.EnableVertexAttribArray(0); ThrowIfOpenGLError(gl);
gl.VertexAttribPointer(0, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), IntPtr.Zero); ThrowIfOpenGLError(gl);
gl.EnableVertexAttribArray(1); ThrowIfOpenGLError(gl);
gl.VertexAttribPointer(1, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), 2 * sizeof(float)); ThrowIfOpenGLError(gl);
gl.BindVertexArray(0); ThrowIfOpenGLError(gl);
}
private void ThrowIfOpenGLError(GlInterface gl)
{
var error = gl.GetError();
if (error != 0)
throw new Exception($"OpenGL context initialization error: {error}");
}
}
private readonly ILogger<CameraView> _logger;
private readonly Dictionary<int, IntPtr> _importedDmaBuffers = new Dictionary<int, nint>();
private bool _isViewFinderStarted = false;
private OpenGLContext? _openGLContext = null;
private Point? _lastMousePosition = null;
private Dictionary<int, Point>? _lastTouchPositions = null;
private float _zoom = 1;
private float _panX = 0;
private float _panY = 0;
public static readonly StyledProperty<Color> BackgroundProperty = AvaloniaProperty.Register<CameraView, Color>(nameof(Background), defaultValue: Colors.Transparent);
public Color Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
public static readonly StyledProperty<Stretch> StretchModeProperty = AvaloniaProperty.Register<CameraView, Stretch>(nameof(StretchMode), defaultValue: Stretch.Uniform);
public Stretch StretchMode
{
get => GetValue(StretchModeProperty);
set => SetValue(StretchModeProperty, value);
}
public static readonly StyledProperty<TileMode> TileModeProperty = AvaloniaProperty.Register<CameraView, TileMode>(nameof(TileMode), defaultValue: TileMode.None);
public TileMode TileMode
{
get => GetValue(TileModeProperty);
set => SetValue(TileModeProperty, value);
}
public CameraView(ILogger<CameraView> logger)
{
_logger = logger;
AvaloniaXamlLoader.Load(this);
_logger.LogInformation("{} initialized", nameof(CameraView));
DoubleTapped += (_, _) =>
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
};
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs ea)
{
if (DataContext is CameraViewModel viewModel)
{
try
{
viewModel.StartViewFinder();
viewModel.RenderRequested += RenderRequested;
_isViewFinderStarted = true;
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Exception while view finder start");
}
}
else
_logger.LogCritical("Cannot start view finder, data context is null or not camera view model");
base.OnAttachedToVisualTree(ea);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs ea)
{
if (_isViewFinderStarted && DataContext is CameraViewModel viewModel)
{
try
{
viewModel.StopViewFinder();
foreach (var (fd, image) in _importedDmaBuffers)
_openGLContext?.EglDestroyImageKHR!(_openGLContext.EglDisplay, image);
_isViewFinderStarted = false;
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Exception while view finder stop");
}
viewModel.RenderRequested -= RenderRequested;
}
else if (_isViewFinderStarted)
_logger.LogCritical("Cannot stop view finder, data context is null or not camera view model");
_isViewFinderStarted = false;
base.OnDetachedFromLogicalTree(ea);
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
var position = ea.GetPosition(this);
// Рассчитываем позицию в координатах изображения (до масштабирования)
var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom;
var imageY = (-position.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертирована
// Применяем масштаб
float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f;
_zoom *= zoomFactor;
_zoom = Math.Clamp(_zoom, 1f, 10f);
_zoom = (float)Math.Round(_zoom, 2);
if (_zoom != 1f)
{
// Пересчитываем смещение так, чтобы указатель остался на месте
_panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom);
_panY = (float)(-position.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертирована
}
else
{
// Если зум == 1, то возвращаем изображение к исходному смещению
_panX = 0f;
_panY = 0f;
}
}
base.OnPointerWheelChanged(ea);
}
protected override void OnPointerPressed(PointerPressedEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (ea.Properties.IsLeftButtonPressed)
{
if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
}
else
_lastMousePosition = ea.GetPosition(this);
}
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is null)
_lastTouchPositions = new Dictionary<int, Point>();
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
else if (_lastTouchPositions.ContainsKey(ea.Pointer.Id))
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
}
base.OnPointerPressed(ea);
}
protected override void OnPointerReleased(PointerReleasedEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (!ea.Properties.IsLeftButtonPressed)
_lastMousePosition = null;
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is not null && _lastTouchPositions.ContainsKey(ea.Pointer.Id))
_lastTouchPositions.Remove(ea.Pointer.Id);
}
base.OnPointerReleased(ea);
}
protected override void OnPointerMoved(PointerEventArgs ea)
{
if (ea.Pointer.Type == PointerType.Mouse)
{
if (_lastMousePosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем
{
base.OnPointerMoved(ea);
return;
}
var position = ea.GetPosition(this);
_panX -= (float)(_lastMousePosition.Value.X - position.X);
_panY += (float)(_lastMousePosition.Value.Y - position.Y); // Ось Y инвертирована
_lastMousePosition = position;
}
else if (ea.Pointer.Type == PointerType.Touch)
{
if (_lastTouchPositions is null)
{
base.OnPointerMoved(ea);
return;
}
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
{
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
base.OnPointerMoved(ea);
return;
}
if (_lastTouchPositions.Count() == 1 && _zoom != 1) // Если есть только одно прикосновение и зум != 1, то только перемещение
{
var position = ea.GetPosition(this);
_panX -= (float)(_lastTouchPositions[ea.Pointer.Id].X - position.X);
_panY += (float)(_lastTouchPositions[ea.Pointer.Id].Y - position.Y); // Ось Y инвертирована
_lastTouchPositions[ea.Pointer.Id] = position;
}
else if (_lastTouchPositions.Count() == 2) // Если два прикосновения, то перемещение с приближением
{
var lastPosition1 = _lastTouchPositions.Skip(0).First().Value;
var lastPosition2 = _lastTouchPositions.Skip(1).First().Value;
var lastDistance = Point.Distance(lastPosition1, lastPosition2);
var lastCenter = PointHelper.Center(lastPosition1, lastPosition2);
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
var newPosition1 = _lastTouchPositions.Skip(0).First().Value;
var newPosition2 = _lastTouchPositions.Skip(1).First().Value;
var newDistance = Point.Distance(newPosition1, newPosition2);
var newCenter = PointHelper.Center(newPosition1, newPosition2);
var zoomFactor = newDistance / lastDistance;
var newZoom = (float)(_zoom * zoomFactor);
newZoom = (float)Math.Clamp(newZoom, 1f, 10f);
var imageX = (newCenter.X - Bounds.Width / 2 - _panX) / _zoom;
var imageY = (-newCenter.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертирована
_zoom = newZoom;
if (_zoom > 1f)
{
_panX = (float)(newCenter.X - Bounds.Width / 2 - imageX * _zoom);
_panY = (float)(-newCenter.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертирована
}
else
{
_zoom = 1f;
_panX = 0f;
_panY = 0f;
}
}
}
base.OnPointerMoved(ea);
}
public override void Render(DrawingContext context)
{
context.FillRectangle(new SolidColorBrush(Background), new Rect(0, 0, Bounds.Width, Bounds.Height));
base.Render(context);
}
private void RenderRequested(object? sender, EventArgs ea)
{
Dispatcher.UIThread.Invoke(() =>
{
var transformBounds = this.GetTransformedBounds();
if (transformBounds is not null &&
(transformBounds.Value.Clip.Bottom != 0 ||
transformBounds.Value.Clip.Top != 0 ||
transformBounds.Value.Clip.Left != 0 ||
transformBounds.Value.Clip.Right != 0))
RequestNextFrameRendering();
});
}
protected override void OnOpenGlInit(GlInterface gl)
{
_logger.LogInformation("OpenGL initialized");
_logger.LogInformation("GL version: {}", gl.GetString(GlConsts.GL_VERSION));
_logger.LogInformation("GL vendor: {}", gl.GetString(GlConsts.GL_VENDOR));
_logger.LogInformation("GL renderer: {}", gl.GetString(GlConsts.GL_RENDERER));
try
{
_openGLContext = new OpenGLContext(gl);
_logger.LogInformation("OpenGL context initialized");
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Error while OpenGL initialization");
}
base.OnOpenGlInit(gl);
}
protected override void OnOpenGlRender(GlInterface gl, int fb)
{
if (_openGLContext is null)
{
_logger.LogError("OpenGL render called with uninitialized EGL");
return;
}
try
{
(LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)? frame = null;
while (frame is null && (DataContext as CameraViewModel)?.CanGrabViewFinderFrame() == true)
frame = (DataContext as CameraViewModel)?.GrabViewFinderFrame();
if (frame is null)
return;
var (frameBuffer, streamConfiguration) = frame.Value;
// 0. Проверяем что полученный фрейм ожидаемый правильный формат
var width = streamConfiguration.Size.Width;
if (width <= 0)
{
_logger.LogError("Cannot render frame, stream configuration validation error, expected frame width >= 0, got {}", streamConfiguration.Size.Width);
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
return;
}
var height = streamConfiguration.Size.Height;
if (height <= 0)
{
_logger.LogError("Cannot render frame, stream configuration validation error, expected frame height >= 0, got {}", streamConfiguration.Size.Height);
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
return;
}
if (streamConfiguration.PixelFormat.Fourcc != OpenGLContext.DRM_FORMAT_BGRX8888)
{
_logger.LogError("Cannot render frame, stream configuration validation error, expected fourcc: 0x{:X}, got 0x{:X}", OpenGLContext.DRM_FORMAT_BGRX8888, streamConfiguration.PixelFormat.Fourcc);
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
return;
}
if (streamConfiguration.PixelFormat.Modifier != 0)
{
_logger.LogError("Cannot render frame, stream configuration validation error, expected DRM modifier: 0x{:X}, got 0x{:X}", 0, streamConfiguration.PixelFormat.Modifier);
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
return;
}
if (frameBuffer.Planes.Count() != 1)
{
_logger.LogError("Cannot render frame, planes validation error, expected planes count: 0, got {}", frameBuffer.Planes.Count());
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
return;
}
var plane = frameBuffer.Planes.First();
// if (plane.Length != width * height * 4)
// {
// _logger.LogError("Cannot render frame, plane validation error, expected plane size 0x{:X}, got 0x{:X}", width * height * 4, plane.Length);
// (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
// return;
// }
var fd = plane.Fd.Get();
if (fd <= 0)
{
_logger.LogError("Cannot render frame, FD validation error, FD >= 0, got 0x{:X}", fd);
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
return;
}
var stretchMode = StretchMode;
var tileMode = TileMode;
// 1. Очищаем экран
gl.Disable(GlConsts.GL_SCISSOR_TEST);
gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height);
gl.ClearColor(0f, 0f, 0f, 0f);
gl.Clear(GlConsts.GL_COLOR_BUFFER_BIT);
// 2. Создаём EGL изображение из dma-buf
IntPtr image;
if (_importedDmaBuffers.ContainsKey(fd))
{
// 2.1. Если EGL изображение для fd уже создано, то берём его из словаря
image = _importedDmaBuffers[fd];
}
else
{
// 2.2. Если EGL изображение для fd не создано, то создаём
var attribs = new[]
{
EglConsts.EGL_WIDTH, (int)width,
EglConsts.EGL_HEIGHT, (int)height,
OpenGLContext.EGL_LINUX_DRM_FOURCC_EXT, unchecked((int)OpenGLContext.DRM_FORMAT_BGRX8888),
OpenGLContext.EGL_DMA_BUF_PLANE0_FD_EXT, fd,
OpenGLContext.EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
OpenGLContext.EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)(plane.Length / height),
EglConsts.EGL_NONE
};
image = _openGLContext.EglCreateImageKHR.Invoke(_openGLContext.EglDisplay, IntPtr.Zero, OpenGLContext.EGL_LINUX_DMA_BUF_EXT, IntPtr.Zero, attribs);
_importedDmaBuffers.Add(fd, image);
}
// 3. Биндим текстуры текстуры
gl.ActiveTexture(GlConsts.GL_TEXTURE0);
gl.BindTexture(GlConsts.GL_TEXTURE_2D, _openGLContext.Texture);
// 4. Привязываем EGL изображение к текстуре
_openGLContext.GlEGLImageTargetTexture2DOES!(GlConsts.GL_TEXTURE_2D, image);
// 5. Вычисляем матрицы трансформаций
var mvp = ComputeMvp(stretchMode, width, height);
var zoom = Matrix4x4.CreateScale(_zoom, _zoom, 1f);
var translate = Matrix4x4.CreateTranslation((float)(_panX / Bounds.Width * 2), (float)(_panY / Bounds.Height * 2), 0);
mvp *= zoom;
mvp *= translate;
var mvpArray = new[]
{
mvp.M11, mvp.M12, mvp.M13, mvp.M14,
mvp.M21, mvp.M22, mvp.M23, mvp.M24,
mvp.M31, mvp.M32, mvp.M33, mvp.M34,
mvp.M41, mvp.M42, mvp.M43, mvp.M44,
};
unsafe
{
fixed (float* pMvp = mvpArray)
gl.UniformMatrix4fv(_openGLContext.UMvpLocation, 1, false, pMvp);
}
var uv = ComputeUv(tileMode);
var uvArray = new[]
{
uv.M11, uv.M12, 0f,
uv.M21, uv.M22, 0f,
uv.M31, uv.M32, 1f
};
unsafe
{
fixed (float* pUv = uvArray)
OpenGLContext.glUniformMatrix3fv(_openGLContext.UUvLocation, 1, false, new IntPtr(pUv));
}
// 6. Запускаем шейдер
gl.UseProgram(_openGLContext.Program);
OpenGLContext.glUniform1i(_openGLContext.UTexLocation, 0);
// 7. Рисуем вершины на экране
gl.BindVertexArray(_openGLContext.VAO);
gl.DrawArrays(OpenGLContext.GL_TRIANGLE_STRIP, 0, 4);
// 8. Уничтожаем EGL изображение
// _openGLContext.EglDestroyImageKHR!(_openGLContext.EglDisplay, image);
(DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while OpenGL render");
}
}
private Matrix4x4 ComputeMvp(Stretch stretch, float imageWidth, float imageHeight)
{
float viewWidth = (float)Bounds.Width;
float viewHeight = (float)Bounds.Height;
float sx = 1f;
float sy = 1f;
float viewAspect = viewWidth / viewHeight;
float imgAspect = imageWidth / imageHeight;
switch (stretch)
{
case Stretch.None:
sx = imageWidth / viewWidth;
sy = imageHeight / viewHeight;
break;
case Stretch.Fill:
break;
case Stretch.Uniform:
if (imgAspect > viewAspect)
sy = viewAspect / imgAspect;
else
sx = imgAspect / viewAspect;
break;
case Stretch.UniformToFill:
if (imgAspect > viewAspect)
sx = imgAspect / viewAspect;
else
sy = viewAspect / imgAspect;
break;
}
return Matrix4x4.CreateScale(sx, sy, 1f);
}
private Matrix3x2 ComputeUv(TileMode mode)
{
float sx = 1f;
float sy = 1f;
float ox = 0f;
float oy = 0f;
switch (mode)
{
case TileMode.Tile:
sx = sy = 2f;
break;
case TileMode.FlipX:
sx = -1f;
ox = 1f;
break;
case TileMode.FlipY:
sy = -1f;
oy = 1f;
break;
case TileMode.FlipXY:
sx = sy = -1f;
ox = oy = 1f;
break;
}
return new Matrix3x2(
sx, 0,
0, sy,
ox, oy
);
}
private (float sx, float sy) ComputeScale(Stretch mode, float imageWidth, float imageHeight)
{
float viewWidth = (float)Bounds.Width;
float viewHeight = (float)Bounds.Height;
float scaleX = viewWidth / imageWidth;
float scaleY = viewHeight / imageHeight;
switch (mode)
{
case Stretch.None:
return (imageWidth / viewWidth, imageHeight / viewHeight);
case Stretch.Fill:
return (1f, 1f);
case Stretch.Uniform:
var sMin = MathF.Min(scaleX, scaleY);
return (imageWidth * sMin / viewWidth, imageHeight * sMin / viewHeight);
case Stretch.UniformToFill:
var sMax = MathF.Max(scaleX, scaleY);
return (imageWidth * sMax / viewWidth, imageHeight * sMax / viewHeight);
default:
return (1f, 1f);
}
}
}
@@ -1,94 +0,0 @@
<UserControl
x:Class="GSS2.UI.Core.Views.IlluminatorControllerView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:GSS2.UI.Core.Behaviors"
xmlns:converters="using:GSS2.UI.Core.Converters"
xmlns:layout="using:Avalonia.Layout"
xmlns:local="using:GSS2.UI.Core.Views"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
x:DataType="vm:IlluminatorControllerViewModel">
<UserControl.Styles>
<Style Selector="Slider.horizontal">
<Setter Property="VerticalAlignment" Value="Stretch" />
<Setter Property="Maximum" Value="1" />
<Setter Property="Minimum" Value="0" />
<Setter Property="Orientation" Value="Vertical" />
</Style>
<Style Selector="Slider.vertical">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Maximum" Value="1" />
<Setter Property="Minimum" Value="0" />
<Setter Property="Orientation" Value="Horizontal" />
</Style>
</UserControl.Styles>
<Grid>
<Grid IsVisible="{Binding Orientation, Converter={x:Static converters:OrientationToBoolConverter.IsHorizontal}}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="20">
<Slider Classes="horizontal" Value="{Binding WhiteIntensity}" Maximum="0.03" />
<Slider Classes="horizontal" Value="{Binding Uv365Intensity}" />
<Slider Classes="horizontal" Value="{Binding Uv254Intensity}" />
</StackPanel>
<ToggleButton
Grid.Row="1"
Margin="0,10,0,0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsChecked="{Binding Enabled}">
<TextBlock Text="Включить" FontSize="18"/>
</ToggleButton>
</Grid>
<Grid IsVisible="{Binding Orientation, Converter={x:Static converters:OrientationToBoolConverter.IsVertical}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="20">
<Slider Classes="vertical" Value="{Binding WhiteIntensity}" Maximum="0.03" />
<Slider Classes="vertical" Value="{Binding Uv365Intensity}" />
<Slider Classes="vertical" Value="{Binding Uv254Intensity}" />
</StackPanel>
<ToggleButton
Grid.Column="1"
Margin="0,0,10,0"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsChecked="{Binding Enabled}">
<TextBlock Text="Включить" FontSize="18"/>
</ToggleButton>
</Grid>
</Grid>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.UI.Core.Views;
public partial class IlluminatorControllerView : UserControl
{
public IlluminatorControllerView()
{
InitializeComponent();
}
}
-9
View File
@@ -1,9 +0,0 @@
<lvc:PieChart
x:Class="GSS2.UI.Core.Views.ResourceBarView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
x:DataType="vm:ResourceBarViewModel"
Bounds="{Binding Bounds, Mode=OneWayToSource}"
Series="{Binding Series}" />
@@ -1,11 +0,0 @@
using LiveChartsCore.SkiaSharpView.Avalonia;
namespace GSS2.UI.Core.Views;
public partial class ResourceBarView : PieChart
{
public ResourceBarView()
{
InitializeComponent();
}
}
@@ -1,13 +0,0 @@
<lvc:CartesianChart
x:Class="GSS2.UI.Core.Views.ResourceChartView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
x:DataType="vm:ResourceChartViewModel"
Bounds="{Binding Bounds, Mode=OneWayToSource}"
EasingFunction="{x:Null}"
Series="{Binding Series}"
XAxes="{Binding XAxes}"
YAxes="{Binding YAxes}"
ZoomMode="None" />
@@ -1,11 +0,0 @@
using LiveChartsCore.SkiaSharpView.Avalonia;
namespace GSS2.UI.Core.Views;
public partial class ResourceChartView : CartesianChart
{
public ResourceChartView()
{
InitializeComponent();
}
}
@@ -1,53 +0,0 @@
<UserControl
x:Class="GSS2.UI.Core.Views.ResourceCpuUsageBarsView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:GSS2.UI.Core.Behaviors"
xmlns:local="using:GSS2.UI.Core.Views"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
x:DataType="vm:ResourceCpuUsageBarsViewModel"
Bounds="{Binding Bounds, Mode=OneWayToSource}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsCoresColumnVisible}" />
<ColumnDefinition Width="*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsCoresColumnVisible}" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:ResourceBarView
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
DataContext="{Binding Cpu}" />
<local:ResourceBarView
Grid.Row="0"
Grid.Column="1"
DataContext="{Binding Cpu0}" />
<local:ResourceBarView
Grid.Row="0"
Grid.Column="2"
DataContext="{Binding Cpu1}" />
<local:ResourceBarView
Grid.Row="1"
Grid.Column="1"
DataContext="{Binding Cpu2}" />
<local:ResourceBarView
Grid.Row="1"
Grid.Column="2"
DataContext="{Binding Cpu3}" />
</Grid>
</UserControl>
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.UI.Core.Views;
public partial class ResourceCpuUsageBarsView : UserControl
{
public ResourceCpuUsageBarsView()
{
InitializeComponent();
}
}
-33
View File
@@ -1,33 +0,0 @@
<UserControl
x:Class="GSS2.UI.Core.Views.ResourcesView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:GSS2.UI.Core.Behaviors"
xmlns:local="using:GSS2.UI.Core.Views"
xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia"
xmlns:vm="using:GSS2.UI.Core.ViewModels"
x:DataType="vm:ResourcesViewModel"
Bounds="{Binding Bounds, Mode=OneWayToSource}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsChartsColumnVisible}" />
<ColumnDefinition Width="*" behaviors:GridColumnHideBehavior.IsVisible="{Binding IsBarsColumnVisible}" />
</Grid.ColumnDefinitions>
<Grid Column="0" RowDefinitions="*,*,*">
<local:ResourceChartView Grid.Row="0" DataContext="{Binding CpuUsageChartViewModel}" />
<local:ResourceChartView Grid.Row="1" DataContext="{Binding CpuTemperatureChartViewModel}" />
<local:ResourceChartView Grid.Row="2" DataContext="{Binding MemoryUsageChartViewModel}" />
</Grid>
<Grid Column="1" RowDefinitions="*,*,*">
<local:ResourceCpuUsageBarsView Grid.Row="0" DataContext="{Binding CpuUsageBarsViewModel}" />
<local:ResourceBarView Grid.Row="1" DataContext="{Binding CpuTemperatureBarViewModel}" />
<local:ResourceBarView Grid.Row="2" DataContext="{Binding MemoryUsageBarViewModel}" />
</Grid>
</Grid>
</UserControl>
-11
View File
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.UI.Core.Views;
public partial class ResourcesView : UserControl
{
public ResourcesView()
{
InitializeComponent();
}
}
+59
View File
@@ -3,10 +3,69 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:GSS2" xmlns:local="using:GSS2"
xmlns:converters="using:GSS2.UI.Core.Converters"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
<Application.Resources>
<converters:NotEqualsConverter x:Key="NotEqualsConverter" />
<converters:EqualsConverter x:Key="EqualsConverter" />
<converters:GreaterThanConverter x:Key="GreaterThanConverter" />
<converters:GreaterThanOrEqualConverter x:Key="GreaterThanOrEqualConverter" />
<converters:LessThanConverter x:Key="LessThanConverter" />
<converters:LessThanOrEqualConverter x:Key="LessThanOrEqualConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
<SolidColorBrush x:Key="MenuBackground" Color="#00357a" />
<SolidColorBrush x:Key="MenuBorder" Color="#0865b2" />
<SolidColorBrush x:Key="Overlay" Color="#0865b23f" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonDangerBackground" Color="#b20808" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
</Application.Resources>
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<Style Selector="Button">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource ButtonDangerBackground}" />
</Style>
<Style Selector="Button.vertical">
<Setter Property="MinWidth" Value="40" />
</Style>
<Style Selector="TextBox">
<Setter Property="FontSize" Value="20"/>
</Style>
<Style Selector="TextBox.singleline">
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="TextBox.multiline">
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="AcceptsReturn" Value="True" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<Style Selector="TextBlock">
<Setter Property="FontSize" Value="20"/>
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="TextBlock.mini">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="0,0,0,0" />
</Style>
</Application.Styles> </Application.Styles>
</Application> </Application>
+8 -2
View File
@@ -18,6 +18,8 @@ public partial class Application : Avalonia.Application
{ {
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private readonly bool _fullscreen; private readonly bool _fullscreen;
private readonly double _width;
private readonly double _height;
private readonly ILogger<Application> _logger; private readonly ILogger<Application> _logger;
private readonly DependencyInjectionViewLocator _viewLocator; private readonly DependencyInjectionViewLocator _viewLocator;
@@ -25,10 +27,12 @@ public partial class Application : Avalonia.Application
public NavigationService? Navigation { get; private set; } = null; public NavigationService? Navigation { get; private set; } = null;
public Application(IServiceProvider services, bool fullscreen = false) public Application(IServiceProvider services, bool fullscreen = false, int width = 0, int height = 0)
{ {
_services = services; _services = services;
_fullscreen = fullscreen; _fullscreen = fullscreen;
_width = width <= 0 ? double.NaN : width;
_height = height <= 0 ? double.NaN : height;
// Создаём AvaloniaLogSink, который перенаправляет логи из Avalonia в хост // Создаём AvaloniaLogSink, который перенаправляет логи из Avalonia в хост
var loggerFactory = _services.GetRequiredService<ILoggerFactory>(); var loggerFactory = _services.GetRequiredService<ILoggerFactory>();
@@ -124,7 +128,9 @@ public partial class Application : Avalonia.Application
{ {
WindowDecorations = _fullscreen ? Avalonia.Controls.WindowDecorations.None : Avalonia.Controls.WindowDecorations.Full, WindowDecorations = _fullscreen ? Avalonia.Controls.WindowDecorations.None : Avalonia.Controls.WindowDecorations.Full,
Topmost = _fullscreen, Topmost = _fullscreen,
WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal WindowState = _fullscreen ? Avalonia.Controls.WindowState.FullScreen : Avalonia.Controls.WindowState.Normal,
Width = _width,
Height = _height
}, },
}; };
desktop.MainWindow = mainWindow; desktop.MainWindow = mainWindow;
+23 -7
View File
@@ -28,6 +28,8 @@ public static class UI
public static readonly Option<RenderingModes> RenderingMode; public static readonly Option<RenderingModes> RenderingMode;
public static readonly Option<bool> HideConsole; public static readonly Option<bool> HideConsole;
public static readonly Option<bool> Mock; public static readonly Option<bool> Mock;
public static readonly Option<int> Width;
public static readonly Option<int> Height;
static UI() static UI()
{ {
@@ -56,6 +58,16 @@ public static class UI
Description = "Запуск в фиктивном режиме без инициализации и использования периферии", Description = "Запуск в фиктивном режиме без инициализации и использования периферии",
DefaultValueFactory = (result) => false DefaultValueFactory = (result) => false
}; };
Width = new("--width")
{
Description = "Ширина окна, если меньше или равна 0, то устанавливается ширина по-умолчанию",
DefaultValueFactory = (result) => 0
};
Height = new("--height")
{
Description = "Высота окна, если меньше или равна 0, то устанавливается высота по-умолчанию",
DefaultValueFactory = (result) => 0
};
// Изменяем описания стандартных опций // Изменяем описания стандартных опций
Helper.TranslateDefaultOptionDescriptions(RootCommand); Helper.TranslateDefaultOptionDescriptions(RootCommand);
@@ -65,12 +77,16 @@ public static class UI
RootCommand.Add(RenderingMode); RootCommand.Add(RenderingMode);
RootCommand.Add(HideConsole); RootCommand.Add(HideConsole);
RootCommand.Add(Mock); RootCommand.Add(Mock);
RootCommand.Add(Width);
RootCommand.Add(Height);
RootCommand.SetAction(CommandAction); RootCommand.SetAction(CommandAction);
Command.Add(Fullscreen); Command.Add(Fullscreen);
Command.Add(RenderingMode); Command.Add(RenderingMode);
Command.Add(HideConsole); Command.Add(HideConsole);
Command.Add(Mock); Command.Add(Mock);
Command.Add(Width);
Command.Add(Height);
Command.SetAction(CommandAction); Command.SetAction(CommandAction);
} }
@@ -92,6 +108,8 @@ public static class UI
var renderingMode = result.GetValue(RenderingMode); var renderingMode = result.GetValue(RenderingMode);
var hideConsole = result.GetValue(HideConsole); var hideConsole = result.GetValue(HideConsole);
var mock = result.GetValue(Mock); var mock = result.GetValue(Mock);
var width = result.GetValue(Width);
var height = result.GetValue(Height);
// Если выбран режим рендеринга DRM, то приложение будет рисоваться вместе с консолью, // Если выбран режим рендеринга DRM, то приложение будет рисоваться вместе с консолью,
// поэтому если не перехватить консоль, то вывод в неё будет рисоваться вместе с интерфейсом // поэтому если не перехватить консоль, то вывод в неё будет рисоваться вместе с интерфейсом
@@ -105,7 +123,7 @@ public static class UI
_ = host.RunAsync(); _ = host.RunAsync();
// Собираем графическое приложение // Собираем графическое приложение
var ui = BuildApplication(host, fullscreen); var ui = BuildApplication(host, fullscreen, width, height);
switch (renderingMode) switch (renderingMode)
{ {
case RenderingModes.X11: case RenderingModes.X11:
@@ -160,15 +178,13 @@ public static class UI
builder.Services.AddAmineContentContext(builder.Configuration); builder.Services.AddAmineContentContext(builder.Configuration);
// Добавляем сервисы аппаратной части и прочее // Добавляем сервисы аппаратной части и прочее
builder.Services.AddLightsService("Hardware:Lights");
if (!mock) if (!mock)
{ {
builder.Services.AddLightsService("Hardware:Lights"); // builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
builder.Services.AddIlluminatorService("Hardware:Illuminator"); builder.Services.AddIlluminatorService("Hardware:Illuminator");
builder.Services.AddCameraService("Hardware:Camera", true); builder.Services.AddCameraService("Hardware:Camera", true);
builder.Services.AddComputeResourcesService();
} }
builder.Services.AddImageStorageService(new DirectoryInfo(builder.Configuration.GetValue<string>("ImageStorage") ?? "images"));
builder.Services.AddAmineContentImageCapturerService(mock); builder.Services.AddAmineContentImageCapturerService(mock);
builder.Services.AddAmineContentAnalyzerService(); builder.Services.AddAmineContentAnalyzerService();
@@ -193,8 +209,8 @@ public static class UI
return host; return host;
} }
private static AppBuilder BuildApplication(IHost host, bool fullscreen) => private static AppBuilder BuildApplication(IHost host, bool fullscreen, int width, int height) =>
AppBuilder.Configure(() => new Application(host.Services, fullscreen)) AppBuilder.Configure(() => new Application(host.Services, fullscreen, width, height))
.UsePlatformDetect() .UsePlatformDetect()
.UseSkia() .UseSkia()
.LogToTrace() .LogToTrace()
+6 -8
View File
@@ -27,10 +27,6 @@ public static class DependencyInjectionHelper
.AddSingleton<AmineContentView>() .AddSingleton<AmineContentView>()
// Core // Core
.AddSingleton<GSS2.UI.Core.ViewModels.MainWindowViewModel>() .AddSingleton<GSS2.UI.Core.ViewModels.MainWindowViewModel>()
.AddSingleton<GSS2.UI.Core.ViewModels.CameraViewModel>()
.AddSingleton<GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel>()
.AddSingleton<GSS2.UI.Core.Views.CameraView>()
.AddSingleton<GSS2.UI.Core.Views.IlluminatorControllerView>()
// База // База
.AddSingleton<ViewModels.DeleteConfirmViewModel>() .AddSingleton<ViewModels.DeleteConfirmViewModel>()
.AddSingleton<ViewModels.NumericKeyboardViewModel>() .AddSingleton<ViewModels.NumericKeyboardViewModel>()
@@ -38,16 +34,18 @@ public static class DependencyInjectionHelper
.AddSingleton<Views.NumericKeyboardView>() .AddSingleton<Views.NumericKeyboardView>()
// Общие // Общие
.AddSingleton<ViewModels.Common.SystemViewModel>() .AddSingleton<ViewModels.Common.SystemViewModel>()
.AddSingleton<ViewModels.Common.CameraViewModel>() // .AddSingleton<ViewModels.Common.CameraViewModel>()
.AddSingleton<Views.Common.SystemView>() // .AddSingleton<Views.Common.SystemView>()
.AddSingleton<Views.Common.CameraView>() // .AddSingleton<Views.Common.CameraView>()
// AmineContent // AmineContent
.AddSingleton<ViewModels.AmineContent.StartViewModel>() .AddSingleton<ViewModels.AmineContent.StartViewModel>()
.AddSingleton<ViewModels.AmineContent.AnalysisViewModel>() .AddSingleton<ViewModels.AmineContent.AnalysisViewModel>()
.AddSingleton<ViewModels.AmineContent.SampleCalibrationViewModel>() .AddSingleton<ViewModels.AmineContent.SampleCalibrationViewModel>()
.AddSingleton<ViewModels.AmineContent.SeparatorCalibrationViewModel>() .AddSingleton<ViewModels.AmineContent.SeparatorCalibrationViewModel>()
.AddSingleton<ViewModels.AmineContent.BrandCalibrationViewModel>()
.AddSingleton<Views.AmineContent.StartView>() .AddSingleton<Views.AmineContent.StartView>()
.AddSingleton<Views.AmineContent.AnalysisView>() .AddSingleton<Views.AmineContent.AnalysisView>()
.AddSingleton<Views.AmineContent.SampleCalibrationView>() .AddSingleton<Views.AmineContent.SampleCalibrationView>()
.AddSingleton<Views.AmineContent.SeparatorCalibrationView>(); .AddSingleton<Views.AmineContent.SeparatorCalibrationView>()
.AddSingleton<Views.AmineContent.BrandCalibrationView>();
} }
+6 -11
View File
@@ -4,15 +4,7 @@ public partial class Program
{ {
// Аргументы командной строки // Аргументы командной строки
private static List<string> _args = new List<string>(); private static List<string> _args = new List<string>();
public static string[] Args public static string[] Args => _args.ToArray();
{
get
{
var copy = new string[_args.Count()];
_args.CopyTo(copy);
return copy;
}
}
/// <summary> /// <summary>
/// Точка входа /// Точка входа
@@ -60,8 +52,11 @@ public partial class Program
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine("Ошибка при выполнении программы:"); Console.Error.WriteLine("Ошибка при выполнении программы:");
Console.Write(ex.Message); Console.Error.WriteLine(ex.Message);
Console.Write(ex.StackTrace); if (ex.InnerException is not null)
Console.Error.WriteLine($"Внутреннее исключение: {ex.InnerException}");
Console.Error.WriteLine(ex.StackTrace);
Environment.Exit(1);
return 1; return 1;
} }
} }
File diff suppressed because it is too large Load Diff
@@ -31,6 +31,45 @@ public partial class BrandCalibrationViewModel : ViewModelBase
} }
} }
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
/// <summary>
/// <see cref="BrandRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="BrandRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="BrandRecord.BrandName"/>
/// </summary>
[ObservableProperty] public partial string BrandName { get; set; } = "";
/// <summary>
/// <see cref="BrandRecord.MixtureName"/>
/// </summary>
[ObservableProperty] public partial string MixtureName { get; set; } = "";
/// <summary>
/// <see cref="BrandRecord.MixtureNormalRate"/>
/// </summary>
[ObservableProperty] public partial double MixtureNormalRate { get; set; } = -1;
/// <summary>
/// <see cref="BrandRecord.Comment"/>
/// </summary>
[ObservableProperty] public partial string Comment { get; set; } = "";
}
/// <summary> /// <summary>
/// Логгер /// Логгер
/// </summary> /// </summary>
@@ -60,30 +99,33 @@ public partial class BrandCalibrationViewModel : ViewModelBase
/// Текущий выбранный элемент меню. Равен -1 если элемент не выбран /// Текущий выбранный элемент меню. Равен -1 если элемент не выбран
/// </summary> /// </summary>
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary> /// <summary>
/// <see cref="BrandRecord.Id"/> выбранного элемента /// Запись
/// </summary> /// </summary>
[ObservableProperty] public partial int SelectedId { get; set; } = 0; [ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <summary> /// <summary>
/// <see cref="BrandRecord.Uuid"/> выбранного элемента /// Текущий экран информационной секции
/// </summary> /// </summary>
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString(); [ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
/// <summary> /// <summary>
/// <see cref="BrandRecord.BrandName"/> выбранного элемента /// Можно ли переходить на следующий экран информационной секции
/// </summary> /// </summary>
[ObservableProperty] public partial string SelectedBrandName { get; set; } = ""; [ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
/// <summary> /// <summary>
/// <see cref="BrandRecord.MixtureName"/> выбранного элемента /// Можно ли переходить на предыдущий экран информационной секции
/// </summary> /// </summary>
[ObservableProperty] public partial string SelectedMixtureName { get; set; } = ""; [ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
/// <summary> /// <summary>
/// <see cref="BrandRecord.MixtureNormalRate"/> выбранного элемента /// Происходит ли переход между экранами информационной секции
/// </summary> /// </summary>
[ObservableProperty] public partial double SelectedMixtureNormalRate { get; set; } = -1; [ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
/// <summary> /// <summary>
/// Можно ли производить перемещение между экранами /// Можно ли взаимодействовать с объектом
/// </summary> /// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; } = true; [ObservableProperty] public partial bool CanInteract { get; set; } = true;
public BrandCalibrationViewModel( public BrandCalibrationViewModel(
ILogger<BrandCalibrationViewModel> logger, ILogger<BrandCalibrationViewModel> logger,
@@ -109,8 +151,9 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Подписываемся на изменения свойств для обновления CanScroll // Подписываемся на изменения свойств для обновления CanScroll
PropertyChanged += (_, ea) => PropertyChanged += (_, ea) =>
{ {
if (ea.PropertyName == nameof(MenuOpened)) if (ea.PropertyName == nameof(MenuOpened) ||
UpdateCanScroll(); ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
}; };
_logger.LogInformation("Инициализировано"); _logger.LogInformation("Инициализировано");
@@ -133,11 +176,14 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Если запись найдена, то заполняем поля из базы данных // Если запись найдена, то заполняем поля из базы данных
if (selectedRecord is not null) if (selectedRecord is not null)
{ {
SelectedId = selectedRecord.Id; Record.Id = selectedRecord.Id;
SelectedUuid = selectedRecord.Uuid; Record.Uuid = selectedRecord.Uuid;
SelectedBrandName = selectedRecord.BrandName; Record.CreateDateTime = selectedRecord.CreateDateTime;
SelectedMixtureName = selectedRecord.MixtureName; Record.EditDateTime = selectedRecord.EditDateTime;
SelectedMixtureNormalRate = selectedRecord.MixtureNormalRate; Record.BrandName = selectedRecord.BrandName;
Record.MixtureName = selectedRecord.MixtureName;
Record.MixtureNormalRate = selectedRecord.MixtureNormalRate;
Record.Comment = selectedRecord.Comment;
return; return;
} }
// Если значение не найдено в базе данных // Если значение не найдено в базе данных
@@ -156,19 +202,24 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Если элемент не выбран, заполняем поля пустыми значениями // Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент"); _logger.LogTrace("Выбран новый элемент");
SelectedId = 0; var dateTime = DateTime.UtcNow;
SelectedUuid = Guid.NewGuid().ToString(); Record.Id = 0;
SelectedBrandName = ""; Record.Uuid = Guid.NewGuid().ToString();
SelectedMixtureName = ""; Record.CreateDateTime = dateTime;
SelectedMixtureNormalRate = -1; Record.EditDateTime = dateTime;
Record.BrandName = "";
Record.MixtureName = "";
Record.MixtureNormalRate = -1;
Record.Comment = "";
} }
/// <summary> /// <summary>
/// Обновить значение <see cref="CanScroll"/> /// Обновить значение <see cref="CanInteract"/>
/// </summary> /// </summary>
private void UpdateCanScroll() private void UpdateCanInteract()
{ {
CanScroll = !MenuOpened; CanInteract = !MenuOpened;
CanInteract &= !InfoSectionIsScrolling;
} }
@@ -186,6 +237,15 @@ public partial class BrandCalibrationViewModel : ViewModelBase
/// </summary> /// </summary>
[RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened; [RelayCommand] private void ToggleMenu() => MenuOpened = !MenuOpened;
/// <summary>
/// Переключить информационную секцию на следующую
/// </summary>
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
/// <summary>
/// Переключить информационную секцию на предыдущую
/// </summary>
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
/// <summary> /// <summary>
/// Создать новую запись /// Создать новую запись
/// </summary> /// </summary>
@@ -212,7 +272,7 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи // TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try try
{ {
_logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName); _logger.LogTrace("Удаление записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
// Если запись не выбрана, то просто создаём новую запись // Если запись не выбрана, то просто создаём новую запись
if (MenuSelectedIndex < 0) if (MenuSelectedIndex < 0)
{ {
@@ -247,7 +307,7 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Сохраняем индекс удалённый записи и находим её в базе данных // Сохраняем индекс удалённый записи и находим её в базе данных
var deletedMenuIndex = MenuSelectedIndex; var deletedMenuIndex = MenuSelectedIndex;
var deletedRecordId = SelectedId; var deletedRecordId = Record.Id;
var deletedRecord = _context.BrandRecords.Find(deletedRecordId); var deletedRecord = _context.BrandRecords.Find(deletedRecordId);
// Создаём новую запись // Создаём новую запись
CreateRecord(); CreateRecord();
@@ -299,17 +359,21 @@ public partial class BrandCalibrationViewModel : ViewModelBase
{ {
try try
{ {
_logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), SelectedId, nameof(BrandRecord.BrandName), SelectedBrandName, nameof(BrandRecord.MixtureName), SelectedMixtureName); _logger.LogTrace("Сохранение записи: {}={}, {}={}, {}={}", nameof(BrandRecord.Id), Record.Id, nameof(BrandRecord.BrandName), Record.BrandName, nameof(BrandRecord.MixtureName), Record.MixtureName);
// Если запись не выбрана // Если запись не выбрана
if (MenuSelectedIndex < 0) if (MenuSelectedIndex < 0)
{ {
_logger.LogTrace("Создаём новую запись"); _logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Создаём новую запись // Создаём новую запись
var newRecord = new BrandRecord( var newRecord = new BrandRecord(
uuid: SelectedUuid, uuid: Record.Uuid,
brandName: SelectedBrandName, createDateTime: dateTime,
mixtureName: SelectedMixtureName, editDateTime: dateTime,
mixtureNormalRate: SelectedMixtureNormalRate brandName: Record.BrandName,
mixtureName: Record.MixtureName,
mixtureNormalRate: Record.MixtureNormalRate,
comment: Record.Comment
); );
// Добавляем запись в базу данных // Добавляем запись в базу данных
@@ -328,22 +392,22 @@ public partial class BrandCalibrationViewModel : ViewModelBase
else else
{ {
// Находим запись в базе данных // Находим запись в базе данных
var attachedRecord = _context.BrandRecords.Find(SelectedId); var attachedRecord = _context.BrandRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку // Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null) if (attachedRecord is null)
{ {
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.BrandRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.BrandRecords));
return; return;
} }
_logger.LogTrace("Записываем изменения в базу данных"); _logger.LogTrace("Записываем изменения в базу данных");
// Если в данных есть изменения, то записываем их
if (attachedRecord.BrandName != SelectedBrandName) // Записываем изменения
attachedRecord.BrandName = SelectedBrandName; attachedRecord.EditDateTime = DateTime.UtcNow;
if (attachedRecord.MixtureName != SelectedMixtureName) attachedRecord.BrandName = Record.BrandName;
attachedRecord.MixtureName = SelectedMixtureName; attachedRecord.MixtureName = Record.MixtureName;
if (attachedRecord.MixtureNormalRate != SelectedMixtureNormalRate) attachedRecord.MixtureNormalRate = Record.MixtureNormalRate;
attachedRecord.MixtureNormalRate = SelectedMixtureNormalRate; attachedRecord.Comment = Record.Comment;
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
_context.SaveChanges(); _context.SaveChanges();
@@ -351,8 +415,8 @@ public partial class BrandCalibrationViewModel : ViewModelBase
// Записываем изменения в меню // Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню"); _logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].BrandName = SelectedBrandName; MenuItems[MenuSelectedIndex].BrandName = Record.BrandName;
MenuItems[MenuSelectedIndex].MixtureName = SelectedMixtureName; MenuItems[MenuSelectedIndex].MixtureName = Record.MixtureName;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -53,6 +53,63 @@ public partial class SampleCalibrationViewModel : ViewModelBase
} }
} }
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
/// <summary>
/// <see cref="SampleRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="SampleRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.SampleName"/>
/// </summary>
[ObservableProperty] public partial string SampleName { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.SampleComment"/>
/// </summary>
[ObservableProperty] public partial string SampleComment { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.SamplingDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime SamplingDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.SamplingPlace"/>
/// </summary>
[ObservableProperty] public partial string SamplingPlace { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.MixtureActualRate"/>
/// </summary>
[ObservableProperty] public partial double MixtureActualRate { get; set; } = -1;
/// <summary>
/// <see cref="SampleRecord.MeasuredContent"/>
/// </summary>
[ObservableProperty] public partial double MeasuredContent { get; set; } = -1;
/// <summary>
/// Представление <see cref="SampleRecord.ImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
/// <summary>
/// Данные <see cref="SampleRecord.FeatureIds"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Features { get; set; } = new();
}
/// <summary> /// <summary>
/// Логгер /// Логгер
/// </summary> /// </summary>
@@ -83,19 +140,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// </summary> /// </summary>
private CancellationTokenSource? _captureImagesTaskCts = null; private CancellationTokenSource? _captureImagesTaskCts = null;
/// <summary>
/// <see cref="SampleRecord.BrandId"/> выбранного элемента
/// </summary>
private int _selectedBrandId = 0;
/// <summary>
/// Данные <see cref="SampleRecord.ImageId"/> выбранного элемента
/// </summary>
private string? _selectedImageData = null;
/// <summary>
/// Данные <see cref="SampleRecord.FeatureIds"/> выбранного элемента
/// </summary>
private List<float[]> _selectedFeaturesData = new();
/// <summary> /// <summary>
/// Меню открыто /// Меню открыто
/// </summary> /// </summary>
@@ -109,43 +153,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// </summary> /// </summary>
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary>
/// Запись
/// </summary>
[ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <summary>
/// <see cref="SampleRecord.Id"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial int SelectedId { get; set; } = 0;
/// <summary>
/// <see cref="SampleRecord.Uuid"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.SampleName"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedSampleName { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.SampleComment"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedSampleComment { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.SamplingDateTime"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial DateTime SelectedSamplingDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.SamplingPlace"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedSamplingPlace { get; set; } = "";
/// <summary>
/// <see cref="SampleRecord.MixtureActualRate"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial double SelectedMixtureActualRate { get; set; } = -1;
/// <summary>
/// <see cref="SampleRecord.MeasuredContent"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial double SelectedMeasuredContent { get; set; } = -1;
/// <summary>
/// Представление <see cref="SampleRecord.ImageId"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
/// <summary> /// <summary>
/// Список элементов выпадающего списка марок /// Список элементов выпадающего списка марок
/// </summary> /// </summary>
@@ -172,7 +184,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
/// </summary> /// </summary>
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
/// <summary> /// <summary>
/// Текущий экран информационной секции /// Текущий экран информационной секции
/// </summary> /// </summary>
@@ -200,9 +211,9 @@ public partial class SampleCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
/// <summary> /// <summary>
/// Можно ли производить перемещение между экранами /// Можно ли взаимодействовать с объектом
/// </summary> /// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; } [ObservableProperty] public partial bool CanInteract { get; set; } = true;
public SampleCalibrationViewModel( public SampleCalibrationViewModel(
ILogger<SampleCalibrationViewModel> logger, ILogger<SampleCalibrationViewModel> logger,
@@ -220,13 +231,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_navigation = navigation; _navigation = navigation;
_logger.LogInformation("Инициализация"); _logger.LogInformation("Инициализация");
// Получаем записи меню из базы данных
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
if (MenuItems.Count() > 0) // Если записи есть,
MenuSelectedIndex = 0; // Выбираем первую запись
else // Если записей нет,
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
// Получаем записи выпадающего списка марок из базы данных // Получаем записи выпадающего списка марок из базы данных
BrandComboBoxItems = new ObservableCollection<BrandComboBoxItemViewModel>(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName))); BrandComboBoxItems = new ObservableCollection<BrandComboBoxItemViewModel>(_context.BrandRecords.Select(r => new BrandComboBoxItemViewModel(r.Id, r.BrandName, r.MixtureName)));
// Подписываемся на изменения базы данных для отслеживания изменений списка марок // Подписываемся на изменения базы данных для отслеживания изменений списка марок
@@ -242,7 +246,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
return; return;
// Если запись добавлена // Если запись добавлена
if (ea.NewState is EntityState.Added) if (ea.OldState is EntityState.Added && ea.NewState is EntityState.Unchanged)
{ {
// Добавляем запись // Добавляем запись
BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName)); BrandComboBoxItems.Add(new BrandComboBoxItemViewModel(brandRecord.Id, brandRecord.BrandName, brandRecord.MixtureName));
@@ -284,13 +288,20 @@ public partial class SampleCalibrationViewModel : ViewModelBase
}); });
}; };
// Получаем записи меню из базы данных
MenuItems = new ObservableCollection<MenuItemViewModel>(_context.SampleRecords.Select(r => new MenuItemViewModel(r.Id, r.SampleName)));
if (MenuItems.Count() > 0) // Если записи есть,
MenuSelectedIndex = 0; // Выбираем первую запись
else // Если записей нет,
MenuSelectedIndex = -1; // Устанавливаем, что запись не выбрана
// Подписываемся на изменения свойств для обновления CanScroll // Подписываемся на изменения свойств для обновления CanScroll
PropertyChanged += (_, ea) => PropertyChanged += (_, ea) =>
{ {
if (ea.PropertyName == nameof(MenuOpened) || if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling) || ea.PropertyName == nameof(MainSectionIsScrolling) ||
ea.PropertyName == nameof(InfoSectionIsScrolling)) ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanScroll(); UpdateCanInteract();
}; };
_logger.LogInformation("Инициализировано"); _logger.LogInformation("Инициализировано");
@@ -314,32 +325,36 @@ public partial class SampleCalibrationViewModel : ViewModelBase
if (selectedRecord is not null) if (selectedRecord is not null)
{ {
// Заполняем простые поля // Заполняем простые поля
SelectedId = selectedRecord.Id; Record.Id = selectedRecord.Id;
SelectedUuid = selectedRecord.Uuid; Record.Uuid = selectedRecord.Uuid;
SelectedSampleName = selectedRecord.SampleName; Record.CreateDateTime = selectedRecord.CreateDateTime;
SelectedSampleComment = selectedRecord.SampleComment; Record.EditDateTime = selectedRecord.EditDateTime;
SelectedSamplingDateTime = selectedRecord.SamplingDateTime; Record.SampleName = selectedRecord.SampleName;
SelectedSamplingPlace = selectedRecord.SamplingPlace; Record.SampleComment = selectedRecord.SampleComment;
SelectedMixtureActualRate = selectedRecord.MixtureActualRate; Record.SamplingDateTime = selectedRecord.SamplingDateTime;
SelectedMeasuredContent = selectedRecord.MeasuredContent; Record.SamplingPlace = selectedRecord.SamplingPlace;
_selectedBrandId = selectedRecord.BrandId; Record.MixtureActualRate = selectedRecord.MixtureActualRate;
Record.MeasuredContent = selectedRecord.MeasuredContent;
if (selectedRecord.Features.Count() > 0)
Record.Features = selectedRecord.Features.Chunk(AnalyzerService.FEATURES_LENGTH).Select(v => v.ToArray()).ToList();
else
Record.Features = new List<float[]>();
// Ищем изображение в базе данных // Ищем изображение в базе данных
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку // Если изображение не найдено в базе данных, то логгируем как ошибку
if (imageRecord is null) if (imageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение, оно заполнится null если запись не найдена // В любом случае заполняем изображение
_selectedImageData = imageRecord?.Base64; Record.Image.Data = imageRecord?.Base64 ?? "";
SelectedImage.Data = _selectedImageData;
// Ищем марку в базе данных // Ищем марку в базе данных
var brandRecord = _context.BrandRecords.Find(_selectedBrandId); var brandRecord = _context.BrandRecords.Find(selectedRecord.BrandId);
// Если марка не найдена в базе данных // Если марка не найдена в базе данных
if (brandRecord is null) if (brandRecord is null)
{ {
// Логгируем как ошибку // Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", _selectedBrandId, nameof(_context.BrandRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.BrandId, nameof(_context.BrandRecords));
// Устанавливаем, что элемент не выбран // Устанавливаем, что элемент не выбран
BrandComboBoxSelectedIndex = -1; BrandComboBoxSelectedIndex = -1;
} }
@@ -356,12 +371,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
BrandComboBoxSelectedIndex = BrandComboBoxItems.IndexOf(brandComboBoxItem); BrandComboBoxSelectedIndex = BrandComboBoxItems.IndexOf(brandComboBoxItem);
} }
// Ищем все признаки в базе данных
_selectedFeaturesData = _context.FeatureRecords
.Where(r => r.SampleRecordId == SelectedId)
.Select(r => r.Values.ToArray())
.ToList();
return; return;
} }
// Если значение не найдено в базе данных // Если значение не найдено в базе данных
@@ -380,36 +389,30 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если элемент не выбран, заполняем поля пустыми значениями // Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент"); _logger.LogTrace("Выбран новый элемент");
SelectedId = 0; var dateTime = DateTime.UtcNow;
SelectedUuid = Guid.NewGuid().ToString(); Record.Id = 0;
SelectedSampleName = ""; Record.Uuid = Guid.NewGuid().ToString();
SelectedSampleComment = ""; Record.CreateDateTime = dateTime;
SelectedSamplingDateTime = DateTime.UtcNow; Record.EditDateTime = dateTime;
SelectedSamplingPlace = ""; Record.SampleName = "";
SelectedMixtureActualRate = -1; Record.SampleComment = "";
SelectedMeasuredContent = -1; Record.SamplingDateTime = dateTime;
SelectedImage.Data = null; Record.SamplingPlace = "";
Record.MixtureActualRate = -1;
Record.MeasuredContent = -1;
Record.Image.Data = "";
Record.Features = new List<float[]>();
BrandComboBoxSelectedIndex = -1; BrandComboBoxSelectedIndex = -1;
} }
/// <summary> /// <summary>
/// Вызывается при изменении <see cref="BrandComboBoxSelectedIndexC"/> /// Обновить значение <see cref="CanInteract"/>
/// </summary> /// </summary>
partial void OnBrandComboBoxSelectedIndexChanged(int value) private void UpdateCanInteract()
{ {
if (value >= 0 && value < BrandComboBoxItems.Count()) CanInteract = !MenuOpened;
_selectedBrandId = BrandComboBoxItems[value].Id; CanInteract &= !MainSectionIsScrolling;
} CanInteract &= !InfoSectionIsScrolling;
/// <summary>
/// Обновить значение <see cref="CanScroll"/>
/// </summary>
private void UpdateCanScroll()
{
CanScroll = !MenuOpened;
CanScroll &= !MainSectionIsScrolling;
CanScroll &= !InfoSectionIsScrolling;
CanScroll &= !ImagesIsCapturing;
} }
// Команды управления меню // Команды управления меню
@@ -449,7 +452,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и // Если уже создана новая запись, то MenuSelectedIndex может быть равен -1 и
// при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется, // при простом присвоении MenuSelectedIndex = -1 событие OnMenuSelectedIndexChanged не вызовется,
// потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1, // потому что событие срабатывает только при изменении значения, а значение было -1 и привилось ему -1,
// поэтому если SelectedRecord != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged // поэтому если Record.Record != -1, то нужно явно вызвать метод OnMenuSelectedIndexChanged
if (MenuSelectedIndex != -1) if (MenuSelectedIndex != -1)
MenuSelectedIndex = -1; MenuSelectedIndex = -1;
else else
@@ -465,7 +468,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи // TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try try
{ {
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName); _logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.SampleName), Record.SampleName);
// Если запись не выбрана, то просто создаём новую запись // Если запись не выбрана, то просто создаём новую запись
if (MenuSelectedIndex < 0) if (MenuSelectedIndex < 0)
{ {
@@ -500,7 +503,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Сохраняем индекс удалённый записи и находим её в базе данных // Сохраняем индекс удалённый записи и находим её в базе данных
var deletedMenuIndex = MenuSelectedIndex; var deletedMenuIndex = MenuSelectedIndex;
var deletedRecordId = SelectedId; var deletedRecordId = Record.Id;
var deletedRecord = _context.SampleRecords.Find(deletedRecordId); var deletedRecord = _context.SampleRecords.Find(deletedRecordId);
// Создаём новую запись // Создаём новую запись
CreateRecord(); CreateRecord();
@@ -510,7 +513,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку // Если удалённая запись не найдена в базе данных, то логгируем как ошибку
if (deletedRecord is null) if (deletedRecord is null)
{ {
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SampleRecords));
return; return;
} }
@@ -552,33 +555,50 @@ public partial class SampleCalibrationViewModel : ViewModelBase
{ {
try try
{ {
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), SelectedId, nameof(SampleRecord.SampleName), SelectedSampleName); _logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SampleRecord.Id), Record.Id, nameof(SampleRecord.SampleName), Record.SampleName);
// Если запись не выбрана // Если запись не выбрана
if (MenuSelectedIndex < 0) if (MenuSelectedIndex < 0)
{ {
// Создаём запись изображения // Создаём запись изображения
_logger.LogTrace("Создаём новую запись изображения"); var imageId = 0;
// Заполняем запись изображения if (!string.IsNullOrEmpty(Record.Image.Data))
var imageRecord = new ImageRecord(base64: _selectedImageData ?? ""); {
// Добавляем запись изображения в базу данных _logger.LogTrace("Создаём новую запись изображения");
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); // Заполняем запись изображения
// Сохраняем изменения в базе данных var imageRecord = new ImageRecord(base64: Record.Image.Data);
_context.SaveChanges(); // Добавляем запись изображения в базу данных
_logger.LogTrace("Запись изображения сохранена"); var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
imageId = attachedImageRecord.Entity.Id;
}
// Определяем Id выбранной марки
var selectedBrandId = 0;
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count());
// Если значение больше 0, то достаём Id из списка элементов
else if (BrandComboBoxSelectedIndex >= 0)
selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id;
_logger.LogTrace("Создаём новую запись"); _logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Заполняем запись // Заполняем запись
var newRecord = new SampleRecord( var newRecord = new SampleRecord(
uuid: SelectedUuid, uuid: Record.Uuid,
sampleName: SelectedSampleName, createDateTime: dateTime,
sampleComment: SelectedSampleComment, editDateTime: dateTime,
samplingDateTime: SelectedSamplingDateTime, sampleName: Record.SampleName,
brandId: _selectedBrandId, sampleComment: Record.SampleComment,
samplingPlace: SelectedSamplingPlace, samplingDateTime: Record.SamplingDateTime,
mixtureActualRate: SelectedMixtureActualRate, brandId: selectedBrandId,
measuredContent: SelectedMeasuredContent, samplingPlace: Record.SamplingPlace,
imageId: attachedImageRecord.Entity.Id, mixtureActualRate: Record.MixtureActualRate,
featureIds: [] measuredContent: Record.MeasuredContent,
imageId: imageId,
features: Record.Features
); );
// Добавляем запись в базу данных // Добавляем запись в базу данных
var attachedRecord = _context.SampleRecords.Add(newRecord); var attachedRecord = _context.SampleRecords.Add(newRecord);
@@ -586,29 +606,6 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_context.SaveChanges(); _context.SaveChanges();
_logger.LogTrace("Запись сохранена"); _logger.LogTrace("Запись сохранена");
_logger.LogTrace("Создаём новые записи признаков");
// Заполняем записи признаков и добавляем их в базу данных
var attachedFeatureRecords = new List<FeatureRecord>();
foreach (var feature in _selectedFeaturesData)
{
var featureRecord = new FeatureRecord(
values: feature,
sampleRecordId: attachedRecord.Entity.Id
);
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
attachedFeatureRecords.Add(attachedFeatureRecord.Entity);
}
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Записи признаков сохранены");
_logger.LogTrace("Заполняем признаки в сохранённой записи");
// Добавляем идентификаторы признаков в запись
attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id));
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Сохранение записи завершено");
// Добавляем новый элемент в меню // Добавляем новый элемент в меню
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName)); MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.SampleName));
// Выбираем последний элемент из меню // Выбираем последний элемент из меню
@@ -618,11 +615,11 @@ public partial class SampleCalibrationViewModel : ViewModelBase
else else
{ {
// Находим запись в базе данных // Находим запись в базе данных
var attachedRecord = _context.SampleRecords.Find(SelectedId); var attachedRecord = _context.SampleRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку // Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null) if (attachedRecord is null)
{ {
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SampleRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SampleRecords));
return; return;
} }
@@ -630,79 +627,65 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Ищем запись изображения в базе данных // Ищем запись изображения в базе данных
var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId); var attachedImageRecord = _context.ImageRecords.Find(attachedRecord.ImageId);
// Если запись не найдена // Если запись изображения не найдена
if (attachedImageRecord is null) if (attachedImageRecord is null)
{ {
// Логгируем как ошибку // Если изображение есть
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords)); if (!string.IsNullOrEmpty(Record.Image.Data))
_logger.LogTrace("Создаём новую запись изображения");
// Заполняем запись изображения
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
// Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
}
// Ищем записи признаков в базе данных
var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SampleRecordId == SelectedId);
// Если количество признаков в базе данных больше чем текущее
if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count())
{
// Находим количество признаков которое нужно удалить
var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count();
// Логгируем
_logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count());
// Удаляем необходимое количество признаков из конца
attachedFeatureRecords.TakeLast(featuresRecordsToRemove);
}
// Заполняем все имеющиеся признаки в базе данных новыми значениями
foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate())
attachedFeatureRecord.Values = _selectedFeaturesData[i];
// Если количество признаков в безе данных меньше чем текущее
if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count())
{
// Находим количество признаков которое нужно добавить
var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count();
// Логгируем
_logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count());
// Заполняем записи недостающих признаков и добавляем их в базу данных
for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++)
{ {
var featureRecord = new FeatureRecord( _logger.LogTrace("Создаём новую запись изображения");
values: _selectedFeaturesData[i], // Заполняем запись изображения
sampleRecordId: attachedRecord.Id var imageRecord = new ImageRecord(base64: Record.Image.Data);
); // Добавляем запись изображения в базу данных
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord); attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
attachedFeatureRecords.Append(attachedFeatureRecord.Entity); // Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
attachedRecord.ImageId = attachedImageRecord.Id;
}
}
// Если запись изображения найдена
else
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения
attachedImageRecord.Base64 = Record.Image.Data;
// Сохраняем изменения в базе данных
_context.SaveChanges();
}
// Если изображения нет
else
{
// Удаляем изображение из базы данных
_context.ImageRecords.Remove(attachedImageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
attachedRecord.ImageId = 0;
} }
} }
_context.SaveChanges(); // Определяем Id выбранной марки
_logger.LogTrace("Записи признаков сохранена"); var selectedBrandId = 0;
// Если значение выходит за верхнюю границу списка элементов меню, то логгируем как ошибку
if (BrandComboBoxSelectedIndex >= BrandComboBoxItems.Count())
_logger.LogError("Значение {}({}) больше количества элементов {}({})", nameof(BrandComboBoxSelectedIndex), BrandComboBoxSelectedIndex, nameof(BrandComboBoxItems), BrandComboBoxItems.Count());
// Если значение больше 0, то достаём Id из списка элементов
else if (BrandComboBoxSelectedIndex >= 0)
selectedBrandId = BrandComboBoxItems[BrandComboBoxSelectedIndex].Id;
// Если в данных есть изменения, то записываем их // Записываем изменения
if (attachedRecord.SampleName != SelectedSampleName) attachedRecord.EditDateTime = DateTime.UtcNow;
attachedRecord.SampleName = SelectedSampleName; attachedRecord.SampleName = Record.SampleName;
if (attachedRecord.SampleComment != SelectedSampleComment) attachedRecord.SampleComment = Record.SampleComment;
attachedRecord.SampleComment = SelectedSampleComment; attachedRecord.SamplingDateTime = Record.SamplingDateTime;
if (attachedRecord.SamplingDateTime != SelectedSamplingDateTime) attachedRecord.BrandId = selectedBrandId;
attachedRecord.SamplingDateTime = SelectedSamplingDateTime; attachedRecord.SamplingPlace = Record.SamplingPlace;
if (attachedRecord.BrandId != _selectedBrandId) attachedRecord.MixtureActualRate = Record.MixtureActualRate;
attachedRecord.BrandId = _selectedBrandId; attachedRecord.MeasuredContent = Record.MeasuredContent;
if (attachedRecord.SamplingPlace != SelectedSamplingPlace) attachedRecord.Features = Record.Features.SelectMany(fs => fs).ToList();
attachedRecord.SamplingPlace = SelectedSamplingPlace;
if (attachedRecord.MixtureActualRate != SelectedMixtureActualRate)
attachedRecord.MixtureActualRate = SelectedMixtureActualRate;
if (attachedRecord.MeasuredContent != SelectedMeasuredContent)
attachedRecord.MeasuredContent = SelectedMeasuredContent;
if (attachedRecord.ImageId != attachedImageRecord.Id)
attachedRecord.ImageId = attachedImageRecord.Id;
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
_context.SaveChanges(); _context.SaveChanges();
@@ -710,7 +693,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Записываем изменения в меню // Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню"); _logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].SampleName = SelectedSampleName; MenuItems[MenuSelectedIndex].SampleName = Record.SampleName;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -736,10 +719,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
_captureImagesTaskCts = new CancellationTokenSource(); _captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token; var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}";
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Запускаем задачу получения изображений // Запускаем задачу получения изображений
_сaptureImagesTask = Task.Run(async () => _сaptureImagesTask = Task.Run(async () =>
@@ -748,7 +728,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
var imagesData = new List<Task<ImageData>>(); var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
@@ -761,7 +741,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}"); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
@@ -793,7 +773,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений"); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
@@ -846,7 +826,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
var (avgImage, roi, features) = result.Value; var (avgImage, roi, features) = result.Value;
// Сохраняем вектор признаков // Сохраняем вектор признаков
_selectedFeaturesData = features; Record.Features = features;
// Обрабатываем среднее изображение // Обрабатываем среднее изображение
avgImage = new OpenCvSharp.Mat(avgImage, roi); avgImage = new OpenCvSharp.Mat(avgImage, roi);
@@ -861,15 +841,14 @@ public partial class SampleCalibrationViewModel : ViewModelBase
// Кодируем среднее изображение // Кодируем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
_selectedImageData = Convert.ToBase64String(avgData);
// Очищаем память среднего изображения // Очищаем память среднего изображения
avgImage.Dispose(); avgImage.Dispose();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData); _ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
}, cancellationToken); }, cancellationToken);
// Создаём задачу которая выполнится после завершения всех других // Создаём задачу которая выполнится после завершения всех других
@@ -922,7 +901,7 @@ public partial class SampleCalibrationViewModel : ViewModelBase
} }
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => _ = Dispatcher.UIThread.InvokeAsync(() =>
{ {
@@ -35,6 +35,45 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
} }
} }
/// <summary>
/// Внутренняя модель записи
/// </summary>
public partial class RecordViewModel : ViewModelBase
{
/// <summary>
/// <see cref="SeparatorRecord.Id"/>
/// </summary>
[ObservableProperty] public partial int Id { get; set; } = 0;
/// <summary>
/// <see cref="SeparatorRecord.Uuid"/>
/// </summary>
[ObservableProperty] public partial string Uuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SampleRecord.CreateDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime CreateDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SampleRecord.EditDateTime"/>
/// </summary>
[ObservableProperty] public partial DateTime EditDateTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// <see cref="SeparatorRecord.Type"/>
/// </summary>
[ObservableProperty] public partial string Type { get; set; } = "";
/// <summary>
/// <see cref="SeparatorRecord.Comment"/>
/// </summary>
[ObservableProperty] public partial string Comment { get; set; } = "";
/// <summary>
/// Представление <see cref="SeparatorRecord.ImageId"/>
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel Image { get; set; } = new();
/// <summary>
/// Данные <see cref="SeparatorRecord.Features"/>
/// </summary>
[ObservableProperty] public partial List<float[]> Features { get; set; } = new();
}
/// <summary> /// <summary>
/// Логгер /// Логгер
/// </summary> /// </summary>
@@ -65,15 +104,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// </summary> /// </summary>
private CancellationTokenSource? _captureImagesTaskCts = null; private CancellationTokenSource? _captureImagesTaskCts = null;
/// <summary>
/// Данные <see cref="SeparatorRecord.ImageId"/> выбранного элемента
/// </summary>
private string? _selectedImageData = null;
/// <summary>
/// Данные <see cref="SeparatorRecord.FeatureIds"/> выбранного элемента
/// </summary>
private List<float[]> _selectedFeaturesData = new();
/// <summary> /// <summary>
/// Меню открыто /// Меню открыто
/// </summary> /// </summary>
@@ -88,21 +118,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1; [ObservableProperty] public partial int MenuSelectedIndex { get; set; } = -1;
/// <summary> /// <summary>
/// <see cref="SeparatorRecord.Id"/> выбранного элемента /// Запись
/// </summary> /// </summary>
[ObservableProperty] public partial int SelectedId { get; set; } = 0; [ObservableProperty] public partial RecordViewModel Record { get; set; } = new();
/// <summary>
/// <see cref="SeparatorRecord.Uuid"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedUuid { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// <see cref="SeparatorRecord.Type"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial string SelectedType { get; set; } = "";
/// <summary>
/// Представление <see cref="SeparatorRecord.ImageId"/> выбранного элемента
/// </summary>
[ObservableProperty] public partial AsyncImageRecordViewModel SelectedImage { get; set; } = new();
/// <summary> /// <summary>
/// Текущий экран главной секции /// Текущий экран главной секции
@@ -112,6 +130,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// Можно ли переходить на следующий экран главной секции /// Можно ли переходить на следующий экран главной секции
/// </summary> /// </summary>
[ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false; [ObservableProperty] public partial bool MainSectionCanScrollBackward { get; set; } = false;
/// <summary> /// <summary>
/// Можно ли переходить на предыдущий экран главной секции /// Можно ли переходить на предыдущий экран главной секции
/// </summary> /// </summary>
@@ -120,6 +139,22 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// Происходит ли переход между экранами главной секции /// Происходит ли переход между экранами главной секции
/// </summary> /// </summary>
[ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false; [ObservableProperty] public partial bool MainSectionIsScrolling { get; set; } = false;
/// <summary>
/// Текущий экран информационной секции
/// </summary>
[ObservableProperty] public partial int InfoSectionCurrentIndex { get; set; } = 0;
/// <summary>
/// Можно ли переходить на следующий экран информационной секции
/// </summary>
[ObservableProperty] public partial bool InfoSectionCanScrollBackward { get; set; } = false;
/// <summary>
/// Можно ли переходить на предыдущий экран информационной секции
/// </summary>
[ObservableProperty] public partial bool InfoSectionCanScrollForward { get; set; } = false;
/// <summary>
/// Происходит ли переход между экранами информационной секции
/// </summary>
[ObservableProperty] public partial bool InfoSectionIsScrolling { get; set; } = false;
/// <summary> /// <summary>
/// Проводится ли съёмка изображений /// Проводится ли съёмка изображений
@@ -131,9 +166,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
[ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = ""; [ObservableProperty] public partial string ImagesCapturingProgress { get; set; } = "";
/// <summary> /// <summary>
/// Можно ли производить перемещение между экранами /// Можно ли взаимодействовать с объектом
/// </summary> /// </summary>
[ObservableProperty] public partial bool CanScroll { get; set; } [ObservableProperty] public partial bool CanInteract { get; set; } = true;
public SeparatorCalibrationViewModel( public SeparatorCalibrationViewModel(
ILogger<SeparatorCalibrationViewModel> logger, ILogger<SeparatorCalibrationViewModel> logger,
@@ -162,8 +197,9 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
PropertyChanged += (_, ea) => PropertyChanged += (_, ea) =>
{ {
if (ea.PropertyName == nameof(MenuOpened) || if (ea.PropertyName == nameof(MenuOpened) ||
ea.PropertyName == nameof(MainSectionIsScrolling)) ea.PropertyName == nameof(MainSectionIsScrolling) ||
UpdateCanScroll(); ea.PropertyName == nameof(InfoSectionIsScrolling))
UpdateCanInteract();
}; };
_logger.LogInformation("Инициализировано"); _logger.LogInformation("Инициализировано");
@@ -187,24 +223,26 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
if (selectedRecord is not null) if (selectedRecord is not null)
{ {
// Заполняем простые поля // Заполняем простые поля
SelectedId = selectedRecord.Id; Record.Id = selectedRecord.Id;
SelectedUuid = selectedRecord.Uuid; Record.Uuid = selectedRecord.Uuid;
SelectedType = selectedRecord.Type; Record.CreateDateTime = selectedRecord.CreateDateTime;
Record.EditDateTime = selectedRecord.EditDateTime;
Record.Type = selectedRecord.Type;
Record.Comment = selectedRecord.Comment;
if (selectedRecord.Features.Count() > 0)
Record.Features = selectedRecord.Features.Chunk(AnalyzerService.FEATURES_LENGTH).Select(v => v.ToArray()).ToList();
else
Record.Features = new List<float[]>();
// Ищем изображение в базе данных // Ищем изображение в базе данных
var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId); var imageRecord = _context.ImageRecords.Find(selectedRecord.ImageId);
// Если изображение не найдено в базе данных, то логгируем как ошибку // Если изображение не найдено в базе данных, то логгируем как ошибку
if (imageRecord is null) if (imageRecord is null)
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", selectedRecord.ImageId, nameof(_context.ImageRecords));
// В любом случае заполняем изображение, оно заполнится null если запись не найдена // В любом случае заполняем изображение
_selectedImageData = imageRecord?.Base64; Record.Image.Data = imageRecord?.Base64 ?? "";
SelectedImage.Data = _selectedImageData;
// Ищем все признаки в базе данных // Ищем все признаки в базе данных
_selectedFeaturesData = _context.FeatureRecords
.Where(r => r.SeparatorRecordId == SelectedId)
.Select(r => r.Values.ToArray())
.ToList();
return; return;
} }
@@ -224,20 +262,25 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Если элемент не выбран, заполняем поля пустыми значениями // Если элемент не выбран, заполняем поля пустыми значениями
_logger.LogTrace("Выбран новый элемент"); _logger.LogTrace("Выбран новый элемент");
SelectedId = 0; var dateTime = DateTime.UtcNow;
SelectedUuid = Guid.NewGuid().ToString(); Record.Id = 0;
SelectedType = ""; Record.Uuid = Guid.NewGuid().ToString();
SelectedImage.Data = null; Record.CreateDateTime = dateTime;
Record.EditDateTime = dateTime;
Record.Type = "";
Record.Comment = "";
Record.Image.Data = "";
Record.Features = new List<float[]>();
} }
/// <summary> /// <summary>
/// Обновить значение <see cref="CanScroll"/> /// Обновить значение <see cref="CanInteract"/>
/// </summary> /// </summary>
private void UpdateCanScroll() private void UpdateCanInteract()
{ {
CanScroll = !MenuOpened; CanInteract = !MenuOpened;
CanScroll &= !MainSectionIsScrolling; CanInteract &= !MainSectionIsScrolling;
CanScroll &= !ImagesIsCapturing; CanInteract &= !InfoSectionIsScrolling;
} }
// Команды управления меню // Команды управления меню
@@ -258,6 +301,14 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
/// Переключить главную секцию информация <-> изображения /// Переключить главную секцию информация <-> изображения
/// </summary> /// </summary>
[RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0; [RelayCommand] private void SwitchMainSection() => MainSectionCurrentIndex = MainSectionCurrentIndex == 0 ? 1 : 0;
/// <summary>
/// Переключить информационную секцию на следующую
/// </summary>
[RelayCommand] private void InfoSectionPrevious() => InfoSectionCurrentIndex--;
/// <summary>
/// Переключить информационную секцию на предыдущую
/// </summary>
[RelayCommand] private void InfoSectionNext() => InfoSectionCurrentIndex++;
/// <summary> /// <summary>
/// Создать новую запись /// Создать новую запись
@@ -285,7 +336,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи // TODO: Добавить определение и отображение количества затрагиваемых, записей которые будут повреждены при удалении этой записи
try try
{ {
_logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType); _logger.LogTrace("Удаление записи: {}={}, {}={}", nameof(SeparatorRecord.Id), Record.Id, nameof(SeparatorRecord.Type), Record.Type);
// Если запись не выбрана, то просто создаём новую запись // Если запись не выбрана, то просто создаём новую запись
if (MenuSelectedIndex < 0) if (MenuSelectedIndex < 0)
{ {
@@ -320,7 +371,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Сохраняем индекс удалённый записи и находим её в базе данных // Сохраняем индекс удалённый записи и находим её в базе данных
var deletedMenuIndex = MenuSelectedIndex; var deletedMenuIndex = MenuSelectedIndex;
var deletedRecordId = SelectedId; var deletedRecordId = Record.Id;
var deletedRecord = _context.SeparatorRecords.Find(deletedRecordId); var deletedRecord = _context.SeparatorRecords.Find(deletedRecordId);
// Создаём новую запись // Создаём новую запись
CreateRecord(); CreateRecord();
@@ -330,7 +381,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Если удалённая запись не найдена в базе данных, то логгируем как ошибку // Если удалённая запись не найдена в базе данных, то логгируем как ошибку
if (deletedRecord is null) if (deletedRecord is null)
{ {
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.BrandRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", deletedRecordId, nameof(_context.SeparatorRecords));
return; return;
} }
@@ -372,27 +423,36 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
{ {
try try
{ {
_logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), SelectedId, nameof(SeparatorRecord.Type), SelectedType); _logger.LogTrace("Сохранение записи: {}={}, {}={}", nameof(SeparatorRecord.Id), Record.Id, nameof(SeparatorRecord.Type), Record.Type);
// Если запись не выбрана // Если запись не выбрана
if (MenuSelectedIndex < 0) if (MenuSelectedIndex < 0)
{ {
// Создаём запись изображения // Создаём запись изображения
_logger.LogTrace("Создаём новую запись изображения"); var imageId = 0;
// Заполняем запись изображения if (!string.IsNullOrEmpty(Record.Image.Data))
var imageRecord = new ImageRecord(base64: _selectedImageData ?? ""); {
// Добавляем запись изображения в базу данных _logger.LogTrace("Создаём новую запись изображения");
var attachedImageRecord = _context.ImageRecords.Attach(imageRecord); // Заполняем запись изображения
// Сохраняем изменения в базе данных var imageRecord = new ImageRecord(base64: Record.Image.Data);
_context.SaveChanges(); // Добавляем запись изображения в базу данных
_logger.LogTrace("Запись изображения сохранена"); var attachedImageRecord = _context.ImageRecords.Attach(imageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
imageId = attachedImageRecord.Entity.Id;
}
_logger.LogTrace("Создаём новую запись"); _logger.LogTrace("Создаём новую запись");
var dateTime = DateTime.UtcNow;
// Заполняем запись // Заполняем запись
var newRecord = new SeparatorRecord( var newRecord = new SeparatorRecord(
uuid: SelectedUuid, uuid: Record.Uuid,
type: SelectedType, createDateTime: dateTime,
imageId: attachedImageRecord.Entity.Id, editDateTime: dateTime,
featureIds: [] type: Record.Type,
comment: Record.Comment,
imageId: imageId,
features: Record.Features
); );
// Добавляем запись в базу данных // Добавляем запись в базу данных
var attachedRecord = _context.SeparatorRecords.Add(newRecord); var attachedRecord = _context.SeparatorRecords.Add(newRecord);
@@ -400,29 +460,6 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
_context.SaveChanges(); _context.SaveChanges();
_logger.LogTrace("Запись сохранена"); _logger.LogTrace("Запись сохранена");
_logger.LogTrace("Создаём новые записи признаков");
// Заполняем записи признаков и добавляем их в базу данных
var attachedFeatureRecords = new List<FeatureRecord>();
foreach (var feature in _selectedFeaturesData)
{
var featureRecord = new FeatureRecord(
values: feature,
sampleRecordId: attachedRecord.Entity.Id
);
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord);
attachedFeatureRecords.Add(attachedFeatureRecord.Entity);
}
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Записи признаков сохранены");
_logger.LogTrace("Заполняем признаки в сохранённой записи");
// Добавляем идентификаторы признаков в запись
attachedRecord.Entity.FeatureIds.AddRange(attachedFeatureRecords.Select(r => r.Id));
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Сохранение записи завершено");
// Добавляем новый элемент в меню // Добавляем новый элемент в меню
MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.Type)); MenuItems.Add(new MenuItemViewModel(attachedRecord.Entity.Id, attachedRecord.Entity.Type));
// Выбираем последний элемент из меню // Выбираем последний элемент из меню
@@ -432,11 +469,11 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
else else
{ {
// Находим запись в базе данных // Находим запись в базе данных
var attachedRecord = _context.SeparatorRecords.Find(SelectedId); var attachedRecord = _context.SeparatorRecords.Find(Record.Id);
// Если запись не найдена, то логгируем как ошибку // Если запись не найдена, то логгируем как ошибку
if (attachedRecord is null) if (attachedRecord is null)
{ {
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", SelectedId, nameof(_context.SeparatorRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", Record.Id, nameof(_context.SeparatorRecords));
return; return;
} }
@@ -449,62 +486,48 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
{ {
// Логгируем как ошибку // Логгируем как ошибку
_logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords)); _logger.LogError("Не удалось найти элемент с Id({}) в таблице {}", attachedRecord.ImageId, nameof(_context.ImageRecords));
_logger.LogTrace("Создаём новую запись изображения"); // Если изображение есть
// Заполняем запись изображения if (!string.IsNullOrEmpty(Record.Image.Data))
var imageRecord = new ImageRecord(base64: _selectedImageData ?? "");
// Добавляем запись изображения в базу данных
attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
// Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
}
// Ищем записи признаков в базе данных
var attachedFeatureRecords = _context.FeatureRecords.Where(r => r.SeparatorRecordId == SelectedId);
// Если количество признаков в базе данных больше чем текущее
if (attachedFeatureRecords.Count() > _selectedFeaturesData.Count())
{
// Находим количество признаков которое нужно удалить
var featuresRecordsToRemove = attachedFeatureRecords.Count() - _selectedFeaturesData.Count();
// Логгируем
_logger.LogTrace("Количество записей признаков в базе данных больше чем текущее, удаление лишних записей: {} ({} - {})", featuresRecordsToRemove, attachedFeatureRecords.Count(), _selectedFeaturesData.Count());
// Удаляем необходимое количество признаков из конца
attachedFeatureRecords.TakeLast(featuresRecordsToRemove);
}
// Заполняем все имеющиеся признаки в базе данных новыми значениями
foreach (var (i, attachedFeatureRecord) in attachedFeatureRecords.Enumerate())
attachedFeatureRecord.Values = _selectedFeaturesData[i];
// Если количество признаков в безе данных меньше чем текущее
if (attachedFeatureRecords.Count() < _selectedFeaturesData.Count())
{
// Находим количество признаков которое нужно добавить
var featuresRecordsToAdd = _selectedFeaturesData.Count() - attachedFeatureRecords.Count();
// Логгируем
_logger.LogTrace("Количество записей признаков в базе данных меньше чем текущее, добавление недостающих записей: {} ({} - {})", featuresRecordsToAdd, _selectedFeaturesData.Count(), attachedFeatureRecords.Count());
// Заполняем записи недостающих признаков и добавляем их в базу данных
for (var i = attachedFeatureRecords.Count(); i < _selectedFeaturesData.Count(); i++)
{ {
var featureRecord = new FeatureRecord( _logger.LogTrace("Создаём новую запись изображения");
values: _selectedFeaturesData[i], // Заполняем запись изображения
sampleRecordId: attachedRecord.Id var imageRecord = new ImageRecord(base64: Record.Image.Data);
); // Добавляем запись изображения в базу данных
var attachedFeatureRecord = _context.FeatureRecords.Attach(featureRecord); attachedImageRecord = _context.ImageRecords.Attach(imageRecord).Entity;
attachedFeatureRecords.Append(attachedFeatureRecord.Entity); // Сохраняем изменения в базе данных
_context.SaveChanges();
_logger.LogTrace("Запись изображения сохранена");
attachedRecord.ImageId = attachedImageRecord.Id;
}
}
// Если запись найдена
else
{
// Если изображение есть
if (!string.IsNullOrEmpty(Record.Image.Data))
{
_logger.LogTrace("Записываем изменения в запись изображения");
// Вносим изменения
attachedImageRecord.Base64 = Record.Image.Data;
// Сохраняем изменения в базе данных
_context.SaveChanges();
}
// Если изображения нет
else
{
// Удаляем изображение из базы данных
_context.ImageRecords.Remove(attachedImageRecord);
// Сохраняем изменения в базе данных
_context.SaveChanges();
attachedRecord.ImageId = 0;
} }
} }
_context.SaveChanges(); // Записываем изменения
_logger.LogTrace("Записи признаков сохранена"); attachedRecord.EditDateTime = DateTime.UtcNow;
attachedRecord.Type = Record.Type;
// Если в данных есть изменения, то записываем их attachedRecord.Comment = Record.Comment;
if (attachedRecord.Type != SelectedType) attachedRecord.Features = Record.Features.SelectMany(fs => fs).ToList();
attachedRecord.Type = SelectedType;
if (attachedRecord.ImageId != attachedImageRecord.Id)
attachedRecord.ImageId = attachedImageRecord.Id;
// Для признаков особый случай, они в любом случае будут перезаписаны, потому что так быстрее
attachedRecord.FeatureIds = attachedFeatureRecords.Select(r => r.Id).ToList();
// Сохраняем изменения в базе данных // Сохраняем изменения в базе данных
_context.SaveChanges(); _context.SaveChanges();
@@ -512,7 +535,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Записываем изменения в меню // Записываем изменения в меню
_logger.LogTrace("Записываем изменения в меню"); _logger.LogTrace("Записываем изменения в меню");
MenuItems[MenuSelectedIndex].Type = SelectedType; MenuItems[MenuSelectedIndex].Type = Record.Type;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -538,10 +561,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
_captureImagesTaskCts = new CancellationTokenSource(); _captureImagesTaskCts = new CancellationTokenSource();
var cancellationToken = _captureImagesTaskCts.Token; var cancellationToken = _captureImagesTaskCts.Token;
// Обновляем данные в интерфейсе ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}";
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: 0 / {ImageCapturerService.IlluminatorIntensities.Count()}");
// Запускаем получение изображений асинхронно // Запускаем получение изображений асинхронно
_сaptureImagesTask = Task.Run(async () => _сaptureImagesTask = Task.Run(async () =>
@@ -550,7 +570,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
var imagesData = new List<Task<ImageData>>(); var imagesData = new List<Task<ImageData>>();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesIsCapturing = true);
@@ -563,7 +583,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}"); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Получение изображений: {i + 1} / {ImageCapturerService.IlluminatorIntensities.Count()}");
@@ -595,7 +615,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений"); _ = Dispatcher.UIThread.InvokeAsync(() => ImagesCapturingProgress = $"Обработка изображений");
@@ -648,7 +668,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
var (avgImage, roi, features) = result.Value; var (avgImage, roi, features) = result.Value;
// Сохраняем вектор признаков // Сохраняем вектор признаков
_selectedFeaturesData = features; Record.Features = features;
// Обрабатываем среднее изображение // Обрабатываем среднее изображение
avgImage = new OpenCvSharp.Mat(avgImage, roi); avgImage = new OpenCvSharp.Mat(avgImage, roi);
@@ -663,15 +683,14 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
// Кодируем среднее изображение // Кодируем среднее изображение
OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData); OpenCvSharp.Cv2.ImEncode(".png", avgImage, out var avgData);
_selectedImageData = Convert.ToBase64String(avgData);
// Очищаем память среднего изображения // Очищаем память среднего изображения
avgImage.Dispose(); avgImage.Dispose();
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => SelectedImage.Data = _selectedImageData); _ = Dispatcher.UIThread.InvokeAsync(() => Record.Image.Data = Convert.ToBase64String(avgData));
}, cancellationToken); }, cancellationToken);
// Создаём задачу которая выполнится после завершения всех других // Создаём задачу которая выполнится после завершения всех других
@@ -724,7 +743,7 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
} }
// Обновляем данные в интерфейсе // Обновляем данные в интерфейсе
// Dispatcher.UIThread.Invoke нужен потому-что этот метод может (и скорее всего будет) вызван в другом потоке, // Dispatcher.UIThread.Invoke нужен потому-что неизвестно как будет выполнятся эта задача,
// а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан // а изменения данных в интерфейсе могут быть вызваны только из потока в котором интерфейс был создан
_ = Dispatcher.UIThread.InvokeAsync(() => _ = Dispatcher.UIThread.InvokeAsync(() =>
{ {
@@ -737,6 +756,8 @@ public partial class SeparatorCalibrationViewModel : ViewModelBase
}); });
} }
// Команда отмены задачи съёмки изображений /// <summary>
/// Отменить задачу съёмки изображения
/// </summary>
[RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel(); [RelayCommand] private void CancelImageCapturing() => _captureImagesTaskCts?.Cancel();
} }
+37 -7
View File
@@ -7,6 +7,8 @@ using GSS2.ViewModels.AmineContent;
using ViewModelBase = GSS2.UI.Core.ViewModels.ViewModelBase; using ViewModelBase = GSS2.UI.Core.ViewModels.ViewModelBase;
using System.ComponentModel;
namespace GSS2.ViewModels; namespace GSS2.ViewModels;
public partial class AmineContentViewModel : ViewModelBase public partial class AmineContentViewModel : ViewModelBase
@@ -17,26 +19,54 @@ public partial class AmineContentViewModel : ViewModelBase
[ObservableProperty] public partial SampleCalibrationViewModel SampleCalibration { get; set; } [ObservableProperty] public partial SampleCalibrationViewModel SampleCalibration { get; set; }
[ObservableProperty] public partial SeparatorCalibrationViewModel SeparatorCalibration { get; set; } [ObservableProperty] public partial SeparatorCalibrationViewModel SeparatorCalibration { get; set; }
// [ObservableProperty] public partial CameraViewModel Camera { get; set; } // [ObservableProperty] public partial CameraViewModel Camera { get; set; }
[ObservableProperty] public partial SystemViewModel System { get; set; } [ObservableProperty] public partial BrandCalibrationViewModel BrandCalibration { get; set; }
// [ObservableProperty] public partial SystemViewModel System { get; set; }
[ObservableProperty] public partial int CurrentIndex { get; set; } [ObservableProperty] public partial int CurrentIndex { get; set; } = 0;
[ObservableProperty] public partial bool IsScrolling { get; set; } [ObservableProperty] public partial bool IsScrolling { get; set; } = false;
[ObservableProperty] public partial bool CanInteract { get; set; } = true;
public AmineContentViewModel( public AmineContentViewModel(
NavigationService navigationService, NavigationService navigationService,
AnalysisViewModel analysis, AnalysisViewModel analysis,
SampleCalibrationViewModel sampleCalibration, SampleCalibrationViewModel sampleCalibration,
SeparatorCalibrationViewModel separatorCalibration, SeparatorCalibrationViewModel separatorCalibration,
// CameraViewModel camera, BrandCalibrationViewModel brandCalibration
SystemViewModel system
) )
{ {
_navigationService = navigationService; _navigationService = navigationService;
Analysis = analysis; Analysis = analysis;
SampleCalibration = sampleCalibration; SampleCalibration = sampleCalibration;
SeparatorCalibration = separatorCalibration; SeparatorCalibration = separatorCalibration;
// Camera = camera; BrandCalibration = brandCalibration;
System = system;
PropertyChanged += (_, ea) =>
{
if (ea.PropertyName == nameof(IsScrolling))
UpdateCanInteract();
};
Analysis.PropertyChanged += SubViewModelPropertyChanged;
SampleCalibration.PropertyChanged += SubViewModelPropertyChanged;
SeparatorCalibration.PropertyChanged += SubViewModelPropertyChanged;
BrandCalibration.PropertyChanged += SubViewModelPropertyChanged;
}
private void SubViewModelPropertyChanged(object? sender, PropertyChangedEventArgs ea)
{
if (ea.PropertyName == nameof(Analysis.CanInteract) ||
ea.PropertyName == nameof(Analysis.ImagesIsCapturing))
UpdateCanInteract();
}
private void UpdateCanInteract()
{
CanInteract = !IsScrolling;
CanInteract &= Analysis.CanInteract;
CanInteract &= SampleCalibration.CanInteract;
CanInteract &= SeparatorCalibration.CanInteract;
CanInteract &= BrandCalibration.CanInteract;
CanInteract &= !Analysis.ImagesIsCapturing;
CanInteract &= !SampleCalibration.ImagesIsCapturing;
CanInteract &= !SeparatorCalibration.ImagesIsCapturing;
} }
[RelayCommand] [RelayCommand]
-20
View File
@@ -1,20 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using GSS2.UI.Core.ViewModels;
namespace GSS2.ViewModels.Common;
public partial class CameraViewModel : ViewModelBase
{
[ObservableProperty] public partial GSS2.UI.Core.ViewModels.CameraViewModel Camera { get; set; }
[ObservableProperty] public partial GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel Illuminator { get; set; }
public CameraViewModel(
GSS2.UI.Core.ViewModels.CameraViewModel camera,
GSS2.UI.Core.ViewModels.IlluminatorControllerViewModel illuminator
)
{
Camera = camera;
Illuminator = illuminator;
}
}
File diff suppressed because it is too large Load Diff
@@ -16,8 +16,12 @@ public partial class AnalysisView : UserControl
private void OnChartsUpdate(object? sender, EventArgs ea) private void OnChartsUpdate(object? sender, EventArgs ea)
{ {
MeanValuesChart.CoreChart.Update(); ProcessedAreasHistogram.CoreChart.Update();
VariancesChart.CoreChart.Update(); UnprocessedAreasHistogram.CoreChart.Update();
QualitiesChart.CoreChart.Update(); HotspotAreasHistogram.CoreChart.Update();
OpticalIntegralsHistogram.CoreChart.Update();
InnerStdDevsHistogram.CoreChart.Update();
InnerHeterogeneitiesHistogram.CoreChart.Update();
ParticleValuesHistogram.CoreChart.Update();
} }
} }
+188 -100
View File
@@ -14,7 +14,7 @@
<Grid Background="{StaticResource MainBackground}"> <Grid Background="{StaticResource MainBackground}">
<!-- Основная форма --> <!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}"> <Grid IsEnabled="{Binding CanInteract}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" --> <ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
@@ -40,12 +40,14 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню --> <ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
<ColumnDefinition Width="*" /> <!-- Информация --> <ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing> <Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Информация --> <RowDefinition Height="*" /> <!-- Информация -->
<RowDefinition Height="Auto" /> <!-- Кнопки -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing> <Grid.RowSpacing>5</Grid.RowSpacing>
@@ -55,115 +57,201 @@
Classes="vertical" Classes="vertical"
Command="{Binding ToggleMenuCommand}" Command="{Binding ToggleMenuCommand}"
Grid.Column="0" Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="3"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
> >
<TextBlock Text="☰" /> <TextBlock Text="☰"/>
</Button> </Button>
<!-- Информация --> <!-- Информация -->
<Grid Grid.Column="1"> <core:SectionPanel
CanScrollBackward="{Binding InfoSectionCanScrollBackward}"
CanScrollForward="{Binding InfoSectionCanScrollForward}"
ClipToBounds="True"
CurrentIndex="{Binding InfoSectionCurrentIndex}"
Cyclic="True"
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="0"
IsScrolling="{Binding InfoSectionIsScrolling}"
Orientation="Horizontal"
>
<Grid.ColumnDefinitions> <!-- Id / Uuid / BrandName / MixtureName / MixtureNormalRate -->
<ColumnDefinition Width="*" /> <Grid>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- BrandName (подпись) | MixtureName (подпись) -->
<RowDefinition Height="Auto" /> <!-- BrandName (поле ) | MixtureName (поле ) -->
<RowDefinition Height="Auto" /> <!-- MixtureNormalRate (подпись) -->
<RowDefinition Height="Auto" /> <!-- MixtureNormalRate (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- BrandName (подпись) -->
<RowDefinition Height="Auto" /> <!-- BrandName (поле ) -->
<RowDefinition Height="Auto" /> <!-- MixtureName (подпись) | MixtureNormalRate (подпись) -->
<RowDefinition Height="Auto" /> <!-- MixtureName (поле ) | MixtureNormalRate (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions>
<!-- Id (подпись) --> <Grid.RowSpacing>5</Grid.RowSpacing>
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="0"
Text="ИД:"
/>
<!-- Id (поле) -->
<TextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="ИД"
Text="{Binding SelectedId}"
/>
<!-- Uuid (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="1"
Grid.Row="0"
Text="Уникальный ИД:"
/>
<!-- Uuid (поле) -->
<TextBox
Classes="singleline"
Grid.Column="1"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="Уникальный ИД"
Text="{Binding SelectedUuid}"
/>
<!-- BrandName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="2"
Text="Марка продукта:"
/>
<!-- BrandName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="3"
PlaceholderText="Марка продукта"
Text="{Binding SelectedBrandName}"
/>
<!-- MixtureName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="1"
Grid.Row="2"
Text="Марка смеси:"
/>
<!-- MixtureName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="1"
Grid.Row="3"
PlaceholderText="Марка смеси"
Text="{Binding SelectedMixtureName}"
/>
<!-- MixtureNormalRate (подпись) -->
<TextBlock
Classes="mini" Text="Норма расхода смеси (кг/т | гр/кг):"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
/>
<!-- MixtureNormalRate (поле) -->
<controls:TouchNumericUpDown
Classes="singleline"
FormatString="0.0000"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="5"
Increment="0.0001"
PlaceholderText="Норма расхода смеси (кг/т | гр/кг)"
Value="{Binding SelectedMixtureNormalRate}"
/>
</Grid> <!-- Id (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="0"
Text="ИД:"
/>
<!-- Id (поле) -->
<TextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="ИД"
Text="{Binding Record.Id}"
/>
<!-- Uuid (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="1"
Grid.Row="0"
Text="Уникальный ИД:"
/>
<!-- Uuid (поле) -->
<TextBox
Classes="singleline"
Grid.Column="1"
Grid.Row="1"
IsReadOnly="True"
PlaceholderText="Уникальный ИД"
Text="{Binding Record.Uuid}"
/>
<!-- BrandName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="2"
Text="Марка продукта:"
/>
<!-- BrandName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="3"
PlaceholderText="Марка продукта"
Text="{Binding Record.BrandName}"
/>
<!-- MixtureName (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.Row="4"
Text="Марка смеси:"
/>
<!-- MixtureName (поле) -->
<controls:TouchTextBox
Classes="singleline"
Grid.Column="0"
Grid.Row="5"
PlaceholderText="Марка смеси"
Text="{Binding Record.MixtureName}"
/>
<!-- MixtureNormalRate (подпись) -->
<TextBlock
Classes="mini" Text="Норма расхода смеси (кг/т | гр/кг):"
Grid.Column="1"
Grid.Row="4"
/>
<!-- MixtureNormalRate (поле) -->
<controls:TouchNumericUpDown
Classes="singleline"
FormatString="0.0000"
Grid.Column="1"
Grid.Row="5"
Increment="0.0001"
PlaceholderText="Норма расхода смеси (кг/т | гр/кг)"
Value="{Binding Record.MixtureNormalRate}"
/>
</Grid>
<!-- Comment -->
<Grid>
<Grid.RowSpacing>5</Grid.RowSpacing>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Comment (подпись) -->
<RowDefinition Height="*" /> <!-- Comment (поле ) -->
</Grid.RowDefinitions>
<!-- Comment (подпись) -->
<TextBlock
Classes="mini"
Grid.Row="0"
Text="Дополнительная информация:"
/>
<!-- Comment (поле) -->
<controls:TouchTextBox
Classes="multiline"
Grid.Row="1"
PlaceholderText="Дополнительная информация"
Text="{Binding Record.Comment}"
/>
</Grid>
</core:SectionPanel>
<!-- Кнопка влево -->
<Button
Command="{Binding InfoSectionPreviousCommand}"
Grid.Column="1"
Grid.Row="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="InfoSectionCanScrollBackward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
<TextBlock Text="◀"/>
</Button>
<!-- Кнопка вправо -->
<Button
Command="{Binding InfoSectionNextCommand}"
Grid.Column="2"
Grid.Row="1"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
>
<Button.IsEnabled>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="InfoSectionCanScrollForward" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="InfoSectionIsScrolling" />
</MultiBinding>
</Button.IsEnabled>
<TextBlock Text="▶"/>
</Button>
</Grid> </Grid>
@@ -14,7 +14,7 @@
<Grid Background="{StaticResource MainBackground}"> <Grid Background="{StaticResource MainBackground}">
<!-- Основная форма --> <!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}"> <Grid IsEnabled="{Binding CanInteract}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" --> <ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
@@ -51,8 +51,8 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <!-- Кнопка меню --> <ColumnDefinition Width="Auto" /> <!-- Кнопка меню -->
<ColumnDefinition Width="*" /> <!-- Информация --> <ColumnDefinition Width="*" /> <!-- Информация -->
<ColumnDefinition Width="*" /> <!-- Информация --> <ColumnDefinition Width="*" /> <!-- Информация -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.ColumnSpacing>5</Grid.ColumnSpacing> <Grid.ColumnSpacing>5</Grid.ColumnSpacing>
@@ -101,12 +101,12 @@
<Grid.ColumnSpacing>5</Grid.ColumnSpacing> <Grid.ColumnSpacing>5</Grid.ColumnSpacing>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) --> <RowDefinition Height="Auto" /> <!-- Id (подпись) | Uuid (подпись) -->
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) --> <RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- SampleName (подпись) | BrandComboBox (подпись) --> <RowDefinition Height="Auto" /> <!-- SampleName (подпись) | BrandComboBox (подпись) -->
<RowDefinition Height="Auto" /> <!-- SampleName (поле ) | BrandComboBox (поле ) --> <RowDefinition Height="Auto" /> <!-- SampleName (поле ) | BrandComboBox (поле ) -->
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (подпись) | SamplingPlace (подпись) --> <RowDefinition Height="Auto" /> <!-- SamplingDateTime (подпись) | SamplingPlace (подпись) -->
<RowDefinition Height="Auto" /> <!-- SamplingDateTime (поле ) | SamplingPlace (поле ) --> <RowDefinition Height="Auto" /> <!-- SamplingDateTime (поле ) | SamplingPlace (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство --> <RowDefinition Height="*" /> <!-- Пустое пространство -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
@@ -126,7 +126,7 @@
Grid.Row="1" Grid.Row="1"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="ИД" PlaceholderText="ИД"
Text="{Binding SelectedId}" Text="{Binding Record.Id}"
/> />
<!-- Uuid (подпись) --> <!-- Uuid (подпись) -->
<TextBlock <TextBlock
@@ -142,7 +142,7 @@
Grid.Row="1" Grid.Row="1"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Уникальный ИД" PlaceholderText="Уникальный ИД"
Text="{Binding SelectedUuid}" Text="{Binding Record.Uuid}"
/> />
<!-- SampleName (подпись) --> <!-- SampleName (подпись) -->
<TextBlock <TextBlock
@@ -156,7 +156,7 @@
Grid.Row="3" Grid.Row="3"
Grid.Column="0" Grid.Column="0"
Classes="singleline" Classes="singleline"
Text="{Binding SelectedSampleName}" Text="{Binding Record.SampleName}"
PlaceholderText="Название пробы" PlaceholderText="Название пробы"
/> />
<!-- BrandComboBox (подпись) --> <!-- BrandComboBox (подпись) -->
@@ -180,7 +180,7 @@
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SampleCalibrationViewModel+BrandComboBoxItemViewModel"> <DataTemplate x:DataType="vm:SampleCalibrationViewModel+BrandComboBoxItemViewModel">
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
> >
<Run Text="{Binding Id, StringFormat='[{0}] '}" /> <Run Text="{Binding Id, StringFormat='[{0}] '}" />
@@ -204,7 +204,7 @@
Grid.Column="0" Grid.Column="0"
Grid.Row="5" Grid.Row="5"
PlaceholderText="Дата и время отбора пробы" PlaceholderText="Дата и время отбора пробы"
Text="{Binding SelectedSamplingDateTime, Converter={StaticResource DateTimeConverter}}" Text="{Binding Record.SamplingDateTime, Converter={StaticResource DateTimeConverter}}"
/> />
<!-- SamplingPlace (подпись) --> <!-- SamplingPlace (подпись) -->
<TextBlock <TextBlock
@@ -219,7 +219,7 @@
Grid.Column="1" Grid.Column="1"
Grid.Row="5" Grid.Row="5"
PlaceholderText="Место отбора пробы" PlaceholderText="Место отбора пробы"
Text="{Binding SelectedSamplingPlace}" Text="{Binding Record.SamplingPlace}"
/> />
</Grid> </Grid>
@@ -258,7 +258,7 @@
Grid.Row="1" Grid.Row="1"
Increment="0.0001" Increment="0.0001"
PlaceholderText="Фактический расход смеси (кг/т | гр/кг)" PlaceholderText="Фактический расход смеси (кг/т | гр/кг)"
Value="{Binding SelectedMixtureActualRate}" Value="{Binding Record.MixtureActualRate}"
/> />
<!-- MeasuredContent (подпись) --> <!-- MeasuredContent (подпись) -->
<TextBlock <TextBlock
@@ -274,7 +274,7 @@
Grid.Row="1" Grid.Row="1"
Increment="0.0001" Increment="0.0001"
PlaceholderText="Измеренное содержание смеси (кг/т | гр/кг)" PlaceholderText="Измеренное содержание смеси (кг/т | гр/кг)"
Value="{Binding SelectedMeasuredContent}" Value="{Binding Record.MeasuredContent}"
/> />
<!-- SampleComment (подпись) --> <!-- SampleComment (подпись) -->
<TextBlock <TextBlock
@@ -290,7 +290,7 @@
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Grid.Row="3" Grid.Row="3"
PlaceholderText="Дополнительная информация о пробе" PlaceholderText="Дополнительная информация о пробе"
Text="{Binding SelectedSampleComment}" Text="{Binding Record.SampleComment}"
/> />
</Grid> </Grid>
@@ -363,15 +363,14 @@
<core:ZoomPanImage <core:ZoomPanImage
Background="Black" Background="Black"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
ImageSource="{Binding SelectedImage.Image}" ImageSource="{Binding Record.Image.Image}"
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
> >
<core:ZoomPanImage.IsVisible> <core:ZoomPanImage.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" /> <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</core:ZoomPanImage.IsVisible> </core:ZoomPanImage.IsVisible>
@@ -379,11 +378,11 @@
</core:ZoomPanImage> </core:ZoomPanImage>
<!-- Оверлей "Изображение загружается" --> <!-- Оверлей "Изображение загружается" -->
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" > <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" >
<StackPanel.IsVisible> <StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedImage.IsLoading" /> <Binding Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</StackPanel.IsVisible> </StackPanel.IsVisible>
@@ -402,8 +401,8 @@
> >
<TextBlock.IsVisible> <TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" /> <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</TextBlock.IsVisible> </TextBlock.IsVisible>
@@ -417,8 +416,8 @@
<StackPanel.IsVisible> <StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" /> <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Path="ImagesIsCapturing" /> <Binding Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</StackPanel.IsVisible> </StackPanel.IsVisible>
@@ -433,18 +432,20 @@
<!-- Кнопка "Получить изображения" --> <!-- Кнопка "Получить изображения" -->
<Button <Button
Command="{Binding CaptureImageCommand}"
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}" IsVisible="{Binding !ImagesIsCapturing}"
> >
<TextBlock Text="Получить изображения"/> <TextBlock Text="Получить изображения"/>
</Button> </Button>
<!-- Кнопка "Отменить съёмку" --> <!-- Кнопка "Отменить съёмку" -->
<Button <Button
Command="{Binding CancelImageCapturingCommand}"
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}" IsVisible="{Binding ImagesIsCapturing}"
> >
<TextBlock Text="Отменить съёмку"/> <TextBlock Text="Отменить съёмку"/>
</Button> </Button>
@@ -14,7 +14,7 @@
<Grid Background="{StaticResource MainBackground}"> <Grid Background="{StaticResource MainBackground}">
<!-- Основная форма --> <!-- Основная форма -->
<Grid IsEnabled="{Binding !MenuOpened}"> <Grid IsEnabled="{Binding CanInteract}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" --> <ColumnDefinition Width="*" /> <!-- Кнопка "Удалить" -->
@@ -80,7 +80,8 @@
<RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) --> <RowDefinition Height="Auto" /> <!-- Id (поле ) | Uuid (поле ) -->
<RowDefinition Height="Auto" /> <!-- Type (подпись) --> <RowDefinition Height="Auto" /> <!-- Type (подпись) -->
<RowDefinition Height="Auto" /> <!-- Type (поле ) --> <RowDefinition Height="Auto" /> <!-- Type (поле ) -->
<RowDefinition Height="*" /> <!-- Пустое пространство --> <RowDefinition Height="Auto" /> <!-- Comment (подпись) -->
<RowDefinition Height="*" /> <!-- Comment (поле ) -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.RowSpacing>5</Grid.RowSpacing> <Grid.RowSpacing>5</Grid.RowSpacing>
@@ -99,7 +100,7 @@
Grid.Row="1" Grid.Row="1"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="ИД" PlaceholderText="ИД"
Text="{Binding SelectedId}" Text="{Binding Record.Id}"
/> />
<!-- Uuid (подпись) --> <!-- Uuid (подпись) -->
<TextBlock <TextBlock
@@ -115,7 +116,7 @@
Grid.Row="1" Grid.Row="1"
IsReadOnly="True" IsReadOnly="True"
PlaceholderText="Уникальный ИД" PlaceholderText="Уникальный ИД"
Text="{Binding SelectedUuid}" Text="{Binding Record.Uuid}"
/> />
<!-- Type (подпись) --> <!-- Type (подпись) -->
<TextBlock <TextBlock
@@ -131,25 +132,27 @@
Grid.ColumnSpan="2" Grid.ColumnSpan="2"
Grid.Row="3" Grid.Row="3"
PlaceholderText="Тип" PlaceholderText="Тип"
Text="{Binding SelectedType}" Text="{Binding Record.Type}"
/> />
</Grid> <!-- Comment (подпись) -->
<TextBlock
Classes="mini"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Text="Дополнительная информация:"
/>
<!-- Comment (поле) -->
<controls:TouchTextBox
Classes="multiline"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="5"
PlaceholderText="Дополнительная информация"
Text="{Binding Record.Comment}"
/>
<!-- Информация -->
<Grid Grid.Column="1">
<!-- ИД (подпись) -->
<!-- <TextBlock Grid.Row="0" Grid.Column="1" Classes="mini" Text="ИД:" /> -->
<!-- ИД (поле) -->
<!-- <TextBox Grid.Row="1" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Id}" PlaceholderText="ИД" IsReadOnly="True" /> -->
<!-- Тип (подпись) -->
<!-- <TextBlock Grid.Row="2" Grid.Column="1" Classes="mini" Text="Тип:" /> -->
<!-- Тип (поле) -->
<!-- <controls:TouchTextBox Grid.Row="3" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Type}" PlaceholderText="Тип" /> -->
<!-- Партия (подпись) -->
<!-- <TextBlock Grid.Row="4" Grid.Column="1" Classes="mini" Text="Партия:" /> -->
<!-- Партия (поле) -->
<!-- <controls:TouchTextBox Grid.Row="5" Grid.Column="1" Classes="singleline" Text="{Binding EditingRecord.Batch}" PlaceholderText="Партия" /> -->
</Grid> </Grid>
</Grid> </Grid>
@@ -167,24 +170,18 @@
<!-- Изображение --> <!-- Изображение -->
<Grid> <Grid>
<!-- Изображение -->
<Grid>
</Grid>
<!-- Изображение --> <!-- Изображение -->
<core:ZoomPanImage <core:ZoomPanImage
Background="Black" Background="Black"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
ImageSource="{Binding SelectedImage.Image}" ImageSource="{Binding Record.Image.Image}"
IsVisible="{Binding SelectedImage.IsLoading, Converter={x:Static BoolConverters.Not}}"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
> >
<core:ZoomPanImage.IsVisible> <core:ZoomPanImage.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNotNull}" Path="SelectedImage.Data" /> <Binding Converter="{x:Static StringConverters.IsNotNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</core:ZoomPanImage.IsVisible> </core:ZoomPanImage.IsVisible>
@@ -196,7 +193,7 @@
<StackPanel.IsVisible> <StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="SelectedImage.IsLoading" /> <Binding Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</StackPanel.IsVisible> </StackPanel.IsVisible>
@@ -215,8 +212,8 @@
> >
<TextBlock.IsVisible> <TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" /> <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</TextBlock.IsVisible> </TextBlock.IsVisible>
@@ -230,8 +227,8 @@
<StackPanel.IsVisible> <StackPanel.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Converter="{x:Static ObjectConverters.IsNull}" Path="SelectedImage.Data" /> <Binding Converter="{x:Static StringConverters.IsNullOrEmpty}" Path="Record.Image.Data" />
<Binding Converter="{x:Static BoolConverters.Not}" Path="SelectedImage.IsLoading" /> <Binding Converter="{x:Static BoolConverters.Not}" Path="Record.Image.IsLoading" />
<Binding Path="ImagesIsCapturing" /> <Binding Path="ImagesIsCapturing" />
</MultiBinding> </MultiBinding>
</StackPanel.IsVisible> </StackPanel.IsVisible>
@@ -246,18 +243,20 @@
<!-- Кнопка "Получить изображения" --> <!-- Кнопка "Получить изображения" -->
<Button <Button
Command="{Binding CaptureImageCommand}"
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CaptureImageCommand}" IsVisible="{Binding !ImagesIsCapturing}"
> >
<TextBlock Text="Получить изображения"/> <TextBlock Text="Получить изображения"/>
</Button> </Button>
<!-- Кнопка "Отменить съёмку" --> <!-- Кнопка "Отменить съёмку" -->
<Button <Button
Command="{Binding CancelImageCapturingCommand}"
Grid.Row="1" Grid.Row="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Command="{Binding CancelImageCapturingCommand}" IsVisible="{Binding ImagesIsCapturing}"
> >
<TextBlock Text="Отменить съёмку"/> <TextBlock Text="Отменить съёмку"/>
</Button> </Button>
+27 -31
View File
@@ -36,31 +36,30 @@
<Border Background="{StaticResource MainBackground}" BorderThickness="0"> <Border Background="{StaticResource MainBackground}" BorderThickness="0">
<Grid Margin="5" RowSpacing="5"> <Grid Margin="5">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Меню --> <RowDefinition Height="Auto"/> <!-- Меню -->
<RowDefinition Height="*"/> <!-- Основной контент --> <RowDefinition Height="*"/> <!-- Основной контент -->
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Меню --> <Grid.RowSpacing>5</Grid.RowSpacing>
<ScrollViewer Grid.Row="0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
<ScrollViewer.IsEnabled> <!-- Меню -->
<MultiBinding Converter="{x:Static BoolConverters.And}"> <ScrollViewer
<Binding Converter="{x:Static BoolConverters.Not}" Path="IsScrolling" /> Grid.Row="0"
<Binding Converter="{x:Static BoolConverters.Not}" Path="SeparatorCalibration.MenuOpened" /> HorizontalScrollBarVisibility="Hidden"
</MultiBinding> IsEnabled="{Binding CanInteract}"
</ScrollViewer.IsEnabled> VerticalScrollBarVisibility="Disabled"
>
<Grid ColumnSpacing="5"> <Grid ColumnSpacing="5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Анализ --> <ColumnDefinition Width="*" MinWidth="200"/> <!-- Анализ -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Калибровка проб --> <ColumnDefinition Width="*" MinWidth="200"/> <!-- Марки -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Калибровка сепаратора --> <ColumnDefinition Width="*" MinWidth="200"/> <!-- Сепараторы -->
<!-- <ColumnDefinition Width="*" MinWidth="200"/> --> <!-- Камера --> <ColumnDefinition Width="*" MinWidth="200"/> <!-- Пробы -->
<ColumnDefinition Width="*" MinWidth="200"/> <!-- Система -->
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Анализ --> <!-- Анализ -->
@@ -68,24 +67,19 @@
<TextBlock Classes="menu-text" Text="Анализ"/> <TextBlock Classes="menu-text" Text="Анализ"/>
</Button> </Button>
<!-- Калибровка проб --> <!-- Марки -->
<Button Grid.Column="1" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="1"> <Button Grid.Column="1" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="1">
<TextBlock Classes="menu-text" Text="Калибровка проб"/> <TextBlock Classes="menu-text" Text="Марки"/>
</Button> </Button>
<!-- Калибровка сепараторов --> <!-- Сепараторы -->
<Button Grid.Column="2" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="2"> <Button Grid.Column="2" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="2">
<TextBlock Classes="menu-text" Text="Калибровка сепараторов"/> <TextBlock Classes="menu-text" Text="Сепараторы"/>
</Button> </Button>
<!-- Камера --> <!-- Пробы -->
<!-- <Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3"> <Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="3">
<TextBlock Classes="menu-text" Text="Камера"/> <TextBlock Classes="menu-text" Text="Пробы"/>
</Button> -->
<!-- Система -->
<Button Grid.Column="3" Classes="menu" Command="{Binding ChangeSectionCommand}" CommandParameter="4">
<TextBlock Classes="menu-text" Text="Система"/>
</Button> </Button>
</Grid> </Grid>
@@ -93,14 +87,16 @@
<!-- Основной контент --> <!-- Основной контент -->
<core:SectionPanel Grid.Row="1" ClipToBounds="True" CurrentIndex="{Binding CurrentIndex}" Orientation="Horizontal" IsScrolling="{Binding IsScrolling}"> <core:SectionPanel Grid.Row="1" ClipToBounds="True" CurrentIndex="{Binding CurrentIndex}" Orientation="Horizontal" IsScrolling="{Binding IsScrolling}">
<ContentControl Content="{Binding Analysis}"/> <!-- Анализ --> <ContentControl Content="{Binding Analysis}"/> <!-- Анализ -->
<ContentControl Content="{Binding SampleCalibration}"/> <!-- Калибровка проб --> <ContentControl Content="{Binding BrandCalibration}"/> <!-- Марки -->
<ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Калибровка сепаратора --> <ContentControl Content="{Binding SeparatorCalibration}"/> <!-- Сепараторы -->
<!-- <ContentControl Content="{Binding Camera}"/> --> <!-- Камера --> <ContentControl Content="{Binding SampleCalibration}"/> <!-- Пробы -->
<ContentControl Content="{Binding System}"/> <!-- Система --> <!-- <ContentControl Content="{Binding Camera}"/> --> <!-- Камера -->
<!-- <ContentControl Content="{Binding System}"/> --> <!-- Система -->
</core:SectionPanel> </core:SectionPanel>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>
-50
View File
@@ -1,50 +0,0 @@
<UserControl
x:Class="GSS2.Views.Common.CameraView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GSS2.ViewModels.Common"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:DataType="vm:CameraViewModel">
<UserControl.Resources>
<SolidColorBrush x:Key="MainBackground" Color="#001e46" />
<SolidColorBrush x:Key="ButtonBackground" Color="#0865b2" />
<SolidColorBrush x:Key="ButtonForeground" Color="#ffffff" />
<x:Double x:Key="ButtonFontSize">20</x:Double>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="Button">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="Background" Value="{StaticResource ButtonBackground}" />
<Setter Property="Foreground" Value="{StaticResource ButtonForeground}" />
</Style>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="FontSize" Value="{StaticResource ButtonFontSize}"/>
</Style>
</UserControl.Styles>
<Border BorderThickness="0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <!-- Камера -->
<ColumnDefinition Width="Auto"/> <!-- Осветитель -->
</Grid.ColumnDefinitions>
<ContentControl Grid.Column="0" Content="{Binding Camera}"/> <!-- Камера -->
<ContentControl Grid.Column="1" Content="{Binding Illuminator}"/> <!-- Осветитель -->
</Grid>
</Border>
</UserControl>
-11
View File
@@ -1,11 +0,0 @@
using Avalonia.Controls;
namespace GSS2.Views.Common;
public partial class CameraView : UserControl
{
public CameraView()
{
InitializeComponent();
}
}
View File
View File
View File
View File
+1 -2
View File
@@ -20,8 +20,7 @@
} }
}, },
"ConnectionStrings": { "ConnectionStrings": {
"AmineContentCalibrationDatabasePath": "AmineContentCalibration.db", "AmineContentDatabasePath": "AmineContent.db"
"AmineContentResultsDatabasePath": "AmineContentResults.db"
}, },
"Hardware": { "Hardware": {
"Illuminator": { "Illuminator": {
BIN
View File
Binary file not shown.