Compare commits
261
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23235626ba | ||
|
|
9444959efe | ||
|
|
7a10b9f4b6 | ||
|
|
d440dbcc58 | ||
|
|
ae787622d2 | ||
|
|
d248630ddc | ||
|
|
b15d0a0a08 | ||
|
|
683ca25aaf | ||
|
|
d3cc50b6e8 | ||
|
|
8c3c866eff | ||
|
|
78907de455 | ||
|
|
500adfa789 | ||
|
|
9658fc28bd | ||
|
|
42ad98ab1f | ||
|
|
bd0a876b5b | ||
|
|
806cf0d670 | ||
|
|
82a51293b9 | ||
|
|
4cd7867e04 | ||
|
|
6560278568 | ||
|
|
08a233b2c5 | ||
|
|
7aa92d4806 | ||
|
|
468bcf786e | ||
|
|
21324985db | ||
|
|
4529602d97 | ||
|
|
bce5af56e0 | ||
|
|
f8643c4de1 | ||
|
|
161b20fea7 | ||
|
|
882e894fba | ||
|
|
79c87bedfd | ||
|
|
b1324c567c | ||
|
|
d2627be55b | ||
|
|
2ba6092385 | ||
|
|
fee0651595 | ||
|
|
58fc16d70b | ||
|
|
ddf8996f01 | ||
|
|
0c2cc0dad2 | ||
|
|
1e0c65f213 | ||
|
|
c53a2d9078 | ||
|
|
6223bc925f | ||
|
|
aca1d79023 | ||
|
|
45d12dea6f | ||
|
|
2cee6cb0f2 | ||
|
|
e9cda221bb | ||
|
|
c6181a76ec | ||
|
|
cd24373c80 | ||
|
|
a464b1fc21 | ||
|
|
b8f6af7112 | ||
|
|
1c8d01562f | ||
|
|
2608ee4de6 | ||
|
|
e63f706f41 | ||
|
|
94cd07cd3c | ||
|
|
967ca817ba | ||
|
|
979637cc07 | ||
|
|
5c7ff08a78 | ||
|
|
a64d1a82ed | ||
|
|
bf1766f90a | ||
|
|
1b789ad5d5 | ||
|
|
a7404d0e24 | ||
|
|
a64327ac36 | ||
|
|
c886303458 | ||
|
|
38003592df | ||
|
|
82c2b17a1f | ||
|
|
2660c564a6 | ||
|
|
7e2b4ed908 | ||
|
|
c1960f2150 | ||
|
|
4124443f36 | ||
|
|
6bab62a062 | ||
|
|
c426df990c | ||
|
|
20614fb9e6 | ||
|
|
302431b64c | ||
|
|
db34c579d0 | ||
|
|
83afddc918 | ||
|
|
cf77faf972 | ||
|
|
684513ee59 | ||
|
|
45a6d59b5e | ||
|
|
73c8fecdf2 | ||
|
|
8c77df207a | ||
|
|
1c6f9af2bb | ||
|
|
0cfde7f06b | ||
|
|
d3c361b544 | ||
|
|
b35b7d92f9 | ||
|
|
52bd0d01ce | ||
|
|
eb4f7ad971 | ||
|
|
8677e49680 | ||
|
|
29faef86e3 | ||
|
|
6ef42fcc21 | ||
|
|
d2d41df155 | ||
|
|
6d2477be4a | ||
|
|
57152ae749 | ||
|
|
88b8bc5534 | ||
|
|
41408b30da | ||
|
|
136be96e1b | ||
|
|
0258e54b4e | ||
|
|
d3c8813012 | ||
|
|
b8e54e1fc9 | ||
|
|
054075a341 | ||
|
|
8d42232589 | ||
|
|
e4d4d75642 | ||
|
|
2b5a0ffe14 | ||
|
|
a0d53f9ec9 | ||
|
|
dfa8693be9 | ||
|
|
8157a752bd | ||
|
|
4dd805bb1c | ||
|
|
1e45f48c75 | ||
|
|
d833eca584 | ||
|
|
c966a7034c | ||
|
|
f051d51ae0 | ||
|
|
267159e0a9 | ||
|
|
26db172153 | ||
|
|
2e75f65b4a | ||
|
|
432cb91e5a | ||
|
|
b0083afa10 | ||
|
|
955b59a195 | ||
|
|
b904d0ca41 | ||
|
|
f34046df2a | ||
|
|
889782068e | ||
|
|
d12ee008db | ||
|
|
6c3c6cd0ef | ||
|
|
5803f06bdb | ||
|
|
a4acf30771 | ||
|
|
a4ef208233 | ||
|
|
be6b40b5cd | ||
|
|
8738edd0c5 | ||
|
|
c71fe75446 | ||
|
|
c619c7d3b8 | ||
|
|
f3e5527145 | ||
|
|
39941842d4 | ||
|
|
3488939995 | ||
|
|
0f2ef5a5bf | ||
|
|
55d2381e8a | ||
|
|
126322584f | ||
|
|
4d9ddef3ca | ||
|
|
7139cdbe80 | ||
|
|
93d7456ccd | ||
|
|
7a8ce7b6ab | ||
|
|
865528e344 | ||
|
|
02bb9ee9fa | ||
|
|
79de40515a | ||
|
|
e38c6c1199 | ||
|
|
dbb35a203e | ||
|
|
a88a094e86 | ||
|
|
ffaebbdf07 | ||
|
|
e7d04b55b3 | ||
|
|
8301971906 | ||
|
|
693cf7e4e2 | ||
|
|
2fa212e6ff | ||
|
|
a210a0d1f2 | ||
|
|
4cd191fcb2 | ||
|
|
e21833fb5f | ||
|
|
b746ceb34a | ||
|
|
83c1f0e817 | ||
|
|
a88a5a8db2 | ||
|
|
271200a8da | ||
|
|
b703297f5e | ||
|
|
637d1d021d | ||
|
|
dbe424b17a | ||
|
|
be0525a319 | ||
|
|
0ce130f0ef | ||
|
|
c1ee29cd06 | ||
|
|
28607e2729 | ||
|
|
f1bc71fb92 | ||
|
|
69b607f963 | ||
|
|
6ac3ea87ef | ||
|
|
38973e0030 | ||
|
|
37c89a4c9c | ||
|
|
0075362f92 | ||
|
|
38d28599ad | ||
|
|
c9a4b0f3ce | ||
|
|
b74f1fd644 | ||
|
|
26b7aa7e8b | ||
|
|
92f63e744d | ||
|
|
b0581d81ef | ||
|
|
540b9af871 | ||
|
|
d11f7107fe | ||
|
|
3c5a28f69d | ||
|
|
110b880304 | ||
|
|
7867044c0a | ||
|
|
88903f3dac | ||
|
|
4e223549ee | ||
|
|
2ea0df8faf | ||
|
|
061e85ca6b | ||
|
|
269e650b38 | ||
|
|
a3bfc7a43e | ||
|
|
c2ff40d816 | ||
|
|
eb09ae2eca | ||
|
|
d1b6247bb7 | ||
|
|
c5f52e06fa | ||
|
|
9700ba7900 | ||
|
|
bc72130d09 | ||
|
|
ad208f9633 | ||
|
|
ad2bfd3639 | ||
|
|
5e637ae02e | ||
|
|
63f8dd7467 | ||
|
|
aac3af8714 | ||
|
|
378917f59d | ||
|
|
76e7ccb4a6 | ||
|
|
953b02a6e8 | ||
|
|
71a66dca2d | ||
|
|
978c620287 | ||
|
|
8489dafdf7 | ||
|
|
cfc52f195b | ||
|
|
68a1eb9941 | ||
|
|
c1573390b5 | ||
|
|
5d60523e87 | ||
|
|
91c63e0792 | ||
|
|
0940df2f1a | ||
|
|
16a76720b8 | ||
|
|
7101fd679e | ||
|
|
86299da682 | ||
|
|
39b951b709 | ||
|
|
a475bd80f3 | ||
|
|
4e64bcd117 | ||
|
|
fbd0219a4a | ||
|
|
104a68d9dc | ||
|
|
33a5e479a9 | ||
|
|
5482d4ac96 | ||
|
|
0d984a77b3 | ||
|
|
7e95e18207 | ||
|
|
38fa86d243 | ||
|
|
4c1ce73de9 | ||
|
|
eea80d39c0 | ||
|
|
15dc325275 | ||
|
|
ca333d1772 | ||
|
|
a4ece06b05 | ||
|
|
992452052e | ||
|
|
75192bbd09 | ||
|
|
6199bd48c5 | ||
|
|
25434d70d4 | ||
|
|
de6dd45dcb | ||
|
|
74f2e99ed3 | ||
|
|
99b1f4a171 | ||
|
|
8c10aa3752 | ||
|
|
7170da4a70 | ||
|
|
842f009b42 | ||
|
|
7f30ca0cd1 | ||
|
|
93e207c211 | ||
|
|
64cafc088a | ||
|
|
8a0da27a07 | ||
|
|
5a8478e782 | ||
|
|
368cd95114 | ||
|
|
c13cb725f0 | ||
|
|
c6d4a016b5 | ||
|
|
eda0644b4b | ||
|
|
5b73835d94 | ||
|
|
9848b97082 | ||
|
|
0b429ffbc2 | ||
|
|
ef146efaf0 | ||
|
|
431061a685 | ||
|
|
b6edd9c0a0 | ||
|
|
8d7c1adf37 | ||
|
|
3376ac41b4 | ||
|
|
f9181fee74 | ||
|
|
4c77d6c06e | ||
|
|
b7db39853b | ||
|
|
66157ba988 | ||
|
|
e7e6fc7920 | ||
|
|
ff5954d85b | ||
|
|
c09e266b2c | ||
|
|
545a747ebc | ||
|
|
98ba7d2dee | ||
|
|
78835c1af6 |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Root Type="DevExpress.CodeRush.Foundation.CodePlaces.Options.FavoritesListContainer">
|
||||
<Options Language="Neutral">
|
||||
<Groups />
|
||||
</Options>
|
||||
</Root>
|
||||
+74
-1
@@ -1,3 +1,10 @@
|
||||
# Generated build outputs for all projects, including Debug/Release archives.
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Generated test results and hardware diagnostic captures.
|
||||
[Tt]est[Rr]esults/
|
||||
|
||||
AppDiagnostic/bin/
|
||||
AppDiagnostic/obj/
|
||||
Common/bin/
|
||||
@@ -90,4 +97,70 @@ Users/obj/
|
||||
.idea/
|
||||
*.suo
|
||||
*.bak
|
||||
TBF.sln.DotSettings.user
|
||||
TBF.sln.DotSettings.user
|
||||
/ExternalProjects
|
||||
/GenesisCordonelTester
|
||||
/packages/*
|
||||
!/packages/Common/
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterfaceLogger.log
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelTesterLogger.log
|
||||
/GenesisCordonelInterface/bin/Debug/SirtConfig.json
|
||||
/GenesisCordonelInterface/bin/Release
|
||||
/GenesisCordonelInterface/obj/Debug
|
||||
/GenesisCordonelInterface/obj/Release
|
||||
/ExternalProjects
|
||||
/AppDiagnostic.Logging/bin/Debug/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/bin/Debug/net472/AppDiagnostic.Logging.pdb
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/obj/Debug/net472/AppDiagnostic.Logging.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Metrology.Measurements.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Metrology.Measurements.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.CordonelPreadjustmentUi.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.CordonelPreadjustmentUi.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.GenesisToolBox.exe
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Ui.GenesisToolBox.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/XylemCommonUiLegacyGenCtl.dll
|
||||
/GenesisCordonelInterface/bin/Debug/XylemCommonUiLegacyGenCtl.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/de/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.resources.dll
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterface.exe
|
||||
/GenesisCordonelInterface/bin/Debug/GenesisCordonelInterface.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/TempFlansh.exe
|
||||
/GenesisCordonelInterface/bin/Debug/TempFlansh.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.ProtocolCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.Interfaces.Protocols.TransmitProtocol.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.eRegister.eRegisterCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.eRegister.eRegisterCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.DataPackages.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.RequestProtocol.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.Genesis.Protocols.StreamingProtocol.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxConfig.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxConfig.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Hardware.WaterMeter.MagFlux.MagFluxCore.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.RelatePcb.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.RelatePcb.pdb
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.ServiceCore.dll
|
||||
/GenesisCordonelInterface/bin/Debug/Xylem.Common.Logic.ServiceCore.pdb
|
||||
/GenesisCordonelInterface/RuntimePackage/Package
|
||||
/SharedComponents/obj/SharedComponents.csproj.nuget.g.props
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/obj/Release/net472/AppDiagnostic.Logging.pdb
|
||||
/AppDiagnostic.Logging/bin/Release/net472/AppDiagnostic.Logging.dll
|
||||
/AppDiagnostic.Logging/bin/Release/net472/AppDiagnostic.Logging.pdb
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<RootNamespace>AppDiagnostic.Logging</RootNamespace>
|
||||
<AssemblyName>AppDiagnostic.Logging</AssemblyName>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<ProjectGuid>{A7AF8ED2-362E-4DBB-B607-EBA2820719EC}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NLog" Version="5.2.2" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,41 @@
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.Layouts;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
public static class AppDiagnosticConnector
|
||||
{
|
||||
private static readonly object syncRoot = new object();
|
||||
|
||||
public static void Register()
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
LoggingConfiguration configuration = LogManager.Configuration ?? new LoggingConfiguration();
|
||||
NLog.Targets.Target existingTarget = configuration.FindTargetByName("appDiagnostic");
|
||||
|
||||
if (existingTarget != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = new AppDiagnosticTarget
|
||||
{
|
||||
Name = "appDiagnostic",
|
||||
PipeName = DiagnosticProtocol.DefaultPipeName,
|
||||
QueueLimit = DiagnosticProtocol.DefaultQueueLimit,
|
||||
ReconnectDelayMilliseconds = DiagnosticProtocol.DefaultReconnectDelayMilliseconds,
|
||||
ConnectTimeoutMilliseconds = DiagnosticProtocol.DefaultConnectTimeoutMilliseconds,
|
||||
Layout = new SimpleLayout("${message}")
|
||||
};
|
||||
|
||||
configuration.AddTarget(target.Name, target);
|
||||
configuration.AddRule(LogLevel.Trace, LogLevel.Fatal, target, "*");
|
||||
|
||||
LogManager.Configuration = configuration;
|
||||
LogManager.ReconfigExistingLoggers();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
[Target("AppDiagnostic")]
|
||||
public sealed class AppDiagnosticTarget : TargetWithLayout
|
||||
{
|
||||
private readonly object connectionLock = new object();
|
||||
private readonly string applicationName;
|
||||
private readonly int processId;
|
||||
|
||||
private BlockingCollection<DiagnosticLogEntry> queue;
|
||||
private CancellationTokenSource cancellation;
|
||||
private Task senderTask;
|
||||
private NamedPipeClientStream pipeClient;
|
||||
|
||||
public AppDiagnosticTarget()
|
||||
{
|
||||
using (Process process = Process.GetCurrentProcess())
|
||||
{
|
||||
applicationName = process.ProcessName;
|
||||
processId = process.Id;
|
||||
}
|
||||
|
||||
PipeName = DiagnosticProtocol.DefaultPipeName;
|
||||
QueueLimit = DiagnosticProtocol.DefaultQueueLimit;
|
||||
ReconnectDelayMilliseconds =
|
||||
DiagnosticProtocol.DefaultReconnectDelayMilliseconds;
|
||||
ConnectTimeoutMilliseconds =
|
||||
DiagnosticProtocol.DefaultConnectTimeoutMilliseconds;
|
||||
}
|
||||
|
||||
public string PipeName { get; set; }
|
||||
|
||||
public int QueueLimit { get; set; }
|
||||
|
||||
public int ReconnectDelayMilliseconds { get; set; }
|
||||
|
||||
public int ConnectTimeoutMilliseconds { get; set; }
|
||||
|
||||
protected override void InitializeTarget()
|
||||
{
|
||||
base.InitializeTarget();
|
||||
|
||||
int queueLimit = QueueLimit > 0
|
||||
? QueueLimit
|
||||
: DiagnosticProtocol.DefaultQueueLimit;
|
||||
|
||||
queue = new BlockingCollection<DiagnosticLogEntry>(queueLimit);
|
||||
cancellation = new CancellationTokenSource();
|
||||
|
||||
senderTask = Task.Factory.StartNew(
|
||||
ProcessQueue,
|
||||
cancellation.Token,
|
||||
TaskCreationOptions.LongRunning,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
protected override void Write(LogEventInfo logEvent)
|
||||
{
|
||||
if (logEvent == null || queue == null || queue.IsAddingCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new DiagnosticLogEntry
|
||||
{
|
||||
TimestampUtc = logEvent.TimeStamp.ToUniversalTime(),
|
||||
MachineName = Environment.MachineName,
|
||||
ApplicationName = applicationName,
|
||||
ProcessId = processId,
|
||||
LoggerName = logEvent.LoggerName ?? string.Empty,
|
||||
Level = logEvent.Level?.Name ?? string.Empty,
|
||||
Message = Layout != null
|
||||
? Layout.Render(logEvent)
|
||||
: logEvent.FormattedMessage,
|
||||
Exception = logEvent.Exception?.ToString() ?? string.Empty
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
queue.TryAdd(entry);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// A concurrent NLog shutdown may dispose the queue.
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// A concurrent NLog shutdown may complete the queue.
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CloseTarget()
|
||||
{
|
||||
BlockingCollection<DiagnosticLogEntry> currentQueue = queue;
|
||||
CancellationTokenSource currentCancellation = cancellation;
|
||||
|
||||
if (currentQueue != null)
|
||||
{
|
||||
currentQueue.CompleteAdding();
|
||||
}
|
||||
|
||||
if (currentCancellation != null)
|
||||
{
|
||||
currentCancellation.Cancel();
|
||||
}
|
||||
|
||||
CloseConnection();
|
||||
|
||||
try
|
||||
{
|
||||
senderTask?.Wait(2000);
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
// Cancellation during application shutdown is expected.
|
||||
}
|
||||
|
||||
currentQueue?.Dispose();
|
||||
currentCancellation?.Dispose();
|
||||
|
||||
queue = null;
|
||||
cancellation = null;
|
||||
senderTask = null;
|
||||
|
||||
base.CloseTarget();
|
||||
}
|
||||
|
||||
private void ProcessQueue()
|
||||
{
|
||||
CancellationToken token = cancellation.Token;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (DiagnosticLogEntry entry in
|
||||
queue.GetConsumingEnumerable(token))
|
||||
{
|
||||
SendWhenConnected(entry, token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Target shutdown is expected to cancel the sender.
|
||||
}
|
||||
}
|
||||
|
||||
private void SendWhenConnected(
|
||||
DiagnosticLogEntry entry,
|
||||
CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureConnected();
|
||||
DiagnosticMessageSerializer.WriteFramed(pipeClient, entry);
|
||||
return;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException ||
|
||||
exception is TimeoutException ||
|
||||
exception is InvalidOperationException)
|
||||
{
|
||||
CloseConnection();
|
||||
|
||||
int delay = ReconnectDelayMilliseconds > 0
|
||||
? ReconnectDelayMilliseconds
|
||||
: DiagnosticProtocol.DefaultReconnectDelayMilliseconds;
|
||||
|
||||
if (token.WaitHandle.WaitOne(delay))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
lock (connectionLock)
|
||||
{
|
||||
if (pipeClient != null && pipeClient.IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseConnectionWithoutLock();
|
||||
|
||||
pipeClient = new NamedPipeClientStream(
|
||||
".",
|
||||
string.IsNullOrWhiteSpace(PipeName)
|
||||
? DiagnosticProtocol.DefaultPipeName
|
||||
: PipeName,
|
||||
PipeDirection.Out,
|
||||
PipeOptions.Asynchronous);
|
||||
|
||||
int timeout = ConnectTimeoutMilliseconds > 0
|
||||
? ConnectTimeoutMilliseconds
|
||||
: DiagnosticProtocol.DefaultConnectTimeoutMilliseconds;
|
||||
|
||||
pipeClient.Connect(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseConnection()
|
||||
{
|
||||
lock (connectionLock)
|
||||
{
|
||||
CloseConnectionWithoutLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseConnectionWithoutLock()
|
||||
{
|
||||
if (pipeClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pipeClient.Dispose();
|
||||
pipeClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
[DataContract]
|
||||
public sealed class DiagnosticLogEntry
|
||||
{
|
||||
[DataMember(Order = 1)]
|
||||
public DateTime TimestampUtc { get; set; }
|
||||
|
||||
[DataMember(Order = 2)]
|
||||
public string MachineName { get; set; }
|
||||
|
||||
[DataMember(Order = 3)]
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
[DataMember(Order = 4)]
|
||||
public int ProcessId { get; set; }
|
||||
|
||||
[DataMember(Order = 5)]
|
||||
public string LoggerName { get; set; }
|
||||
|
||||
[DataMember(Order = 6)]
|
||||
public string Level { get; set; }
|
||||
|
||||
[DataMember(Order = 7)]
|
||||
public string Message { get; set; }
|
||||
|
||||
[DataMember(Order = 8)]
|
||||
public string Exception { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.Serialization.Json;
|
||||
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
public static class DiagnosticMessageSerializer
|
||||
{
|
||||
private static readonly DataContractJsonSerializer Serializer =
|
||||
new DataContractJsonSerializer(typeof(DiagnosticLogEntry));
|
||||
|
||||
private static readonly object SerializerLock = new object();
|
||||
|
||||
public static byte[] Serialize(DiagnosticLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
lock (SerializerLock)
|
||||
{
|
||||
Serializer.WriteObject(stream, entry);
|
||||
}
|
||||
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public static DiagnosticLogEntry Deserialize(byte[] payload)
|
||||
{
|
||||
if (payload == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(payload));
|
||||
}
|
||||
|
||||
if (payload.Length == 0 ||
|
||||
payload.Length > DiagnosticProtocol.MaximumMessageSize)
|
||||
{
|
||||
throw new InvalidDataException("Invalid diagnostic message size.");
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream(payload, false))
|
||||
{
|
||||
lock (SerializerLock)
|
||||
{
|
||||
return (DiagnosticLogEntry)Serializer.ReadObject(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteFramed(Stream stream, DiagnosticLogEntry entry)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
byte[] payload = Serialize(entry);
|
||||
|
||||
if (payload.Length > DiagnosticProtocol.MaximumMessageSize)
|
||||
{
|
||||
throw new InvalidDataException("Diagnostic message is too large.");
|
||||
}
|
||||
|
||||
byte[] length = BitConverter.GetBytes(payload.Length);
|
||||
stream.Write(length, 0, length.Length);
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
stream.Flush();
|
||||
}
|
||||
|
||||
public static DiagnosticLogEntry ReadFramed(Stream stream)
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stream));
|
||||
}
|
||||
|
||||
byte[] lengthBuffer = ReadExactly(stream, sizeof(int));
|
||||
int payloadLength = BitConverter.ToInt32(lengthBuffer, 0);
|
||||
|
||||
if (payloadLength <= 0 ||
|
||||
payloadLength > DiagnosticProtocol.MaximumMessageSize)
|
||||
{
|
||||
throw new InvalidDataException("Invalid diagnostic message size.");
|
||||
}
|
||||
|
||||
return Deserialize(ReadExactly(stream, payloadLength));
|
||||
}
|
||||
|
||||
private static byte[] ReadExactly(Stream stream, int count)
|
||||
{
|
||||
byte[] buffer = new byte[count];
|
||||
int offset = 0;
|
||||
|
||||
while (offset < count)
|
||||
{
|
||||
int bytesRead = stream.Read(buffer, offset, count - offset);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
offset += bytesRead;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace AppDiagnostic.Logging
|
||||
{
|
||||
public static class DiagnosticProtocol
|
||||
{
|
||||
public const string DefaultPipeName = "Sensus.AppDiagnostic.LogPipe";
|
||||
public const int MaximumMessageSize = 1024 * 1024;
|
||||
public const int DefaultQueueLimit = 10000;
|
||||
public const int DefaultReconnectDelayMilliseconds = 500;
|
||||
public const int DefaultConnectTimeoutMilliseconds = 250;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# AppDiagnostic.Logging
|
||||
|
||||
Reusable NLog target for sending structured diagnostic events from multiple
|
||||
.NET Framework processes to `AppDiagnostic.exe` through a local named pipe.
|
||||
|
||||
## Producer integration
|
||||
|
||||
Reference this project from the producer application and add the extension and
|
||||
target to its NLog configuration:
|
||||
|
||||
```xml
|
||||
<extensions>
|
||||
<add assembly="AppDiagnostic.Logging" />
|
||||
</extensions>
|
||||
|
||||
<targets>
|
||||
<target name="appDiagnostic"
|
||||
xsi:type="AppDiagnostic"
|
||||
layout="${message}" />
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
<logger name="*" minlevel="Trace" writeTo="appDiagnostic" />
|
||||
</rules>
|
||||
```
|
||||
|
||||
The target writes to a bounded background queue. Producer threads are not
|
||||
blocked while the diagnostic viewer is unavailable. Queued messages are sent
|
||||
after the viewer becomes available; new messages are discarded when the queue
|
||||
limit is reached.
|
||||
|
||||
The receiver can read messages with:
|
||||
|
||||
```csharp
|
||||
DiagnosticLogEntry entry =
|
||||
DiagnosticMessageSerializer.ReadFramed(namedPipeServerStream);
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"projectName": "AppDiagnostic.Logging",
|
||||
"projectPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"packagesPath": "C:\\Users\\MFRNIAK\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\MFRNIAK\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net472"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"dependencies": {
|
||||
"NLog": {
|
||||
"target": "Package",
|
||||
"version": "[5.2.2, )"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.303\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\MFRNIAK\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\MFRNIAK\.nuget\packages\" />
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = AppDiagnostic.Logging
|
||||
build_property.ProjectDir = C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
551ba94934db30a277a1a9cec2dacbe03f8393107e5d0b2436695f9bd62bd569
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\AppDiagnostic.Logging.pdb
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Debug\net472\NLog.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.csproj.CoreCompileInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagn.CFEA7AC0.Up2Date
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Debug\net472\AppDiagnostic.Logging.pdb
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
is_global = true
|
||||
build_property.RootNamespace = AppDiagnostic.Logging
|
||||
build_property.ProjectDir = C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
a53d59482cad49b656138b1af5cf3e55edf2108969aedc574c362be6c671436b
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\AppDiagnostic.Logging.pdb
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\bin\Release\net472\NLog.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.csproj.AssemblyReference.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.AssemblyInfoInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.AssemblyInfo.cs
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.csproj.CoreCompileInputs.cache
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagn.CFEA7AC0.Up2Date
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.dll
|
||||
C:\Sensus_projects\New\LocalBranch_start_at_6.3.2026\tbf-local\tbf\AppDiagnostic.Logging\obj\Release\net472\AppDiagnostic.Logging.pdb
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"version": 4,
|
||||
"targets": {
|
||||
"net472": {
|
||||
"NLog/5.2.2": {
|
||||
"type": "package",
|
||||
"frameworkAssemblies": [
|
||||
"System",
|
||||
"System.Configuration",
|
||||
"System.Core",
|
||||
"System.IO.Compression",
|
||||
"System.Xml"
|
||||
],
|
||||
"compile": {
|
||||
"lib/net46/NLog.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net46/NLog.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"NLog/5.2.2": {
|
||||
"sha512": "r6Q9740g29gTwmTWlsgdIFm0mhNsfNZmbvWKX/Fxmi8X89ZrpUowHM2T2X1lP7RVpND+ef+XnfKL5g6Q1iNGXA==",
|
||||
"type": "package",
|
||||
"path": "nlog/5.2.2",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"N.png",
|
||||
"lib/net35/NLog.dll",
|
||||
"lib/net35/NLog.xml",
|
||||
"lib/net45/NLog.dll",
|
||||
"lib/net45/NLog.xml",
|
||||
"lib/net46/NLog.dll",
|
||||
"lib/net46/NLog.xml",
|
||||
"lib/netstandard1.3/NLog.dll",
|
||||
"lib/netstandard1.3/NLog.xml",
|
||||
"lib/netstandard1.5/NLog.dll",
|
||||
"lib/netstandard1.5/NLog.xml",
|
||||
"lib/netstandard2.0/NLog.dll",
|
||||
"lib/netstandard2.0/NLog.xml",
|
||||
"nlog.5.2.2.nupkg.sha512",
|
||||
"nlog.nuspec"
|
||||
]
|
||||
}
|
||||
},
|
||||
"projectFileDependencyGroups": {
|
||||
"net472": [
|
||||
"NLog >= 5.2.2"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\MFRNIAK\\.nuget\\packages\\": {},
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"projectName": "AppDiagnostic.Logging",
|
||||
"projectPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"packagesPath": "C:\\Users\\MFRNIAK\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"fallbackFolders": [
|
||||
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
|
||||
],
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\MFRNIAK\\AppData\\Roaming\\NuGet\\NuGet.Config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config",
|
||||
"C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net472"
|
||||
],
|
||||
"sources": {
|
||||
"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {},
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.300"
|
||||
},
|
||||
"frameworks": {
|
||||
"net472": {
|
||||
"framework": "net472",
|
||||
"targetAlias": "net472",
|
||||
"dependencies": {
|
||||
"NLog": {
|
||||
"target": "Package",
|
||||
"version": "[5.2.2, )"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.303\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "ezwKOPZAAXU=",
|
||||
"success": true,
|
||||
"projectFilePath": "C:\\Sensus_projects\\New\\LocalBranch_start_at_6.3.2026\\tbf-local\\tbf\\AppDiagnostic.Logging\\AppDiagnostic.Logging.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\MFRNIAK\\.nuget\\packages\\nlog\\5.2.2\\nlog.5.2.2.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -3,17 +3,12 @@
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
|
||||
</startup>
|
||||
<log4net>
|
||||
<appender name="LogCacheAppender" type="Namespace.LogCacheAppender, AssemblyName">
|
||||
<param name="Cache" value="YourCacheInstance" />
|
||||
<layout type="log4net.Layout.PatternLayout">
|
||||
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
|
||||
</layout>
|
||||
</appender>
|
||||
|
||||
<root>
|
||||
<level value="DEBUG" />
|
||||
<appender-ref ref="LogCacheAppender" />
|
||||
</root>
|
||||
</log4net>
|
||||
</configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
|
||||
@@ -33,9 +33,6 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="log4net, Version=3.0.3.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\log4net.3.0.3\lib\net462\log4net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
@@ -51,12 +48,15 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="BufferedListView.cs">
|
||||
<SubType>Component</SubType>
|
||||
<Compile Include="ColorRuleEditorForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="DiagApi.cs" />
|
||||
<Compile Include="LogAdapter.cs" />
|
||||
<Compile Include="LogFilter.cs" />
|
||||
<Compile Include="DiagnosticLogFormatter.cs" />
|
||||
<Compile Include="DiagnosticLogStore.cs" />
|
||||
<Compile Include="DiagnosticPipeServer.cs" />
|
||||
<Compile Include="LogColorizer.cs" />
|
||||
<Compile Include="LogColorRule.cs" />
|
||||
<Compile Include="LogColorRuleRepository.cs" />
|
||||
<Compile Include="MainForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
@@ -78,7 +78,6 @@
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
</Compile>
|
||||
<None Include="packages.config" />
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
@@ -93,9 +92,9 @@
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharedComponents\SharedComponents.csproj">
|
||||
<Project>{8f942729-f454-4c99-ba6c-746962065ae3}</Project>
|
||||
<Name>SharedComponents</Name>
|
||||
<ProjectReference Include="..\AppDiagnostic.Logging\AppDiagnostic.Logging.csproj">
|
||||
<Project>{A7AF8ED2-362E-4DBB-B607-EBA2820719EC}</Project>
|
||||
<Name>AppDiagnostic.Logging</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public sealed class ColorRuleEditorForm : Form
|
||||
{
|
||||
private readonly List<LogColorRule> rules;
|
||||
private readonly ListBox ruleList = new ListBox();
|
||||
private readonly TextBox patternTextBox = new TextBox();
|
||||
private readonly CheckBox enabledCheckBox = new CheckBox();
|
||||
private readonly CheckBox boldCheckBox = new CheckBox();
|
||||
private readonly CheckBox backgroundCheckBox = new CheckBox();
|
||||
private readonly Button foregroundButton = new Button();
|
||||
private readonly Button backgroundButton = new Button();
|
||||
private readonly RichTextBox previewBox = new RichTextBox();
|
||||
private readonly Font previewBoldFont;
|
||||
private bool loadingRule;
|
||||
|
||||
public ColorRuleEditorForm(IEnumerable<LogColorRule> sourceRules)
|
||||
{
|
||||
rules = sourceRules.Select(rule => rule.Clone()).ToList();
|
||||
InitializeWindow();
|
||||
previewBoldFont = new Font(previewBox.Font, FontStyle.Bold);
|
||||
RefreshRuleList(0);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
public List<LogColorRule> Rules => rules.Select(rule => rule.Clone()).ToList();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
previewBoldFont.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeWindow()
|
||||
{
|
||||
Text = "Log color rules";
|
||||
Icon = Properties.Resources.TBF_icon;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
MinimumSize = new Size(760, 500);
|
||||
ClientSize = new Size(860, 560);
|
||||
|
||||
var root = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 1, RowCount = 3, Padding = new Padding(10) };
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 145F));
|
||||
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 42F));
|
||||
|
||||
var split = new SplitContainer { Dock = DockStyle.Fill, SplitterDistance = 280 };
|
||||
split.Panel1.Padding = new Padding(0, 0, 8, 0);
|
||||
split.Panel2.Padding = new Padding(8, 0, 0, 0);
|
||||
|
||||
ruleList.Dock = DockStyle.Fill;
|
||||
ruleList.IntegralHeight = false;
|
||||
ruleList.SelectedIndexChanged += RuleList_SelectedIndexChanged;
|
||||
|
||||
var listButtons = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 36, FlowDirection = FlowDirection.LeftToRight };
|
||||
listButtons.Controls.Add(CreateButton("Add", AddRule_Click));
|
||||
listButtons.Controls.Add(CreateButton("Delete", DeleteRule_Click));
|
||||
listButtons.Controls.Add(CreateButton("Up", MoveUp_Click));
|
||||
listButtons.Controls.Add(CreateButton("Down", MoveDown_Click));
|
||||
split.Panel1.Controls.Add(ruleList);
|
||||
split.Panel1.Controls.Add(listButtons);
|
||||
|
||||
var editor = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2, Padding = new Padding(4) };
|
||||
editor.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 120F));
|
||||
editor.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||||
|
||||
enabledCheckBox.Text = "Rule enabled";
|
||||
enabledCheckBox.AutoSize = true;
|
||||
enabledCheckBox.CheckedChanged += EditorValueChanged;
|
||||
AddEditorRow(editor, "State", enabledCheckBox);
|
||||
|
||||
patternTextBox.Dock = DockStyle.Fill;
|
||||
patternTextBox.TextChanged += EditorValueChanged;
|
||||
AddEditorRow(editor, "Substring", patternTextBox);
|
||||
|
||||
foregroundButton.Text = "Choose text color...";
|
||||
foregroundButton.AutoSize = true;
|
||||
foregroundButton.Click += ForegroundButton_Click;
|
||||
AddEditorRow(editor, "Text color", foregroundButton);
|
||||
|
||||
var backgroundPanel = new FlowLayoutPanel { Dock = DockStyle.Fill, AutoSize = true };
|
||||
backgroundCheckBox.Text = "Use background";
|
||||
backgroundCheckBox.AutoSize = true;
|
||||
backgroundCheckBox.CheckedChanged += EditorValueChanged;
|
||||
backgroundButton.Text = "Choose...";
|
||||
backgroundButton.AutoSize = true;
|
||||
backgroundButton.Click += BackgroundButton_Click;
|
||||
backgroundPanel.Controls.Add(backgroundCheckBox);
|
||||
backgroundPanel.Controls.Add(backgroundButton);
|
||||
AddEditorRow(editor, "Background", backgroundPanel);
|
||||
|
||||
boldCheckBox.Text = "Bold text";
|
||||
boldCheckBox.AutoSize = true;
|
||||
boldCheckBox.CheckedChanged += EditorValueChanged;
|
||||
AddEditorRow(editor, "Font", boldCheckBox);
|
||||
|
||||
var resetButton = CreateButton("Restore default rules", RestoreDefaults_Click);
|
||||
resetButton.AutoSize = true;
|
||||
AddEditorRow(editor, string.Empty, resetButton);
|
||||
split.Panel2.Controls.Add(editor);
|
||||
|
||||
previewBox.Dock = DockStyle.Fill;
|
||||
previewBox.ReadOnly = true;
|
||||
previewBox.WordWrap = true;
|
||||
previewBox.Font = new Font("Consolas", 9F);
|
||||
previewBox.Text = "2026-09-10 14:25:31.042|DEBUG|TBF[14280]|TBF.UI.MainWnd|Device configuration loaded.\n2026-09-10 14:25:31.384|INFO |TBF[14280]|TBF.Rig.StateMachine|Test bench started.\n2026-09-10 14:25:32.018|WARN |TBF[14280]|TBF.Device|Pressure is approaching the limit.\n2026-09-10 14:25:32.611|ERROR|TBF[14280]|TBF.Device|Communication failed.";
|
||||
|
||||
var previewGroup = new GroupBox { Text = "Live preview", Dock = DockStyle.Fill, Padding = new Padding(8) };
|
||||
previewGroup.Controls.Add(previewBox);
|
||||
|
||||
var footer = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft, Padding = new Padding(0, 8, 0, 0) };
|
||||
var okButton = new Button { Text = "OK", DialogResult = DialogResult.OK, AutoSize = true };
|
||||
var cancelButton = new Button { Text = "Cancel", DialogResult = DialogResult.Cancel, AutoSize = true };
|
||||
footer.Controls.Add(okButton);
|
||||
footer.Controls.Add(cancelButton);
|
||||
|
||||
AcceptButton = okButton;
|
||||
CancelButton = cancelButton;
|
||||
root.Controls.Add(split, 0, 0);
|
||||
root.Controls.Add(previewGroup, 0, 1);
|
||||
root.Controls.Add(footer, 0, 2);
|
||||
Controls.Add(root);
|
||||
}
|
||||
|
||||
private static Button CreateButton(string text, EventHandler clickHandler)
|
||||
{
|
||||
var button = new Button { Text = text, AutoSize = true };
|
||||
button.Click += clickHandler;
|
||||
return button;
|
||||
}
|
||||
|
||||
private static void AddEditorRow(TableLayoutPanel panel, string labelText, Control control)
|
||||
{
|
||||
int row = panel.RowCount++;
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
var label = new Label { Text = labelText, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(3, 8, 3, 3) };
|
||||
control.Margin = new Padding(3, 5, 3, 5);
|
||||
panel.Controls.Add(label, 0, row);
|
||||
panel.Controls.Add(control, 1, row);
|
||||
}
|
||||
|
||||
private void RefreshRuleList(int selectedIndex)
|
||||
{
|
||||
ruleList.BeginUpdate();
|
||||
ruleList.Items.Clear();
|
||||
foreach (LogColorRule rule in rules)
|
||||
{
|
||||
ruleList.Items.Add(rule);
|
||||
}
|
||||
ruleList.EndUpdate();
|
||||
|
||||
if (rules.Count > 0)
|
||||
{
|
||||
ruleList.SelectedIndex = Math.Max(0, Math.Min(selectedIndex, rules.Count - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void RuleList_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loadingRule = true;
|
||||
enabledCheckBox.Checked = rule.Enabled;
|
||||
patternTextBox.Text = rule.Pattern;
|
||||
foregroundButton.BackColor = rule.ForeColor;
|
||||
backgroundCheckBox.Checked = rule.UseBackColor;
|
||||
backgroundButton.Enabled = rule.UseBackColor;
|
||||
backgroundButton.BackColor = rule.UseBackColor ? rule.BackColor : SystemColors.Control;
|
||||
boldCheckBox.Checked = rule.Bold;
|
||||
loadingRule = false;
|
||||
}
|
||||
|
||||
private void EditorValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (loadingRule)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rule.Enabled = enabledCheckBox.Checked;
|
||||
rule.Pattern = patternTextBox.Text;
|
||||
rule.UseBackColor = backgroundCheckBox.Checked;
|
||||
rule.Bold = boldCheckBox.Checked;
|
||||
backgroundButton.Enabled = rule.UseBackColor;
|
||||
int index = ruleList.SelectedIndex;
|
||||
RefreshRuleList(index);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void ForegroundButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var dialog = new ColorDialog { Color = rule.ForeColor, FullOpen = true })
|
||||
{
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rule.ForeColorArgb = dialog.Color.ToArgb();
|
||||
foregroundButton.BackColor = dialog.Color;
|
||||
RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void BackgroundButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
LogColorRule rule = ruleList.SelectedItem as LogColorRule;
|
||||
if (rule == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color initialColor = rule.UseBackColor ? rule.BackColor : SystemColors.Window;
|
||||
using (var dialog = new ColorDialog { Color = initialColor, FullOpen = true })
|
||||
{
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rule.BackColorArgb = dialog.Color.ToArgb();
|
||||
rule.UseBackColor = true;
|
||||
backgroundCheckBox.Checked = true;
|
||||
backgroundButton.BackColor = dialog.Color;
|
||||
RefreshPreview();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRule_Click(object sender, EventArgs e)
|
||||
{
|
||||
rules.Add(new LogColorRule { Pattern = "NEW SUBSTRING", ForeColorArgb = Color.MediumPurple.ToArgb() });
|
||||
RefreshRuleList(rules.Count - 1);
|
||||
patternTextBox.Focus();
|
||||
patternTextBox.SelectAll();
|
||||
}
|
||||
|
||||
private void DeleteRule_Click(object sender, EventArgs e)
|
||||
{
|
||||
int index = ruleList.SelectedIndex;
|
||||
if (index < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rules.RemoveAt(index);
|
||||
RefreshRuleList(Math.Min(index, rules.Count - 1));
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void MoveUp_Click(object sender, EventArgs e)
|
||||
{
|
||||
MoveSelectedRule(-1);
|
||||
}
|
||||
|
||||
private void MoveDown_Click(object sender, EventArgs e)
|
||||
{
|
||||
MoveSelectedRule(1);
|
||||
}
|
||||
|
||||
private void MoveSelectedRule(int offset)
|
||||
{
|
||||
int index = ruleList.SelectedIndex;
|
||||
int newIndex = index + offset;
|
||||
if (index < 0 || newIndex < 0 || newIndex >= rules.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogColorRule rule = rules[index];
|
||||
rules.RemoveAt(index);
|
||||
rules.Insert(newIndex, rule);
|
||||
RefreshRuleList(newIndex);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void RestoreDefaults_Click(object sender, EventArgs e)
|
||||
{
|
||||
rules.Clear();
|
||||
rules.AddRange(LogColorRule.CreateDefaults());
|
||||
RefreshRuleList(0);
|
||||
RefreshPreview();
|
||||
}
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
int selectionStart = previewBox.SelectionStart;
|
||||
int selectionLength = previewBox.SelectionLength;
|
||||
string text = previewBox.Text;
|
||||
LogColorizer.Apply(previewBox, 0, text.Length, text, rules, previewBox.Font, previewBoldFont);
|
||||
previewBox.Select(Math.Min(selectionStart, previewBox.TextLength), Math.Min(selectionLength, Math.Max(0, previewBox.TextLength - selectionStart)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using AppDiagnostic.Logging;
|
||||
using System;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public static class DiagnosticLogFormatter
|
||||
{
|
||||
public static string Format(DiagnosticLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string level = (entry.Level ?? string.Empty).ToUpperInvariant().PadRight(5);
|
||||
string application = string.IsNullOrWhiteSpace(entry.ApplicationName) ? "Unknown" : entry.ApplicationName;
|
||||
string logger = entry.LoggerName ?? string.Empty;
|
||||
string message = entry.Message ?? string.Empty;
|
||||
string exception = string.IsNullOrWhiteSpace(entry.Exception) ? string.Empty : Environment.NewLine + entry.Exception;
|
||||
|
||||
return $"{entry.TimestampUtc.ToLocalTime():yyyy-MM-dd HH:mm:ss.fff}|{level}|{application}[{entry.ProcessId}]|{logger}|{message}{exception}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using AppDiagnostic.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public sealed class DiagnosticLogStore
|
||||
{
|
||||
private const int MaximumLogCount = 10000;
|
||||
private static readonly DiagnosticLogStore instance = new DiagnosticLogStore();
|
||||
private readonly object syncRoot = new object();
|
||||
private readonly List<DiagnosticLogEntry> entries = new List<DiagnosticLogEntry>();
|
||||
private long totalReceived;
|
||||
private long version;
|
||||
|
||||
private DiagnosticLogStore()
|
||||
{
|
||||
}
|
||||
|
||||
public static DiagnosticLogStore Instance => instance;
|
||||
|
||||
public long TotalReceived
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return totalReceived;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long Version
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(DiagnosticLogEntry entry)
|
||||
{
|
||||
if (entry == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
entries.Add(entry);
|
||||
totalReceived++;
|
||||
version++;
|
||||
|
||||
int excessCount = entries.Count - MaximumLogCount;
|
||||
if (excessCount > 0)
|
||||
{
|
||||
entries.RemoveRange(0, excessCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<DiagnosticLogEntry> GetSnapshot()
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return entries.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
entries.Clear();
|
||||
version++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using AppDiagnostic.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public sealed class DiagnosticPipeServer : IDisposable
|
||||
{
|
||||
private readonly Action<DiagnosticLogEntry> messageReceived;
|
||||
private readonly CancellationTokenSource cancellation = new CancellationTokenSource();
|
||||
private readonly object syncRoot = new object();
|
||||
private readonly List<NamedPipeServerStream> connectedClients = new List<NamedPipeServerStream>();
|
||||
private NamedPipeServerStream waitingServer;
|
||||
private Task serverTask;
|
||||
|
||||
public DiagnosticPipeServer(Action<DiagnosticLogEntry> messageReceived)
|
||||
{
|
||||
this.messageReceived = messageReceived ?? throw new ArgumentNullException(nameof(messageReceived));
|
||||
}
|
||||
|
||||
public int ConnectedClientCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
return connectedClients.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (serverTask != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
serverTask = Task.Run((Action)AcceptConnections);
|
||||
}
|
||||
|
||||
private void AcceptConnections()
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
NamedPipeServerStream server = null;
|
||||
|
||||
try
|
||||
{
|
||||
server = new NamedPipeServerStream(DiagnosticProtocol.DefaultPipeName, PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
waitingServer = server;
|
||||
}
|
||||
|
||||
server.WaitForConnection();
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
waitingServer = null;
|
||||
connectedClients.Add(server);
|
||||
}
|
||||
|
||||
NamedPipeServerStream connectedServer = server;
|
||||
server = null;
|
||||
Task.Run(() => ReadClient(connectedServer));
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
if (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
if (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
if (ReferenceEquals(waitingServer, server))
|
||||
{
|
||||
waitingServer = null;
|
||||
}
|
||||
}
|
||||
|
||||
server?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadClient(NamedPipeServerStream client)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (client.IsConnected && !cancellation.IsCancellationRequested)
|
||||
{
|
||||
DiagnosticLogEntry entry = DiagnosticMessageSerializer.ReadFramed(client);
|
||||
messageReceived(entry);
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
// The producer application was closed.
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// The producer application disconnected.
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// The diagnostic application is shutting down.
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (syncRoot)
|
||||
{
|
||||
connectedClients.Remove(client);
|
||||
}
|
||||
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
cancellation.Cancel();
|
||||
|
||||
lock (syncRoot)
|
||||
{
|
||||
waitingServer?.Dispose();
|
||||
waitingServer = null;
|
||||
|
||||
foreach (NamedPipeServerStream client in connectedClients.ToArray())
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
connectedClients.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
serverTask?.Wait(2000);
|
||||
}
|
||||
catch (AggregateException)
|
||||
{
|
||||
// Cancellation during shutdown is expected.
|
||||
}
|
||||
|
||||
cancellation.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
[Serializable]
|
||||
public sealed class LogColorRule
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string Pattern { get; set; } = string.Empty;
|
||||
public int ForeColorArgb { get; set; } = Color.Black.ToArgb();
|
||||
public int BackColorArgb { get; set; }
|
||||
public bool UseBackColor { get; set; }
|
||||
public bool Bold { get; set; }
|
||||
|
||||
public Color ForeColor => Color.FromArgb(ForeColorArgb);
|
||||
public Color BackColor => Color.FromArgb(BackColorArgb);
|
||||
|
||||
public LogColorRule Clone()
|
||||
{
|
||||
return new LogColorRule
|
||||
{
|
||||
Enabled = Enabled,
|
||||
Pattern = Pattern,
|
||||
ForeColorArgb = ForeColorArgb,
|
||||
BackColorArgb = BackColorArgb,
|
||||
UseBackColor = UseBackColor,
|
||||
Bold = Bold
|
||||
};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string state = Enabled ? string.Empty : "[disabled] ";
|
||||
return state + Pattern;
|
||||
}
|
||||
|
||||
public static List<LogColorRule> CreateDefaults()
|
||||
{
|
||||
return new List<LogColorRule>
|
||||
{
|
||||
Create("|TRACE|", Color.DimGray, false),
|
||||
Create("|DEBUG|", Color.RoyalBlue, false),
|
||||
Create("|INFO |", Color.ForestGreen, false),
|
||||
Create("|WARN |", Color.DarkOrange, true),
|
||||
Create("|ERROR|", Color.Crimson, true),
|
||||
Create("|FATAL|", Color.White, true, Color.DarkRed)
|
||||
};
|
||||
}
|
||||
|
||||
private static LogColorRule Create(string pattern, Color foreground, bool bold, Color? background = null)
|
||||
{
|
||||
return new LogColorRule
|
||||
{
|
||||
Enabled = true,
|
||||
Pattern = pattern,
|
||||
ForeColorArgb = foreground.ToArgb(),
|
||||
BackColorArgb = background?.ToArgb() ?? Color.Empty.ToArgb(),
|
||||
UseBackColor = background.HasValue,
|
||||
Bold = bold
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public static class LogColorRuleRepository
|
||||
{
|
||||
private static readonly string settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Sensus", "AppDiagnostic");
|
||||
private static readonly string settingsPath = Path.Combine(settingsFolder, "color-rules.xml");
|
||||
|
||||
public static List<LogColorRule> Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(settingsPath))
|
||||
{
|
||||
return LogColorRule.CreateDefaults();
|
||||
}
|
||||
|
||||
var serializer = new XmlSerializer(typeof(List<LogColorRule>));
|
||||
using (FileStream stream = File.OpenRead(settingsPath))
|
||||
{
|
||||
var rules = serializer.Deserialize(stream) as List<LogColorRule>;
|
||||
return rules == null || rules.Count == 0 ? LogColorRule.CreateDefaults() : rules;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LogColorRule.CreateDefaults();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save(IEnumerable<LogColorRule> rules)
|
||||
{
|
||||
Directory.CreateDirectory(settingsFolder);
|
||||
|
||||
var serializer = new XmlSerializer(typeof(List<LogColorRule>));
|
||||
using (FileStream stream = File.Create(settingsPath))
|
||||
{
|
||||
serializer.Serialize(stream, new List<LogColorRule>(rules));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public static class LogColorizer
|
||||
{
|
||||
public static void Apply(RichTextBox target, int startIndex, int length, string text, IEnumerable<LogColorRule> rules, Font regularFont, Font boldFont)
|
||||
{
|
||||
if (target == null || length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
target.Select(startIndex, length);
|
||||
target.SelectionColor = target.ForeColor;
|
||||
target.SelectionBackColor = target.BackColor;
|
||||
target.SelectionFont = regularFont;
|
||||
|
||||
foreach (LogColorRule rule in rules)
|
||||
{
|
||||
if (rule == null || !rule.Enabled || string.IsNullOrEmpty(rule.Pattern))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int searchIndex = 0;
|
||||
while (searchIndex < text.Length)
|
||||
{
|
||||
int matchIndex = text.IndexOf(rule.Pattern, searchIndex, StringComparison.OrdinalIgnoreCase);
|
||||
if (matchIndex < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
target.Select(startIndex + matchIndex, rule.Pattern.Length);
|
||||
target.SelectionColor = rule.ForeColor;
|
||||
target.SelectionBackColor = rule.UseBackColor ? rule.BackColor : target.BackColor;
|
||||
target.SelectionFont = rule.Bold ? boldFont : regularFont;
|
||||
searchIndex = matchIndex + rule.Pattern.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+281
-250
@@ -1,278 +1,309 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
private System.Windows.Forms.ToolStrip mainToolStrip;
|
||||
private System.Windows.Forms.ToolStripButton pauseButton;
|
||||
private System.Windows.Forms.ToolStripButton refreshButton;
|
||||
private System.Windows.Forms.ToolStripButton clearButton;
|
||||
private System.Windows.Forms.ToolStripButton saveButton;
|
||||
private System.Windows.Forms.ToolStripButton copyButton;
|
||||
private System.Windows.Forms.ToolStripButton copyAllButton;
|
||||
private System.Windows.Forms.ToolStripButton wrapButton;
|
||||
private System.Windows.Forms.ToolStripButton autoScrollButton;
|
||||
private System.Windows.Forms.ToolStripButton alwaysOnTopButton;
|
||||
private System.Windows.Forms.ToolStripButton colorRulesButton;
|
||||
private System.Windows.Forms.ToolStripButton closeButton;
|
||||
private System.Windows.Forms.TableLayoutPanel filterPanel;
|
||||
private System.Windows.Forms.Label filterLabel;
|
||||
private System.Windows.Forms.TextBox filterTextBox;
|
||||
private System.Windows.Forms.Label levelLabel;
|
||||
private System.Windows.Forms.ComboBox levelComboBox;
|
||||
private System.Windows.Forms.Label applicationLabel;
|
||||
private System.Windows.Forms.ComboBox applicationComboBox;
|
||||
private System.Windows.Forms.Button applyFilterButton;
|
||||
private System.Windows.Forms.Button clearFilterButton;
|
||||
private System.Windows.Forms.RichTextBox logView;
|
||||
private System.Windows.Forms.StatusStrip statusStrip;
|
||||
private System.Windows.Forms.ToolStripStatusLabel receiverStatusLabel;
|
||||
private System.Windows.Forms.ToolStripStatusLabel statusSpacer;
|
||||
private System.Windows.Forms.ToolStripStatusLabel countStatusLabel;
|
||||
private System.Windows.Forms.ContextMenuStrip logContextMenu;
|
||||
private System.Windows.Forms.ToolStripMenuItem copySelectionMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem copyLineMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem copyAllMenuItem;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
if (disposing && components != null)
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
//private BufferedListView memoListView;
|
||||
private System.Windows.Forms.ColumnHeader columnHeaderTime;
|
||||
private System.Windows.Forms.ColumnHeader columnHeaderMessage;
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.columnHeaderTime = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.columnHeaderMessage = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader()));
|
||||
this.panel1 = new System.Windows.Forms.Panel();
|
||||
this.refreshMemoButton = new System.Windows.Forms.Button();
|
||||
this.button6 = new System.Windows.Forms.Button();
|
||||
this.button7 = new System.Windows.Forms.Button();
|
||||
this.filterManagementPanel = new System.Windows.Forms.Panel();
|
||||
this.button5 = new System.Windows.Forms.Button();
|
||||
this.filterButton = new System.Windows.Forms.Button();
|
||||
this.components = new System.ComponentModel.Container();
|
||||
this.mainToolStrip = new System.Windows.Forms.ToolStrip();
|
||||
this.pauseButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.refreshButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.clearButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.saveButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.copyButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.copyAllButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.wrapButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.autoScrollButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.alwaysOnTopButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.colorRulesButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.closeButton = new System.Windows.Forms.ToolStripButton();
|
||||
this.filterPanel = new System.Windows.Forms.TableLayoutPanel();
|
||||
this.filterLabel = new System.Windows.Forms.Label();
|
||||
this.filterTextBox = new System.Windows.Forms.TextBox();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.wndControlPanel = new System.Windows.Forms.Panel();
|
||||
this.button2 = new System.Windows.Forms.Button();
|
||||
this.flowControlsPanel = new System.Windows.Forms.Panel();
|
||||
this.runButton = new System.Windows.Forms.Button();
|
||||
this.stopButton = new System.Windows.Forms.Button();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.memoListView = new BufferedListView();
|
||||
this.panel1.SuspendLayout();
|
||||
this.filterManagementPanel.SuspendLayout();
|
||||
this.wndControlPanel.SuspendLayout();
|
||||
this.flowControlsPanel.SuspendLayout();
|
||||
this.levelLabel = new System.Windows.Forms.Label();
|
||||
this.levelComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.applicationLabel = new System.Windows.Forms.Label();
|
||||
this.applicationComboBox = new System.Windows.Forms.ComboBox();
|
||||
this.applyFilterButton = new System.Windows.Forms.Button();
|
||||
this.clearFilterButton = new System.Windows.Forms.Button();
|
||||
this.logContextMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
|
||||
this.copySelectionMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.copyLineMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.copyAllMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.logView = new System.Windows.Forms.RichTextBox();
|
||||
this.statusStrip = new System.Windows.Forms.StatusStrip();
|
||||
this.receiverStatusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.statusSpacer = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.countStatusLabel = new System.Windows.Forms.ToolStripStatusLabel();
|
||||
this.mainToolStrip.SuspendLayout();
|
||||
this.filterPanel.SuspendLayout();
|
||||
this.logContextMenu.SuspendLayout();
|
||||
this.statusStrip.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// columnHeaderTime
|
||||
//
|
||||
this.columnHeaderTime.Text = "Time";
|
||||
this.columnHeaderTime.Width = 150;
|
||||
//
|
||||
// columnHeaderMessage
|
||||
//
|
||||
this.columnHeaderMessage.Text = "Message";
|
||||
this.columnHeaderMessage.Width = 350;
|
||||
//
|
||||
// panel1
|
||||
//
|
||||
this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.panel1.Controls.Add(this.refreshMemoButton);
|
||||
this.panel1.Controls.Add(this.button6);
|
||||
this.panel1.Controls.Add(this.button7);
|
||||
this.panel1.Location = new System.Drawing.Point(189, 356);
|
||||
this.panel1.Name = "panel1";
|
||||
this.panel1.Size = new System.Drawing.Size(283, 26);
|
||||
this.panel1.TabIndex = 16;
|
||||
//
|
||||
// refreshMemoButton
|
||||
//
|
||||
this.refreshMemoButton.Location = new System.Drawing.Point(108, 0);
|
||||
this.refreshMemoButton.Name = "refreshMemoButton";
|
||||
this.refreshMemoButton.Size = new System.Drawing.Size(84, 26);
|
||||
this.refreshMemoButton.TabIndex = 17;
|
||||
this.refreshMemoButton.Text = "Refresh memo";
|
||||
this.refreshMemoButton.UseVisualStyleBackColor = true;
|
||||
this.refreshMemoButton.Click += new System.EventHandler(this.refreshMemoButton_Click);
|
||||
//
|
||||
// button6
|
||||
//
|
||||
this.button6.Location = new System.Drawing.Point(0, 0);
|
||||
this.button6.Name = "button6";
|
||||
this.button6.Size = new System.Drawing.Size(102, 26);
|
||||
this.button6.TabIndex = 4;
|
||||
this.button6.Text = "Clean diag cache";
|
||||
this.button6.UseVisualStyleBackColor = true;
|
||||
this.button6.Click += new System.EventHandler(this.button6_Click);
|
||||
//
|
||||
// button7
|
||||
//
|
||||
this.button7.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.button7.Location = new System.Drawing.Point(198, 0);
|
||||
this.button7.Name = "button7";
|
||||
this.button7.Size = new System.Drawing.Size(72, 26);
|
||||
this.button7.TabIndex = 5;
|
||||
this.button7.Text = "Save logs";
|
||||
this.button7.UseVisualStyleBackColor = true;
|
||||
this.button7.Click += new System.EventHandler(this.button7_Click);
|
||||
//
|
||||
// filterManagementPanel
|
||||
//
|
||||
this.filterManagementPanel.Controls.Add(this.button5);
|
||||
this.filterManagementPanel.Controls.Add(this.filterButton);
|
||||
this.filterManagementPanel.Controls.Add(this.filterTextBox);
|
||||
this.filterManagementPanel.Controls.Add(this.label2);
|
||||
this.filterManagementPanel.Location = new System.Drawing.Point(127, 6);
|
||||
this.filterManagementPanel.Name = "filterManagementPanel";
|
||||
this.filterManagementPanel.Size = new System.Drawing.Size(393, 26);
|
||||
this.filterManagementPanel.TabIndex = 15;
|
||||
//
|
||||
// button5
|
||||
//
|
||||
this.button5.Enabled = false;
|
||||
this.button5.Location = new System.Drawing.Point(294, 4);
|
||||
this.button5.Name = "button5";
|
||||
this.button5.Size = new System.Drawing.Size(81, 20);
|
||||
this.button5.TabIndex = 11;
|
||||
this.button5.Text = "Add new filter";
|
||||
this.button5.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterButton
|
||||
//
|
||||
this.filterButton.Location = new System.Drawing.Point(225, 4);
|
||||
this.filterButton.Name = "filterButton";
|
||||
this.filterButton.Size = new System.Drawing.Size(64, 20);
|
||||
this.filterButton.TabIndex = 10;
|
||||
this.filterButton.Text = "Set filter";
|
||||
this.filterButton.UseVisualStyleBackColor = true;
|
||||
this.filterButton.Click += new System.EventHandler(this.filterButton_Click_1);
|
||||
//
|
||||
// filterTextBox
|
||||
//
|
||||
this.filterTextBox.Location = new System.Drawing.Point(33, 4);
|
||||
this.filterTextBox.Name = "filterTextBox";
|
||||
this.filterTextBox.Size = new System.Drawing.Size(187, 20);
|
||||
this.filterTextBox.TabIndex = 7;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.AutoSize = true;
|
||||
this.label2.Location = new System.Drawing.Point(3, 8);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(29, 13);
|
||||
this.label2.TabIndex = 6;
|
||||
this.label2.Text = "Filter";
|
||||
//
|
||||
// wndControlPanel
|
||||
//
|
||||
this.wndControlPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.wndControlPanel.Controls.Add(this.button2);
|
||||
this.wndControlPanel.Location = new System.Drawing.Point(831, 356);
|
||||
this.wndControlPanel.Name = "wndControlPanel";
|
||||
this.wndControlPanel.Size = new System.Drawing.Size(70, 26);
|
||||
this.wndControlPanel.TabIndex = 14;
|
||||
//
|
||||
// button2
|
||||
//
|
||||
this.button2.Location = new System.Drawing.Point(3, 0);
|
||||
this.button2.Name = "button2";
|
||||
this.button2.Size = new System.Drawing.Size(64, 26);
|
||||
this.button2.TabIndex = 2;
|
||||
this.button2.Text = "Close";
|
||||
this.button2.UseVisualStyleBackColor = true;
|
||||
this.button2.Click += new System.EventHandler(this.button2_Click);
|
||||
//
|
||||
// flowControlsPanel
|
||||
//
|
||||
this.flowControlsPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
||||
this.flowControlsPanel.Controls.Add(this.runButton);
|
||||
this.flowControlsPanel.Controls.Add(this.stopButton);
|
||||
this.flowControlsPanel.Location = new System.Drawing.Point(12, 356);
|
||||
this.flowControlsPanel.Name = "flowControlsPanel";
|
||||
this.flowControlsPanel.Size = new System.Drawing.Size(133, 26);
|
||||
this.flowControlsPanel.TabIndex = 13;
|
||||
//
|
||||
// runButton
|
||||
//
|
||||
this.runButton.Enabled = false;
|
||||
this.runButton.Location = new System.Drawing.Point(0, 0);
|
||||
this.runButton.Name = "runButton";
|
||||
this.runButton.Size = new System.Drawing.Size(64, 26);
|
||||
this.runButton.TabIndex = 4;
|
||||
this.runButton.Text = "Run";
|
||||
this.runButton.UseVisualStyleBackColor = true;
|
||||
this.runButton.Click += new System.EventHandler(this.runButton_Click);
|
||||
//
|
||||
// stopButton
|
||||
//
|
||||
this.stopButton.Location = new System.Drawing.Point(69, 0);
|
||||
this.stopButton.Name = "stopButton";
|
||||
this.stopButton.Size = new System.Drawing.Size(64, 26);
|
||||
this.stopButton.TabIndex = 5;
|
||||
this.stopButton.Text = "Stop";
|
||||
this.stopButton.UseVisualStyleBackColor = true;
|
||||
this.stopButton.Click += new System.EventHandler(this.stopButton_Click);
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.AutoSize = true;
|
||||
this.label1.Location = new System.Drawing.Point(12, 19);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(76, 13);
|
||||
this.label1.TabIndex = 12;
|
||||
this.label1.Text = "Flow of events";
|
||||
//
|
||||
// memoListView
|
||||
//
|
||||
this.memoListView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.memoListView.FullRowSelect = true;
|
||||
this.memoListView.GridLines = true;
|
||||
this.memoListView.HideSelection = false;
|
||||
this.memoListView.Location = new System.Drawing.Point(12, 38);
|
||||
this.memoListView.Name = "memoListView";
|
||||
this.memoListView.Size = new System.Drawing.Size(889, 312);
|
||||
this.memoListView.TabIndex = 0;
|
||||
this.memoListView.UseCompatibleStateImageBehavior = false;
|
||||
this.memoListView.View = System.Windows.Forms.View.Details;
|
||||
|
||||
this.memoListView.DoubleClick += new System.EventHandler(this.MemoListView_DoubleClick);
|
||||
//
|
||||
//
|
||||
// mainToolStrip
|
||||
//
|
||||
this.mainToolStrip.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
|
||||
this.mainToolStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.pauseButton,
|
||||
this.refreshButton,
|
||||
this.clearButton,
|
||||
this.saveButton,
|
||||
new System.Windows.Forms.ToolStripSeparator(),
|
||||
this.copyButton,
|
||||
this.copyAllButton,
|
||||
new System.Windows.Forms.ToolStripSeparator(),
|
||||
this.wrapButton,
|
||||
this.autoScrollButton,
|
||||
this.alwaysOnTopButton,
|
||||
this.colorRulesButton,
|
||||
this.closeButton});
|
||||
this.mainToolStrip.Location = new System.Drawing.Point(0, 0);
|
||||
this.mainToolStrip.Name = "mainToolStrip";
|
||||
this.mainToolStrip.Padding = new System.Windows.Forms.Padding(6, 3, 6, 3);
|
||||
this.mainToolStrip.Size = new System.Drawing.Size(1184, 31);
|
||||
this.mainToolStrip.TabIndex = 0;
|
||||
//
|
||||
// pauseButton
|
||||
//
|
||||
this.pauseButton.CheckOnClick = true;
|
||||
this.pauseButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.pauseButton.Text = "Pause";
|
||||
this.pauseButton.ToolTipText = "Pause screen updates while continuing to receive logs";
|
||||
this.pauseButton.Click += new System.EventHandler(this.pauseButton_Click);
|
||||
//
|
||||
// refreshButton
|
||||
//
|
||||
this.refreshButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.refreshButton.Text = "Refresh";
|
||||
this.refreshButton.Click += new System.EventHandler(this.refreshButton_Click);
|
||||
//
|
||||
// clearButton
|
||||
//
|
||||
this.clearButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.clearButton.Text = "Clear";
|
||||
this.clearButton.Click += new System.EventHandler(this.clearButton_Click);
|
||||
//
|
||||
// saveButton
|
||||
//
|
||||
this.saveButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.saveButton.Text = "Save...";
|
||||
this.saveButton.Click += new System.EventHandler(this.saveButton_Click);
|
||||
//
|
||||
// copyButton
|
||||
//
|
||||
this.copyButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.copyButton.Text = "Copy";
|
||||
this.copyButton.ToolTipText = "Copy selected text or the current log line";
|
||||
this.copyButton.Click += new System.EventHandler(this.copyButton_Click);
|
||||
//
|
||||
// copyAllButton
|
||||
//
|
||||
this.copyAllButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.copyAllButton.Text = "Copy all";
|
||||
this.copyAllButton.Click += new System.EventHandler(this.copyAllButton_Click);
|
||||
//
|
||||
// wrapButton
|
||||
//
|
||||
this.wrapButton.Checked = true;
|
||||
this.wrapButton.CheckOnClick = true;
|
||||
this.wrapButton.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.wrapButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.wrapButton.Text = "Wrap lines";
|
||||
this.wrapButton.Click += new System.EventHandler(this.wrapButton_Click);
|
||||
//
|
||||
// autoScrollButton
|
||||
//
|
||||
this.autoScrollButton.Checked = true;
|
||||
this.autoScrollButton.CheckOnClick = true;
|
||||
this.autoScrollButton.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.autoScrollButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.autoScrollButton.Text = "Auto-scroll";
|
||||
//
|
||||
// alwaysOnTopButton
|
||||
//
|
||||
this.alwaysOnTopButton.CheckOnClick = true;
|
||||
this.alwaysOnTopButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.alwaysOnTopButton.Text = "Always on top";
|
||||
this.alwaysOnTopButton.Click += new System.EventHandler(this.alwaysOnTopButton_Click);
|
||||
//
|
||||
// colorRulesButton
|
||||
//
|
||||
this.colorRulesButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.colorRulesButton.Text = "Color rules...";
|
||||
this.colorRulesButton.Click += new System.EventHandler(this.colorRulesButton_Click);
|
||||
//
|
||||
// closeButton
|
||||
//
|
||||
this.closeButton.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
|
||||
this.closeButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
|
||||
this.closeButton.Text = "Close";
|
||||
this.closeButton.Click += new System.EventHandler(this.closeButton_Click);
|
||||
//
|
||||
// filterPanel
|
||||
//
|
||||
this.filterPanel.ColumnCount = 8;
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 42F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 42F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 105F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 72F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 180F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F));
|
||||
this.filterPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 66F));
|
||||
this.filterPanel.Controls.Add(this.filterLabel, 0, 0);
|
||||
this.filterPanel.Controls.Add(this.filterTextBox, 1, 0);
|
||||
this.filterPanel.Controls.Add(this.levelLabel, 2, 0);
|
||||
this.filterPanel.Controls.Add(this.levelComboBox, 3, 0);
|
||||
this.filterPanel.Controls.Add(this.applicationLabel, 4, 0);
|
||||
this.filterPanel.Controls.Add(this.applicationComboBox, 5, 0);
|
||||
this.filterPanel.Controls.Add(this.applyFilterButton, 6, 0);
|
||||
this.filterPanel.Controls.Add(this.clearFilterButton, 7, 0);
|
||||
this.filterPanel.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.filterPanel.Location = new System.Drawing.Point(0, 31);
|
||||
this.filterPanel.Name = "filterPanel";
|
||||
this.filterPanel.Padding = new System.Windows.Forms.Padding(6, 5, 6, 5);
|
||||
this.filterPanel.RowCount = 1;
|
||||
this.filterPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
|
||||
this.filterPanel.Size = new System.Drawing.Size(1184, 38);
|
||||
this.filterPanel.TabIndex = 1;
|
||||
//
|
||||
// filter controls
|
||||
//
|
||||
this.filterLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.filterLabel.AutoSize = true;
|
||||
this.filterLabel.Text = "Filter";
|
||||
this.filterTextBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.filterTextBox.KeyDown += new System.Windows.Forms.KeyEventHandler(this.filterTextBox_KeyDown);
|
||||
this.levelLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.levelLabel.AutoSize = true;
|
||||
this.levelLabel.Text = "Level";
|
||||
this.levelComboBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.levelComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.levelComboBox.SelectedIndexChanged += new System.EventHandler(this.filterValueChanged);
|
||||
this.applicationLabel.Anchor = System.Windows.Forms.AnchorStyles.Left;
|
||||
this.applicationLabel.AutoSize = true;
|
||||
this.applicationLabel.Text = "Application";
|
||||
this.applicationComboBox.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.applicationComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.applicationComboBox.SelectedIndexChanged += new System.EventHandler(this.filterValueChanged);
|
||||
this.applyFilterButton.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.applyFilterButton.Text = "Apply";
|
||||
this.applyFilterButton.Click += new System.EventHandler(this.applyFilterButton_Click);
|
||||
this.clearFilterButton.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.clearFilterButton.Text = "Reset";
|
||||
this.clearFilterButton.Click += new System.EventHandler(this.clearFilterButton_Click);
|
||||
//
|
||||
// logContextMenu
|
||||
//
|
||||
this.logContextMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.copySelectionMenuItem,
|
||||
this.copyLineMenuItem,
|
||||
this.copyAllMenuItem});
|
||||
this.copySelectionMenuItem.Text = "Copy selection";
|
||||
this.copySelectionMenuItem.Click += new System.EventHandler(this.copySelectionMenuItem_Click);
|
||||
this.copyLineMenuItem.Text = "Copy current line";
|
||||
this.copyLineMenuItem.Click += new System.EventHandler(this.copyLineMenuItem_Click);
|
||||
this.copyAllMenuItem.Text = "Copy all visible logs";
|
||||
this.copyAllMenuItem.Click += new System.EventHandler(this.copyAllButton_Click);
|
||||
//
|
||||
// logView
|
||||
//
|
||||
this.logView.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.logView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||
this.logView.ContextMenuStrip = this.logContextMenu;
|
||||
this.logView.DetectUrls = false;
|
||||
this.logView.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.logView.Font = new System.Drawing.Font("Consolas", 9F);
|
||||
this.logView.HideSelection = false;
|
||||
this.logView.Location = new System.Drawing.Point(0, 69);
|
||||
this.logView.Name = "logView";
|
||||
this.logView.ReadOnly = true;
|
||||
this.logView.Size = new System.Drawing.Size(1184, 554);
|
||||
this.logView.TabIndex = 2;
|
||||
this.logView.Text = "";
|
||||
this.logView.WordWrap = true;
|
||||
//
|
||||
// statusStrip
|
||||
//
|
||||
this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.receiverStatusLabel,
|
||||
this.statusSpacer,
|
||||
this.countStatusLabel});
|
||||
this.statusStrip.Location = new System.Drawing.Point(0, 623);
|
||||
this.statusStrip.Name = "statusStrip";
|
||||
this.statusStrip.Size = new System.Drawing.Size(1184, 22);
|
||||
this.statusStrip.TabIndex = 3;
|
||||
this.receiverStatusLabel.Text = "Listening";
|
||||
this.statusSpacer.Spring = true;
|
||||
this.countStatusLabel.Text = "Received: 0 | Visible: 0";
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(913, 388);
|
||||
this.Controls.Add(this.memoListView);
|
||||
this.Controls.Add(this.panel1);
|
||||
this.Controls.Add(this.filterManagementPanel);
|
||||
this.Controls.Add(this.wndControlPanel);
|
||||
this.Controls.Add(this.flowControlsPanel);
|
||||
this.Controls.Add(this.label1);
|
||||
this.ClientSize = new System.Drawing.Size(1184, 645);
|
||||
this.Controls.Add(this.logView);
|
||||
this.Controls.Add(this.filterPanel);
|
||||
this.Controls.Add(this.mainToolStrip);
|
||||
this.Controls.Add(this.statusStrip);
|
||||
this.KeyPreview = true;
|
||||
this.MinimumSize = new System.Drawing.Size(860, 480);
|
||||
this.Name = "MainForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Application live diagnostic";
|
||||
this.panel1.ResumeLayout(false);
|
||||
this.filterManagementPanel.ResumeLayout(false);
|
||||
this.filterManagementPanel.PerformLayout();
|
||||
this.wndControlPanel.ResumeLayout(false);
|
||||
this.flowControlsPanel.ResumeLayout(false);
|
||||
this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.MainForm_KeyDown);
|
||||
this.mainToolStrip.ResumeLayout(false);
|
||||
this.mainToolStrip.PerformLayout();
|
||||
this.filterPanel.ResumeLayout(false);
|
||||
this.filterPanel.PerformLayout();
|
||||
this.logContextMenu.ResumeLayout(false);
|
||||
this.statusStrip.ResumeLayout(false);
|
||||
this.statusStrip.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Panel panel1;
|
||||
private System.Windows.Forms.Button button6;
|
||||
private System.Windows.Forms.Button button7;
|
||||
private System.Windows.Forms.Panel filterManagementPanel;
|
||||
private System.Windows.Forms.Button button5;
|
||||
private System.Windows.Forms.Button filterButton;
|
||||
private System.Windows.Forms.TextBox filterTextBox;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.Panel wndControlPanel;
|
||||
private System.Windows.Forms.Button button2;
|
||||
private System.Windows.Forms.Panel flowControlsPanel;
|
||||
private System.Windows.Forms.Button runButton;
|
||||
private System.Windows.Forms.Button stopButton;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Button refreshMemoButton;
|
||||
private BufferedListView memoListView;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+392
-214
@@ -1,267 +1,445 @@
|
||||
using log4net.Config;
|
||||
using log4net;
|
||||
using AppDiagnostic.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Data;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using log4net.Appender;
|
||||
using SharedComponents;
|
||||
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
|
||||
using System.IO; // Pre File.WriteAllLines a prácu so súbormi
|
||||
using System.Linq;
|
||||
|
||||
namespace AppDiagnostic
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private bool stopUpdate = false; // Premenná na zastavenie aktualizácie
|
||||
private int lastDisplayedLogIndex = 0;
|
||||
private bool autoScrollEnabled = true; // Automatický posun, predvolene povolený
|
||||
private MainForm appDiagnosticForm;
|
||||
private Timer refreshTimer;
|
||||
private LogAdapter _logAdapter; // Pre LogAdapter
|
||||
private LogFilter _logFilter; // LogFilter objekt pre filtráciu
|
||||
private readonly Func<int> connectedClientCountProvider;
|
||||
private readonly Func<bool> activationRequestProvider;
|
||||
private readonly Timer refreshTimer;
|
||||
private readonly Font regularLogFont;
|
||||
private readonly Font boldLogFont;
|
||||
private List<LogColorRule> colorRules;
|
||||
private long lastRenderedVersion = -1;
|
||||
private int observedEntryCount;
|
||||
private int visibleEntryCount;
|
||||
private bool loadingFilters;
|
||||
|
||||
public MainForm()
|
||||
public MainForm() : this(null, null)
|
||||
{
|
||||
this.Icon = Properties.Resources.TBF_icon;
|
||||
}
|
||||
|
||||
public MainForm(Func<int> connectedClientCountProvider, Func<bool> activationRequestProvider)
|
||||
{
|
||||
this.connectedClientCountProvider = connectedClientCountProvider;
|
||||
this.activationRequestProvider = activationRequestProvider;
|
||||
InitializeComponent();
|
||||
Icon = Properties.Resources.TBF_icon;
|
||||
|
||||
// Inicializujeme LogAdapter a priradíme ho k memoListView
|
||||
_logAdapter = new LogAdapter(memoListView);
|
||||
regularLogFont = new Font(logView.Font, FontStyle.Regular);
|
||||
boldLogFont = new Font(logView.Font, FontStyle.Bold);
|
||||
colorRules = LogColorRuleRepository.Load();
|
||||
|
||||
RefreshLogView();
|
||||
InitializeFilters();
|
||||
|
||||
// Pripojenie na udalosť pri pridaní nového logu
|
||||
// nakoľko sa memo refreshuje cyklicky, tak refresh na udalosť nepotrebujem
|
||||
//LiveLogCache.Instance.LogAdded += OnLogAdded;
|
||||
refreshTimer = new Timer { Interval = 250 };
|
||||
refreshTimer.Tick += refreshTimer_Tick;
|
||||
refreshTimer.Start();
|
||||
|
||||
// Vytvoríme LogFilter objekt
|
||||
//_logFilter = new LogFilter(memoListView);
|
||||
}
|
||||
|
||||
private void OnLogAdded(string log)
|
||||
{
|
||||
try
|
||||
{
|
||||
RefreshLogView();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"Error in OnLogAdded: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshLogView()
|
||||
{
|
||||
// Skontrolujte, či voláme z iného vlákna
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke((Action)RefreshLogView);
|
||||
return;
|
||||
}
|
||||
|
||||
memoListView.BeginUpdate(); // Zabraňuje vizuálnym aktualizáciám počas vykresľovania
|
||||
|
||||
try
|
||||
{
|
||||
// Skontrolujte, či je užívateľ na poslednom viditeľnom zázname
|
||||
if (memoListView.Items.Count > 0)
|
||||
{
|
||||
var lastVisibleIndex = memoListView.TopItem.Index + memoListView.ClientRectangle.Height / memoListView.Items[0].Bounds.Height;
|
||||
autoScrollEnabled = lastVisibleIndex >= memoListView.Items.Count - 1;
|
||||
}
|
||||
|
||||
// Získajte nové logy od posledného indexu
|
||||
var logs = LiveLogCache.Instance.GetLogs(lastDisplayedLogIndex, LiveLogCache.Instance.Logs.Count - lastDisplayedLogIndex);
|
||||
|
||||
// Pridajte len nové logy
|
||||
foreach (var log in logs)
|
||||
{
|
||||
memoListView.Items.Add(new ListViewItem(log));
|
||||
}
|
||||
|
||||
// Aktualizujte posledný zobrazený index
|
||||
lastDisplayedLogIndex = LiveLogCache.Instance.Logs.Count;
|
||||
|
||||
// Ak je autoScrollEnabled, posuňte sa na posledný záznam
|
||||
if (autoScrollEnabled && memoListView.Items.Count > 0)
|
||||
{
|
||||
memoListView.EnsureVisible(memoListView.Items.Count - 1);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
memoListView.EndUpdate(); // Umožní vizuálne aktualizácie
|
||||
}
|
||||
}
|
||||
|
||||
// Táto metóda sa spustí pri scrollovaní v memoListView (tzn. ak sa používateľ dostane na spodok listu)
|
||||
private void memoListView_Scroll(object sender, EventArgs e)
|
||||
{
|
||||
// Skontrolujte, či používateľ dosiahol spodok ListView
|
||||
if (memoListView.Items.Count > 0 &&
|
||||
memoListView.Items[memoListView.Items.Count - 1].Bounds.Bottom <= memoListView.ClientSize.Height)
|
||||
{
|
||||
// Ak áno, načítame ďalšie logy
|
||||
RefreshLogView();
|
||||
}
|
||||
}
|
||||
|
||||
private void button6_Click(object sender, EventArgs e)
|
||||
{
|
||||
//LiveLogCache.Instance.AddLog("Nový log z hlavnej aplikácie");
|
||||
|
||||
LiveLogCache.Instance.ClearLogs();
|
||||
ClearListView();
|
||||
}
|
||||
|
||||
private void runButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (stopUpdate) // Ak je časovač zastavený, spustíme ho
|
||||
{
|
||||
stopUpdate = false; // Zastav aktualizáciu
|
||||
runButton.Enabled = false; // Zakážeme tlačidlo Start počas behu
|
||||
stopButton.Enabled = true; // Povolenie tlačidla Stop
|
||||
refreshTimer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (!stopUpdate) // Ak časovač beží, môžeme ho zastaviť
|
||||
{
|
||||
stopUpdate = true; // Spusti aktualizáciu
|
||||
runButton.Enabled = true; // Povolenie tlačidla Start
|
||||
stopButton.Enabled = false; // Zakážeme tlačidlo Stop
|
||||
refreshTimer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearListView()
|
||||
{
|
||||
if (InvokeRequired)
|
||||
{
|
||||
Invoke(new Action(ClearListView));
|
||||
return;
|
||||
}
|
||||
|
||||
memoListView.Items.Clear();
|
||||
}
|
||||
|
||||
private void refreshMemoButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void button7_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveLogsToFile();
|
||||
//LiveLogCache.Instance.AddLog("Toto je nový log.");
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
// Odpojenie udalosti, keď sa okno zatvára, inak môže po opätovnom spustení okna a pridaní nového itemu do listu zhhodiť program
|
||||
// nakoľko sa memo refreshuje cyklicky, tak refresh na udalosť nepotrebujem
|
||||
//LiveLogCache.Instance.LogAdded -= OnLogAdded;
|
||||
|
||||
_logAdapter = null;
|
||||
|
||||
refreshTimer.Stop();
|
||||
refreshTimer.Dispose();
|
||||
regularLogFont.Dispose();
|
||||
boldLogFont.Dispose();
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
private void filterButton_Click_1(object sender, EventArgs e)
|
||||
private void InitializeFilters()
|
||||
{
|
||||
// Aplikujeme filter na základe textu z filterTextBox
|
||||
string filterText = filterTextBox.Text;
|
||||
//_logFilter.ApplyFilter(filterText); // Aplikovanie filtra
|
||||
_logAdapter.ApplyFilter(filterText);
|
||||
loadingFilters = true;
|
||||
levelComboBox.Items.AddRange(new object[] { "All levels", "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL" });
|
||||
levelComboBox.SelectedIndex = 0;
|
||||
applicationComboBox.Items.Add("All applications");
|
||||
applicationComboBox.SelectedIndex = 0;
|
||||
loadingFilters = false;
|
||||
}
|
||||
private void SubForm_FormClosing(object sender, FormClosingEventArgs e)
|
||||
|
||||
private void refreshTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
ProcessActivationRequest();
|
||||
RefreshView(false);
|
||||
}
|
||||
|
||||
private void ProcessActivationRequest()
|
||||
{
|
||||
if (activationRequestProvider == null || !activationRequestProvider())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (WindowState == FormWindowState.Minimized)
|
||||
{
|
||||
WindowState = FormWindowState.Normal;
|
||||
}
|
||||
|
||||
Show();
|
||||
BringToFront();
|
||||
Activate();
|
||||
}
|
||||
|
||||
private void RefreshView(bool forceFullRefresh)
|
||||
{
|
||||
List<DiagnosticLogEntry> snapshot = DiagnosticLogStore.Instance.GetSnapshot();
|
||||
UpdateApplicationFilter(snapshot);
|
||||
|
||||
if (pauseButton.Checked)
|
||||
{
|
||||
UpdateStatus(snapshot.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
long currentVersion = DiagnosticLogStore.Instance.Version;
|
||||
if (!forceFullRefresh && currentVersion == lastRenderedVersion)
|
||||
{
|
||||
UpdateStatus(snapshot.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
bool canAppend = !forceFullRefresh && snapshot.Count > observedEntryCount;
|
||||
if (canAppend)
|
||||
{
|
||||
AppendEntries(snapshot.Skip(observedEntryCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
RebuildView(snapshot);
|
||||
}
|
||||
|
||||
observedEntryCount = snapshot.Count;
|
||||
lastRenderedVersion = currentVersion;
|
||||
UpdateStatus(snapshot.Count);
|
||||
}
|
||||
|
||||
private void RebuildView(IEnumerable<DiagnosticLogEntry> entries)
|
||||
{
|
||||
logView.SuspendLayout();
|
||||
logView.Clear();
|
||||
visibleEntryCount = 0;
|
||||
AppendEntries(entries, false);
|
||||
logView.ResumeLayout();
|
||||
|
||||
if (autoScrollButton.Checked)
|
||||
{
|
||||
ScrollToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendEntries(IEnumerable<DiagnosticLogEntry> entries, bool scrollAfterAppend = true)
|
||||
{
|
||||
int originalSelectionStart = logView.SelectionStart;
|
||||
int originalSelectionLength = logView.SelectionLength;
|
||||
|
||||
foreach (DiagnosticLogEntry entry in entries)
|
||||
{
|
||||
if (!MatchesFilter(entry))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AppendEntry(entry);
|
||||
visibleEntryCount++;
|
||||
}
|
||||
|
||||
if (scrollAfterAppend && autoScrollButton.Checked)
|
||||
{
|
||||
ScrollToEnd();
|
||||
}
|
||||
else if (!autoScrollButton.Checked)
|
||||
{
|
||||
int safeStart = Math.Min(originalSelectionStart, logView.TextLength);
|
||||
int safeLength = Math.Min(originalSelectionLength, logView.TextLength - safeStart);
|
||||
logView.Select(safeStart, safeLength);
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendEntry(DiagnosticLogEntry entry)
|
||||
{
|
||||
string text = DiagnosticLogFormatter.Format(entry);
|
||||
|
||||
if (logView.TextLength > 0)
|
||||
{
|
||||
logView.AppendText(Environment.NewLine);
|
||||
}
|
||||
|
||||
int startIndex = logView.TextLength;
|
||||
logView.AppendText(text);
|
||||
LogColorizer.Apply(logView, startIndex, text.Length, text, colorRules, regularLogFont, boldLogFont);
|
||||
logView.Select(logView.TextLength, 0);
|
||||
}
|
||||
|
||||
private bool MatchesFilter(DiagnosticLogEntry entry)
|
||||
{
|
||||
string selectedLevel = levelComboBox.SelectedItem as string;
|
||||
if (!string.IsNullOrEmpty(selectedLevel) && selectedLevel != "All levels" && !string.Equals(entry.Level, selectedLevel, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string selectedApplication = applicationComboBox.SelectedItem as string;
|
||||
if (!string.IsNullOrEmpty(selectedApplication) && selectedApplication != "All applications" && !string.Equals(entry.ApplicationName, selectedApplication, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string filter = filterTextBox.Text.Trim();
|
||||
return filter.Length == 0 || DiagnosticLogFormatter.Format(entry).IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private void UpdateApplicationFilter(IEnumerable<DiagnosticLogEntry> entries)
|
||||
{
|
||||
string selectedApplication = applicationComboBox.SelectedItem as string ?? "All applications";
|
||||
List<string> applications = entries.Select(entry => entry.ApplicationName).Where(name => !string.IsNullOrWhiteSpace(name)).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(name => name).ToList();
|
||||
List<string> currentApplications = applicationComboBox.Items.Cast<string>().Skip(1).ToList();
|
||||
|
||||
if (applications.SequenceEqual(currentApplications, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loadingFilters = true;
|
||||
applicationComboBox.BeginUpdate();
|
||||
applicationComboBox.Items.Clear();
|
||||
applicationComboBox.Items.Add("All applications");
|
||||
applicationComboBox.Items.AddRange(applications.Cast<object>().ToArray());
|
||||
applicationComboBox.SelectedItem = applicationComboBox.Items.Cast<string>().FirstOrDefault(item => string.Equals(item, selectedApplication, StringComparison.OrdinalIgnoreCase)) ?? "All applications";
|
||||
applicationComboBox.EndUpdate();
|
||||
loadingFilters = false;
|
||||
}
|
||||
|
||||
private void UpdateStatus(int storedCount)
|
||||
{
|
||||
int clients = connectedClientCountProvider?.Invoke() ?? 0;
|
||||
receiverStatusLabel.Text = pauseButton.Checked ? $"Paused | Connected sources: {clients}" : $"Listening | Connected sources: {clients}";
|
||||
countStatusLabel.Text = $"Received: {DiagnosticLogStore.Instance.TotalReceived:N0} | Stored: {storedCount:N0} | Visible: {visibleEntryCount:N0}";
|
||||
}
|
||||
|
||||
private void ScrollToEnd()
|
||||
{
|
||||
logView.Select(logView.TextLength, 0);
|
||||
logView.ScrollToCaret();
|
||||
}
|
||||
|
||||
private void pauseButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
pauseButton.Text = pauseButton.Checked ? "Resume" : "Pause";
|
||||
|
||||
if (!pauseButton.Checked)
|
||||
{
|
||||
RefreshView(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus(DiagnosticLogStore.Instance.GetSnapshot().Count);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
private void clearButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
DiagnosticLogStore.Instance.Clear();
|
||||
observedEntryCount = 0;
|
||||
visibleEntryCount = 0;
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
private void saveButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var dialog = new SaveFileDialog())
|
||||
{
|
||||
dialog.Filter = "Text files (*.txt)|*.txt|Log files (*.log)|*.log|All files (*.*)|*.*";
|
||||
dialog.Title = "Save visible diagnostic logs";
|
||||
dialog.DefaultExt = "log";
|
||||
dialog.FileName = $"AppDiagnostic_{DateTime.Now:yyyyMMdd_HHmmss}.log";
|
||||
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(dialog.FileName, logView.Text);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
MessageBox.Show(this, $"Unable to save diagnostic logs: {exception.Message}", "Save error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
CopySelectionOrCurrentLine();
|
||||
}
|
||||
|
||||
private void copySelectionMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (logView.SelectionLength > 0)
|
||||
{
|
||||
CopyText(logView.SelectedText);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyLineMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
CopyCurrentLine();
|
||||
}
|
||||
|
||||
private void copyAllButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
CopyText(logView.Text);
|
||||
}
|
||||
|
||||
private void CopySelectionOrCurrentLine()
|
||||
{
|
||||
if (logView.SelectionLength > 0)
|
||||
{
|
||||
CopyText(logView.SelectedText);
|
||||
}
|
||||
else
|
||||
{
|
||||
CopyCurrentLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyCurrentLine()
|
||||
{
|
||||
if (logView.Lines.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int lineIndex = logView.GetLineFromCharIndex(logView.SelectionStart);
|
||||
if (lineIndex >= 0 && lineIndex < logView.Lines.Length)
|
||||
{
|
||||
CopyText(logView.Lines[lineIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
private void CopyText(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Vaša logika pri uzatváraní formy
|
||||
// this.Hide(); // Podforma sa len skryje
|
||||
Clipboard.SetText(text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Ošetrenie výnimky
|
||||
MessageBox.Show("Chyba pri zatváraní pod-aplikácie: " + ex.Message);
|
||||
MessageBox.Show(this, $"Unable to copy text: {exception.Message}", "Clipboard error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLogsToFile()
|
||||
private void wrapButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (SaveFileDialog saveFileDialog = new SaveFileDialog())
|
||||
{
|
||||
saveFileDialog.Filter = "Text Files (*.txt)|*.txt|All Files (*.*)|*.*";
|
||||
saveFileDialog.Title = "Save TBF live logs";
|
||||
saveFileDialog.DefaultExt = "txt";
|
||||
saveFileDialog.FileName = "TBFLiveLogs.txt";
|
||||
logView.WordWrap = wrapButton.Checked;
|
||||
}
|
||||
|
||||
if (saveFileDialog.ShowDialog() == DialogResult.OK)
|
||||
private void alwaysOnTopButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
TopMost = alwaysOnTopButton.Checked;
|
||||
}
|
||||
|
||||
private void colorRulesButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var editor = new ColorRuleEditorForm(colorRules))
|
||||
{
|
||||
if (editor.ShowDialog(this) != DialogResult.OK)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Načítanie všetkých logov z LiveLogCache
|
||||
var logs = LiveLogCache.Instance.GetLogs(0, LiveLogCache.Instance.GetCount());
|
||||
|
||||
// Uloženie logov do vybraného súboru
|
||||
File.WriteAllLines(saveFileDialog.FileName, logs);
|
||||
|
||||
MessageBox.Show("Logs saved successfully.", "Save TBF live logs", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"An error occurred while saving TBF live logs: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
colorRules = editor.Rules;
|
||||
|
||||
try
|
||||
{
|
||||
LogColorRuleRepository.Save(colorRules);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
MessageBox.Show(this, $"Unable to save color rules: {exception.Message}", "Settings error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
|
||||
RefreshView(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void button2_Click(object sender, EventArgs e)
|
||||
private void applyFilterButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
this.Close();
|
||||
//this.Hide(); // Podforma sa len skryje
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
private void MemoListView_DoubleClick(object sender, EventArgs e)
|
||||
private void clearFilterButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (memoListView.SelectedItems.Count > 0)
|
||||
loadingFilters = true;
|
||||
filterTextBox.Clear();
|
||||
levelComboBox.SelectedIndex = 0;
|
||||
applicationComboBox.SelectedIndex = 0;
|
||||
loadingFilters = false;
|
||||
RefreshView(true);
|
||||
}
|
||||
|
||||
private void filterValueChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (!loadingFilters)
|
||||
{
|
||||
var selectedItem = memoListView.SelectedItems[0];
|
||||
int selectedIndex = memoListView.Items.IndexOf(selectedItem);
|
||||
RefreshView(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedIndex >= 0)
|
||||
{
|
||||
// Zrušenie filtra
|
||||
_logAdapter._filterText = string.Empty;
|
||||
private void filterTextBox_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Enter)
|
||||
{
|
||||
RefreshView(true);
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
else if (e.KeyCode == Keys.Escape)
|
||||
{
|
||||
clearFilterButton_Click(sender, EventArgs.Empty);
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Načítanie logov od indexu vybraného riadku
|
||||
var logs = LiveLogCache.Instance.GetLogs(selectedIndex, LiveLogCache.Instance.GetCount() - selectedIndex);
|
||||
private void closeButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
// Obnovenie ListView s novými údajmi
|
||||
memoListView.BeginUpdate();
|
||||
memoListView.Items.Clear();
|
||||
foreach (var log in logs)
|
||||
{
|
||||
var item = new ListViewItem(log)
|
||||
{
|
||||
BackColor = memoListView.Items.Count % 2 == 0 ? Color.White : Color.FromArgb(240, 240, 240)
|
||||
};
|
||||
memoListView.Items.Add(item);
|
||||
}
|
||||
memoListView.EndUpdate();
|
||||
}
|
||||
private void MainForm_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Control && e.KeyCode == Keys.F)
|
||||
{
|
||||
filterTextBox.Focus();
|
||||
filterTextBox.SelectAll();
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
else if (e.Control && e.KeyCode == Keys.S)
|
||||
{
|
||||
saveButton_Click(sender, EventArgs.Empty);
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
else if (e.KeyCode == Keys.F5)
|
||||
{
|
||||
RefreshView(true);
|
||||
e.SuppressKeyPress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
@@ -14,9 +15,26 @@ namespace AppDiagnostic
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
bool isFirstInstance;
|
||||
|
||||
using (var instanceMutex = new Mutex(true, "Sensus.AppDiagnostic.SingleInstance", out isFirstInstance))
|
||||
using (var activationEvent = new EventWaitHandle(false, EventResetMode.AutoReset, "Sensus.AppDiagnostic.Activate"))
|
||||
{
|
||||
if (!isFirstInstance)
|
||||
{
|
||||
activationEvent.Set();
|
||||
return;
|
||||
}
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
using (var diagnosticServer = new DiagnosticPipeServer(DiagnosticLogStore.Instance.Add))
|
||||
{
|
||||
diagnosticServer.Start();
|
||||
Application.Run(new MainForm(() => diagnosticServer.ConnectedClientCount, () => activationEvent.WaitOne(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,14 @@
|
||||
<Reference Include="log4net, Version=2.0.15.0, Culture=neutral, PublicKeyToken=669e0ddf0bb1aa2a, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\log4net.2.0.15\lib\net45\log4net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NLog.5.2.2\lib\net46\NLog.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
@@ -90,7 +95,9 @@
|
||||
<Compile Include="Utils.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Forms\ListViewEx.resx">
|
||||
<DependentUpon>ListViewEx.cs</DependentUpon>
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Common.Iperl
|
||||
public double Flow(double scalingFactor) { return 0.225 * scalingFactor * (double)FlowRaw; }
|
||||
public double Volume(double scalingFactor) { return 0.0000625 * scalingFactor * (double)VolumeRawExt; }
|
||||
public Int32 FlipTime() { return Impedance; }
|
||||
public decimal TimestampDec() { return (decimal)TimestampExt / (decimal)8192; }
|
||||
public decimal TimestampDec() { return (decimal)TimestampExt; }
|
||||
public double VolumeDelta(double scalingFactor, OptoTelegramRaw previous) { return (previous == null) ? 0 : Volume(scalingFactor) - previous.Volume(scalingFactor); }
|
||||
public decimal TimeDelta() { return TimestampDec() - TestStartTimestampDec; }
|
||||
public string Label()
|
||||
|
||||
+55
-54
@@ -2,6 +2,7 @@
|
||||
/// Copyright (c) 2016-2021 Sensus Slovensko a.s.
|
||||
///
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Common
|
||||
{
|
||||
@@ -286,97 +287,97 @@ namespace Common
|
||||
|
||||
public static bool IsQuantity(Unit unit, Quantity quantity)
|
||||
{
|
||||
return (quantity == GetQuantity(unit));
|
||||
return GetQuantity(unit).Contains(quantity);
|
||||
}
|
||||
|
||||
public static Quantity GetQuantity(Unit unit)
|
||||
public static Quantity[] GetQuantity(Unit unit)
|
||||
{
|
||||
switch (unit)
|
||||
{
|
||||
case Unit.pulse:
|
||||
case Unit.degree:
|
||||
return Quantity.Pulses;
|
||||
return new[] { Quantity.Pulses };
|
||||
|
||||
case Unit.ml:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
case Unit.l:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
case Unit.dm3:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
case Unit.USgal:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
case Unit.UKgal:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
case Unit.cf:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
case Unit.m3:
|
||||
return Quantity.Volume;
|
||||
return new[] { Quantity.Volume, Quantity.MultiFunctionalVariables };
|
||||
|
||||
case Unit.lph:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.cfph:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.lpm:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.USgalpm:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.m3ph:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.lps:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.USgalps:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.m3pm:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
case Unit.cfs:
|
||||
return Quantity.Flow;
|
||||
return new[] { Quantity.Flow };
|
||||
|
||||
case Unit.gps:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.gpm:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.gph:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.kgps:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.kgpm:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.kgph:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.tps:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.tpm:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.tph:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.lbps:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.lbpm:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
case Unit.lbph:
|
||||
return Quantity.MassFlow;
|
||||
return new[] { Quantity.MassFlow};
|
||||
|
||||
case Unit.g:
|
||||
return Quantity.Mass;
|
||||
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
|
||||
case Unit.oz:
|
||||
return Quantity.Mass;
|
||||
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
|
||||
case Unit.lb:
|
||||
return Quantity.Mass;
|
||||
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
|
||||
case Unit.kg:
|
||||
return Quantity.Mass;
|
||||
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
|
||||
case Unit.t:
|
||||
return Quantity.Mass;
|
||||
return new[] { Quantity.Mass, Quantity.MultiFunctionalVariables };
|
||||
|
||||
case Unit.ms:
|
||||
case Unit.s:
|
||||
case Unit.min:
|
||||
case Unit.hour:
|
||||
return Quantity.Time;
|
||||
return new[] { Quantity.Time };
|
||||
|
||||
case Unit.C:
|
||||
case Unit.F:
|
||||
case Unit.K:
|
||||
return Quantity.Temperature;
|
||||
return new[] { Quantity.Temperature };
|
||||
|
||||
case Unit.Pa:
|
||||
case Unit.hPa:
|
||||
@@ -386,14 +387,14 @@ namespace Common
|
||||
case Unit.psi:
|
||||
case Unit.bar:
|
||||
case Unit.MPa:
|
||||
return Quantity.Pressure;
|
||||
return new[] { Quantity.Pressure };
|
||||
|
||||
case Unit.RPct:
|
||||
return Quantity.Humidity;
|
||||
return new[] { Quantity.Humidity };
|
||||
|
||||
case Unit.Promile:
|
||||
case Unit.Pct:
|
||||
return Quantity.Error;
|
||||
return new[] { Quantity.Error };
|
||||
|
||||
case Unit.mm:
|
||||
case Unit.cm:
|
||||
@@ -402,23 +403,23 @@ namespace Common
|
||||
case Unit.foot:
|
||||
case Unit.yard:
|
||||
case Unit.m:
|
||||
return Quantity.Length;
|
||||
return new[] { Quantity.Length };
|
||||
|
||||
case Unit.kgpm3:
|
||||
case Unit.kgpm3:
|
||||
case Unit.kgpl:
|
||||
return Quantity.Density;
|
||||
return new[] { Quantity.Density };
|
||||
|
||||
case Unit.J:
|
||||
case Unit.J:
|
||||
case Unit.kJ:
|
||||
case Unit.MJ:
|
||||
case Unit.Wh:
|
||||
case Unit.kWh:
|
||||
case Unit.MWh:
|
||||
return Quantity.Energy;
|
||||
return new[] { Quantity.Energy };
|
||||
|
||||
case Unit.uSpcm:
|
||||
case Unit.mSpm:
|
||||
return Quantity.Conductivity;
|
||||
return new[] { Quantity.Conductivity };
|
||||
|
||||
case Unit.ppl:
|
||||
case Unit.ppdm3:
|
||||
@@ -428,22 +429,22 @@ namespace Common
|
||||
case Unit.dm3pp:
|
||||
case Unit.lpdeg:
|
||||
case Unit.dm3pdeg:
|
||||
return Quantity.PulsePerLtr;
|
||||
return new[] { Quantity.PulsePerLtr };
|
||||
|
||||
case Unit.ppkWh:
|
||||
case Unit.kWhpp:
|
||||
return Quantity.PulsePerKWh;
|
||||
|
||||
return new[] { Quantity.PulsePerKWh };
|
||||
|
||||
case Unit.A:
|
||||
case Unit.mA:
|
||||
return Quantity.Current;
|
||||
|
||||
return new[] { Quantity.Current };
|
||||
|
||||
case Unit.V:
|
||||
case Unit.mV:
|
||||
return Quantity.Voltage;
|
||||
return new[] { Quantity.Voltage };
|
||||
|
||||
default:
|
||||
return Quantity.Number;
|
||||
return new[] { Quantity.Number };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="log4net" version="2.0.15" targetFramework="net472" />
|
||||
<package id="NLog" version="5.2.2" targetFramework="net472" />
|
||||
</packages>
|
||||
@@ -126,7 +126,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\Strings.cs.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.de.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.de.resx">
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Resources\Strings.fr.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.it.resx" />
|
||||
<EmbeddedResource Include="Resources\Strings.pl.resx" />
|
||||
|
||||
+27
-2
@@ -1,3 +1,28 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/></startup></configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /></startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
|
||||
Binary file not shown.
@@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.IO.Ports;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
|
||||
+27
-2
@@ -1,3 +1,28 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0" /></startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.API
|
||||
{
|
||||
public class Enums
|
||||
{
|
||||
public enum DataStorageReaderTypes : sbyte
|
||||
{
|
||||
LoginPasswordsReader,
|
||||
CalibrationParamsReader
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,967 @@
|
||||
using CordonelPreadjustmentUi;
|
||||
using CordonelPreadjustmentUi.Processes.Itinerary;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Config;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Ports;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xylem.Common.Hardware.WaterMeter.WaterMeterCore;
|
||||
using Xylem.Common.Logic.ProductionOrderCore.OrderData;
|
||||
using Xylem.Common.Ui.CordonelPreadjustmentUi;
|
||||
using static GenesisCordonelInterface.API.PublicModels;
|
||||
|
||||
namespace GenesisCordonelInterface.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Public-facing facade for external applications integrating with
|
||||
/// Genesis Cordonel Interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class exposes a simplified and controlled API for external callers.
|
||||
/// It validates public input, maps public request models to internal models,
|
||||
/// forwards operations to the internal GCI implementation and exposes status
|
||||
/// notifications for meter batch changes.
|
||||
///
|
||||
/// This layer should stay thin. Business logic and meter communication are
|
||||
/// handled by <see cref="InterfaceGCIToLaatzen"/>.
|
||||
/// </remarks>
|
||||
public class InterfaceOutsideToGCI
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal GCI implementation used by this public facade.
|
||||
/// </summary>
|
||||
public readonly InterfaceGCIToLaatzen _innerMeterAPI;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when meter batch status information changes.
|
||||
/// </summary>
|
||||
public event Action<List<MeterBatchDebugStatus>> MeterBatchStatusChanged;
|
||||
private readonly IMeterLoginPasswordReader loginPasswordsReader;
|
||||
private readonly IPreAdjustmentCalibrationParamsReader calibrationParamsReader;
|
||||
private readonly Core.DataStorage.Writing.CalibrationResultWriter calibrationResultWriter =
|
||||
new Core.DataStorage.Writing.CalibrationResultWriter(null);
|
||||
|
||||
/// <summary>Stores one detached completion record without accessing meter ports.</summary>
|
||||
public Task<CalibrationWriteResult> StorePreadjustmentCompletionResultAsync(
|
||||
PreadjustmentCompletionRecord record, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
// Capture the progress instance and slot for this write, even if a new
|
||||
// batch is initialized while the database operation is still finishing.
|
||||
var progress = _innerMeterAPI?._progressProcess;
|
||||
int? slot = record.Slot;
|
||||
Action<string> diagnostic = null;
|
||||
if (progress != null && slot.HasValue && slot.Value > 0)
|
||||
diagnostic = message => progress.DebugMessage(message, slot.Value, "DB");
|
||||
return calibrationResultWriter.StorePreadjustmentCompletionAsync(record, token, diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>Creates a synthetic Completion record and exercises the real configured SQL writer.</summary>
|
||||
public async Task<CompletionProcessResult> StoreSimulatedCompletionResultAsync(
|
||||
int slot, int pcbId, bool phaseSuccess, string phaseError,
|
||||
CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (slot <= 0) throw new ArgumentOutOfRangeException(nameof(slot));
|
||||
// Synthetic fixture values belong to GCI, not to the hardware Completion process.
|
||||
var record = new PreadjustmentCompletionRecord
|
||||
{
|
||||
Slot = slot, PcbId = pcbId, MeterSize = 2,
|
||||
ZeroOffset1 = -20, ZeroOffset2 = -30, ZeroOffset3 = -67,
|
||||
FirstHitUpdatePeriod = 10, FirstHitShift = 4,
|
||||
FirstHitPercent1 = 17, FirstHitPercent2 = 13, FirstHitPercent3 = 17,
|
||||
ToFTempOffset1 = 22260, ToFTempOffset2 = -115410, ToFTempOffset3 = 14992,
|
||||
Date = DateTime.UtcNow, Successful = phaseSuccess
|
||||
};
|
||||
// Optional diagnostic subscribers must never change the storage outcome.
|
||||
try { diagnostic?.Invoke("Simulated calibration data; actual SQL write requested. Fixture MeterSize=2."); }
|
||||
catch (Exception) { }
|
||||
var write = await calibrationResultWriter.StorePreadjustmentCompletionAsync(
|
||||
record, token, diagnostic).ConfigureAwait(false);
|
||||
var result = new CompletionProcessResult
|
||||
{
|
||||
Slot = slot, Success = phaseSuccess, ProcessName = "PreAdjustment_Completion",
|
||||
ErrorMessage = phaseError
|
||||
};
|
||||
result.DatabaseRecords.Add(record);
|
||||
result.DatabaseWrites.Add(new CompletionDatabaseWriteResult { Slot = slot, Result = write });
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Stores Q3 values supplied by TBF; does not perform Q3 calibration.</summary>
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationResultAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return calibrationResultWriter.StoreQ3CalibrationAsync(record, token);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationResultAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
return calibrationResultWriter.StoreQ3CalibrationAsync(record, token, diagnostic);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreSimulatedQ3CalibrationResultAsync(
|
||||
int pcbId, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
try { diagnostic?.Invoke("Simulated Q3 fixture values; actual SQL write requested."); }
|
||||
catch (Exception) { }
|
||||
return calibrationResultWriter.StoreQ3CalibrationAsync(new Q3CalibrationRecord
|
||||
{
|
||||
PcbId = pcbId, P1Offset = -0.3, P1Cal = 15260,
|
||||
P2Offset = -0.3, P2Cal = 15451, P3Offset = -0.3, P3Cal = 15204,
|
||||
Date = DateTime.UtcNow
|
||||
}, token, diagnostic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the public GCI facade.
|
||||
/// </summary>
|
||||
public InterfaceOutsideToGCI()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the public GCI facade.
|
||||
/// </summary>
|
||||
public InterfaceOutsideToGCI(
|
||||
InterfaceGCIToLaatzen innerMeterApi,
|
||||
IMeterLoginPasswordReader passwordReader,
|
||||
IPreAdjustmentCalibrationParamsReader calibrationReader)
|
||||
: this(innerMeterApi, passwordReader, calibrationReader, null)
|
||||
{
|
||||
}
|
||||
|
||||
public InterfaceOutsideToGCI(
|
||||
InterfaceGCIToLaatzen innerMeterApi,
|
||||
IMeterLoginPasswordReader passwordReader,
|
||||
IPreAdjustmentCalibrationParamsReader calibrationReader,
|
||||
GciDataStorageConfig storageConfig)
|
||||
{
|
||||
_innerMeterAPI = innerMeterApi?? throw new ArgumentNullException(nameof(innerMeterApi));
|
||||
loginPasswordsReader = passwordReader ?? throw new ArgumentNullException(nameof(passwordReader));
|
||||
calibrationParamsReader = calibrationReader ?? throw new ArgumentNullException(nameof(calibrationReader));
|
||||
calibrationResultWriter = new Core.DataStorage.Writing.CalibrationResultWriter(storageConfig);
|
||||
|
||||
//init handlers
|
||||
//_innerMeterAPI._progressProcess.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
|
||||
}
|
||||
|
||||
private async void RequestedCalibrationParamsFromDb(
|
||||
object sender,
|
||||
EventArgsProcessProgress e)
|
||||
{
|
||||
var progress = e.Value;
|
||||
var source = progress.CancellationSource;
|
||||
var token = source.Token;
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
DataQuery query = new DataQuery();
|
||||
query.QueryParams.Add(((int)e.Value.Setting.MeterSize).ToString());
|
||||
|
||||
Dictionary<string, UInt32> calibrationParams = await ReadPreAdjustmentCalibrationParamsAsync(query, token).ConfigureAwait(false);
|
||||
|
||||
lock (progress)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
|
||||
progress.PushedCalibrationParams = calibrationParams;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
|
||||
progress.DebugMessage($"Calibration params reading failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (progress)
|
||||
{
|
||||
if (!token.IsCancellationRequested && ReferenceEquals(progress.CancellationSource, source))
|
||||
progress.CalibrationParamsReadEvent.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Laatzen ToolBox actions
|
||||
|
||||
#region ================================== PORT DETECTION ==================================
|
||||
|
||||
public PortDetectionResult DetectStreamingPort(int slot)
|
||||
{
|
||||
var result = _innerMeterAPI?.DetectStreamingPort(slot);
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PortDetectionResult> DetectStreamingPortAsync(
|
||||
int slot,
|
||||
CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var result = await _innerMeterAPI?.DetectStreamingPortAsync(slot, token);
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
public PortDetectionResult DetectRequestPort(int slot)
|
||||
{
|
||||
var result = _innerMeterAPI?.DetectRequestPort(slot);
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PortDetectionResult> DetectRequestPortAsync(
|
||||
int slot,
|
||||
CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var result = await _innerMeterAPI?.DetectRequestPortAsync(slot, token);
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== INIT/UPDATE/GET slot ==================================
|
||||
|
||||
/// <summary>Acquires streaming reception until the returned lease is disposed.</summary>
|
||||
public Task<IDisposable> AcquirePreadjustmentStreamingPortAsync(
|
||||
int slot, GciPortConfig streamingPort, CancellationToken token = default)
|
||||
{
|
||||
if (streamingPort == null) throw new ArgumentNullException(nameof(streamingPort));
|
||||
var mappedPort = ModelsMapping.MapPort(streamingPort);
|
||||
if (!mappedPort.HasValue) throw new ArgumentException("A streaming port is required.", nameof(streamingPort));
|
||||
return _innerMeterAPI.AcquirePreadjustmentStreamingPortAsync(slot, mappedPort.Value, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a meter slot using the provided slot configuration.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// Slot initialization request containing slot id, configuration source,
|
||||
/// password source, request port and streaming port.
|
||||
/// </param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Result describing whether the slot was created, updated, already existed or failed.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="request"/> is null.
|
||||
/// </exception>
|
||||
public async Task<GciInitSlotResult> InitSlotAsync(
|
||||
GciInitSlotRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
var result = await _innerMeterAPI.InitSlotAsync(
|
||||
request.SlotId,
|
||||
ModelsMapping.MapConfigSource(request.ConfigSource),
|
||||
ModelsMapping.MapPasswordSource(request.PasswordSource),
|
||||
ModelsMapping.MapPort(request.RequestPort),
|
||||
ModelsMapping.MapPort(request.StreamingPort),
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates configuration of an existing meter slot.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// Slot configuration request containing updated configuration source,
|
||||
/// password source and port settings.
|
||||
/// </param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Result describing whether the slot update succeeded or failed.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="request"/> is null.
|
||||
/// </exception>
|
||||
public async Task<GciInitSlotResult> UpdateSlotAsync(
|
||||
GciInitSlotRequest request,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
var result = await _innerMeterAPI.UpdateSlotAsync(
|
||||
request.SlotId,
|
||||
ModelsMapping.MapConfigSource(request.ConfigSource),
|
||||
ModelsMapping.MapPasswordSource(request.PasswordSource),
|
||||
ModelsMapping.MapPort(request.RequestPort),
|
||||
ModelsMapping.MapPort(request.StreamingPort),
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about one meter slot.
|
||||
/// </summary>
|
||||
/// <param name="slotId">Slot id to query. Must be greater than zero.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Slot information including existence, connection state, login state,
|
||||
/// PCB id and configured communication ports.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="slotId"/> is invalid.
|
||||
/// </exception>
|
||||
public async Task<GciSlotInfo> GetSlotAsync(
|
||||
int slotId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slotId <= 0)
|
||||
throw new ArgumentException("Invalid slot id.");
|
||||
|
||||
var result = await _innerMeterAPI.GetOneMeterInfo(slotId, token).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about all currently initialized meter slots.
|
||||
/// </summary>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Collection of slot information records for all known meters.
|
||||
/// </returns>
|
||||
public async Task<GciAllSlotsInfo> GetAllSlotsAsync(
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var result = await _innerMeterAPI.GetAllMetersInfo(token).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans one meter slot and releases its runtime resources.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id to clean. Must be greater than zero.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Result describing whether the slot cleanup succeeded or failed.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="slot"/> is invalid.
|
||||
/// </exception>
|
||||
public async Task<GciCleanSlotResult> CleanSlotAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.", nameof(slot));
|
||||
|
||||
var result = await _innerMeterAPI.CleanSlotAsync(slot, token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans all initialized meter slots and releases related runtime resources.
|
||||
/// </summary>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Result describing whether cleanup of all slots succeeded or failed.
|
||||
/// </returns>
|
||||
public async Task<GciCleanAllSlotsResult> CleanAllSlotsAsync(
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var result = await _innerMeterAPI.CleanAllSlotsAsync(token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ================================== PASSWORD ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Sets runtime password for the meter assigned to the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="password">Password to assign to the meter.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Result containing password update status.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when slot id is invalid or password is empty.
|
||||
/// </exception>
|
||||
public async Task<GciSetPasswordResult> SetPasswordAsync(
|
||||
int slot,
|
||||
string password,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentException("Password is empty.");
|
||||
|
||||
var result = await _innerMeterAPI.SetMeterPasswordAsync(slot, password, token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== LOGIN ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Logs in to the meter assigned to the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Login result containing login state and status message.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="slot"/> is invalid.
|
||||
/// </exception>
|
||||
public async Task<PublicModels.GciLoginResult> LoginOneSlotAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.");
|
||||
|
||||
var result = await _innerMeterAPI.LoginOneSlotAsync(slot, token).ConfigureAwait(false);
|
||||
return result;
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ================================== CONNECTION ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Connects the meter assigned to the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Connection result containing connection state and status message.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="slot"/> is invalid.
|
||||
/// </exception>
|
||||
public async Task<GciConnectResult> ConnectOneSlotAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.");
|
||||
|
||||
var result = await _innerMeterAPI.ConnectOneSlotAsync(slot, token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the meter assigned to the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Disconnect result containing final connection state and status message.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="slot"/> is invalid.
|
||||
/// </exception>
|
||||
public async Task<GciDisconnectResult> DisconnectAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.");
|
||||
|
||||
var result = await _innerMeterAPI.DisconnectAsync(slot, token).ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ================================== PCB ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Reads PCB identifier from the meter assigned to the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>Result containing PCB id and read status.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when <paramref name="slot"/> is invalid.
|
||||
/// </exception>
|
||||
public async Task<GciGetPcbIdResult> GetPcbIdAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.");
|
||||
|
||||
var result = await _innerMeterAPI.GetPcbIdAsync(slot, token).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ================================== READ ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Reads a firmware register value from the meter assigned to the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="registerName">Register identifier to read.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Register read result containing raw register value and operation status.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when slot id or register name is invalid.
|
||||
/// </exception>
|
||||
public async Task<RegisterReadResult> ReadRegisterAsync(
|
||||
int slot,
|
||||
string registerName,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.", nameof(slot));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(registerName))
|
||||
throw new ArgumentException("Register name is empty.", nameof(registerName));
|
||||
|
||||
var result = await _innerMeterAPI
|
||||
.ReadRegisterAsync(slot, registerName, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== WRITE ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to a firmware register.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="registerName">Register identifier to write.</param>
|
||||
/// <param name="value">Value to write.</param>
|
||||
/// <param name="storeToDevice">
|
||||
/// Indicates whether configuration should be permanently stored.
|
||||
/// </param>
|
||||
/// <param name="refreshSystemState">
|
||||
/// Indicates whether firmware system state should be refreshed after write.
|
||||
/// </param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Register write result describing write status.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when slot id or register name is invalid.
|
||||
/// </exception>
|
||||
public async Task<RegisterWriteResult> WriteRegisterAsync(
|
||||
int slot,
|
||||
string registerName,
|
||||
object value,
|
||||
bool storeToDevice = false,
|
||||
bool refreshSystemState = false,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.", nameof(slot));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(registerName))
|
||||
throw new ArgumentException("Register name is empty.", nameof(registerName));
|
||||
|
||||
var result = await _innerMeterAPI
|
||||
.WriteRegisterAsync(
|
||||
slot,
|
||||
registerName,
|
||||
value,
|
||||
storeToDevice,
|
||||
refreshSystemState,
|
||||
token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ================================== Password ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Updates meter password for the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id. Must be greater than zero.</param>
|
||||
/// <param name="password">New password.</param>
|
||||
/// <param name="token">Cancellation token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>
|
||||
/// Password update result.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Thrown when slot id or password is invalid.
|
||||
/// </exception>
|
||||
public async Task<GciSetPasswordResult> SetMeterPasswordAsync(
|
||||
int slot,
|
||||
string password,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (slot <= 0)
|
||||
throw new ArgumentException("Invalid slot id.", nameof(slot));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentException("Password is empty.", nameof(password));
|
||||
|
||||
var result = await _innerMeterAPI
|
||||
.SetMeterPasswordAsync(slot, password, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
//RaiseMeterBatchStatusChanged();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== DEBUG STATUS ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Gets runtime diagnostic information for all active workers.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Collection containing worker state, queue information,
|
||||
/// current operation and activity timestamps.
|
||||
/// </returns>
|
||||
public List<WorkerDebugStatus> GetWorkerDebugStatuses()
|
||||
{
|
||||
return _innerMeterAPI?.GetWorkerDebugStatuses();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets runtime diagnostic information for all meter slots.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Collection containing slot state, connection state,
|
||||
/// selected state and communication configuration.
|
||||
/// </returns>
|
||||
public List<MeterBatchDebugStatus> GetMeterBatchDebugStatuses()
|
||||
{
|
||||
return _innerMeterAPI?.GetMeterBatchDebugStatuses();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises meter batch status change notification.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intended to notify external consumers after changes in meter state.
|
||||
/// </remarks>
|
||||
public void RaiseMeterBatchStatusChanged()
|
||||
{
|
||||
var statuses = GetMeterBatchDebugStatuses();
|
||||
|
||||
var handler = MeterBatchStatusChanged;
|
||||
if (handler != null)
|
||||
handler(statuses);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== SLOT SELECTION ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Sets selection state for a slot.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id.</param>
|
||||
/// <param name="selected">Selection state.</param>
|
||||
public void SetSlotSelected(int slot, bool selected)
|
||||
{
|
||||
_innerMeterAPI?.SetSlotSelected(slot, selected);
|
||||
RaiseMeterBatchStatusChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified slot is selected.
|
||||
/// </summary>
|
||||
/// <param name="slot">Slot id.</param>
|
||||
/// <returns>
|
||||
/// True if slot is selected; otherwise false.
|
||||
/// </returns>
|
||||
public bool IsSlotSelected(int slot)
|
||||
{
|
||||
return (bool)(_innerMeterAPI?.IsSlotSelected(slot));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all selected slot identifiers.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Ordered collection of selected slot ids.
|
||||
/// </returns>
|
||||
public List<int> GetSelectedSlots()
|
||||
{
|
||||
return _innerMeterAPI?.GetSelectedSlots();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== Register names ==================================
|
||||
|
||||
/// <summary>
|
||||
/// Gets all available firmware register identifiers.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Ordered collection of register names.
|
||||
/// </returns>
|
||||
public List<string> GetAllRegisterNames()
|
||||
{
|
||||
return _innerMeterAPI?.GetAllRegisterNames();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Laatzen Preadjustment processes
|
||||
|
||||
#region ================================== PreAdjustment ==================================
|
||||
public PreAdjustmentInitializationResult Preadjustment_Initialization(
|
||||
ProcessProgress pp,
|
||||
List<MeterStateControl> mc)
|
||||
{
|
||||
//init handlers
|
||||
pp.OnRequestedCalibrationParamsFromDb -= RequestedCalibrationParamsFromDb;
|
||||
pp.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
|
||||
|
||||
return _innerMeterAPI?.Preadjustment_Initialization(pp, mc);
|
||||
}
|
||||
|
||||
public PreAdjustmentInitializationResult Preadjustment_Initialization(
|
||||
ProcessProgress pp,
|
||||
List<MeterStateControl> mc,
|
||||
CancellationToken token)
|
||||
{
|
||||
pp.OnRequestedCalibrationParamsFromDb -= RequestedCalibrationParamsFromDb;
|
||||
pp.OnRequestedCalibrationParamsFromDb += RequestedCalibrationParamsFromDb;
|
||||
return _innerMeterAPI?.Preadjustment_Initialization(pp, mc, token);
|
||||
}
|
||||
|
||||
public void CancelPreadjustment()
|
||||
{
|
||||
_innerMeterAPI?.CancelPreadjustment();
|
||||
}
|
||||
|
||||
public Task<PreadjustmentDetectResult> PreAdjustment_DetectAsync(
|
||||
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_DetectAsync(selectedSlots, token);
|
||||
}
|
||||
|
||||
public PreadjustmentDetectResult PreAdjustment_DetectDirect(
|
||||
IEnumerable<PublicModels.MeterBatchDebugStatus> selectedSlots,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_DetectDirect(selectedSlots, token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_PreparationAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_PreparationAsync(slot, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect()
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_PreparationDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_PreparationDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_PreparationDirect(token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_AmplitudeTestAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_AmplitudeTestAsync(slot, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect()
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_AmplitudeTestDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_AmplitudeTestDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_AmplitudeTestDirect(token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationAsync(slot, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect()
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect(token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_TemperatureCalibrationDirect(
|
||||
CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDirect(temperature, token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_TemperatureCalibrationAsync(
|
||||
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationAsync(slot, temperature, token);
|
||||
}
|
||||
|
||||
public TemperatureCalibrationResult PreAdjustment_TemperatureCalibrationDetailedDirect(
|
||||
CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDetailedDirect(temperature, token);
|
||||
}
|
||||
|
||||
public Task<TemperatureCalibrationResult> PreAdjustment_TemperatureCalibrationDetailedAsync(
|
||||
int slot, CalibrationTemperatureInput temperature, CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_TemperatureCalibrationDetailedAsync(slot, temperature, token);
|
||||
}
|
||||
|
||||
public bool PreAdjustment_PushTemperature(
|
||||
double temperature)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_PushTemperature(temperature);
|
||||
}
|
||||
|
||||
public bool PreAdjustment_PushTemperature(
|
||||
double temperature,
|
||||
CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
return _innerMeterAPI.PreAdjustment_PushTemperature(temperature);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_OffsetTestAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_OffsetTestAsync(slot, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect()
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_OffsetTestDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_OffsetTestDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_OffsetTestDirect(token);
|
||||
}
|
||||
|
||||
public Task<PreAdjustmentProcessResult> PreAdjustment_CompletionAsync(
|
||||
int slot,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_CompletionAsync(slot, token);
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect()
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_CompletionDirect();
|
||||
}
|
||||
|
||||
public PreAdjustmentProcessResult PreAdjustment_CompletionDirect(CancellationToken token)
|
||||
{
|
||||
return _innerMeterAPI.PreAdjustment_CompletionDirect(token);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ================================== UNI DATA STORAGE READER ==================================
|
||||
|
||||
//LoginPasswords reading
|
||||
|
||||
/// <summary>
|
||||
/// Reads meter login password from configured GCI data storage.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing PCB ID or another configured lookup value.
|
||||
/// </param>
|
||||
/// <param name="token">
|
||||
/// Cancellation token used to cancel the asynchronous operation.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Password if found; otherwise null.
|
||||
/// </returns>
|
||||
public Task<string> ReadMeterLoginPasswordAsync(
|
||||
DataQuery query,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return loginPasswordsReader.ReadMeterLoginPasswordAsync(
|
||||
query,
|
||||
token);
|
||||
}
|
||||
|
||||
//CalibrationParams reading
|
||||
|
||||
/// <summary>
|
||||
/// Reads pre-adjustment calibration parameters
|
||||
/// from configured GCI data storage.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing meter size or another configured lookup value.
|
||||
/// </param>
|
||||
/// <param name="token">
|
||||
/// Cancellation token used to cancel the asynchronous operation.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Dictionary:
|
||||
///
|
||||
/// Key:
|
||||
/// Calibration parameter name
|
||||
///
|
||||
/// Value:
|
||||
/// Calibration parameter value
|
||||
/// </returns>
|
||||
public Task<Dictionary<string, UInt32>> ReadPreAdjustmentCalibrationParamsAsync(
|
||||
DataQuery query,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return calibrationParamsReader.ReadCalibrationParamsAsync(
|
||||
query,
|
||||
token);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
|
||||
using static GenesisCordonelInterface.API.InterfaceOutsideToGCI;
|
||||
using static GenesisCordonelInterface.API.PublicModels;
|
||||
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
|
||||
|
||||
namespace GenesisCordonelInterface.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Public data contract layer for the Genesis Cordonel Interface (GCI).
|
||||
///
|
||||
/// This class defines all Data Transfer Objects (DTOs) that are exposed
|
||||
/// to external consumers (e.g. TBF, UI, or other integration layers).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Provide stable, dependency-free models for external usage
|
||||
/// - Decouple internal GCI implementation (GenesisMeter, Xylem libraries)
|
||||
/// from external systems
|
||||
/// - Define request/response contracts for all supported operations
|
||||
/// - Contain mapping methods between public DTOs and internal domain models
|
||||
///
|
||||
/// Architecture:
|
||||
/// External world (TBF / UI)
|
||||
/// ↓
|
||||
/// GciPublicModels (this layer)
|
||||
/// ↓
|
||||
/// Internal GCI API (InterfaceGCIToLaatzen, GenesisMeter, etc.)
|
||||
///
|
||||
/// Notes:
|
||||
/// - Public models must NOT expose internal types (e.g. GenesisMeter, IPort, etc.)
|
||||
/// - All mapping between internal and external representations must be done here
|
||||
/// - DTOs are designed to be simple, serializable, and stable over time
|
||||
/// - Any change in internal implementation should not affect these models
|
||||
///
|
||||
/// Pattern:
|
||||
/// Each operation follows a consistent structure:
|
||||
/// Request → Operation → Result
|
||||
///
|
||||
/// Example:
|
||||
/// GciInitSlotRequest → InitSlot → GciInitSlotResult
|
||||
/// GetSlot → GciSlotInfo
|
||||
/// GetPcbId → GciGetPcbIdResult
|
||||
///
|
||||
/// This layer acts as a boundary between domain logic and integration logic.
|
||||
/// </summary>
|
||||
public class ModelsMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Mapping methods between public DTOs and internal GCI models.
|
||||
/// Ensures separation between external contracts and internal domain objects.
|
||||
/// </summary>
|
||||
#region ================================== OUTERN/INTERN and back models mapping ==================================
|
||||
public static PasswordSource MapPasswordSource(PublicModels.GciPasswordSource src)
|
||||
{
|
||||
return (PasswordSource)src;
|
||||
}
|
||||
|
||||
public static ConfigSource MapConfigSource(PublicModels.GciConfigSource src)
|
||||
{
|
||||
return (ConfigSource)src;
|
||||
}
|
||||
|
||||
public static Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig? MapPort(PublicModels.GciPortConfig port)
|
||||
{
|
||||
if (port == null)
|
||||
return null;
|
||||
|
||||
Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig result = new Xylem.Common.Hardware.Interfaces.Ports.PortCore.PortConfig();
|
||||
|
||||
result.PortName = port.PortName;
|
||||
result.Type = port.Type;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static PublicModels.GciInitSlotResult MapInitResult(PublicModels.GciInitSlotResult result)
|
||||
{
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
return new PublicModels.GciInitSlotResult
|
||||
{
|
||||
SlotId = result.SlotId,
|
||||
Success = result.Success,
|
||||
Message = result.Message
|
||||
};
|
||||
}
|
||||
|
||||
public static PublicModels.GciPasswordSource MapPasswordSourceBack(PasswordSource src)
|
||||
{
|
||||
return (PublicModels.GciPasswordSource)src;
|
||||
}
|
||||
|
||||
public static PublicModels.GciConfigSource MapConfigSourceBack(ConfigSource src)
|
||||
{
|
||||
return (PublicModels.GciConfigSource)src;
|
||||
}
|
||||
|
||||
public static PublicModels.GciPortConfig MapPortBack(PortConfig? port)
|
||||
{
|
||||
if (!port.HasValue)
|
||||
return null;
|
||||
|
||||
PortConfig value = port.Value;
|
||||
|
||||
return new PublicModels.GciPortConfig
|
||||
{
|
||||
PortName = value.PortName,
|
||||
Type = value.Type
|
||||
};
|
||||
}
|
||||
|
||||
public static PublicModels.GciPortConfig MapPortBack(IPort port)
|
||||
{
|
||||
if (port == null)
|
||||
return null;
|
||||
|
||||
return new PublicModels.GciPortConfig
|
||||
{
|
||||
PortName = port.GetPortName(),
|
||||
Type = port.GetType().Name
|
||||
};
|
||||
}
|
||||
|
||||
public static PublicModels.GciPortConfig MapPortBack(string portName, string portTyoe)
|
||||
{
|
||||
return new PublicModels.GciPortConfig
|
||||
{
|
||||
PortName = portName,
|
||||
Type = portTyoe
|
||||
};
|
||||
}
|
||||
|
||||
public static MeterBatchDebugStatus ToMeterBatchDebugStatus(GciSlotInfo slot)
|
||||
{
|
||||
if (slot == null)
|
||||
return null;
|
||||
|
||||
return new MeterBatchDebugStatus
|
||||
{
|
||||
Slot = slot.SlotId,
|
||||
Selected = true,
|
||||
PcbId = slot.PcbId,
|
||||
IsConnected = false,
|
||||
IsLoggedOn = false,
|
||||
RequestPort = slot.RequestPort == null ? "" : slot.RequestPort.PortName,
|
||||
StreamingPort = slot.StreamingPort == null ? "" : slot.StreamingPort.PortName,
|
||||
FwVersion = "",
|
||||
InterfaceVersion = ""
|
||||
};
|
||||
}
|
||||
|
||||
public static List<MeterBatchDebugStatus> ToMeterBatchDebugStatusList(
|
||||
IEnumerable<GciSlotInfo> slots)
|
||||
{
|
||||
if (slots == null)
|
||||
return new List<MeterBatchDebugStatus>();
|
||||
|
||||
return slots
|
||||
.Where(x => x != null)
|
||||
.Select(ToMeterBatchDebugStatus)
|
||||
.ToList();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Xylem.Common.Hardware.Interfaces.Ports.PortCore;
|
||||
using static GenesisCordonelInterface.API.InterfaceOutsideToGCI;
|
||||
using static Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.GenesisMeter;
|
||||
|
||||
namespace GenesisCordonelInterface.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Public data contract layer for the Genesis Cordonel Interface (GCI).
|
||||
///
|
||||
/// This class defines all Data Transfer Objects (DTOs) that are exposed
|
||||
/// to external consumers (e.g. TBF, UI, or other integration layers).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Provide stable, dependency-free models for external usage
|
||||
/// - Decouple internal GCI implementation (GenesisMeter, Xylem libraries)
|
||||
/// from external systems
|
||||
/// - Define request/response contracts for all supported operations
|
||||
/// - Contain mapping methods between public DTOs and internal domain models
|
||||
///
|
||||
/// Architecture:
|
||||
/// External world (TBF / UI)
|
||||
/// ↓
|
||||
/// GciPublicModels (this layer)
|
||||
/// ↓
|
||||
/// Internal GCI API (InterfaceGCIToLaatzen, GenesisMeter, etc.)
|
||||
///
|
||||
/// Notes:
|
||||
/// - Public models must NOT expose internal types (e.g. GenesisMeter, IPort, etc.)
|
||||
/// - All mapping between internal and external representations must be done here
|
||||
/// - DTOs are designed to be simple, serializable, and stable over time
|
||||
/// - Any change in internal implementation should not affect these models
|
||||
///
|
||||
/// Pattern:
|
||||
/// Each operation follows a consistent structure:
|
||||
/// Request → Operation → Result
|
||||
///
|
||||
/// Example:
|
||||
/// GciInitSlotRequest → InitSlot → GciInitSlotResult
|
||||
/// GetSlot → GciSlotInfo
|
||||
/// GetPcbId → GciGetPcbIdResult
|
||||
///
|
||||
/// This layer acts as a boundary between domain logic and integration logic.
|
||||
/// </summary>
|
||||
public class PublicModels
|
||||
{
|
||||
/// <summary>Persisted values for one meter and one preadjustment attempt.</summary>
|
||||
public sealed class PreadjustmentCompletionRecord
|
||||
{
|
||||
// Diagnostic routing only. Not a database column or SQL parameter.
|
||||
public int? Slot { get; set; }
|
||||
public int PcbId { get; set; }
|
||||
public int? MeterSize { get; set; }
|
||||
public int? ZeroOffset1 { get; set; }
|
||||
public int? ZeroOffset2 { get; set; }
|
||||
public int? ZeroOffset3 { get; set; }
|
||||
public int? FirstHitUpdatePeriod { get; set; }
|
||||
public int? FirstHitShift { get; set; }
|
||||
public int? FirstHitPercent1 { get; set; }
|
||||
public int? FirstHitPercent2 { get; set; }
|
||||
public int? FirstHitPercent3 { get; set; }
|
||||
public int? ToFTempOffset1 { get; set; }
|
||||
public int? ToFTempOffset2 { get; set; }
|
||||
public int? ToFTempOffset3 { get; set; }
|
||||
// Timestamp of the attempt, not the time of a possible later write retry.
|
||||
public DateTime Date { get; set; }
|
||||
public bool Successful { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Q3 values calculated and confirmed by the caller; no port access.</summary>
|
||||
public sealed class Q3CalibrationRecord
|
||||
{
|
||||
public int PcbId { get; set; }
|
||||
// Preserve the existing TBF meaning/unit of each offset (do not rescale here).
|
||||
public double? P1Offset { get; set; }
|
||||
public int? P1Cal { get; set; }
|
||||
public double? P2Offset { get; set; }
|
||||
public int? P2Cal { get; set; }
|
||||
public double? P3Offset { get; set; }
|
||||
public int? P3Cal { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Completion outcome plus detached SQL inputs and separate write outcomes.</summary>
|
||||
public sealed class CompletionProcessResult : PreAdjustmentProcessResult
|
||||
{
|
||||
public List<PreadjustmentCompletionRecord> DatabaseRecords { get; set; } =
|
||||
new List<PreadjustmentCompletionRecord>();
|
||||
public List<CompletionDatabaseWriteResult> DatabaseWrites { get; set; } =
|
||||
new List<CompletionDatabaseWriteResult>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString() + ", SQL=[" + string.Join("; ",
|
||||
DatabaseWrites.Select(w => "Slot=" + w.Slot + ", " + w.Result.Status)) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CompletionDatabaseWriteResult
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public CalibrationWriteResult Result { get; set; }
|
||||
}
|
||||
|
||||
public enum CalibrationWriteStatus
|
||||
{
|
||||
Failed = 0, Stored = 1, Disabled = 2, Simulated = 3,
|
||||
// The connection failed during commit; verify the DB before retrying.
|
||||
OutcomeUnknown = 4
|
||||
}
|
||||
|
||||
/// <summary>Database outcome, independent of the physical calibration outcome.</summary>
|
||||
public sealed class CalibrationWriteResult
|
||||
{
|
||||
public CalibrationWriteStatus Status { get; set; }
|
||||
public bool Success { get { return Status == CalibrationWriteStatus.Stored; } }
|
||||
public int PcbId { get; set; }
|
||||
public long? RecordId { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Status={0}, PcbId={1}, RecordId={2}, Error={3}",
|
||||
Status, PcbId, RecordId, ErrorMessage ?? "None");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HideSensitiveValues { get; set; } = true;
|
||||
|
||||
private static string FormatPassword(string password)
|
||||
{
|
||||
if (!HideSensitiveValues)
|
||||
return password ?? "<null>";
|
||||
|
||||
if (string.IsNullOrEmpty(password))
|
||||
return "<empty>";
|
||||
|
||||
return "********";
|
||||
}
|
||||
/// <summary>
|
||||
/// Public DTOs exposed to external systems.
|
||||
/// These models represent the contract of the GCI API.
|
||||
/// They must remain stable and independent of internal implementation.
|
||||
/// </summary>
|
||||
#region ================================== PUBLIC MODELS ===========================================
|
||||
|
||||
public class RegisterReadResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string RegisterName { get; set; }
|
||||
public string RawHex { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"Success={0}, RegisterName={1}, RawHex={2}, ErrorMessage={3}",
|
||||
Success,
|
||||
RegisterName,
|
||||
RawHex,
|
||||
ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public class WorkerDebugStatus
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int QueueLength { get; set; }
|
||||
public bool IsBusy { get; set; }
|
||||
public string CurrentOperation { get; set; }
|
||||
public string LastError { get; set; }
|
||||
public DateTime LastActivity { get; set; }
|
||||
}
|
||||
|
||||
public class MeterBatchDebugStatus
|
||||
{
|
||||
public int Slot { get; set; }
|
||||
public bool Selected { get; set; }
|
||||
public string PcbId { get; set; }
|
||||
public bool IsConnected { get; set; }
|
||||
public bool IsLoggedOn { get; set; }
|
||||
public string RequestPort { get; set; }
|
||||
public string StreamingPort { get; set; }
|
||||
public string RequestPortType { get; set; }
|
||||
public string FwVersion { get; set; }
|
||||
public string InterfaceVersion { get; set; }
|
||||
}
|
||||
|
||||
public class RegisterWriteResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string RegisterName { get; set; }
|
||||
public object WrittenValue { get; set; }
|
||||
public bool StoreToDevice { get; set; }
|
||||
public bool RefreshSystemState { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Success={Success}, " +
|
||||
$"Register={RegisterName ?? "<null>"}, " +
|
||||
$"Value={WrittenValue ?? "<null>"}, " +
|
||||
$"StoreToDevice={StoreToDevice}, " +
|
||||
$"RefreshSystemState={RefreshSystemState}, " +
|
||||
$"Error={ErrorMessage ?? "<none>"}";
|
||||
}
|
||||
}
|
||||
|
||||
public class PortDetectionResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public int Slot { get; set; }
|
||||
public string PortName { get; set; }
|
||||
public string PcbId { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"Slot={0}, Success={1}, PortName={2}, PcbId={3}, ErrorMessage={4}",
|
||||
Slot,
|
||||
Success,
|
||||
PortName,
|
||||
PcbId,
|
||||
ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public class GciSlotInfo
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Exists { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public bool IsConnected { get; set; }
|
||||
public bool IsLoggedOn { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string PcbId { get; set; }
|
||||
public string Password { get; set; }
|
||||
public GciConfigSource ConfigSource { get; set; }
|
||||
public GciPasswordSource PasswordSource { get; set; }
|
||||
public GciPortConfig RequestPort { get; set; }
|
||||
public GciPortConfig StreamingPort { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Exists={1}, Success={2}, IsConnected={3}, IsLoggedOn={4}, Message={5}, PcbId={6}, Password={7}, ConfigSource={8}, PasswordSource={9}, RequestPort={10}, StreamingPort={11}",
|
||||
SlotId,
|
||||
Exists,
|
||||
Success,
|
||||
IsConnected,
|
||||
IsLoggedOn,
|
||||
Message,
|
||||
PcbId,
|
||||
FormatPassword(Password),
|
||||
ConfigSource,
|
||||
PasswordSource,
|
||||
RequestPort,
|
||||
StreamingPort);
|
||||
}
|
||||
}
|
||||
|
||||
public class GciAllSlotsInfo
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public List<GciSlotInfo> Slots { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Success={Success}, Count={Slots?.Count ?? 0}, Message={Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, Message={2}",
|
||||
SlotId,
|
||||
Success,
|
||||
Message);
|
||||
}
|
||||
}
|
||||
|
||||
public enum GciPasswordSource
|
||||
{
|
||||
RestApi = 0,
|
||||
OfflineFile = 1,
|
||||
InterfaceInputPassword = 2,
|
||||
}
|
||||
public enum GciConfigSource
|
||||
{
|
||||
FileConfig = 0,
|
||||
InterfaceInputConfig = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection of port settings
|
||||
/// </summary>
|
||||
public class GciPortConfig
|
||||
{
|
||||
public string PortName { get; set; }
|
||||
public string Type { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("PortName={0}, Type={1}", PortName, Type);
|
||||
}
|
||||
}
|
||||
|
||||
public static GciPortConfig CreatePortConfig(string portName, string type)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(portName))
|
||||
return null;
|
||||
|
||||
return new GciPortConfig
|
||||
{
|
||||
PortName = portName,
|
||||
Type = MapPortType(type)
|
||||
};
|
||||
}
|
||||
|
||||
public static string MapPortType(string type)
|
||||
{
|
||||
switch ((type ?? "").Trim().ToUpperInvariant())
|
||||
{
|
||||
case "RFID":
|
||||
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.RfidSerialPort";
|
||||
|
||||
case "UART":
|
||||
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.UartSerialPort";
|
||||
|
||||
case "IRDA":
|
||||
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort";
|
||||
|
||||
default:
|
||||
return "Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.IrdaSerialPort";
|
||||
}
|
||||
}
|
||||
|
||||
public class GciInitSlotRequest
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public GciConfigSource ConfigSource { get; set; }
|
||||
public GciPasswordSource PasswordSource { get; set; }
|
||||
public GciPortConfig RequestPort { get; set; }
|
||||
public GciPortConfig StreamingPort { get; set; }
|
||||
public string Password { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, GciConfigSource={1}, PasswordSource={2}, RequestPort={3}, StreamingPort={4}, Password={5}",
|
||||
SlotId,
|
||||
ConfigSource,
|
||||
PasswordSource,
|
||||
RequestPort,
|
||||
StreamingPort,
|
||||
FormatPassword(Password));
|
||||
}
|
||||
}
|
||||
|
||||
public class GciInitSlotResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public string PcbId { get; set; }
|
||||
|
||||
public bool Created { get; set; }
|
||||
|
||||
public bool Updated { get; set; }
|
||||
|
||||
public bool AlreadyExists { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, Message={2}, PcbId={3}, Created={4}, Updated={5}, AlreadyExists={6}",
|
||||
SlotId,
|
||||
Success,
|
||||
Message,
|
||||
PcbId,
|
||||
Created,
|
||||
Updated,
|
||||
AlreadyExists);
|
||||
}
|
||||
}
|
||||
|
||||
public class GciCleanAllSlotsResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"Success={0}, Message={1}",
|
||||
Success,
|
||||
Message);
|
||||
}
|
||||
}
|
||||
public class GciCleanSlotResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, Message={2}",
|
||||
SlotId,
|
||||
Success,
|
||||
Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class GciGetPcbIdResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string PcbId { get; set; }
|
||||
public string Message { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, PcbId={2}, Message={3}",
|
||||
SlotId,
|
||||
Success,
|
||||
PcbId,
|
||||
Message);
|
||||
}
|
||||
}
|
||||
public class GciConnectResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string PcbId { get; set; }
|
||||
public bool IsConnected { get; set; }
|
||||
public string Message { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, PcbId={2}, IsConnected={3}, Message={4}",
|
||||
SlotId,
|
||||
Success,
|
||||
PcbId,
|
||||
IsConnected,
|
||||
Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class GciLoginResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string PcbId { get; set; }
|
||||
public bool IsLoggedOn { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Slot={SlotId}, Success={Success}, PcbId={PcbId}, IsLoggedOn={IsLoggedOn}, Message={Message ?? "<none>"}";
|
||||
}
|
||||
}
|
||||
|
||||
public class GciRegisterSnapshot
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string RawValue { get; set; }
|
||||
public string Min { get; set; }
|
||||
public string Max { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string IsAvailable { get; set; }
|
||||
public string Privilege { get; set; }
|
||||
}
|
||||
|
||||
public class GciDisconnectResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, Message={2}",
|
||||
SlotId,
|
||||
Success,
|
||||
Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class GciSetPasswordResult
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"SlotId={0}, Success={1}, Password={2}, Message={3}",
|
||||
SlotId,
|
||||
Success,
|
||||
FormatPassword(Password),
|
||||
Message);
|
||||
}
|
||||
}
|
||||
|
||||
public class PreAdjustmentProcessResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public int Slot { get; set; }
|
||||
public string ProcessName { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"Success={Success}, " +
|
||||
$"Slot={Slot}, " +
|
||||
$"Process={ProcessName}, " +
|
||||
$"Error={ErrorMessage ?? "None"}";
|
||||
}
|
||||
}
|
||||
|
||||
public class TemperatureCalibrationSlotResult : PreAdjustmentProcessResult
|
||||
{
|
||||
public decimal? ReferenceTemperatureCelsius { get; set; }
|
||||
public decimal? MeterTemperatureCelsius { get; set; }
|
||||
public decimal? AbsoluteDifferenceCelsius { get; set; }
|
||||
// The effective existing process limit, reported for diagnostics only.
|
||||
public decimal? DeviationLimitCelsius { get; set; }
|
||||
public int SampleCount { get; set; }
|
||||
public bool? TemperatureCheckPassed { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var culture = System.Globalization.CultureInfo.InvariantCulture;
|
||||
return base.ToString() + string.Format(culture,
|
||||
", Reference={0} C, Meter={1} C, Difference={2} C, Limit={3} C, Samples={4}, Check={5}",
|
||||
ReferenceTemperatureCelsius?.ToString(culture) ?? "N/A",
|
||||
MeterTemperatureCelsius?.ToString(culture) ?? "N/A",
|
||||
AbsoluteDifferenceCelsius?.ToString(culture) ?? "N/A",
|
||||
DeviationLimitCelsius?.ToString(culture) ?? "N/A", SampleCount,
|
||||
TemperatureCheckPassed.HasValue ? (TemperatureCheckPassed.Value ? "PASSED" : "FAILED") : "NOT_EVALUATED");
|
||||
}
|
||||
}
|
||||
|
||||
public class TemperatureCalibrationResult : PreAdjustmentProcessResult
|
||||
{
|
||||
public List<TemperatureCalibrationSlotResult> Slots { get; set; } =
|
||||
new List<TemperatureCalibrationSlotResult>();
|
||||
public bool IsSimulated { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString() + ", Simulated=" + IsSimulated +
|
||||
", TemperatureResults=[" + string.Join("; ", Slots.Select(s => s.ToString())) + "]";
|
||||
}
|
||||
|
||||
// Preserve status for failures before process creation and for simulation.
|
||||
// Missing measurements must never be represented as a measured zero.
|
||||
public static TemperatureCalibrationResult FromProcessResult(
|
||||
PreAdjustmentProcessResult result, bool simulated = false)
|
||||
{
|
||||
if (result == null) throw new ArgumentNullException(nameof(result));
|
||||
var detailed = result as TemperatureCalibrationResult;
|
||||
if (detailed != null) return detailed;
|
||||
return new TemperatureCalibrationResult
|
||||
{
|
||||
Success = result.Success, Slot = result.Slot, ProcessName = result.ProcessName,
|
||||
ErrorMessage = result.ErrorMessage, IsSimulated = simulated
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class PreadjustmentDetectResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public int DetectedMeterCount { get; set; }
|
||||
public int DetectedThermometerCount { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"Success={Success}, " +
|
||||
$"Meters={DetectedMeterCount}, " +
|
||||
$"Thermometers={DetectedThermometerCount}, " +
|
||||
$"Error={ErrorMessage ?? "None"}";
|
||||
}
|
||||
}
|
||||
|
||||
public class PreAdjustmentInitializationResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Slots { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return
|
||||
$"Success={Success}, " +
|
||||
$"Slots={Slots}, " +
|
||||
$"Error={ErrorMessage ?? "None"}";
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Xylem.Common.CommonCore" publicKeyToken="null" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.8.18.17954" newVersion="2.8.18.17954" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"_Comment": "GCI DataStorage configuration",
|
||||
|
||||
"DataStorage": {
|
||||
|
||||
"MeterLoginPasswords": {
|
||||
|
||||
"___Documentation___": {
|
||||
"Description": "Reads login passwords for meters by PCB ID",
|
||||
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
|
||||
"DataSource": "Database connection string",
|
||||
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [PcbId]=QUERYPARAM -> PCB ID provided during GetPasswordAsync()."
|
||||
},
|
||||
|
||||
"Name": "MeterLoginPasswords",
|
||||
"Type": "LocalDatabase",
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"QueryTemplate": "SELECT [Password] FROM [dbo].[SkeletonKeys] WHERE [PcbId] = QUERYPARAM"
|
||||
},
|
||||
|
||||
"PreAdjustmentCalibrationParams": {
|
||||
|
||||
"___Documentation___": {
|
||||
"Description": "Reads pre-adjustment calibration parameters by meter size",
|
||||
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
|
||||
"DataSource": "Database connection string",
|
||||
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [MeterSize]=QUERYPARAM -> meter size provided during ReadCalibrationParamsAsync()."
|
||||
},
|
||||
|
||||
"Name": "PreAdjustmentCalibrationParams",
|
||||
"Type": "LocalDatabase",
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"QueryTemplate": "SELECT [ParameterName], [ParameterValue] FROM [dbo].[PreAdjustmentCalibrationParams] WHERE [MeterSize] = QUERYPARAM"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace GenesisCordonelInterface.Core.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Root GCI configuration object loaded from gci_config.json.
|
||||
///
|
||||
/// This class represents the top-level configuration structure
|
||||
/// and serves as an entry point for all configurable GCI features.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// {
|
||||
/// "DataStorageSection":
|
||||
/// {
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// </summary>
|
||||
public class GciConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Data storage configuration section.
|
||||
///
|
||||
/// Contains definitions for all configured readers and writers,
|
||||
/// such as:
|
||||
///
|
||||
/// - MeterLoginPasswords
|
||||
/// - PreAdjustmentCalibrationParams
|
||||
/// - future storage providers
|
||||
/// </summary>
|
||||
public DataStorage.Config.GciDataStorageConfig DataStorageSection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads GCI configuration from gci_config.json.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Locate configuration file
|
||||
/// - Deserialize JSON into strongly typed objects
|
||||
/// - Normalize relative file paths
|
||||
/// - Preserve database connection strings and REST URLs
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// Config/
|
||||
/// gci_config.json
|
||||
///
|
||||
/// Data/
|
||||
/// meter_passwords.csv
|
||||
///
|
||||
/// Relative paths:
|
||||
/// Data\file.csv
|
||||
///
|
||||
/// become:
|
||||
///
|
||||
/// C:\App\bin\Debug\Data\file.csv
|
||||
///
|
||||
/// Database sources remain unchanged:
|
||||
///
|
||||
/// Server=(localdb)\MojaDB;Database=...
|
||||
/// </summary>
|
||||
public static class GciConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads default GCI configuration from:
|
||||
///
|
||||
/// Config\gci_config.json
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Loaded GCI configuration.
|
||||
/// </returns>
|
||||
public static GciConfig LoadDefault()
|
||||
{
|
||||
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
string configPath = Path.Combine(
|
||||
baseDirectory,
|
||||
"Config",
|
||||
"gci_config.json");
|
||||
|
||||
return Load(configPath, baseDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads GCI configuration from specified file.
|
||||
/// </summary>
|
||||
/// <param name="configPath">
|
||||
/// Path to configuration json file.
|
||||
/// </param>
|
||||
/// <param name="baseDirectory">
|
||||
/// Base directory used for resolving relative paths.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Loaded configuration object.
|
||||
/// </returns>
|
||||
public static GciConfig Load(
|
||||
string configPath,
|
||||
string baseDirectory = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configPath))
|
||||
throw new ArgumentException(
|
||||
"Config path must not be empty.",
|
||||
nameof(configPath));
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
throw new FileNotFoundException(
|
||||
"GCI config file was not found.",
|
||||
configPath);
|
||||
|
||||
string json = File.ReadAllText(configPath);
|
||||
|
||||
GciConfig config =
|
||||
JsonConvert.DeserializeObject<GciConfig>(json);
|
||||
|
||||
if (config == null)
|
||||
throw new InvalidOperationException(
|
||||
"GCI config could not be loaded.");
|
||||
|
||||
NormalizeDataStoragePaths(
|
||||
config,
|
||||
baseDirectory ?? Path.GetDirectoryName(configPath));
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes paths for all configured data storage entries.
|
||||
///
|
||||
/// Converts relative file paths into absolute paths.
|
||||
/// Database connection strings remain untouched.
|
||||
/// </summary>
|
||||
/// <param name="config">
|
||||
/// Loaded configuration object.
|
||||
/// </param>
|
||||
/// <param name="baseDirectory">
|
||||
/// Base path used for relative resolution.
|
||||
/// </param>
|
||||
private static void NormalizeDataStoragePaths(
|
||||
GciConfig config,
|
||||
string baseDirectory)
|
||||
{
|
||||
if (config.DataStorageSection == null)
|
||||
return;
|
||||
|
||||
NormalizePath(
|
||||
config.DataStorageSection.MeterLoginPasswords,
|
||||
baseDirectory);
|
||||
|
||||
NormalizePath(
|
||||
config.DataStorageSection.PreAdjustmentCalibrationParams,
|
||||
baseDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts relative file paths into absolute paths.
|
||||
///
|
||||
/// Applies only to:
|
||||
/// - LocalCsv
|
||||
/// - RemoteCsv
|
||||
/// - LocalJson
|
||||
/// - RemoteJson
|
||||
///
|
||||
/// Does not modify:
|
||||
/// - Database connection strings
|
||||
/// - REST URLs
|
||||
/// </summary>
|
||||
/// <param name="storageConfig">
|
||||
/// Storage configuration.
|
||||
/// </param>
|
||||
/// <param name="baseDirectory">
|
||||
/// Base path for resolution.
|
||||
/// </param>
|
||||
private static void NormalizePath(
|
||||
DataStorageConfig storageConfig,
|
||||
string baseDirectory)
|
||||
{
|
||||
if (storageConfig == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(storageConfig.DataSource))
|
||||
return;
|
||||
|
||||
// Database connection strings and URLs
|
||||
// must never be treated as file paths.
|
||||
if (storageConfig.Type == DataStorageType.LocalDatabase ||
|
||||
storageConfig.Type == DataStorageType.RemoteDatabase ||
|
||||
storageConfig.Type == DataStorageType.RestApi)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(storageConfig.DataSource))
|
||||
return;
|
||||
|
||||
storageConfig.DataSource =
|
||||
Path.GetFullPath(
|
||||
Path.Combine(
|
||||
baseDirectory,
|
||||
storageConfig.DataSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"_Comment": "GCI DataStorage configuration",
|
||||
|
||||
"DataStorageSection": {
|
||||
|
||||
"MeterLoginPasswords": {
|
||||
|
||||
"___Documentation___": {
|
||||
"Description": "Reads login passwords for meters by PCB ID",
|
||||
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
|
||||
"DataSource": "Database connection string",
|
||||
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [PcbId]=QUERYPARAM -> PCB ID provided during GetPasswordAsync()."
|
||||
},
|
||||
|
||||
"Name": "MeterLoginPasswords",
|
||||
"Type": "LocalDatabase",
|
||||
"DataSource": "Server=(localdb)\\SensusLocalDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"QueryTemplate": "SELECT [Password] FROM [dbo].[SkeletonKeys] WHERE [PcbId] = QUERYPARAM"
|
||||
},
|
||||
|
||||
"PreAdjustmentCalibrationParams": {
|
||||
|
||||
"___Documentation___": {
|
||||
"Description": "Reads pre-adjustment calibration parameters by meter size",
|
||||
"Type": "Supported: LocalDatabase, RemoteDatabase, LocalCsv, LocalJson, RestApi",
|
||||
"DataSource": "Database connection string",
|
||||
"QueryTemplate": "QUERYPARAM is placeholder for runtime value. Example: WHERE [MeterSize]=QUERYPARAM -> meter size provided during ReadCalibrationParamsAsync()."
|
||||
},
|
||||
|
||||
"Name": "PreAdjustmentCalibrationParams",
|
||||
"Type": "LocalDatabase",
|
||||
"DataSource": "Server=(localdb)\\SensusLocalDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"QueryTemplate": "SELECT [ParameterName], [ParameterValue] FROM [dbo].[PreAdjustmentCalibrationParams] WHERE [MeterSize] = QUERYPARAM"
|
||||
},
|
||||
|
||||
"PreadjustmentCompletionWriter": {
|
||||
"Enabled": true,
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"CommandText": "INSERT INTO [dbo].[PreadjustmentCompletion] ([PcbId], [ZeroOffset1], [ZeroOffset2], [ZeroOffset3], [FirstHitUpdatePeriod], [FirstHitShift], [FirstHitPercent1], [FirstHitPercent2], [FirstHitPercent3], [ToFTempOffset1], [ToFTempOffset2], [ToFTempOffset3], [MeterSize], [Date], [Successful]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @ZeroOffset1, @ZeroOffset2, @ZeroOffset3, @FirstHitUpdatePeriod, @FirstHitShift, @FirstHitPercent1, @FirstHitPercent2, @FirstHitPercent3, @ToFTempOffset1, @ToFTempOffset2, @ToFTempOffset3, @MeterSize, @Date, @Successful);",
|
||||
"UseStoredProcedure": false,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ParameterMappings": {
|
||||
"PcbId": "@PcbId",
|
||||
"ZeroOffset1": "@ZeroOffset1",
|
||||
"ZeroOffset2": "@ZeroOffset2",
|
||||
"ZeroOffset3": "@ZeroOffset3",
|
||||
"FirstHitUpdatePeriod": "@FirstHitUpdatePeriod",
|
||||
"FirstHitShift": "@FirstHitShift",
|
||||
"FirstHitPercent1": "@FirstHitPercent1",
|
||||
"FirstHitPercent2": "@FirstHitPercent2",
|
||||
"FirstHitPercent3": "@FirstHitPercent3",
|
||||
"ToFTempOffset1": "@ToFTempOffset1",
|
||||
"ToFTempOffset2": "@ToFTempOffset2",
|
||||
"ToFTempOffset3": "@ToFTempOffset3",
|
||||
"MeterSize": "@MeterSize",
|
||||
"Date": "@Date",
|
||||
"Successful": "@Successful"
|
||||
}
|
||||
},
|
||||
"Q3CalibrationWriter": {
|
||||
"Enabled": true,
|
||||
"DataSource": "Server=(localdb)\\MojaDB;Database=UnionTownCalibAndSkeleton;Integrated Security=True;",
|
||||
"CommandText": "INSERT INTO [dbo].[Q3Calibration] ([PcbId], [P1Offset], [P1Cal], [P2Offset], [P2Cal], [P3Offset], [P3Cal], [Date]) OUTPUT INSERTED.[Id] VALUES (@PcbId, @P1Offset, @P1Cal, @P2Offset, @P2Cal, @P3Offset, @P3Cal, @Date);",
|
||||
"UseStoredProcedure": false,
|
||||
"CommandTimeoutSeconds": 30,
|
||||
"ParameterMappings": {
|
||||
"PcbId": "@PcbId",
|
||||
"P1Offset": "@P1Offset",
|
||||
"P1Cal": "@P1Cal",
|
||||
"P2Offset": "@P2Offset",
|
||||
"P2Cal": "@P2Cal",
|
||||
"P3Offset": "@P3Offset",
|
||||
"P3Cal": "@P3Cal",
|
||||
"Date": "@Date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Root GCI configuration object loaded from gci_config.json.
|
||||
///
|
||||
/// This class represents the top-level configuration structure
|
||||
/// and serves as an entry point for all configurable GCI features.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// {
|
||||
/// "DataStorage":
|
||||
/// {
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// </summary>
|
||||
public class GciConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Data storage configuration section.
|
||||
///
|
||||
/// Contains definitions for all configured readers and writers,
|
||||
/// such as:
|
||||
///
|
||||
/// - MeterLoginPasswords
|
||||
/// - PreAdjustmentCalibrationParams
|
||||
/// - future storage providers
|
||||
/// </summary>
|
||||
public GciDataStorageConfig DataStorage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads GCI configuration from gci_config.json.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Locate configuration file
|
||||
/// - Deserialize JSON into strongly typed objects
|
||||
/// - Normalize relative file paths
|
||||
/// - Preserve database connection strings and REST URLs
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// Config/
|
||||
/// gci_config.json
|
||||
///
|
||||
/// Data/
|
||||
/// meter_passwords.csv
|
||||
///
|
||||
/// Relative paths:
|
||||
/// Data\file.csv
|
||||
///
|
||||
/// become:
|
||||
///
|
||||
/// C:\App\bin\Debug\Data\file.csv
|
||||
///
|
||||
/// Database sources remain unchanged:
|
||||
///
|
||||
/// Server=(localdb)\MojaDB;Database=...
|
||||
/// </summary>
|
||||
public static class GciConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads default GCI configuration from:
|
||||
///
|
||||
/// Config\gci_config.json
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Loaded GCI configuration.
|
||||
/// </returns>
|
||||
public static GciConfig LoadDefault()
|
||||
{
|
||||
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
string configPath = Path.Combine(
|
||||
baseDirectory,
|
||||
"Config",
|
||||
"gci_config.json");
|
||||
|
||||
return Load(configPath, baseDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads GCI configuration from specified file.
|
||||
/// </summary>
|
||||
/// <param name="configPath">
|
||||
/// Path to configuration json file.
|
||||
/// </param>
|
||||
/// <param name="baseDirectory">
|
||||
/// Base directory used for resolving relative paths.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Loaded configuration object.
|
||||
/// </returns>
|
||||
public static GciConfig Load(
|
||||
string configPath,
|
||||
string baseDirectory = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configPath))
|
||||
throw new ArgumentException(
|
||||
"Config path must not be empty.",
|
||||
nameof(configPath));
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
throw new FileNotFoundException(
|
||||
"GCI config file was not found.",
|
||||
configPath);
|
||||
|
||||
string json = File.ReadAllText(configPath);
|
||||
|
||||
GciConfig config =
|
||||
JsonConvert.DeserializeObject<GciConfig>(json);
|
||||
|
||||
if (config == null)
|
||||
throw new InvalidOperationException(
|
||||
"GCI config could not be loaded.");
|
||||
|
||||
NormalizeDataStoragePaths(
|
||||
config,
|
||||
baseDirectory ?? Path.GetDirectoryName(configPath));
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes paths for all configured data storage entries.
|
||||
///
|
||||
/// Converts relative file paths into absolute paths.
|
||||
/// Database connection strings remain untouched.
|
||||
/// </summary>
|
||||
/// <param name="config">
|
||||
/// Loaded configuration object.
|
||||
/// </param>
|
||||
/// <param name="baseDirectory">
|
||||
/// Base path used for relative resolution.
|
||||
/// </param>
|
||||
private static void NormalizeDataStoragePaths(
|
||||
GciConfig config,
|
||||
string baseDirectory)
|
||||
{
|
||||
if (config.DataStorage == null)
|
||||
return;
|
||||
|
||||
NormalizePath(
|
||||
config.DataStorage.MeterLoginPasswords,
|
||||
baseDirectory);
|
||||
|
||||
NormalizePath(
|
||||
config.DataStorage.PreAdjustmentCalibrationParams,
|
||||
baseDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts relative file paths into absolute paths.
|
||||
///
|
||||
/// Applies only to:
|
||||
/// - LocalCsv
|
||||
/// - RemoteCsv
|
||||
/// - LocalJson
|
||||
/// - RemoteJson
|
||||
///
|
||||
/// Does not modify:
|
||||
/// - Database connection strings
|
||||
/// - REST URLs
|
||||
/// </summary>
|
||||
/// <param name="storageConfig">
|
||||
/// Storage configuration.
|
||||
/// </param>
|
||||
/// <param name="baseDirectory">
|
||||
/// Base path for resolution.
|
||||
/// </param>
|
||||
private static void NormalizePath(
|
||||
DataStorageConfig storageConfig,
|
||||
string baseDirectory)
|
||||
{
|
||||
if (storageConfig == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(storageConfig.DataSource))
|
||||
return;
|
||||
|
||||
// Database connection strings and URLs
|
||||
// must never be treated as file paths.
|
||||
if (storageConfig.Type == DataStorageType.LocalDatabase ||
|
||||
storageConfig.Type == DataStorageType.RemoteDatabase ||
|
||||
storageConfig.Type == DataStorageType.RestApi)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Path.IsPathRooted(storageConfig.DataSource))
|
||||
return;
|
||||
|
||||
storageConfig.DataSource =
|
||||
Path.GetFullPath(
|
||||
Path.Combine(
|
||||
baseDirectory,
|
||||
storageConfig.DataSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains all configured GCI data storage sources.
|
||||
///
|
||||
/// Each property represents one logical use-case
|
||||
/// and points to its storage configuration.
|
||||
///
|
||||
/// Example:
|
||||
/// MeterLoginPasswords
|
||||
/// -> SQL database
|
||||
///
|
||||
/// PreAdjustmentCalibrationParams
|
||||
/// -> CSV file
|
||||
/// </summary>
|
||||
public class GciDataStorageConfig
|
||||
{
|
||||
public GciDatabaseWriterConfig PreadjustmentCompletionWriter { get; set; }
|
||||
public GciDatabaseWriterConfig Q3CalibrationWriter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for meter login password lookup.
|
||||
/// </summary>
|
||||
public DataStorageConfig MeterLoginPasswords { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for pre-adjustment calibration data lookup.
|
||||
/// </summary>
|
||||
public DataStorageConfig PreAdjustmentCalibrationParams { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Config
|
||||
{
|
||||
/// <summary>SQL Server destination for one logical result type.</summary>
|
||||
public sealed class GciDatabaseWriterConfig
|
||||
{
|
||||
// Missing sections in existing configuration files leave writes disabled.
|
||||
public bool Enabled { get; set; }
|
||||
public string DataSource { get; set; }
|
||||
public string CommandText { get; set; }
|
||||
public bool UseStoredProcedure { get; set; }
|
||||
public int CommandTimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Optional complete mapping: DTO property name -> SQL parameter name.
|
||||
/// Null/empty uses the documented default parameter names.
|
||||
/// Commands must return the persisted positive identity as their first scalar.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ParameterMappings { get; set; }
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines common contract for all GCI data storage readers.
|
||||
///
|
||||
/// Implementations can read data from different storage types:
|
||||
/// - SQL database
|
||||
/// - CSV file
|
||||
/// - JSON file
|
||||
/// - REST API
|
||||
///
|
||||
/// Consumers should use this interface instead of concrete readers.
|
||||
/// </summary>
|
||||
public interface IDataStorageReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads data from configured storage using provided query.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Query object containing lookup parameters.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Storage-specific result object.
|
||||
/// </returns>
|
||||
object GetData(Models.DataQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether configured data source is accessible.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Diagnostic result of source test.
|
||||
/// </returns>
|
||||
Models.ReaderDiagnosticResult TestSource(bool enableDiagnostics);
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether configured query is valid for the data source.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Diagnostic result of query test.
|
||||
/// </returns>
|
||||
Models.ReaderDiagnosticResult TestQuery(bool enableDiagnostics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory responsible for creating appropriate
|
||||
/// data storage reader implementations.
|
||||
///
|
||||
/// The reader type is selected according to:
|
||||
///
|
||||
/// DataStorageConfig.Type
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// LocalDatabase
|
||||
/// -> DatabaseDataStorageReader
|
||||
///
|
||||
/// LocalCsv
|
||||
/// -> CsvDataStorageReader
|
||||
///
|
||||
/// LocalJson
|
||||
/// -> JsonDataStorageReader
|
||||
///
|
||||
/// RestApi
|
||||
/// -> RestApiDataStorageReader
|
||||
///
|
||||
/// This factory hides implementation details from
|
||||
/// higher layers and keeps consumers independent
|
||||
/// of storage technology.
|
||||
///
|
||||
/// Trace:
|
||||
///
|
||||
/// MeterLoginPasswordReader
|
||||
/// -> DataStorageReaderFactory.Create()
|
||||
/// -> DatabaseDataStorageReader
|
||||
/// -> CsvDataStorageReader
|
||||
/// -> JsonDataStorageReader
|
||||
/// -> RestApiDataStorageReader
|
||||
/// </summary>
|
||||
public static class DataStorageReaderFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates appropriate reader implementation
|
||||
/// according to storage configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">
|
||||
/// Data storage configuration loaded from gci_config.json.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Configured storage reader implementation.
|
||||
/// </returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Configuration is null.
|
||||
/// </exception>
|
||||
/// <exception cref="NotSupportedException">
|
||||
/// Unsupported storage type.
|
||||
/// </exception>
|
||||
public static IDataStorageReader Create(
|
||||
DataStorageConfig config)
|
||||
{
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
|
||||
switch (config.Type)
|
||||
{
|
||||
case DataStorageType.RemoteDatabase:
|
||||
case DataStorageType.LocalDatabase:
|
||||
|
||||
return new Providers.DatabaseDataStorageReader(config);
|
||||
|
||||
case DataStorageType.RemoteCsv:
|
||||
case DataStorageType.LocalCsv:
|
||||
|
||||
return new Providers.CsvDataStorageReader(config);
|
||||
|
||||
case DataStorageType.RemoteJson:
|
||||
case DataStorageType.LocalJson:
|
||||
|
||||
return new Providers.JsonDataStorageReader(config);
|
||||
|
||||
case DataStorageType.RestApi:
|
||||
|
||||
return new Providers.RestApiDataStorageReader(config);
|
||||
|
||||
default:
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"Unsupported DataStorageType: '{config.Type}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents query input for data storage readers.
|
||||
///
|
||||
/// Query parameters are used as lookup values
|
||||
/// during storage search operations.
|
||||
///
|
||||
/// A single parameter:
|
||||
///
|
||||
/// 231630279
|
||||
///
|
||||
/// executes one lookup.
|
||||
///
|
||||
/// Multiple parameters:
|
||||
///
|
||||
/// 231630279
|
||||
/// 231630243
|
||||
/// 231630148
|
||||
///
|
||||
/// can execute batch operations.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// QueryTemplate:
|
||||
///
|
||||
/// SELECT [Password]
|
||||
/// WHERE [PcbId]=QUERYPARAM
|
||||
///
|
||||
/// Query:
|
||||
///
|
||||
/// QueryParams:
|
||||
/// 231630279
|
||||
///
|
||||
/// Result:
|
||||
///
|
||||
/// Password belonging to 231630279.
|
||||
/// </summary>
|
||||
public class DataQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection of query values.
|
||||
///
|
||||
/// Single item:
|
||||
///
|
||||
/// QueryParams[0]
|
||||
///
|
||||
/// represents one lookup.
|
||||
///
|
||||
/// Multiple items may be processed
|
||||
/// as batch requests.
|
||||
/// </summary>
|
||||
public List<string> QueryParams { get; }
|
||||
= new List<string>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for one storage source.
|
||||
/// Loaded from gci_config.json
|
||||
/// </summary>
|
||||
public class DataStorageConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Logical storage name.
|
||||
/// Example:
|
||||
/// MeterLoginPasswords
|
||||
/// CalibrationParameters
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Storage implementation type.
|
||||
/// </summary>
|
||||
public DataStorageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection string,
|
||||
/// file path,
|
||||
/// URL etc.
|
||||
/// </summary>
|
||||
public string DataSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Query template.
|
||||
/// Example:
|
||||
///
|
||||
/// SQL:
|
||||
/// SELECT Password
|
||||
/// FROM Passwords
|
||||
/// WHERE PcbId=QUERYPARAM
|
||||
///
|
||||
/// CSV:
|
||||
/// SELECT [Password]
|
||||
/// WHERE [PcbId]=QUERYPARAM
|
||||
/// </summary>
|
||||
public string QueryTemplate { get; set; }
|
||||
|
||||
public DataStorageConfig(
|
||||
string name,
|
||||
DataStorageType type,
|
||||
string dataSource,
|
||||
string queryTemplate)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
DataSource = dataSource;
|
||||
QueryTemplate = queryTemplate;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies supported data storage implementations.
|
||||
///
|
||||
/// The type determines which reader implementation
|
||||
/// will be created by DataStorageReaderFactory.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// LocalDatabase
|
||||
/// -> DatabaseDataStorageReader
|
||||
///
|
||||
/// LocalCsv
|
||||
/// -> CsvDataStorageReader
|
||||
///
|
||||
/// RestApi
|
||||
/// -> RestApiDataStorageReader
|
||||
///
|
||||
/// Source:
|
||||
///
|
||||
/// gci_config.json
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// "Type":"LocalDatabase"
|
||||
/// </summary>
|
||||
public enum DataStorageType
|
||||
{
|
||||
/// <summary>
|
||||
/// REST API endpoint.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// https://api.company.com/passwords
|
||||
/// </summary>
|
||||
RestApi,
|
||||
|
||||
/// <summary>
|
||||
/// Database located on remote server.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// SQL server on network machine.
|
||||
/// </summary>
|
||||
RemoteDatabase,
|
||||
|
||||
/// <summary>
|
||||
/// Database located on local machine.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// SQL LocalDB
|
||||
/// SQLite
|
||||
/// local SQL Server instance
|
||||
/// </summary>
|
||||
LocalDatabase,
|
||||
|
||||
/// <summary>
|
||||
/// JSON source stored remotely.
|
||||
/// </summary>
|
||||
RemoteJson,
|
||||
|
||||
/// <summary>
|
||||
/// JSON file stored locally.
|
||||
/// </summary>
|
||||
LocalJson,
|
||||
|
||||
/// <summary>
|
||||
/// CSV source stored remotely.
|
||||
/// </summary>
|
||||
RemoteCsv,
|
||||
|
||||
/// <summary>
|
||||
/// CSV file stored locally.
|
||||
/// </summary>
|
||||
LocalCsv
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents result returned by database-based
|
||||
/// data storage readers.
|
||||
///
|
||||
/// Supports both:
|
||||
///
|
||||
/// - Single-row lookups
|
||||
/// (e.g. MeterLoginPasswords)
|
||||
///
|
||||
/// - Multi-row queries
|
||||
/// (e.g. PreAdjustmentCalibrationParams)
|
||||
/// </summary>
|
||||
internal class DatabaseSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether at least one record
|
||||
/// was found.
|
||||
/// </summary>
|
||||
public bool Found { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Executed SQL query text.
|
||||
/// Mainly intended for diagnostics
|
||||
/// and troubleshooting.
|
||||
/// </summary>
|
||||
public string Query { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// First returned row represented as
|
||||
/// column/value pairs.
|
||||
///
|
||||
/// Preserved for backward compatibility
|
||||
/// with existing readers expecting
|
||||
/// a single database record.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> Values { get; }
|
||||
= new Dictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// All returned rows represented as
|
||||
/// a collection of column/value dictionaries.
|
||||
///
|
||||
/// Intended for queries returning
|
||||
/// multiple records.
|
||||
/// </summary>
|
||||
public List<Dictionary<string, object>> Rows { get; }
|
||||
= new List<Dictionary<string, object>>();
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents diagnostic result of data storage operations.
|
||||
///
|
||||
/// Used for:
|
||||
/// - source validation
|
||||
/// - query validation
|
||||
/// - connection tests
|
||||
/// - detailed diagnostic logging
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// TestSource()
|
||||
/// -> connection successful
|
||||
///
|
||||
/// TestQuery()
|
||||
/// -> column validation
|
||||
///
|
||||
/// Can also carry additional payload data.
|
||||
/// </summary>
|
||||
public class ReaderDiagnosticResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether operation completed successfully.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human readable summary message.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// "Connection successful."
|
||||
///
|
||||
/// or:
|
||||
///
|
||||
/// "Column not found."
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Detailed diagnostic output lines.
|
||||
///
|
||||
/// Used for debugging and troubleshooting.
|
||||
/// </summary>
|
||||
public List<string> Diagnostics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional operation payload.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// loaded CSV lines
|
||||
/// SQL test value
|
||||
/// parsed content
|
||||
/// </summary>
|
||||
public object Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates empty diagnostic result.
|
||||
/// </summary>
|
||||
public ReaderDiagnosticResult()
|
||||
{
|
||||
Diagnostics = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates successful diagnostic result.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// Success message.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Successful result object.
|
||||
/// </returns>
|
||||
public static ReaderDiagnosticResult SuccessResult(
|
||||
string message = "OK")
|
||||
{
|
||||
return new ReaderDiagnosticResult
|
||||
{
|
||||
Success = true,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates failed diagnostic result.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// Failure message.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Failed result object.
|
||||
/// </returns>
|
||||
public static ReaderDiagnosticResult Failure(
|
||||
string message)
|
||||
{
|
||||
return new ReaderDiagnosticResult
|
||||
{
|
||||
Success = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts diagnostic information into display text.
|
||||
///
|
||||
/// Output contains:
|
||||
///
|
||||
/// diagnostic lines
|
||||
/// +
|
||||
/// summary message
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Formatted text representation.
|
||||
/// </returns>
|
||||
public string ToDisplayDiag()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if (Diagnostics != null &&
|
||||
Diagnostics.Count > 0)
|
||||
{
|
||||
foreach (string line in Diagnostics)
|
||||
sb.AppendLine(line);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Message))
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Message))
|
||||
sb.AppendLine(Message);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads and searches CSV based data sources.
|
||||
///
|
||||
/// Supported query syntax:
|
||||
/// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM
|
||||
///
|
||||
/// Example:
|
||||
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
|
||||
///
|
||||
/// Supports:
|
||||
/// - column names: [ColumnName]
|
||||
/// - column indexes: COLUMN(n)
|
||||
///
|
||||
/// Trace:
|
||||
/// MeterLoginPasswordReader
|
||||
/// -> CsvDataStorageReader.GetData()
|
||||
/// -> ConnectToSource()
|
||||
/// -> ExecuteQuery()
|
||||
/// </summary>
|
||||
public class CsvDataStorageReader : IDataStorageReader
|
||||
{
|
||||
private readonly DataStorageConfig config;
|
||||
|
||||
private string resolvedPath;
|
||||
private string[] loadedLines;
|
||||
private string[] loadedHeaders;
|
||||
|
||||
/// <summary>
|
||||
/// Creates CSV data storage reader using provided configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">
|
||||
/// Data storage configuration loaded from gci_config.json.
|
||||
/// </param>
|
||||
public CsvDataStorageReader(DataStorageConfig config)
|
||||
{
|
||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads values from CSV using configured QueryTemplate.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Query parameters used for lookup.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// ReaderDiagnosticResult containing lookup result.
|
||||
/// </returns>
|
||||
public object GetData(DataQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
ReaderDiagnosticResult sourceResult = ConnectToSource(true);
|
||||
if (!sourceResult.Success)
|
||||
throw new InvalidOperationException(sourceResult.Message);
|
||||
|
||||
ReaderDiagnosticResult queryResult = ExecuteQuery(query, true);
|
||||
if (!queryResult.Success)
|
||||
throw new InvalidOperationException(queryResult.Message);
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates CSV source accessibility and content.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Diagnostic result.
|
||||
/// </returns>
|
||||
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
|
||||
{
|
||||
return ConnectToSource(enableDiagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates configured QueryTemplate against CSV header.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Diagnostic result.
|
||||
/// </returns>
|
||||
public ReaderDiagnosticResult TestQuery(bool enableDiagnostics)
|
||||
{
|
||||
ReaderDiagnosticResult sourceResult = ConnectToSource(enableDiagnostics);
|
||||
if (!sourceResult.Success)
|
||||
return sourceResult;
|
||||
|
||||
ReaderDiagnosticResult result = new ReaderDiagnosticResult();
|
||||
|
||||
try
|
||||
{
|
||||
Log(result, enableDiagnostics, "Starting QueryTemplate validation.");
|
||||
|
||||
SearchOrderDefinition definition =
|
||||
SearchOrderParser.Parse(config.QueryTemplate);
|
||||
|
||||
int selectIndex = ResolveColumnIndex(
|
||||
loadedHeaders,
|
||||
definition.SelectColumn);
|
||||
|
||||
int whereIndex = ResolveColumnIndex(
|
||||
loadedHeaders,
|
||||
definition.WhereColumn);
|
||||
|
||||
Log(result, enableDiagnostics, "QueryTemplate parsed successfully.");
|
||||
Log(result, enableDiagnostics, "Select column index: " + selectIndex);
|
||||
Log(result, enableDiagnostics, "Where column index: " + whereIndex);
|
||||
|
||||
result.Success = true;
|
||||
result.Message = "QueryTemplate validation finished successfully.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.Message = ex.Message;
|
||||
Log(result, enableDiagnostics, "ERROR: " + ex.Message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens CSV file and loads its content.
|
||||
/// Validates file existence and header.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Source validation result.
|
||||
/// </returns>
|
||||
private ReaderDiagnosticResult ConnectToSource(bool enableDiagnostics)
|
||||
{
|
||||
ReaderDiagnosticResult result = new ReaderDiagnosticResult();
|
||||
|
||||
try
|
||||
{
|
||||
Log(result, enableDiagnostics, "Starting CSV source connection test.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(config.DataSource))
|
||||
throw new InvalidOperationException("CSV data source is empty.");
|
||||
|
||||
resolvedPath = Path.GetFullPath(config.DataSource);
|
||||
|
||||
Log(result, enableDiagnostics, "Resolved full path: " + resolvedPath);
|
||||
|
||||
if (!File.Exists(resolvedPath))
|
||||
throw new FileNotFoundException("CSV file was not found.", resolvedPath);
|
||||
|
||||
loadedLines = File.ReadAllLines(resolvedPath);
|
||||
|
||||
if (loadedLines == null || loadedLines.Length == 0)
|
||||
throw new InvalidOperationException("CSV file is empty.");
|
||||
|
||||
loadedHeaders = SplitCsvLine(loadedLines[0]);
|
||||
|
||||
if (loadedHeaders == null || loadedHeaders.Length == 0)
|
||||
throw new InvalidOperationException("CSV header is empty.");
|
||||
|
||||
Log(result, enableDiagnostics, "CSV line count: " + loadedLines.Length);
|
||||
Log(result, enableDiagnostics, "CSV header column count: " + loadedHeaders.Length);
|
||||
|
||||
result.Success = true;
|
||||
result.Message = "CSV source connection finished successfully.";
|
||||
result.Data = loadedLines;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.Message = ex.Message;
|
||||
result.Data = null;
|
||||
|
||||
Log(result, enableDiagnostics, "ERROR: " + ex.Message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Executes configured QueryTemplate against loaded CSV data.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Query values used for matching.
|
||||
/// </param>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Query result.
|
||||
/// </returns>
|
||||
private ReaderDiagnosticResult ExecuteQuery(
|
||||
DataQuery query,
|
||||
bool enableDiagnostics)
|
||||
{
|
||||
ReaderDiagnosticResult result = new ReaderDiagnosticResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (query.QueryParams == null || query.QueryParams.Count == 0)
|
||||
throw new InvalidOperationException("DataQuery.QueryParams is empty.");
|
||||
|
||||
SearchOrderDefinition definition =
|
||||
SearchOrderParser.Parse(config.QueryTemplate);
|
||||
|
||||
int selectIndex = ResolveColumnIndex(
|
||||
loadedHeaders,
|
||||
definition.SelectColumn);
|
||||
|
||||
int whereIndex = ResolveColumnIndex(
|
||||
loadedHeaders,
|
||||
definition.WhereColumn);
|
||||
|
||||
List<string> matchedValues = new List<string>();
|
||||
|
||||
foreach (string param in query.QueryParams)
|
||||
{
|
||||
string queryValue = (param ?? string.Empty).Trim();
|
||||
bool found = false;
|
||||
|
||||
for (int lineIndex = 1; lineIndex < loadedLines.Length; lineIndex++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(loadedLines[lineIndex]))
|
||||
continue;
|
||||
|
||||
string[] values = SplitCsvLine(loadedLines[lineIndex]);
|
||||
|
||||
if (whereIndex >= values.Length)
|
||||
continue;
|
||||
|
||||
string currentValue = (values[whereIndex] ?? string.Empty).Trim();
|
||||
|
||||
if (!string.Equals(
|
||||
currentValue,
|
||||
queryValue,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string returnValue =
|
||||
selectIndex < values.Length
|
||||
? values[selectIndex]
|
||||
: string.Empty;
|
||||
|
||||
matchedValues.Add(returnValue);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
Log(result, enableDiagnostics, "No match found for: " + queryValue);
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
|
||||
if (query.QueryParams.Count == 1)
|
||||
{
|
||||
result.Data = matchedValues.Count > 0 ? matchedValues[0] : null;
|
||||
result.Message = matchedValues.Count > 0
|
||||
? "Value found."
|
||||
: "No match found.";
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Data = matchedValues;
|
||||
result.Message =
|
||||
"Batch query finished. Matches found: " +
|
||||
matchedValues.Count +
|
||||
" of " +
|
||||
query.QueryParams.Count +
|
||||
".";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.Message = ex.Message;
|
||||
result.Data = null;
|
||||
|
||||
Log(result, enableDiagnostics, "ERROR: " + ex.Message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves column index either by name or by explicit index.
|
||||
/// </summary>
|
||||
/// <param name="headers">
|
||||
/// CSV header columns.
|
||||
/// </param>
|
||||
/// <param name="columnReference">
|
||||
/// Parsed QueryTemplate column definition.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Zero-based column index.
|
||||
/// </returns>
|
||||
private int ResolveColumnIndex(
|
||||
string[] headers,
|
||||
ColumnReference columnReference)
|
||||
{
|
||||
if (columnReference == null)
|
||||
throw new InvalidOperationException("Column reference is null.");
|
||||
|
||||
if (columnReference.HasIndex)
|
||||
{
|
||||
int index = columnReference.Index.Value;
|
||||
|
||||
if (index < 0 || index >= headers.Length)
|
||||
throw new InvalidOperationException("Column index is out of range.");
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
if (columnReference.HasName)
|
||||
{
|
||||
for (int i = 0; i < headers.Length; i++)
|
||||
{
|
||||
if (string.Equals(
|
||||
headers[i]?.Trim(),
|
||||
columnReference.Name?.Trim(),
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Column '" + columnReference.Name + "' was not found in CSV header.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Column reference is not defined.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits CSV line using common separators.
|
||||
/// Supports:
|
||||
/// ';'
|
||||
/// ','
|
||||
/// '\t'
|
||||
/// </summary>
|
||||
/// <param name="line">
|
||||
/// Input CSV line.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Parsed columns.
|
||||
/// </returns>
|
||||
private string[] SplitCsvLine(string line)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line))
|
||||
return new string[0];
|
||||
|
||||
if (line.Contains(";"))
|
||||
return line.Split(';');
|
||||
|
||||
if (line.Contains(","))
|
||||
return line.Split(',');
|
||||
|
||||
if (line.Contains("\t"))
|
||||
return line.Split('\t');
|
||||
|
||||
return new[] { line };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds diagnostic line when diagnostics are enabled.
|
||||
/// </summary>
|
||||
private void Log(
|
||||
ReaderDiagnosticResult result,
|
||||
bool enableDiagnostics,
|
||||
string message)
|
||||
{
|
||||
if (!enableDiagnostics || result == null)
|
||||
return;
|
||||
|
||||
result.Diagnostics.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads data from SQL Server based data storage.
|
||||
///
|
||||
/// The reader uses configuration loaded from gci_config.json.
|
||||
///
|
||||
/// QueryTemplate must contain QUERYPARAM placeholder.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// SELECT [Password]
|
||||
/// FROM [dbo].[SkeletonKeys]
|
||||
/// WHERE [PcbId] = QUERYPARAM
|
||||
///
|
||||
/// or
|
||||
///
|
||||
/// SELECT [ParameterName], [ParameterValue]
|
||||
/// FROM [dbo].[PreAdjustmentCalibrationParams]
|
||||
/// WHERE [Dn_InternalId] = QUERYPARAM
|
||||
///
|
||||
/// The placeholder is internally converted
|
||||
/// to SQL parameter @value.
|
||||
///
|
||||
/// Supports both:
|
||||
///
|
||||
/// - Single-row lookups
|
||||
/// - Multi-row result sets
|
||||
/// </summary>
|
||||
public class DatabaseDataStorageReader : IDataStorageReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Data storage configuration containing
|
||||
/// connection string and query template.
|
||||
/// </summary>
|
||||
private readonly DataStorageConfig config;
|
||||
|
||||
/// <summary>
|
||||
/// Creates SQL Server data storage reader
|
||||
/// using provided configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">
|
||||
/// Data storage configuration loaded
|
||||
/// from gci_config.json.
|
||||
/// </param>
|
||||
public DatabaseDataStorageReader(
|
||||
DataStorageConfig config)
|
||||
{
|
||||
this.config =
|
||||
config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes configured SQL query and returns
|
||||
/// matching database records.
|
||||
///
|
||||
/// Single-row queries populate:
|
||||
/// DatabaseSearchResult.Values
|
||||
///
|
||||
/// Multi-row queries populate:
|
||||
/// DatabaseSearchResult.Rows
|
||||
///
|
||||
/// For backward compatibility, the first row
|
||||
/// is also stored in Values.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Query object containing lookup parameter.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// DatabaseSearchResult containing returned
|
||||
/// database records.
|
||||
///
|
||||
/// Values contains the first returned row.
|
||||
///
|
||||
/// Rows contains the complete result set.
|
||||
/// </returns>
|
||||
public object GetData(DataQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
ReaderDiagnosticResult sourceResult =
|
||||
TestSource(true);
|
||||
|
||||
if (!sourceResult.Success)
|
||||
throw new InvalidOperationException(sourceResult.Message);
|
||||
|
||||
string sqlText =
|
||||
PrepareSqlText(config.QueryTemplate);
|
||||
|
||||
object queryValue =
|
||||
ExtractQueryValue(query);
|
||||
|
||||
using (SqlConnection connection =
|
||||
new SqlConnection(config.DataSource))
|
||||
|
||||
using (SqlCommand command =
|
||||
new SqlCommand(sqlText, connection))
|
||||
{
|
||||
AddQueryParameter(command, queryValue);
|
||||
|
||||
connection.Open();
|
||||
|
||||
// Full result set is required because some
|
||||
// storage definitions return multiple records
|
||||
// (e.g. PreAdjustmentCalibrationParams).
|
||||
using (SqlDataReader reader =
|
||||
command.ExecuteReader())
|
||||
{
|
||||
DatabaseSearchResult result =
|
||||
new DatabaseSearchResult
|
||||
{
|
||||
Query = sqlText
|
||||
};
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
result.Found = true;
|
||||
|
||||
// Represents one database row.
|
||||
Dictionary<string, object> row =
|
||||
new Dictionary<string, object>();
|
||||
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
object value =
|
||||
reader.GetValue(i);
|
||||
|
||||
object normalizedValue =
|
||||
value == DBNull.Value
|
||||
? null
|
||||
: value;
|
||||
|
||||
row[reader.GetName(i)] =
|
||||
normalizedValue;
|
||||
}
|
||||
|
||||
// Preserve first row for legacy consumers
|
||||
// expecting a single returned database record
|
||||
// (e.g. MeterLoginPasswordReader).
|
||||
if (result.Rows.Count == 0)
|
||||
{
|
||||
foreach (var item in row)
|
||||
{
|
||||
result.Values[item.Key] =
|
||||
item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Store complete database result set.
|
||||
result.Rows.Add(row);
|
||||
}
|
||||
|
||||
if (result.Rows.Count == 0)
|
||||
{
|
||||
result.Found = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether SQL Server connection
|
||||
/// can be opened.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Diagnostic result of SQL connection test.
|
||||
/// </returns>
|
||||
public ReaderDiagnosticResult TestSource(
|
||||
bool enableDiagnostics)
|
||||
{
|
||||
ReaderDiagnosticResult result =
|
||||
new ReaderDiagnosticResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.DataSource))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Data source is empty.");
|
||||
}
|
||||
|
||||
Log(
|
||||
result,
|
||||
enableDiagnostics,
|
||||
"Opening SQL connection.");
|
||||
|
||||
using (SqlConnection connection =
|
||||
new SqlConnection(config.DataSource))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
Log(
|
||||
result,
|
||||
enableDiagnostics,
|
||||
"Connection opened successfully.");
|
||||
|
||||
using (SqlCommand command =
|
||||
new SqlCommand("SELECT 1", connection))
|
||||
{
|
||||
object value =
|
||||
command.ExecuteScalar();
|
||||
|
||||
Log(
|
||||
result,
|
||||
enableDiagnostics,
|
||||
"Test query result: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
result.Message =
|
||||
"Connection to SQL Server OK.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.Message =
|
||||
"Failed to connect to SQL Server. " + ex.Message;
|
||||
|
||||
Log(
|
||||
result,
|
||||
enableDiagnostics,
|
||||
ex.ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether configured SQL query
|
||||
/// can be prepared and executed.
|
||||
/// </summary>
|
||||
/// <param name="enableDiagnostics">
|
||||
/// Enables detailed diagnostic output.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Diagnostic result of query execution test.
|
||||
/// </returns>
|
||||
public ReaderDiagnosticResult TestQuery(
|
||||
bool enableDiagnostics)
|
||||
{
|
||||
ReaderDiagnosticResult result =
|
||||
new ReaderDiagnosticResult();
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.QueryTemplate))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Query template is empty.");
|
||||
}
|
||||
|
||||
string sqlText =
|
||||
PrepareSqlText(config.QueryTemplate);
|
||||
|
||||
Log(result, enableDiagnostics, "Original template:");
|
||||
Log(result, enableDiagnostics, config.QueryTemplate);
|
||||
|
||||
Log(result, enableDiagnostics, "Prepared SQL:");
|
||||
Log(result, enableDiagnostics, sqlText);
|
||||
|
||||
using (SqlConnection connection =
|
||||
new SqlConnection(config.DataSource))
|
||||
|
||||
using (SqlCommand command =
|
||||
new SqlCommand(sqlText, connection))
|
||||
{
|
||||
AddQueryParameter(command, "TEST");
|
||||
|
||||
connection.Open();
|
||||
|
||||
object value =
|
||||
command.ExecuteScalar();
|
||||
|
||||
result.Data = value;
|
||||
|
||||
Log(
|
||||
result,
|
||||
enableDiagnostics,
|
||||
"Query executed successfully.");
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
result.Message =
|
||||
"Query executed successfully.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.Message =
|
||||
"Query execution failed. " + ex.Message;
|
||||
|
||||
Log(
|
||||
result,
|
||||
enableDiagnostics,
|
||||
ex.ToString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts configured QueryTemplate
|
||||
/// to executable SQL text.
|
||||
/// </summary>
|
||||
/// <param name="queryTemplate">
|
||||
/// SQL query template containing
|
||||
/// QUERYPARAM placeholder.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// SQL text with QUERYPARAM replaced
|
||||
/// by @value parameter.
|
||||
/// </returns>
|
||||
private static string PrepareSqlText(
|
||||
string queryTemplate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(queryTemplate))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Query template must not be empty.",
|
||||
nameof(queryTemplate));
|
||||
}
|
||||
|
||||
if (!queryTemplate.Contains("QUERYPARAM"))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Query template must contain QUERYPARAM placeholder.");
|
||||
}
|
||||
|
||||
return queryTemplate.Replace(
|
||||
"QUERYPARAM",
|
||||
"@value");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts first query parameter value.
|
||||
/// </summary>
|
||||
private static object ExtractQueryValue(
|
||||
DataQuery query)
|
||||
{
|
||||
if (query.QueryParams == null ||
|
||||
query.QueryParams.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"DataQuery does not contain any query parameter.");
|
||||
}
|
||||
|
||||
return query.QueryParams[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds lookup parameter to SQL command.
|
||||
/// </summary>
|
||||
private static void AddQueryParameter(
|
||||
SqlCommand command,
|
||||
object value)
|
||||
{
|
||||
command.Parameters.Clear();
|
||||
|
||||
SqlParameter parameter =
|
||||
command.Parameters.Add(
|
||||
"@value",
|
||||
SqlDbType.Variant);
|
||||
|
||||
parameter.Value =
|
||||
value ?? DBNull.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds diagnostic line when diagnostics
|
||||
/// are enabled.
|
||||
/// </summary>
|
||||
private static void Log(
|
||||
ReaderDiagnosticResult result,
|
||||
bool enableDiagnostics,
|
||||
string message)
|
||||
{
|
||||
if (!enableDiagnostics || result == null)
|
||||
return;
|
||||
|
||||
result.Diagnostics.Add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
|
||||
{
|
||||
public class JsonDataStorageReader : IDataStorageReader
|
||||
{
|
||||
private readonly DataStorageConfig config;
|
||||
|
||||
public JsonDataStorageReader(DataStorageConfig config)
|
||||
{
|
||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
public object GetData(DataQuery query)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ReaderDiagnosticResult TestQuery(bool enableDiagnostics)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Providers
|
||||
{
|
||||
public class RestApiDataStorageReader : IDataStorageReader
|
||||
{
|
||||
private readonly DataStorageConfig config;
|
||||
|
||||
public RestApiDataStorageReader(DataStorageConfig config)
|
||||
{
|
||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
public object GetData(DataQuery query)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ReaderDiagnosticResult TestSource(bool enableDiagnostics)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ReaderDiagnosticResult TestQuery(bool enableDiagnostics)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching
|
||||
{
|
||||
/// <summary>
|
||||
/// Parsed representation of QueryTemplate expression.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
|
||||
///
|
||||
/// becomes:
|
||||
///
|
||||
/// SelectColumn:
|
||||
/// [Password]
|
||||
///
|
||||
/// WhereColumn:
|
||||
/// [PcbId]
|
||||
/// </summary>
|
||||
public class SearchOrderDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Column to be returned from lookup result.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// SELECT [Password]
|
||||
///
|
||||
/// returns:
|
||||
///
|
||||
/// [Password]
|
||||
/// </summary>
|
||||
public ColumnReference SelectColumn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Column used for row matching.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// WHERE [PcbId] = QUERYPARAM
|
||||
///
|
||||
/// uses:
|
||||
///
|
||||
/// [PcbId]
|
||||
/// </summary>
|
||||
public ColumnReference WhereColumn { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents one column definition inside QueryTemplate.
|
||||
///
|
||||
/// Supports two forms:
|
||||
///
|
||||
/// [ColumnName]
|
||||
///
|
||||
/// or:
|
||||
///
|
||||
/// COLUMN(number)
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// [Password]
|
||||
///
|
||||
/// COLUMN(2)
|
||||
/// </summary>
|
||||
public class ColumnReference
|
||||
{
|
||||
/// <summary>
|
||||
/// Column name used in named lookup mode.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// [Password]
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based column index used in indexed lookup mode.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// COLUMN(2)
|
||||
/// </summary>
|
||||
public int? Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether column uses name-based lookup.
|
||||
/// </summary>
|
||||
public bool HasName
|
||||
{
|
||||
get
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether column uses index-based lookup.
|
||||
/// </summary>
|
||||
public bool HasIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
return Index.HasValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns human readable representation.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Column expression text.
|
||||
/// </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
if (HasName)
|
||||
return "[" + Name + "]";
|
||||
|
||||
if (HasIndex)
|
||||
return "COLUMN(" + Index.Value + ")";
|
||||
|
||||
return "<undefined column reference>";
|
||||
}
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Common.Searching
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses SQL-like QueryTemplate expressions used by GCI data storage readers.
|
||||
///
|
||||
/// Supported syntax:
|
||||
///
|
||||
/// SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM
|
||||
///
|
||||
/// Examples:
|
||||
///
|
||||
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
|
||||
///
|
||||
/// SELECT COLUMN(1) WHERE COLUMN(0) = QUERYPARAM
|
||||
///
|
||||
/// Mixed forms are also supported:
|
||||
///
|
||||
/// SELECT [Password] WHERE COLUMN(0)=QUERYPARAM
|
||||
///
|
||||
/// The parser converts text expressions into structured
|
||||
/// SearchOrderDefinition objects which are later consumed
|
||||
/// by CSV and other readers.
|
||||
/// </summary>
|
||||
public static class SearchOrderParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Full QueryTemplate validation pattern.
|
||||
///
|
||||
/// Expected syntax:
|
||||
///
|
||||
/// SELECT [Column] WHERE [Column] = QUERYPARAM
|
||||
/// </summary>
|
||||
private static readonly Regex FullPattern = new Regex(
|
||||
@"^\s*SELECT\s+(?<select>\[[^\]]+\]|COLUMN\(\d+\))\s+WHERE\s+(?<where>\[[^\]]+\]|COLUMN\(\d+\))\s*=\s*QUERYPARAM\s*$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Matches:
|
||||
///
|
||||
/// [ColumnName]
|
||||
/// </summary>
|
||||
private static readonly Regex NamedColumnPattern = new Regex(
|
||||
@"^\[(?<name>[^\]]+)\]$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Matches:
|
||||
///
|
||||
/// COLUMN(number)
|
||||
/// </summary>
|
||||
private static readonly Regex IndexedColumnPattern = new Regex(
|
||||
@"^COLUMN\((?<index>\d+)\)$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// Parses QueryTemplate text into structured definition.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// SELECT [Password] WHERE [PcbId] = QUERYPARAM
|
||||
///
|
||||
/// Result:
|
||||
///
|
||||
/// SelectColumn:
|
||||
/// Password
|
||||
///
|
||||
/// WhereColumn:
|
||||
/// PcbId
|
||||
/// </summary>
|
||||
/// <param name="searchOrder">
|
||||
/// QueryTemplate expression.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Parsed query definition.
|
||||
/// </returns>
|
||||
public static SearchOrderDefinition Parse(string searchOrder)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchOrder))
|
||||
throw new InvalidOperationException(
|
||||
"QueryTemplate is empty.");
|
||||
|
||||
Match match = FullPattern.Match(searchOrder);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Invalid QueryTemplate syntax. Expected: SELECT [ReturnColumn] WHERE [MatchColumn] = QUERYPARAM");
|
||||
}
|
||||
|
||||
return new SearchOrderDefinition
|
||||
{
|
||||
SelectColumn =
|
||||
ParseColumnReference(
|
||||
match.Groups["select"].Value),
|
||||
|
||||
WhereColumn =
|
||||
ParseColumnReference(
|
||||
match.Groups["where"].Value)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses single column definition.
|
||||
///
|
||||
/// Supported forms:
|
||||
///
|
||||
/// [ColumnName]
|
||||
///
|
||||
/// COLUMN(number)
|
||||
/// </summary>
|
||||
/// <param name="token">
|
||||
/// Raw token extracted from QueryTemplate.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Parsed column reference.
|
||||
/// </returns>
|
||||
private static ColumnReference ParseColumnReference(
|
||||
string token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
throw new InvalidOperationException(
|
||||
"Column reference token is empty.");
|
||||
|
||||
Match nameMatch =
|
||||
NamedColumnPattern.Match(token);
|
||||
|
||||
if (nameMatch.Success)
|
||||
{
|
||||
return new ColumnReference
|
||||
{
|
||||
Name =
|
||||
nameMatch
|
||||
.Groups["name"]
|
||||
.Value
|
||||
.Trim(),
|
||||
|
||||
Index = null
|
||||
};
|
||||
}
|
||||
|
||||
Match indexMatch =
|
||||
IndexedColumnPattern.Match(token);
|
||||
|
||||
if (indexMatch.Success)
|
||||
{
|
||||
return new ColumnReference
|
||||
{
|
||||
Name = null,
|
||||
|
||||
Index =
|
||||
int.Parse(
|
||||
indexMatch
|
||||
.Groups["index"]
|
||||
.Value)
|
||||
};
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Invalid column reference '" +
|
||||
token +
|
||||
"'. Use [ColumnName] or COLUMN(number).");
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines contract for reading meter login passwords
|
||||
/// from configured GCI data storage.
|
||||
///
|
||||
/// Implementations hide storage details and provide
|
||||
/// a unified API for password lookup.
|
||||
///
|
||||
/// Supported storage types:
|
||||
/// - SQL database
|
||||
/// - CSV
|
||||
/// - JSON
|
||||
/// - REST API
|
||||
/// </summary>
|
||||
public interface IMeterLoginPasswordReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads meter login password synchronously.
|
||||
/// </summary>
|
||||
/// <param name="queryParam">
|
||||
/// PCB ID or another configured lookup value.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Password if found; otherwise null.
|
||||
/// </returns>
|
||||
string GetPassword(DataQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Reads meter login password asynchronously.
|
||||
/// Thread-safe implementation may serialize
|
||||
/// access to the underlying storage.
|
||||
/// </summary>
|
||||
/// <param name="queryParam">
|
||||
/// PCB ID or another configured lookup value.
|
||||
/// </param>
|
||||
/// <param name="token">
|
||||
/// Cancellation token.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Password if found; otherwise null.
|
||||
/// </returns>
|
||||
Task<string> ReadMeterLoginPasswordAsync(
|
||||
DataQuery query,
|
||||
CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads meter login password from configured GCI data storage.
|
||||
/// </summary>
|
||||
public class MeterLoginPasswordReader : IMeterLoginPasswordReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Universal data storage reader selected by configuration.
|
||||
/// Can represent database, CSV, JSON or REST reader.
|
||||
/// </summary>
|
||||
private readonly IDataStorageReader reader;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that only one password lookup is executed at a time.
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim readLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Creates password reader using provided data storage configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">Data storage configuration loaded from gci_config.json.</param>
|
||||
public MeterLoginPasswordReader(DataStorageConfig config)
|
||||
{
|
||||
reader = DataStorageReaderFactory.Create(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reads meter login password using
|
||||
/// the provided data query.
|
||||
///
|
||||
/// Thread-safe implementation may serialize
|
||||
/// access to the underlying storage.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing lookup parameter values.
|
||||
/// </param>
|
||||
/// <param name="token">
|
||||
/// Cancellation token.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Password if found; otherwise null.
|
||||
/// </returns>
|
||||
public async Task<string> ReadMeterLoginPasswordAsync(
|
||||
DataQuery query,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
if (query.QueryParams.Count == 0)
|
||||
throw new ArgumentException(
|
||||
"Query does not contain any parameter.",
|
||||
nameof(query));
|
||||
|
||||
await readLock.WaitAsync(token);
|
||||
|
||||
try
|
||||
{
|
||||
return await Task.Run(
|
||||
() => GetPassword(query),
|
||||
token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
readLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads meter login password using
|
||||
/// the provided data query.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing lookup parameter values.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Password if found; otherwise null.
|
||||
/// </returns>
|
||||
public string GetPassword(
|
||||
DataQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
if (query.QueryParams.Count == 0)
|
||||
throw new ArgumentException(
|
||||
"Query does not contain any parameter.",
|
||||
nameof(query));
|
||||
|
||||
object result =
|
||||
reader.GetData(query);
|
||||
|
||||
if (result is ReaderDiagnosticResult diagnosticResult)
|
||||
{
|
||||
return diagnosticResult.Data?.ToString();
|
||||
}
|
||||
|
||||
if (result is DatabaseSearchResult dbResult)
|
||||
{
|
||||
if (!dbResult.Found ||
|
||||
dbResult.Values == null ||
|
||||
dbResult.Values.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (object value in dbResult.Values.Values)
|
||||
{
|
||||
return value?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return result?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to pre-adjustment calibration parameters
|
||||
/// stored in the configured GCI data source.
|
||||
///
|
||||
/// Expected structure:
|
||||
///
|
||||
/// MeterSize | ParameterName | ParameterValue
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// 2 | GENESISFLOW_MinValidToF | 21990232
|
||||
/// 2 | GENESISFLOW_Timeout | 1
|
||||
///
|
||||
/// Parameters are grouped by MeterSize and returned
|
||||
/// as key/value pairs where:
|
||||
///
|
||||
/// Key = ParameterName
|
||||
/// Value = ParameterValue
|
||||
/// </summary>
|
||||
public interface IPreAdjustmentCalibrationParamsReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads all pre-adjustment calibration parameters
|
||||
/// using the provided data query synchronously.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing meter size as lookup parameter.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Dictionary where key is GENESISFLOW parameter name
|
||||
/// and value is stored parameter value.
|
||||
/// </returns>
|
||||
Dictionary<string, UInt32> GetCalibrationParams(
|
||||
DataQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Reads all pre-adjustment calibration parameters
|
||||
/// using the provided data query asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing meter size as lookup parameter.
|
||||
/// </param>
|
||||
/// <param name="token">
|
||||
/// Cancellation token.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Dictionary where key is GENESISFLOW parameter name
|
||||
/// and value is stored parameter value.
|
||||
/// </returns>
|
||||
Task<Dictionary<string, UInt32>> ReadCalibrationParamsAsync(
|
||||
DataQuery query,
|
||||
CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Contracts;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads pre-adjustment calibration parameters
|
||||
/// from configured GCI data storage.
|
||||
///
|
||||
/// The underlying storage type is selected
|
||||
/// through IDataStorageReader and may represent:
|
||||
///
|
||||
/// - SQL database
|
||||
/// - CSV
|
||||
/// - JSON
|
||||
/// - REST API
|
||||
///
|
||||
/// Expected storage structure:
|
||||
///
|
||||
/// MeterSize | ParameterName | ParameterValue
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// 2 | GENESISFLOW_MinValidToF | 21990232
|
||||
/// 2 | GENESISFLOW_Timeout | 1
|
||||
///
|
||||
/// Returned data are represented as:
|
||||
///
|
||||
/// Key = ParameterName
|
||||
/// Value = ParameterValue
|
||||
/// </summary>
|
||||
public class PreAdjustmentCalibrationParamsReader
|
||||
: IPreAdjustmentCalibrationParamsReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Universal data storage reader selected by configuration.
|
||||
/// Can represent database, CSV, JSON or REST reader.
|
||||
/// </summary>
|
||||
private readonly IDataStorageReader reader;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that only one calibration parameter lookup
|
||||
/// is executed at a time.
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim readLock =
|
||||
new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Creates calibration parameter reader using
|
||||
/// provided data storage configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">
|
||||
/// Data storage configuration loaded from gci_config.json.
|
||||
/// </param>
|
||||
public PreAdjustmentCalibrationParamsReader(
|
||||
DataStorageConfig config)
|
||||
{
|
||||
if (config == null)
|
||||
throw new ArgumentNullException(nameof(config));
|
||||
|
||||
reader = DataStorageReaderFactory.Create(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all pre-adjustment calibration parameters
|
||||
/// using the provided data query asynchronously.
|
||||
///
|
||||
/// The query is expected to contain meter size
|
||||
/// as the first lookup parameter.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing lookup parameter values.
|
||||
/// </param>
|
||||
/// <param name="token">
|
||||
/// Cancellation token.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Dictionary:
|
||||
///
|
||||
/// Key:
|
||||
/// GENESISFLOW parameter name
|
||||
///
|
||||
/// Value:
|
||||
/// Stored parameter value
|
||||
/// </returns>
|
||||
public async Task<Dictionary<string, UInt32>> ReadCalibrationParamsAsync(
|
||||
DataQuery query,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
ValidateQuery(query);
|
||||
|
||||
return await Task.Run(
|
||||
() => GetCalibrationParams(query),
|
||||
token).ConfigureAwait(false); ;
|
||||
|
||||
/*await readLock.WaitAsync(token);
|
||||
|
||||
try
|
||||
{
|
||||
return await Task.Run(
|
||||
() => GetCalibrationParams(query),
|
||||
token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
readLock.Release();
|
||||
}*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads all pre-adjustment calibration parameters
|
||||
/// using the provided data query.
|
||||
///
|
||||
/// The query is expected to contain meter size
|
||||
/// as the first lookup parameter.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query containing lookup parameter values.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Dictionary:
|
||||
///
|
||||
/// Key:
|
||||
/// GENESISFLOW parameter name
|
||||
///
|
||||
/// Value:
|
||||
/// Stored parameter value
|
||||
/// </returns>
|
||||
public Dictionary<string, UInt32> GetCalibrationParams(
|
||||
DataQuery query)
|
||||
{
|
||||
ValidateQuery(query);
|
||||
|
||||
object result =
|
||||
reader.GetData(query);
|
||||
|
||||
return ConvertResultToDictionary(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that data query exists
|
||||
/// and contains at least one lookup parameter.
|
||||
/// </summary>
|
||||
/// <param name="query">
|
||||
/// Data query to validate.
|
||||
/// </param>
|
||||
private static void ValidateQuery(
|
||||
DataQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
if (query.QueryParams == null ||
|
||||
query.QueryParams.Count == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Query does not contain any parameter.",
|
||||
nameof(query));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts provider-specific result objects
|
||||
/// returned by IDataStorageReader into a unified
|
||||
/// dictionary representation.
|
||||
/// </summary>
|
||||
/// <param name="result">
|
||||
/// Raw result returned by the configured storage reader.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// Dictionary containing calibration parameter
|
||||
/// name/value pairs.
|
||||
/// </returns>
|
||||
private Dictionary<string, UInt32> ConvertResultToDictionary(
|
||||
object result)
|
||||
{
|
||||
Dictionary<string, UInt32> values =
|
||||
new Dictionary<string, UInt32>();
|
||||
|
||||
if (result == null)
|
||||
return values;
|
||||
|
||||
if (result is DatabaseSearchResult dbResult)
|
||||
{
|
||||
if (!dbResult.Found)
|
||||
return values;
|
||||
|
||||
foreach (Dictionary<string, object> row in dbResult.Rows)
|
||||
{
|
||||
if (!row.TryGetValue("ParameterName", out object parameterNameObject))
|
||||
continue;
|
||||
|
||||
if (!row.TryGetValue("ParameterValue", out object parameterValueObject))
|
||||
continue;
|
||||
|
||||
string parameterName =
|
||||
parameterNameObject?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(parameterName))
|
||||
continue;
|
||||
|
||||
if (TryConvertToUInt32(parameterValueObject, out UInt32 parameterValue))
|
||||
{
|
||||
values[parameterName] =
|
||||
parameterValue;
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
if (result is ReaderDiagnosticResult diagnosticResult)
|
||||
{
|
||||
if (diagnosticResult.Data is Dictionary<string, UInt32> uintDictionary)
|
||||
return uintDictionary;
|
||||
|
||||
if (diagnosticResult.Data is Dictionary<string, string> stringDictionary)
|
||||
{
|
||||
foreach (var item in stringDictionary)
|
||||
{
|
||||
if (TryConvertToUInt32(item.Value, out UInt32 value))
|
||||
values[item.Key] = value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
if (diagnosticResult.Data is Dictionary<string, object> objectDictionary)
|
||||
{
|
||||
foreach (var item in objectDictionary)
|
||||
{
|
||||
if (TryConvertToUInt32(item.Value, out UInt32 value))
|
||||
values[item.Key] = value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
if (result is Dictionary<string, UInt32> directUIntDictionary)
|
||||
return directUIntDictionary;
|
||||
|
||||
if (result is Dictionary<string, string> directStringDictionary)
|
||||
{
|
||||
foreach (var item in directStringDictionary)
|
||||
{
|
||||
if (TryConvertToUInt32(item.Value, out UInt32 value))
|
||||
values[item.Key] = value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
if (result is Dictionary<string, object> directObjectDictionary)
|
||||
{
|
||||
foreach (var item in directObjectDictionary)
|
||||
{
|
||||
if (TryConvertToUInt32(item.Value, out UInt32 value))
|
||||
values[item.Key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static bool TryConvertToUInt32(
|
||||
object value,
|
||||
out UInt32 result)
|
||||
{
|
||||
result = 0;
|
||||
|
||||
if (value == null)
|
||||
return false;
|
||||
|
||||
if (value is UInt32 uintValue)
|
||||
{
|
||||
result = uintValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is int intValue && intValue >= 0)
|
||||
{
|
||||
result = Convert.ToUInt32(intValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is long longValue &&
|
||||
longValue >= 0 &&
|
||||
longValue <= UInt32.MaxValue)
|
||||
{
|
||||
result = Convert.ToUInt32(longValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
return UInt32.TryParse(
|
||||
value.ToString(),
|
||||
out result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Config;
|
||||
using static GenesisCordonelInterface.API.PublicModels;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.DataStorage.Writing
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores detached result records. Each call owns its connection and transaction;
|
||||
/// slot workers, request sessions and streaming ownership are never involved.
|
||||
/// </summary>
|
||||
public sealed class CalibrationResultWriter
|
||||
{
|
||||
private readonly GciDatabaseWriterConfig completion;
|
||||
private readonly GciDatabaseWriterConfig q3;
|
||||
|
||||
public CalibrationResultWriter(GciDataStorageConfig config)
|
||||
{
|
||||
completion = Snapshot(config?.PreadjustmentCompletionWriter);
|
||||
q3 = Snapshot(config?.Q3CalibrationWriter);
|
||||
}
|
||||
|
||||
private static GciDatabaseWriterConfig Snapshot(GciDatabaseWriterConfig config)
|
||||
{
|
||||
if (config == null) return null;
|
||||
return new GciDatabaseWriterConfig
|
||||
{
|
||||
Enabled = config.Enabled, DataSource = config.DataSource,
|
||||
CommandText = config.CommandText, UseStoredProcedure = config.UseStoredProcedure,
|
||||
CommandTimeoutSeconds = config.CommandTimeoutSeconds,
|
||||
ParameterMappings = config.ParameterMappings == null ? null :
|
||||
new Dictionary<string, string>(config.ParameterMappings, StringComparer.Ordinal)
|
||||
};
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StorePreadjustmentCompletionAsync(
|
||||
PreadjustmentCompletionRecord record, CancellationToken token)
|
||||
{
|
||||
return StorePreadjustmentCompletionAsync(record, token, null);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StorePreadjustmentCompletionAsync(
|
||||
PreadjustmentCompletionRecord record, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
// Materialize all values before the first await; no mutable record is retained.
|
||||
var values = new Dictionary<string, object>
|
||||
{
|
||||
{ "PcbId", record.PcbId }, { "MeterSize", record.MeterSize },
|
||||
{ "ZeroOffset1", record.ZeroOffset1 }, { "ZeroOffset2", record.ZeroOffset2 },
|
||||
{ "ZeroOffset3", record.ZeroOffset3 },
|
||||
{ "FirstHitUpdatePeriod", record.FirstHitUpdatePeriod },
|
||||
{ "FirstHitShift", record.FirstHitShift },
|
||||
{ "FirstHitPercent1", record.FirstHitPercent1 },
|
||||
{ "FirstHitPercent2", record.FirstHitPercent2 },
|
||||
{ "FirstHitPercent3", record.FirstHitPercent3 },
|
||||
{ "ToFTempOffset1", record.ToFTempOffset1 },
|
||||
{ "ToFTempOffset2", record.ToFTempOffset2 },
|
||||
{ "ToFTempOffset3", record.ToFTempOffset3 },
|
||||
{ "Date", record.Date }, { "Successful", record.Successful }
|
||||
};
|
||||
string error = !record.MeterSize.HasValue || record.MeterSize.Value < 0
|
||||
? "MeterSize is required and must not be negative." : null;
|
||||
if (record.Successful && values.Any(v => v.Value == null))
|
||||
error = "Successful preadjustment requires all calibration values.";
|
||||
return WriteAsync(completion, record.PcbId, values, error, token, diagnostic);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token)
|
||||
{
|
||||
return StoreQ3CalibrationAsync(record, token, null);
|
||||
}
|
||||
|
||||
public Task<CalibrationWriteResult> StoreQ3CalibrationAsync(
|
||||
Q3CalibrationRecord record, CancellationToken token, Action<string> diagnostic)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
var values = new Dictionary<string, object>
|
||||
{
|
||||
{ "PcbId", record.PcbId }, { "P1Offset", record.P1Offset },
|
||||
{ "P1Cal", record.P1Cal }, { "P2Offset", record.P2Offset },
|
||||
{ "P2Cal", record.P2Cal }, { "P3Offset", record.P3Offset },
|
||||
{ "P3Cal", record.P3Cal }, { "Date", record.Date }
|
||||
};
|
||||
string error = values.Any(v => v.Value == null)
|
||||
? "Q3 requires the offset and confirmed calibration factor for all three paths."
|
||||
: null;
|
||||
if (values.Values.OfType<double>().Any(v => double.IsNaN(v) || double.IsInfinity(v)))
|
||||
error = "Q3 offsets must be finite numbers.";
|
||||
return WriteAsync(q3, record.PcbId, values, error, token, diagnostic);
|
||||
}
|
||||
|
||||
private static async Task<CalibrationWriteResult> WriteAsync(
|
||||
GciDatabaseWriterConfig config, int pcbId, IDictionary<string, object> values,
|
||||
string validationError, CancellationToken token, Action<string> diagnostic = null)
|
||||
{
|
||||
string operation = values.ContainsKey("Successful") ? "Completion" : "Q3 calibration";
|
||||
Report(diagnostic, operation + " database write: START.");
|
||||
var result = new CalibrationWriteResult { PcbId = pcbId };
|
||||
if (config == null || !config.Enabled)
|
||||
{
|
||||
result.Status = CalibrationWriteStatus.Disabled;
|
||||
result.ErrorMessage = "Database writer is not enabled.";
|
||||
Report(diagnostic, operation + " database write skipped: writer is disabled or not configured.");
|
||||
return result;
|
||||
}
|
||||
bool commitStarted = false;
|
||||
bool connectionOpened = false;
|
||||
string stage = "validation";
|
||||
try
|
||||
{
|
||||
if (validationError != null) throw new ArgumentException(validationError);
|
||||
if (pcbId <= 0) throw new ArgumentException("PcbId must be positive.");
|
||||
if ((DateTime)values["Date"] == default(DateTime))
|
||||
throw new ArgumentException("The calibration timestamp is required.");
|
||||
if (string.IsNullOrWhiteSpace(config.DataSource) || string.IsNullOrWhiteSpace(config.CommandText))
|
||||
throw new ArgumentException("Writer DataSource and CommandText are required.");
|
||||
if (config.CommandTimeoutSeconds < 1)
|
||||
throw new ArgumentException("CommandTimeoutSeconds must be positive.");
|
||||
|
||||
using (var connection = new SqlConnection(config.DataSource))
|
||||
using (var command = new SqlCommand(config.CommandText, connection))
|
||||
{
|
||||
command.CommandType = config.UseStoredProcedure ? CommandType.StoredProcedure : CommandType.Text;
|
||||
command.CommandTimeout = config.CommandTimeoutSeconds;
|
||||
AddParameters(command, config, values);
|
||||
stage = "server connection";
|
||||
// Only log the destination, never credentials or the full connection string.
|
||||
var destination = new SqlConnectionStringBuilder(config.DataSource);
|
||||
Report(diagnostic, "Connecting to server '" + destination.DataSource +
|
||||
"', database '" + destination.InitialCatalog + "'.");
|
||||
await connection.OpenAsync(token).ConfigureAwait(false);
|
||||
connectionOpened = true;
|
||||
Report(diagnostic, "Connection opened.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
stage = "transaction start";
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
command.Transaction = transaction;
|
||||
stage = "data write";
|
||||
Report(diagnostic, "Writing " + operation + " data (" +
|
||||
(config.UseStoredProcedure ? "stored procedure" : "SQL command") + ").");
|
||||
// The command/procedure must return the stored Id (OUTPUT INSERTED.Id).
|
||||
object scalar = await command.ExecuteScalarAsync(token).ConfigureAwait(false);
|
||||
stage = "returned record Id validation";
|
||||
if (scalar == null || scalar == DBNull.Value)
|
||||
throw new InvalidOperationException("Writer did not return a record Id; transaction rolled back.");
|
||||
decimal id = Convert.ToDecimal(scalar, CultureInfo.InvariantCulture);
|
||||
if (id <= 0 || id != decimal.Truncate(id) || id > long.MaxValue)
|
||||
throw new InvalidOperationException("Writer returned an invalid record Id; transaction rolled back.");
|
||||
token.ThrowIfCancellationRequested();
|
||||
Report(diagnostic, "Write command completed; confirming transaction.");
|
||||
stage = "transaction commit";
|
||||
// Once commit starts, cancellation must not hide a committed record.
|
||||
commitStarted = true;
|
||||
transaction.Commit();
|
||||
result.RecordId = (long)id;
|
||||
result.Status = CalibrationWriteStatus.Stored;
|
||||
Report(diagnostic, "Transaction committed; RecordId=" + result.RecordId + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!commitStarted)
|
||||
{
|
||||
Report(diagnostic, operation + " database write cancelled during " + stage + ".");
|
||||
// Disposing the uncommitted transaction rolls back its write.
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Status = commitStarted ? CalibrationWriteStatus.OutcomeUnknown : CalibrationWriteStatus.Failed;
|
||||
result.ErrorMessage = ex.Message;
|
||||
// Server messages can contain record values or login names. Keep
|
||||
// the slot log free of these; retain the original error in the result.
|
||||
var sqlError = ex as SqlException;
|
||||
string detail = sqlError == null ? ex.GetType().Name :
|
||||
"SQL error " + sqlError.Number + ", state " + sqlError.State;
|
||||
Report(diagnostic, operation + " database write failed during " + stage + " (" + detail + ").");
|
||||
if (commitStarted)
|
||||
Report(diagnostic, "Commit outcome is unknown. Verify the database before retrying.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The SQL using scopes have already disposed their resources here.
|
||||
if (connectionOpened) Report(diagnostic, "SQL connection released.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Report(Action<string> diagnostic, string message)
|
||||
{
|
||||
if (diagnostic == null) return;
|
||||
try { diagnostic(message); }
|
||||
catch (Exception)
|
||||
{
|
||||
// A closed/failing diagnostic subscriber must not change the DB outcome.
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddParameters(SqlCommand command, GciDatabaseWriterConfig config,
|
||||
IDictionary<string, object> values)
|
||||
{
|
||||
var mappings = config.ParameterMappings;
|
||||
bool custom = mappings != null && mappings.Count > 0;
|
||||
if (custom && (mappings.Count != values.Count || mappings.Keys.Any(k => !values.ContainsKey(k))))
|
||||
throw new ArgumentException("ParameterMappings must map every documented record field exactly once.");
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var value in values)
|
||||
{
|
||||
string name = custom ? mappings[value.Key] : "@" + value.Key;
|
||||
if (string.IsNullOrWhiteSpace(name) || name.Length < 2 || name.Length > 128 ||
|
||||
name[0] != '@' || !(char.IsLetter(name[1]) || name[1] == '_') ||
|
||||
name.Skip(2).Any(c => !char.IsLetterOrDigit(c) && c != '_') || !names.Add(name))
|
||||
throw new ArgumentException("Invalid or duplicate SQL parameter name for " + value.Key + ".");
|
||||
SqlDbType type = value.Key == "Date" ? SqlDbType.DateTime2 :
|
||||
value.Key == "Successful" ? SqlDbType.Bit :
|
||||
value.Key == "P1Offset" || value.Key == "P2Offset" || value.Key == "P3Offset"
|
||||
? SqlDbType.Float : SqlDbType.Int;
|
||||
command.Parameters.Add(name, type).Value = value.Value ?? DBNull.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using GenesisCordonelInterface.API;
|
||||
using GenesisCordonelInterface.Core.Config;
|
||||
using GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.AccessControl;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using UdsReaderType_CalibrationParams = GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.PreAdjustmentCalibrationParams.PreAdjustmentCalibrationParamsReader;
|
||||
using UdsReaderType_LoginPasswords = GenesisCordonelInterface.Core.DataStorage.Reading.Implementation.MeterLoginPasswords.MeterLoginPasswordReader;
|
||||
using GciPublicModels = GenesisCordonelInterface.API.PublicModels;
|
||||
using GciEnums = GenesisCordonelInterface.API.Enums;
|
||||
using GciType = GenesisCordonelInterface.API.InterfaceOutsideToGCI;
|
||||
using GciDataStorageReadingModels = GenesisCordonelInterface.Core.DataStorage.Reading.Common.Models;
|
||||
using GciGUIType = GenesisCordonelInterface.UI.MainView;
|
||||
|
||||
namespace GenesisCordonelInterface.Core
|
||||
{
|
||||
public class Engine
|
||||
{
|
||||
GciConfig gciConfig;
|
||||
|
||||
public UdsReaderType_LoginPasswords loginPasswordsDataStorageReader;
|
||||
public UdsReaderType_CalibrationParams calibrationParamsStorageReader;
|
||||
//readonly UdsWriterType writer;
|
||||
|
||||
//diag GUI for GCI
|
||||
GciGUIType gciGUI;
|
||||
Form gciGuiHostForm;
|
||||
|
||||
//diag GUI for GciBridge
|
||||
public UserControl gciBridgeGUIUserControl;
|
||||
public UI.MainForm gciBridgeGuiForm;
|
||||
|
||||
public InterfaceOutsideToGCI gciExternalInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the public GCI facade.
|
||||
/// </summary>
|
||||
public Engine()
|
||||
{
|
||||
gciConfig = GciConfigLoader.LoadDefault();
|
||||
|
||||
loginPasswordsDataStorageReader = new UdsReaderType_LoginPasswords
|
||||
(
|
||||
new GciDataStorageReadingModels.DataStorageConfig
|
||||
(
|
||||
nameof(GciEnums.DataStorageReaderTypes.LoginPasswordsReader),
|
||||
GciDataStorageReadingModels.DataStorageType.LocalDatabase,
|
||||
gciConfig.DataStorageSection.MeterLoginPasswords.DataSource,
|
||||
gciConfig.DataStorageSection.MeterLoginPasswords.QueryTemplate
|
||||
)
|
||||
);
|
||||
calibrationParamsStorageReader = new UdsReaderType_CalibrationParams
|
||||
(
|
||||
new GciDataStorageReadingModels.DataStorageConfig
|
||||
(
|
||||
nameof(GciEnums.DataStorageReaderTypes.CalibrationParamsReader),
|
||||
GciDataStorageReadingModels.DataStorageType.LocalDatabase,
|
||||
gciConfig.DataStorageSection.PreAdjustmentCalibrationParams.DataSource,
|
||||
gciConfig.DataStorageSection.PreAdjustmentCalibrationParams.QueryTemplate
|
||||
)
|
||||
);
|
||||
|
||||
gciExternalInterface = new InterfaceOutsideToGCI(
|
||||
new InterfaceGCIToLaatzen(),
|
||||
loginPasswordsDataStorageReader,
|
||||
calibrationParamsStorageReader,
|
||||
gciConfig.DataStorageSection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
Navod na logovanie projektu Laatzen/Common/ pre Genesis Cordonel meradla:
|
||||
1. Princip:
|
||||
- logovanie je zabezpecene pomocou nastroja NLog, ktory pouziva Laatzen
|
||||
- Laatzen vsak pouziva nad NLogom svoj wrapper NLogHelper.cs a teda sa na pracu pri ich projektoch konfiguruje NLog pomocou ich suboru Nlogconfig.xml,
|
||||
no nakolko ja som nechcel zasahovat do ich konfiguracneho suboru NLogu, pouzil som svoj, ktory je definovany v mojom projekte GenesisCordonelInterface,
|
||||
s nazvom nlog.config
|
||||
- z dovodu, ze som teda nechcel zasahovat do ich konfiguracie a zaroven som musel NLog na svoje ucely (presmerovanie logov do mema v mojom projekte),
|
||||
musel som teda konfigurovat svojim konfiguracnym suborom, a zaroven plati ze pouzivam raw NLog.
|
||||
- btw ked som pouzil ich wrapper a zaroven moj konfiguracny subor NLogu, tak pri spusten appky ho aj nacitalo, no nasledne sa nacitali Laatzenacke projekty
|
||||
a nacital sa aj ich konfigurak Nlogu, ci sa prepisal ten moj. Pouzil som ten raw Nlog a uz to bolo ok
|
||||
- takto je mozne presmerovat pomocou nizsie uvedenej metody logy az do mema v mojom projekte a tiez aj do suboru, pricom vsetko definujem v nlog.config
|
||||
mojho projektu a nie v projekte Laatzenu
|
||||
2. prilozene subory UiLogBus.cs a UiTarget.cs musia byt nakopirovane do projektu v ramci Solution s nazvom Common od Laatzenu
|
||||
- kopiruju sa do top adresara projektu
|
||||
- musia obsahovat ""namespace Xylem.Common.Utils.Logging"
|
||||
- v mieste kde chcem logovat, je potrebne:
|
||||
- zadefinovat:
|
||||
private readonly ILogger Logger = NLogHelper.CreateOrGetLogger("GenesisCordonelInterface");
|
||||
- pouzit log:
|
||||
Logger.Error(ex, $"Slot #{slotNR} failed: {ex.Message}");
|
||||
Logger.Trace("CONFIG: Setup loaded.");
|
||||
Logger.Info("CONFIG: Setup loaded.");
|
||||
...whatever
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
/*namespace Xylem.Common.Utils.Logging
|
||||
{
|
||||
public static class UiLogBus
|
||||
{
|
||||
public static event Action<string> MessageReceived;
|
||||
|
||||
public static void Publish(string message)
|
||||
{
|
||||
var handler = MessageReceived;
|
||||
if (handler != null)
|
||||
{
|
||||
handler(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
@@ -0,0 +1,15 @@
|
||||
using NLog;
|
||||
using NLog.Targets;
|
||||
|
||||
/*namespace Xylem.Common.Utils.Logging
|
||||
{
|
||||
[Target("UiTarget")]
|
||||
public class UiTarget : TargetWithLayout
|
||||
{
|
||||
protected override void Write(LogEventInfo logEvent)
|
||||
{
|
||||
string message = Layout.Render(logEvent);
|
||||
UiLogBus.Publish(message);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
@@ -0,0 +1,518 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.Threading
|
||||
{
|
||||
/*
|
||||
ApiWorker – per-slot sequential execution worker
|
||||
|
||||
This class provides a lightweight background worker that executes actions
|
||||
sequentially on a dedicated thread.
|
||||
|
||||
PRIMARY PURPOSE
|
||||
---------------
|
||||
ApiWorker is designed to safely execute hardware-related operations
|
||||
(e.g. meter communication) without blocking the UI thread and without
|
||||
allowing concurrent access to the same device.
|
||||
|
||||
Each ApiWorker instance typically represents:
|
||||
1 worker = 1 slot = 1 meter = 1 communication channel
|
||||
|
||||
KEY PROPERTIES
|
||||
--------------
|
||||
- Single dedicated background thread
|
||||
- FIFO queue (first-in, first-out)
|
||||
- Sequential execution (NO parallelism inside one worker)
|
||||
- Thread-safe enqueueing
|
||||
- Task-based async interface for callers
|
||||
|
||||
WHY THIS IS IMPORTANT
|
||||
--------------------
|
||||
Hardware communication (serial ports, meters, etc.) is usually NOT thread-safe.
|
||||
If multiple commands are executed in parallel, communication may break or corrupt data.
|
||||
|
||||
ApiWorker guarantees:
|
||||
- operations are executed one-by-one
|
||||
- order is preserved
|
||||
- no race conditions on the device
|
||||
|
||||
HIGH-LEVEL FLOW
|
||||
---------------
|
||||
Caller (UI/API)
|
||||
|
|
||||
v
|
||||
RunAsync(...)
|
||||
|
|
||||
v
|
||||
TaskCompletionSource created
|
||||
|
|
||||
v
|
||||
Action wrapped into queue item
|
||||
|
|
||||
v
|
||||
Added to BlockingCollection queue
|
||||
|
|
||||
v
|
||||
Worker thread consumes queue
|
||||
|
|
||||
v
|
||||
Action executed (blocking HW call)
|
||||
|
|
||||
v
|
||||
Result propagated via TaskCompletionSource
|
||||
|
|
||||
v
|
||||
Caller receives result via await
|
||||
|
||||
GRAPH
|
||||
-----
|
||||
Caller thread (UI)
|
||||
|
|
||||
v
|
||||
RunAsync()
|
||||
|
|
||||
v
|
||||
Queue (BlockingCollection)
|
||||
|
|
||||
v
|
||||
-----------------------------
|
||||
| Worker Thread (background)|
|
||||
| while(queue) |
|
||||
| Execute Action |
|
||||
-----------------------------
|
||||
|
|
||||
v
|
||||
Task result (await)
|
||||
|
||||
THREADING MODEL
|
||||
---------------
|
||||
- Producer/Consumer pattern
|
||||
- Producer: any thread calling RunAsync
|
||||
- Consumer: single worker thread
|
||||
- Synchronization handled by BlockingCollection
|
||||
|
||||
MAIN COMPONENTS
|
||||
---------------
|
||||
1. BlockingCollection<Action> queue
|
||||
- thread-safe queue
|
||||
- stores work items
|
||||
- supports blocking consumption
|
||||
|
||||
2. Dedicated Thread
|
||||
- runs WorkerLoop()
|
||||
- continuously processes queue
|
||||
|
||||
3. TaskCompletionSource<T>
|
||||
- bridges sync execution → async API
|
||||
- allows caller to await result
|
||||
|
||||
METHODS
|
||||
-------
|
||||
|
||||
RunAsync<T>(Func<T>)
|
||||
--------------------
|
||||
- Enqueues a function returning a value
|
||||
- Wraps it into Action
|
||||
- Executes on worker thread
|
||||
- Returns Task<T> to caller
|
||||
|
||||
RunAsync(Action)
|
||||
----------------
|
||||
- Convenience overload for void methods
|
||||
- Internally wraps into Func<object>
|
||||
|
||||
WorkerLoop()
|
||||
------------
|
||||
- Infinite loop consuming queue
|
||||
- Executes actions one-by-one
|
||||
- Stops when queue is completed
|
||||
|
||||
Dispose()
|
||||
---------
|
||||
- Stops accepting new items
|
||||
- Cleans up queue
|
||||
- Does NOT forcibly stop running task
|
||||
|
||||
CANCELLATION
|
||||
------------
|
||||
- CancellationToken is checked BEFORE execution
|
||||
- If cancelled → Task is cancelled
|
||||
- Does NOT interrupt running operation
|
||||
|
||||
IMPORTANT LIMITATIONS
|
||||
--------------------
|
||||
- No parallel execution inside one worker (by design)
|
||||
- Long-running action blocks worker thread
|
||||
- No built-in timeout handling
|
||||
- Dispose does not abort running work
|
||||
|
||||
WHEN TO USE
|
||||
-----------
|
||||
✔ Per-device communication (serial, TCP, HW)
|
||||
✔ Ordered execution required
|
||||
✔ UI must stay responsive
|
||||
|
||||
WHEN NOT TO USE
|
||||
---------------
|
||||
✘ CPU parallel processing (use Task.Run / Parallel)
|
||||
✘ High-throughput parallel workloads
|
||||
✘ Fire-and-forget background tasks
|
||||
|
||||
SUMMARY
|
||||
-------
|
||||
ApiWorker is a simple, robust solution for:
|
||||
"Execute commands sequentially per resource, asynchronously from UI"
|
||||
|
||||
It is a perfect fit for:
|
||||
- hardware interfaces
|
||||
- device drivers
|
||||
- IO-bound serialized workflows
|
||||
*/
|
||||
|
||||
public sealed class ApiWorker : IDisposable
|
||||
{
|
||||
private static readonly NLog.ILogger Logger = NLog.LogManager.GetLogger("GenesisCordonelInterface.SlotWorker");
|
||||
private static long nextRequestId;
|
||||
/// <summary>
|
||||
/// Thread-safe FIFO queue holding work items.
|
||||
/// </summary>
|
||||
private readonly BlockingCollection<Action> queue = new BlockingCollection<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated worker thread processing the queue.
|
||||
/// </summary>
|
||||
private readonly Thread thread;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether this worker has been disposed.
|
||||
/// </summary>
|
||||
private bool disposed;
|
||||
|
||||
public string Name { get; private set; }
|
||||
public int QueueLength
|
||||
{
|
||||
get
|
||||
{
|
||||
if (disposed || queue == null)
|
||||
return 0;
|
||||
|
||||
try
|
||||
{
|
||||
return queue.Count;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
public bool IsBusy { get; private set; }
|
||||
public string CurrentOperation { get; private set; }
|
||||
public string LastError { get; private set; }
|
||||
public DateTime LastActivity { get; private set; }
|
||||
public bool IsDisposed
|
||||
{
|
||||
get { return disposed; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new ApiWorker with its own background thread.
|
||||
/// </summary>
|
||||
public ApiWorker(string name)
|
||||
{
|
||||
Name = name;
|
||||
LastActivity = DateTime.Now;
|
||||
|
||||
thread = new Thread(WorkerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = name
|
||||
};
|
||||
|
||||
thread.Start();
|
||||
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] CREATED Worker={0}, WorkerThread={1}, CallerThread={2}",
|
||||
Name,
|
||||
thread.ManagedThreadId,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a function returning a value for sequential execution.
|
||||
/// </summary>
|
||||
public Task<T> RunAsync<T>(
|
||||
Func<T> action,
|
||||
CancellationToken token = default,
|
||||
string operationName = null)
|
||||
{
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
|
||||
if (disposed)
|
||||
throw new ObjectDisposedException(nameof(ApiWorker));
|
||||
|
||||
var tcs = new TaskCompletionSource<T>(
|
||||
TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
long requestId = Interlocked.Increment(ref nextRequestId);
|
||||
string operation = string.IsNullOrWhiteSpace(operationName) ? action.Method.Name : operationName;
|
||||
int callerThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
Stopwatch queueWait = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Debug(
|
||||
"[SLOT-REQUEST] ENQUEUE RequestId={0}, Worker={1}, Operation={2}, CallerThread={3}, QueueLengthBefore={4}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
callerThreadId,
|
||||
QueueLength);
|
||||
|
||||
queue.Add(() =>
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
queueWait.Stop();
|
||||
LastActivity = DateTime.Now;
|
||||
|
||||
Logger.Warn(
|
||||
"[SLOT-REQUEST] CANCELLED_BEFORE_START RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, QueueWaitMs={4}, QueueLength={5}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
queueWait.ElapsedMilliseconds,
|
||||
QueueLength);
|
||||
|
||||
tcs.TrySetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch execution = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
CurrentOperation = operation;
|
||||
LastActivity = DateTime.Now;
|
||||
LastError = null;
|
||||
|
||||
queueWait.Stop();
|
||||
|
||||
Logger.Debug(
|
||||
"[SLOT-REQUEST] START RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, CallerThread={4}, QueueWaitMs={5}, QueueLength={6}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
callerThreadId,
|
||||
queueWait.ElapsedMilliseconds,
|
||||
QueueLength);
|
||||
|
||||
var result = action();
|
||||
|
||||
tcs.TrySetResult(result);
|
||||
|
||||
execution.Stop();
|
||||
|
||||
Logger.Debug(
|
||||
"[SLOT-REQUEST] COMPLETE RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, DurationMs={4}, QueueLength={5}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
execution.ElapsedMilliseconds,
|
||||
QueueLength);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
execution.Stop();
|
||||
LastError = ex.Message;
|
||||
|
||||
Logger.Error(
|
||||
ex,
|
||||
"[SLOT-REQUEST] FAILED RequestId={0}, Worker={1}, Operation={2}, WorkerThread={3}, DurationMs={4}, QueueLength={5}, Error={6}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
execution.ElapsedMilliseconds,
|
||||
QueueLength,
|
||||
ex.Message);
|
||||
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
CurrentOperation = null;
|
||||
LastActivity = DateTime.Now;
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
queueWait.Stop();
|
||||
|
||||
Logger.Warn(
|
||||
"[SLOT-REQUEST] ENQUEUE_CANCELLED RequestId={0}, Worker={1}, Operation={2}, CallerThread={3}, QueueWaitMs={4}",
|
||||
requestId,
|
||||
Name,
|
||||
operation,
|
||||
callerThreadId,
|
||||
queueWait.ElapsedMilliseconds);
|
||||
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
catch (ObjectDisposedException ex)
|
||||
{
|
||||
queueWait.Stop();
|
||||
|
||||
Logger.Error(
|
||||
ex,
|
||||
"[SLOT-REQUEST] ENQUEUE_FAILED RequestId={0}, Worker={1}, Operation={2}, Reason=WorkerDisposed",
|
||||
requestId,
|
||||
Name,
|
||||
operation);
|
||||
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
queueWait.Stop();
|
||||
|
||||
var disposedException = new ObjectDisposedException(nameof(ApiWorker), ex);
|
||||
|
||||
Logger.Error(
|
||||
disposedException,
|
||||
"[SLOT-REQUEST] ENQUEUE_FAILED RequestId={0}, Worker={1}, Operation={2}, Reason=QueueCompleted",
|
||||
requestId,
|
||||
Name,
|
||||
operation);
|
||||
|
||||
tcs.TrySetException(disposedException);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public Task RunAsync(
|
||||
Action action,
|
||||
CancellationToken token = default(CancellationToken),
|
||||
string operationName = null)
|
||||
{
|
||||
return RunAsync<object>(() =>
|
||||
{
|
||||
action();
|
||||
return null;
|
||||
}, token, operationName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enqueues a void action for sequential execution.
|
||||
/// </summary>
|
||||
public Task RunAsync(Action action, CancellationToken token = default)
|
||||
{
|
||||
return RunAsync<object>(() =>
|
||||
{
|
||||
action();
|
||||
return null;
|
||||
}, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main worker loop processing queued actions.
|
||||
/// </summary>
|
||||
private void WorkerLoop()
|
||||
{
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] THREAD_STARTED Worker={0}, WorkerThread={1}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in queue.GetConsumingEnumerable())
|
||||
{
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
item();
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Logger.Debug(
|
||||
"[SLOT-WORKER] QUEUE_DISPOSED Worker={0}, WorkerThread={1}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(
|
||||
ex,
|
||||
"[SLOT-WORKER] THREAD_FAILED Worker={0}, WorkerThread={1}, Error={2}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] THREAD_STOPPED Worker={0}, WorkerThread={1}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the worker and releases resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] DISPOSE_BEGIN Worker={0}, CallerThread={1}, WorkerThread={2}, IsBusy={3}, CurrentOperation={4}, QueueLength={5}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
thread.ManagedThreadId,
|
||||
IsBusy,
|
||||
CurrentOperation ?? "<none>",
|
||||
QueueLength);
|
||||
|
||||
disposed = true;
|
||||
|
||||
queue.CompleteAdding();
|
||||
|
||||
if (Thread.CurrentThread != thread && thread.IsAlive)
|
||||
{
|
||||
bool stopped = thread.Join(1000);
|
||||
if (!stopped)
|
||||
{
|
||||
Logger.Warn(
|
||||
"[SLOT-WORKER] DISPOSE_WAIT_TIMEOUT Worker={0}, WorkerThread={1}, CurrentOperation={2}, QueueLength={3}",
|
||||
Name,
|
||||
thread.ManagedThreadId,
|
||||
CurrentOperation ?? "<none>",
|
||||
QueueLength);
|
||||
}
|
||||
}
|
||||
|
||||
queue.Dispose();
|
||||
|
||||
Logger.Info(
|
||||
"[SLOT-WORKER] DISPOSE_END Worker={0}, CallerThread={1}, WorkerThread={2}, ThreadAlive={3}",
|
||||
Name,
|
||||
Thread.CurrentThread.ManagedThreadId,
|
||||
thread.ManagedThreadId,
|
||||
thread.IsAlive);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GenesisCordonelInterface.Core.Threading
|
||||
{
|
||||
public sealed class RetryResult<T>
|
||||
{
|
||||
public T Result { get; set; }
|
||||
public bool Success { get; set; }
|
||||
public int Attempts { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public bool TimedOut { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"Success={0}, Attempts={1}, Duration={2} ms, TimedOut={3}, Result={4}",
|
||||
Success,
|
||||
Attempts,
|
||||
(int)Duration.TotalMilliseconds,
|
||||
TimedOut,
|
||||
Result == null ? "<null>" : Result.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static class RetryWorker
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes an asynchronous operation with retry and timeout protection.
|
||||
///
|
||||
/// Important:
|
||||
/// The timeout here protects the caller from waiting forever, but it does not forcibly abort
|
||||
/// the underlying hardware operation. Because hardware/COM calls may continue running after
|
||||
/// the timeout signal, this method waits for the original action to finish before starting
|
||||
/// the next retry. This prevents overlapping COM requests on the same device.
|
||||
/// </summary>
|
||||
public static async Task<RetryResult<T>> RunWithRetryAsync<T>(
|
||||
Func<Task<T>> action,
|
||||
Func<T, bool> isSuccess,
|
||||
Action<string> log,
|
||||
Action<string, T> logResult,
|
||||
string operationName,
|
||||
int maxAttempts = 3,
|
||||
int delayMs = 500,
|
||||
int timeoutMs = 30000)
|
||||
{
|
||||
var started = DateTime.Now;
|
||||
T lastResult = default(T);
|
||||
bool timedOut = false;
|
||||
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
||||
{
|
||||
Task<T> actionTask = null;
|
||||
|
||||
try
|
||||
{
|
||||
log?.Invoke($"{operationName} started. Attempt {attempt}/{maxAttempts}");
|
||||
|
||||
// Start real operation, for example Connect/GetPcbId/Login.
|
||||
actionTask = action();
|
||||
|
||||
// Start independent timeout timer.
|
||||
var timeoutTask = Task.Delay(timeoutMs);
|
||||
|
||||
// Wait until either operation completes or timeout expires.
|
||||
var completedTask = await Task.WhenAny(actionTask, timeoutTask)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (completedTask == timeoutTask)
|
||||
{
|
||||
timedOut = true;
|
||||
|
||||
log?.Invoke(
|
||||
$"{operationName} timed out. Attempt {attempt}/{maxAttempts}. " +
|
||||
"Waiting for the running operation to finish before retry.");
|
||||
|
||||
// Critical part:
|
||||
// Do NOT immediately start another retry.
|
||||
// The hardware operation may still be active in ApiWorker.
|
||||
// Starting another attempt immediately could corrupt communication.
|
||||
try
|
||||
{
|
||||
lastResult = await actionTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Invoke(
|
||||
$"{operationName} finished after timeout with exception: " +
|
||||
$"{ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Operation completed before timeout.
|
||||
lastResult = await actionTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Decide whether the returned result means success.
|
||||
if (lastResult != null && isSuccess(lastResult))
|
||||
{
|
||||
return new RetryResult<T>
|
||||
{
|
||||
Result = lastResult,
|
||||
Success = true,
|
||||
Attempts = attempt,
|
||||
Duration = DateTime.Now - started,
|
||||
TimedOut = timedOut
|
||||
};
|
||||
}
|
||||
|
||||
logResult?.Invoke(
|
||||
$"{operationName} failed. Attempt {attempt}/{maxAttempts}",
|
||||
lastResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Covers exceptions thrown before timeout handling or by action startup.
|
||||
log?.Invoke(
|
||||
$"{operationName} exception on attempt {attempt}/{maxAttempts}: " +
|
||||
$"{ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
// Delay before next retry, except after the final attempt.
|
||||
if (attempt < maxAttempts)
|
||||
{
|
||||
log?.Invoke($"{operationName} waiting {delayMs} ms before retry.");
|
||||
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new RetryResult<T>
|
||||
{
|
||||
Result = lastResult,
|
||||
Success = false,
|
||||
Attempts = maxAttempts,
|
||||
Duration = DateTime.Now - started,
|
||||
TimedOut = timedOut
|
||||
};
|
||||
}
|
||||
|
||||
public static void EnsureSuccess<T>(
|
||||
RetryResult<T> retryResult,
|
||||
string operationName)
|
||||
{
|
||||
if (!retryResult.Success)
|
||||
throw new Exception($"{operationName} failed after {retryResult.Attempts} attempts.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{C955D8AC-76B8-42D8-A83F-8AEB56CF2567}</ProjectGuid>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>GenesisCordonelInterface</RootNamespace>
|
||||
<AssemblyName>GenesisCordonelInterface</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Common">
|
||||
<HintPath>..\packages\Common\Common.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Logic.ProductionToProductMapper">
|
||||
<HintPath>..\packages\Common\Logic.ProductionToProductMapper.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NLog.5.2.2\lib\net46\NLog.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Deployment" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Xylem.Common.CommonCore">
|
||||
<HintPath>..\packages\Common\Xylem.Common.CommonCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.CommonCore.Configuration">
|
||||
<HintPath>..\packages\Common\Xylem.Common.CommonCore.Configuration.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.CommonCore.ThreadWatcher">
|
||||
<HintPath>..\packages\Common\Xylem.Common.CommonCore.ThreadWatcher.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.Interfaces.Ports.PortCore">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.Interfaces.Ports.PortCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.Interfaces.Ports.SerialPorts">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.Interfaces.Ports.SerialPorts.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.Applications">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.Applications.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.GenesisConfig.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.Genesis.Registers">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.Genesis.Registers.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.MagFlux.Protocols.RequestProtocol">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.MagFlux.Protocols.RequestProtocol.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.WaterMeterCore">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.WaterMeterCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Hardware.WaterMeter.WaterMeterRegisters">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Hardware.WaterMeter.WaterMeterRegisters.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Logic.ProductionOrderCore">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Logic.ProductionOrderCore.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Logic.SoftwareAccessHelper">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Logic.SoftwareAccessHelper.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Utils.ByteArrayStyle">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Utils.ByteArrayStyle.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Utils.Crc16Ccitt">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Utils.Crc16Ccitt.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Utils.Logging">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Utils.Logging.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Xylem.Common.Utils.ProcessExec">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Utils.ProcessExec.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="API\ModelsMapping.cs" />
|
||||
<Compile Include="API\InterfaceOutsideToGCI.cs" />
|
||||
<Compile Include="API\InterfaceGCIToLaatzen.cs" />
|
||||
<Compile Include="API\PublicModels.cs" />
|
||||
<Compile Include="Core\Config\GciConfig.cs" />
|
||||
<Compile Include="Core\Config\GciConfigLoader.cs" />
|
||||
<Compile Include="Core\DataStorage\Config\GciDataStorageConfig.cs" />
|
||||
<Compile Include="Core\DataStorage\Config\GciDatabaseWriterConfig.cs" />
|
||||
<Compile Include="Core\DataStorage\Writing\CalibrationResultWriter.cs" />
|
||||
<Compile Include="API\Enums.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Providers\CsvDataStorageReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Providers\DatabaseDataStorageReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Models\DatabaseSearchResult.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Models\DataQuery.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Models\DataStorageConfig.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\DataStorageReaderFactory.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Models\DataStorageType.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Contracts\IDataStorageReader..cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Providers\JsonDataStorageReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Models\ReaderDiagnosticResult.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Providers\RestApiDataStorageReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Searching\SearchOrderDefinition.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Common\Searching\SearchOrderParser.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Implementation\MeterLoginPasswords\IMeterLoginPasswordReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Implementation\MeterLoginPasswords\MeterLoginPasswordReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Implementation\PreAdjustmentCalibrationParams\IPreAdjustmentCalibrationParamsReader.cs" />
|
||||
<Compile Include="Core\DataStorage\Reading\Implementation\PreAdjustmentCalibrationParams\PreAdjustmentCalibrationParamsReader.cs" />
|
||||
<Compile Include="Core\Engine\Engine.cs" />
|
||||
<Compile Include="Core\Logging\UiLogBus.cs" />
|
||||
<Compile Include="Core\Logging\UiTarget.cs" />
|
||||
<Compile Include="Core\Threading\ApiWorker\ApiWorker.cs" />
|
||||
<Compile Include="Core\Threading\RetryWorker\RetryWorker.cs" />
|
||||
<Compile Include="UI\Debug\MeterBatchConfigPanel.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\Debug\MeterBatchConfigPanel.Designer.cs">
|
||||
<DependentUpon>MeterBatchConfigPanel.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\Debug\WorkerDebugPanel.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\Debug\WorkerDebugPanel.Designer.cs">
|
||||
<DependentUpon>WorkerDebugPanel.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\Grid\MeterGridColumnConfig.cs" />
|
||||
<Compile Include="UI\Grid\MeterGridConfigProvider.cs" />
|
||||
<Compile Include="UI\Grid\MeterGridManager.cs" />
|
||||
<Compile Include="UI\Grid\MeterRowDto.cs" />
|
||||
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmConfigurations.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmConfigurations.Designer.cs">
|
||||
<DependentUpon>FrmConfigurations.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\FrmCordonelPreadjustmentUI.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\FrmCordonelPreadjustmentUI.Designer.cs">
|
||||
<DependentUpon>FrmCordonelPreadjustmentUI.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmRegisterStore.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmRegisterStore.Designer.cs">
|
||||
<DependentUpon>FrmRegisterStore.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmSetup.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_GenesisToolBox\FrmSetup.Designer.cs">
|
||||
<DependentUpon>FrmSetup.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\MainForm.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\MainForm.Designer.cs">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\PreAdjustmentControl.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\LaatzenAPI_CordonelPreadjustmentUI\PreAdjustmentControl.Designer.cs">
|
||||
<DependentUpon>PreAdjustmentControl.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\MainView.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\MainView.Designer.cs">
|
||||
<DependentUpon>MainView.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\FrmGCIAPI.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\FrmGCIAPI.Designer.cs">
|
||||
<DependentUpon>FrmGCIAPI.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MeterInitView.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MeterInitView.Designer.cs">
|
||||
<DependentUpon>MeterInitView.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MetersActionView.cs">
|
||||
<SubType>UserControl</SubType>
|
||||
</Compile>
|
||||
<Compile Include="UI\StaraTuraAPI_GenesisCordonelInterface\MetersActionView.Designer.cs">
|
||||
<DependentUpon>MetersActionView.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Content Include="docs\images\GciBridge_component_GUI.png" />
|
||||
<Content Include="docs\images\GCI__Onboarding_Overview_drawio.svg" />
|
||||
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__API_architecture__Current_state.svg" />
|
||||
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__API_architecture__Target_state.svg" />
|
||||
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__Data_collection_domain.svg" />
|
||||
<Content Include="docs\images\GCI__Onboarding_Overview_drawio__Meter_management_domain.svg" />
|
||||
<Content Include="docs\images\logo\logo.png" />
|
||||
<Content Include="docs\images\logo\logo.svg" />
|
||||
<Content Include="docs\images\logo\sensus-logo-white-green-rgb.png" />
|
||||
<Content Include="docs\images\logo\sensus-logo-white-green-rgb.svg" />
|
||||
<Content Include="docs\styles\main.css" />
|
||||
<Content Include="RuntimePackage\Build\Copy.targets.xml" />
|
||||
<EmbeddedResource Include="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
</Compile>
|
||||
<EmbeddedResource Include="UI\LaatzenAPI_GenesisToolBox\FrmSetup.resx">
|
||||
<DependentUpon>FrmSetup.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="UI\MainForm.resx">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<Content Include="Core\Logging\ImportantInstructions.txt" />
|
||||
<Content Include="nlog.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<EmbeddedResource Include="UI\LaatzenAPI_CordonelPreadjustmentUI\PreAdjustmentControl.resx">
|
||||
<DependentUpon>PreAdjustmentControl.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="UI\StaraTuraAPI_GenesisCordonelInterface\FrmGCIAPI.resx">
|
||||
<DependentUpon>FrmGCIAPI.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<Content Include="Core\Config\gci_config.json">
|
||||
<Link>Config\gci_config.json</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="Core\Config\gci_config.json" />
|
||||
<None Include="docs\articles\API\index.md" />
|
||||
<None Include="docs\articles\API\PublicModels.md" />
|
||||
<None Include="docs\articles\API\InterfaceGCIToLaatzen.md" />
|
||||
<None Include="docs\articles\API\InterfaceOutsideToGCI.md" />
|
||||
<None Include="docs\docfx.json" />
|
||||
<None Include="docs\index.md" />
|
||||
<None Include="docs\pages\development\page_dev__current_state.md" />
|
||||
<None Include="docs\pages\development\page_dev__home.md" />
|
||||
<None Include="docs\pages\development\page_dev__target_state.md" />
|
||||
<None Include="docs\pages\development\page_dev__migration.md" />
|
||||
<None Include="docs\pages\development\page_dev__refactoring.md" />
|
||||
<None Include="docs\pages\development\page_dev__target_architecture.md" />
|
||||
<None Include="docs\pages\gci\page_gci__app_environment.md" />
|
||||
<None Include="docs\pages\gci\page_gci__internal_architecture.md" />
|
||||
<None Include="docs\pages\gci\page_gci__datastorage.md" />
|
||||
<None Include="docs\pages\gci\page_gci__home.md" />
|
||||
<None Include="docs\pages\gci\page_gci__interfaces.md" />
|
||||
<None Include="docs\pages\gci\page_gci__runtime.md" />
|
||||
<None Include="docs\pages\gci\page_gci__implementation_to_tbf.md" />
|
||||
<None Include="docs\pages\gci\page_gci__implementation_universal.md" />
|
||||
<None Include="docs\pages\gci\page_gci__workers.md" />
|
||||
<None Include="docs\pages\platform\page_platform__configuration_domain.md" />
|
||||
<None Include="docs\pages\platform\page_platform__data_collection_domain.md" />
|
||||
<None Include="docs\pages\platform\page_platform__tbf_implementation.md" />
|
||||
<None Include="docs\pages\platform\page_platform__current_state.md" />
|
||||
<None Include="docs\pages\platform\page_platform__home.md" />
|
||||
<None Include="docs\toc.yml" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
<Compile Include="Properties\Settings.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Config\" />
|
||||
<Folder Include="Core\DataStorage\Writing\NewFolder1\" />
|
||||
<Folder Include="RuntimePackage\Package\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<WCFMetadata Include="Connected Services\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Xylem.Common.Ui.CordonelPreadjustmentUi">
|
||||
<HintPath>..\packages\Common\Xylem.Common.Ui.CordonelPreadjustmentUi.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<ProjectReference Include="..\ExternalProjects\Laatzen\Genesis\Common\Ui\GenesisToolBox\GenesisToolBox.csproj">
|
||||
<Project>{95A50087-99EE-4349-9A17-C547290DB42F}</Project>
|
||||
<Name>GenesisToolBox</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<!-- Import custom GCI runtime packaging logic -->
|
||||
<Import Project="RuntimePackage\Build\Copy.targets.xml" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
using GenesisCordonelInterface.UI;
|
||||
using NLog;
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using Xylem.Common.Utils.Logging;
|
||||
using System.IO;
|
||||
|
||||
namespace GenesisCordonelInterface
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
private static readonly ILogger Logger = NLogHelper.CreateOrGetLogger("GenesisCordonelInterface");
|
||||
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("GenesisCordonelInterface")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Windows Org")]
|
||||
[assembly: AssemblyProduct("GenesisCordonelInterface")]
|
||||
[assembly: AssemblyCopyright("Copyright © Windows Org 2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("c955d8ac-76b8-42d8-a83f-8aeb56cf2567")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,63 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace GenesisCordonelInterface.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GenesisCordonelInterface.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,26 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace GenesisCordonelInterface.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
||||
@@ -0,0 +1,38 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<Target Name="CopyGciRuntimeToRuntimePackage" AfterTargets="Build">
|
||||
|
||||
<PropertyGroup>
|
||||
<GciRuntimePackageDir>$(ProjectDir)RuntimePackage\Package\</GciRuntimePackageDir>
|
||||
<TbfPackageCommonDir>$(ProjectDir)..\packages\Common\</TbfPackageCommonDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<MakeDir Directories="$(GciRuntimePackageDir)" />
|
||||
<MakeDir Directories="$(TbfPackageCommonDir)" />
|
||||
|
||||
<ItemGroup>
|
||||
<GciRuntimeFiles Include="$(TargetDir)*.dll" />
|
||||
<GciRuntimeFiles Include="$(TargetDir)*.pdb" />
|
||||
<GciRuntimeFiles Include="$(TargetDir)*.xml" />
|
||||
<GciRuntimeFiles Include="$(TargetDir)*.exe" />
|
||||
<GciRuntimeFiles Include="$(TargetDir)*.config" />
|
||||
<GciRuntimeFiles Include="$(TargetDir)*.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<Message Text="Copying GCI runtime package to $(GciRuntimePackageDir)" Importance="High" />
|
||||
<Copy
|
||||
SourceFiles="@(GciRuntimeFiles)"
|
||||
DestinationFolder="$(GciRuntimePackageDir)"
|
||||
SkipUnchangedFiles="false"
|
||||
OverwriteReadOnlyFiles="true" />
|
||||
|
||||
<Message Text="Copying GCI runtime package to TBF package common: $(TbfPackageCommonDir)" Importance="High" />
|
||||
<Copy
|
||||
SourceFiles="@(GciRuntimeFiles)"
|
||||
DestinationFolder="$(TbfPackageCommonDir)"
|
||||
SkipUnchangedFiles="false"
|
||||
OverwriteReadOnlyFiles="true" />
|
||||
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
Binary file not shown.
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Xylem.Common.CommonCore" publicKeyToken="null" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.8.18.17954" newVersion="2.8.18.17954" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user